Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 1310] ファイルの概要

このコミットは、Go言語のコンパイラ(gc)における定数評価とリテラル変換に関するバグ修正です。具体的には、src/cmd/gc/const.c ファイルが変更されており、nilリテラルの扱い、特にインターフェース値との比較における挙動が改善されています。

コミット

commit 8bce3b56581ffa758a868fc9a6d7282086c530d2
Author: Ken Thompson <ken@golang.org>
Date:   Tue Dec 9 17:52:41 2008 -0800

    bug127

    R=r
    OCL=20874
    CL=20874

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/8bce3b56581ffa758a868fc9a6d7282086c530d2

元コミット内容

bug127

R=r
OCL=20874
CL=20874

変更の背景

このコミットは、Go言語の初期段階で報告された重要なバグ、bug127https://go.dev/issue/127)を修正するために行われました。bug127は、「nil comparison with interface values」(インターフェース値とnilの比較)という問題で、具体的には、具象型(例えば*int)のnilポインタがインターフェース値に代入された場合、そのインターフェース値と直接nilを比較すると、期待に反してfalseと評価されるというものでした。

Go言語において、インターフェース値は「型」と「値」のペアとして内部的に表現されます。具象型のnilポインタをインターフェースに代入すると、インターフェースの「型」の部分には具象型の情報が残り、「値」の部分がnilとなります。この状態のインターフェース値は、厳密な意味での「nilインターフェース値」(型も値もnil)とは異なるため、i == nilのような比較がfalseになるという誤解を招く挙動がありました。

このバグは、Go言語の型システムとnilのセマンティクスに関する重要な理解のギャップを示しており、コンパイラが定数式やリテラルを評価する際に、この特殊なnilのケースを正しく扱う必要がありました。

前提知識の解説

  • Go言語のコンパイラ(gc: Go言語の公式コンパイラは、初期にはgc(Go Compiler)と呼ばれていました。これはC言語で書かれており、Goのソースコードを機械語に変換する役割を担います。src/cmd/gcディレクトリには、このコンパイラのソースコードが含まれています。
  • 定数評価(Constant Folding): コンパイラの最適化手法の一つで、コンパイル時に定数式(例: 1 + 2)の値を計算し、その結果(例: 3)に置き換えることです。これにより、実行時の計算コストを削減できます。src/cmd/gc/const.cファイルは、この定数評価ロジックの一部を実装しています。
  • リテラル(Literal): ソースコード中に直接記述される値のことです。例えば、123(整数リテラル)、"hello"(文字列リテラル)、true(真偽値リテラル)、そしてnilnilリテラル)などがあります。
  • nil: Go言語におけるゼロ値の一つで、ポインタ、インターフェース、マップ、スライス、チャネルなどの参照型が何も指していない状態を表します。
  • インターフェース値の内部表現: Goのインターフェース値は、内部的には2つのワードで構成されます。1つはインターフェースが保持する具象値の型情報(type)、もう1つはその具象値自体(value)です。
    • nilインターフェース値(var i interface{} = nil)は、typevaluenilです。
    • 具象型のnilポインタを保持するインターフェース値(var p *int = nil; var i interface{} = p)は、typeには*int型が入り、valuenilとなります。この状態では、インターフェース値自体はnilではありません。
  • yyerror: Goコンパイラの内部でエラーメッセージを出力するために使われる関数です。C言語のprintfに似たフォーマット指定子を使用します。
    • %O: オペレーションコード(Node->op)を表示するために使用されます。
    • %E: 式(Node*)を表示するために使用されます。
    • %W: Whatis列挙型(Wlitint, Wlitstrなど、リテラルの種類を示す内部表現)を表示するために使用されます。

技術的詳細

このコミットは、主にsrc/cmd/gc/const.cファイル内のconvlit関数とevconst関数に変更を加えています。

  1. convlit関数の変更 (goto bad1): convlit関数は、リテラルを特定の型に変換する役割を担っています。変更前は、特定の条件(et == TINTER、つまりターゲット型がインターフェース型の場合)でreturn;していました。これは、エラー状態や変換不可能なケースで、単に処理を終了させていた可能性があります。変更後はgoto bad1;となっています。これは、bad1というラベルにジャンプすることで、より統一されたエラー処理パス(例えば、エラーメッセージの出力や、エラー状態の伝播)に移行させることを意図しています。これにより、リテラル変換におけるエラーが適切に報告されるようになります。

  2. evconst関数におけるWlitnilの追加: evconst関数は、定数式の評価を行います。このコミットでは、Wlitnilnilリテラルを表す内部定数)が、定数評価の対象となるリテラルの種類として追加されました。これにより、コンパイラはnilリテラルを他の定数(整数、浮動小数点数、真偽値、文字列)と同様に、定数評価の文脈で認識し、処理できるようになります。これは、nilとの比較を正しく評価するための前提となります。

  3. yyerrorのフォーマット指定子変更 (%Eから%Wへ): yyerror関数呼び出しにおいて、エラーメッセージのフォーマット指定子が%Eから%Wに変更されています。

    • yyerror("illegal combination of literals %O %E, %E", n->op, wl, wr);
    • yyerror("illegal combination of literals %O %W, %W", n->op, wl, wr); この変更は、エラーメッセージで表示する情報が、以前はNode*(式)として扱われていたものが、Whatis列挙型(リテラルの種類)として扱われるようになったことを示唆しています。wlwrwhatis(nl)whatis(nr)の結果であり、これらはリテラルの種類を示すWlit*定数を返します。したがって、%Wを使用することで、より正確なリテラルの種類をエラーメッセージに含めることができるようになります。
  4. evconst関数におけるnil比較の明示的なハンドリング: 最も重要な変更は、evconst関数にnilとの等価性(OEQ)および不等価性(ONE)比較を明示的に処理するケースが追加されたことです。

    case TUP(OEQ, Wlitnil):
        goto settrue;
    case TUP(ONE, Wlitnil):
        goto setfalse;
    

    TUPマクロは、オペレーションコード(OEQONE)とリテラルの種類(Wlitnil)の組み合わせを表現しています。

    • TUP(OEQ, Wlitnil): 何らかの式がnilと等しいかどうかの比較。
    • TUP(ONE, Wlitnil): 何らかの式がnilと等しくないかどうかの比較。 これらのケースが追加されたことで、コンパイラは定数評価の段階で、nilとの比較を直接解決できるようになりました。特に、bug127で問題となっていた「具象型のnilポインタを保持するインターフェース値とnilの比較」のようなケースにおいて、コンパイラがこの新しいロジックを適用し、正しい真偽値(settrueまたはsetfalse)を返すことができるようになります。これにより、実行時に誤った結果を返すことがなくなります。

コアとなるコードの変更箇所

src/cmd/gc/const.cファイルにおいて、以下の変更が行われました。

  1. convlit関数内のreturn;goto bad1;に変更。
  2. evconst関数内の複数のswitch文にcase Wlitnil:が追加。
  3. evconst関数内のyyerror呼び出しのフォーマット指定子が%Eから%Wに変更。
  4. evconst関数内にTUP(OEQ, Wlitnil)TUP(ONE, Wlitnil)の新しいcaseが追加され、それぞれgoto settrue;goto setfalse;が記述された。
--- a/src/cmd/gc/const.c
+++ b/src/cmd/gc/const.c
@@ -67,7 +67,7 @@ convlit(Node *n, Type *t)
 			break;
 		if(et == TINTER)
 			break;
-		return;
+		goto bad1;

 	case Wlitstr:
 		if(isnilinter(t)) {
@@ -212,6 +212,7 @@ evconst(Node *n)
 	case Wlitfloat:
 	case Wlitbool:
 	case Wlitstr:
+	case Wlitnil:
 		break;
 	}

@@ -228,6 +229,7 @@ evconst(Node *n)
 	case Wlitfloat:
 	case Wlitbool:
 	case Wlitstr:
+	case Wlitnil:
 		break;
 	}

@@ -246,7 +248,7 @@ evconst(Node *n)
 		\tnl->val.ctype = CTFLT;
 		\twl = whatis(nl);
 		} else {
-\t\t\tyyerror("illegal combination of literals %O %E, %E", n->op, wl, wr);
+\t\t\tyyerror("illegal combination of literals %O %W, %W", n->op, wl, wr);
 		\treturn;
 		}
 	}
@@ -264,7 +266,7 @@ evconst(Node *n)

 	switch(TUP(n->op, wl)) {
 	default:
-\t\tyyerror("illegal literal %O %E", n->op, wl);
+\t\tyyerror("illegal literal %O %W", n->op, wl);
 		\treturn;

 	case TUP(OADD, Wlitint):
@@ -312,6 +314,11 @@ evconst(Node *n)
 		\tmpdivfltflt(fval, nr->val.u.fval);\n
 		\tbreak;\n
 
+\tcase TUP(OEQ, Wlitnil):\n
+\t\tgoto settrue;\n
+\tcase TUP(ONE, Wlitnil):\n
+\t\tgoto setfalse;\n
+\n 	case TUP(OEQ, Wlitint):\n
 		\tif(mpcmpfixfix(xval, nr->val.u.xval) == 0)\n
 		\t\tgoto settrue;\n

コアとなるコードの解説

このコミットの核心は、Goコンパイラがnilリテラルを定数評価の過程でより正確に扱うようにした点です。

  • convlitgoto bad1: これは、リテラル変換が失敗した場合に、単に処理を中断するのではなく、適切なエラー処理フローに移行させるための変更です。これにより、コンパイラは変換エラーをより堅牢に報告できるようになります。

  • Wlitnilの導入とevconstでの処理: Wlitnilが定数評価の対象として明示的に追加されたことで、コンパイラはnilを他の定数と同様に扱えるようになりました。これにより、nilが関わる式(特に比較)の定数評価が可能になります。

  • yyerrorのフォーマット変更: これは、コンパイラが生成するエラーメッセージの品質向上に寄与します。%Wを使用することで、エラーの原因となっているリテラルの種類(例: Wlitnil)をより明確に表示できるようになり、デバッグが容易になります。

  • nil比較の明示的な定数評価: TUP(OEQ, Wlitnil)TUP(ONE, Wlitnil)のケースが追加されたことが、bug127の直接的な解決策です。これにより、コンパイラはnilとの等価性/不等価性比較を、コンパイル時に真偽値として解決できるようになりました。例えば、var p *int = nil; var i interface{} = p;というコードがあった場合、i == nilという比較は、この変更によってコンパイル時にfalseではなくtrueと正しく評価されるようになります(ただし、この例はコンパイル時定数ではないため、厳密には実行時の比較になりますが、コンパイラがnilのセマンティクスを正しく理解するための基盤となります)。この変更は、Go言語のnilとインターフェースのセマンティクスを、コンパイラの内部処理と一致させる上で非常に重要でした。

これらの変更により、Goコンパイラはnilリテラルを含む定数式、特にnilとの比較を、より正確かつ効率的に処理できるようになり、Go言語の初期の重要なバグの一つが修正されました。

関連リンク

参考にした情報源リンク

  • Go Issue 127: nil comparison with interface values: https://go.dev/issue/127
  • Go言語のインターフェースとnilについて: https://go.dev/blog/laws-of-reflection (Go言語のインターフェースの内部構造とnilの挙動について理解を深めるために参照)
  • Go言語のコンパイラに関する一般的な情報 (Goのソースコード構造、gcの役割など): https://go.dev/src/cmd/gc/ (Goコンパイラのソースコードの場所)
  • C言語のgoto文に関する一般的な情報 (C言語のコード理解のため)
  • printfフォーマット指定子に関する一般的な情報 (C言語のyyerrorのフォーマット指定子理解のため)
  • Go言語の定数評価に関する一般的な情報 (コンパイラの最適化手法としての定数評価の理解のため)