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

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

このコミットは、Goコンパイラの内部処理において、リテラルで初期化される自動配列の挙動を修正するものです。具体的には、配列リテラルで全ての要素が明示的に初期化されない場合に、残りの要素がGoのゼロ値保証に従って適切にゼロクリアされるように変更されています。

コミット

commit 179af0bb19afad46471f08999c9f540d70e20834
Author: Ken Thompson <ken@golang.org>
Date:   Wed Jan 7 12:28:23 2009 -0800

    clear automatic arrays created with literals
    
    R=r
    OCL=22215
    CL=22215

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

https://github.com/golang/go/commit/179af0bb19afad46471f08999c9f540d70e20834

元コミット内容

このコミットは、Go言語のコンパイラ(gc)の一部であるsrc/cmd/gc/walk.cファイルに対する変更です。コミットメッセージは「clear automatic arrays created with literals」(リテラルで作成された自動配列をクリアする)と簡潔に述べられています。これは、配列リテラルを用いて配列が宣言・初期化される際に、明示的に値が与えられなかった要素が適切にゼロ値で埋められるようにする修正であることを示唆しています。

変更内容は以下の通りです。

  • oldarraylit関数がコメントアウトされ、実質的に削除されました。
  • arraylit関数が修正されました。
    • 配列の作成と初期化に関するロジックが変更されました。
    • 特に、配列リテラルで指定された初期化子の数が配列の宣言されたサイズよりも少ない場合に、配列全体をゼロクリアする処理が追加されました。
    • 以前存在した、リテラル初期化子が配列の境界を超える場合のチェック(yyerrorを呼び出す部分)が削除されました。

変更の背景

Go言語には「ゼロ値」の概念があります。これは、変数が宣言されたものの明示的に初期化されなかった場合、その型に応じたデフォルトのゼロ値(数値型なら0、文字列型なら""、ポインタやスライス、マップならnilなど)が自動的に割り当てられるという保証です。配列の場合も同様で、[5]int{1, 2}のように一部の要素のみが初期化された場合、残りの要素はintのゼロ値である0で埋められるべきです。

このコミットが行われた2009年1月は、Go言語がまだ公開される前の開発初期段階でした。当時のコンパイラには、配列リテラルで初期化される自動配列(スタック上に割り当てられる配列)において、明示的に初期化されなかった要素が確実にゼロ値になるという保証が欠けていた可能性があります。もしゼロ値保証が守られない場合、プログラムは未定義の動作を引き起こしたり、予期せぬ値を使用したりするリスクがありました。

このコミットは、Go言語の重要な設計原則である「ゼロ値保証」を、配列リテラルを用いた初期化のケースにおいても徹底するために導入されたと考えられます。これにより、開発者は配列の初期状態について常に予測可能な挙動を期待できるようになります。

前提知識の解説

このコミットの理解には、以下の技術的知識が役立ちます。

  1. Go言語のゼロ値 (Zero Value): Go言語の設計思想の根幹をなす概念の一つです。変数を宣言した際に明示的に初期化しなくても、その型に応じたデフォルト値(ゼロ値)が自動的に割り当てられます。例えば、var i intと宣言するとiは自動的に0に、var s stringと宣言するとsは自動的に""(空文字列)になります。配列の場合、var a [3]intと宣言するとa[0 0 0]となります。このゼロ値保証は、Goプログラムの安全性と予測可能性を高める上で非常に重要です。

  2. Goコンパイラの構造とフェーズ: Goコンパイラ(gc)は、ソースコードを機械語に変換する過程で複数のフェーズを経ます。

    • 字句解析 (Lexing): ソースコードをトークンに分解します。
    • 構文解析 (Parsing): トークンから抽象構文木 (AST: Abstract Syntax Tree) を構築します。
    • 型チェック (Type Checking): ASTの各ノードの型を検証し、型の一貫性を保証します。
    • ASTウォーク/最適化 (AST Walking/Optimization): ASTを走査し、高レベルなGoの構文をより低レベルな中間表現に変換したり、最適化を行ったりします。このコミットが関連するwalk.cファイルはこのフェーズの一部です。
    • コード生成 (Code Generation): 中間表現から最終的な機械語コードを生成します。
  3. 抽象構文木 (AST): コンパイラがソースコードを内部的に表現するためのツリー構造です。ソースコードの各要素(変数、関数呼び出し、演算子など)がノードとして表現され、それらの関係がツリーの枝で示されます。コンパイラはASTを走査(ウォーク)しながら、型チェックや最適化、コード生成を行います。

  4. C言語とコンパイラ開発: Goコンパイラの初期バージョンはC言語で書かれており、src/cmd/gc/walk.cもC言語のコードです。コンパイラ開発では、ASTノードを表す構造体(例: Node)、型情報を表す構造体(例: Type)、そしてそれらを操作する関数(例: nod, list)が頻繁に用いられます。

  5. 配列リテラル (Array Literal): Go言語で配列を初期化する構文の一つです。例えば、[3]int{1, 2, 3}はサイズ3のint型配列を1, 2, 3で初期化します。[...]int{1, 2, 3}のように...を使うと、初期化子の数から配列のサイズが自動的に推論されます。また、[5]int{1, 2}のように初期化子の数がサイズより少ない場合、残りの要素はゼロ値で初期化されます。

技術的詳細

このコミットの核心は、Goコンパイラのwalkフェーズにおける配列リテラルの処理ロジックの変更です。src/cmd/gc/walk.cファイルは、ASTを走査し、Goのソースコードで記述された高レベルな操作を、より低レベルなコンパイラ内部の操作(中間表現)に変換する役割を担っています。

変更の中心はarraylit関数です。この関数は、Goのソースコード中の配列リテラル(例: [...]int{...})を処理し、対応するコンパイラ内部の表現(ASTノードや命令リスト)を生成します。

修正前のarraylit関数(そしてコメントアウトされたoldarraylit関数)は、配列リテラルで指定された初期化子を個々の要素への代入として処理していました。しかし、配列の全ての要素がリテラルで初期化されない場合(例: [5]int{1, 2})、残りの要素が確実にゼロ値になるようにするための明示的な処理が不足していた可能性があります。

新しいarraylit関数では、以下の重要なロジックが追加されました。

if(b >= 0) { // b は配列の境界(サイズ)
    idx = 0;
    r = listfirst(&saver, &n->left);
    if(r != N && r->op == OEMPTY)
        r = N;
    while(r != N) {
        // count initializers
        idx++; // 初期化子の数をカウント
        r = listnext(&saver);
    }
    // if entire array isnt initialized,
    // then clear the array
    if(idx < b) { // 初期化子の数が配列のサイズより少ない場合
        a = nod(OAS, var, N); // 配列変数 (var) にゼロ値 (N) を代入するASTノードを作成
        addtop = list(addtop, a); // そのノードをコンパイラの命令リストに追加
    }
}

このコードブロックは、配列リテラルが処理される際に実行されます。

  1. b >= 0は、配列のサイズが確定していることを確認します(...でサイズが推論される場合も含む)。
  2. idxは、配列リテラルで明示的に指定された初期化子の数をカウントします。
  3. if(idx < b)の条件は、初期化子の数が配列の宣言されたサイズよりも少ない場合に真となります。
  4. この条件が真の場合、a = nod(OAS, var, N);という行が実行されます。
    • nodは新しいASTノードを作成するコンパイラ内部の関数です。
    • OASは「代入 (Assignment)」操作を表すオペコードです。
    • varは現在処理している配列リテラルに対応する配列変数のASTノードです。
    • Nは、このコンテキストではGoのゼロ値を表す特別なASTノード(またはNULLポインタ)を意味します。
    • したがって、この行は「var(配列全体)にゼロ値を代入する」という操作を表すASTノードを生成します。
  5. addtop = list(addtop, a);は、生成された代入ノードを、コンパイラが後で処理する命令のリスト(addtop)に追加します。これにより、コンパイル時に配列全体がゼロ値で初期化されるコードが生成されることが保証されます。

この変更により、Goのゼロ値保証が配列リテラルに対しても厳密に適用されるようになりました。例えば、var arr [5]int = [5]int{1, 2}というコードがあった場合、arr[0]arr[1]はそれぞれ12に初期化され、arr[2], arr[3], arr[4]は明示的に0に初期化されるようになります。

また、以前のコードにあった「literal array initializer out of bounds」(リテラル配列初期化子が境界外)というエラーチェックが削除されています。これは、この新しいゼロクリアロジックが導入されたことで、初期化子の数が配列のサイズを超えるような不正なケースは、このwalkフェーズよりも前のコンパイラフェーズ(例えば型チェックフェーズ)で既に捕捉されるようになったため、ここで再度チェックする必要がなくなったことを示唆しています。

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

src/cmd/gc/walk.cファイルにおけるarraylit関数の変更がコアです。

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -3499,63 +3499,63 @@ loop:
  	goto loop;
  }
  
-Node*
-oldarraylit(Node *n)
-{
-	Iter saver;
-	Type *t;
-	Node *var, *r, *a;
-	int idx;
-
-	t = n->type;
-	if(t->etype != TARRAY)
-		fatal("arraylit: not array");
-
-	if(t->bound < 0) {
-		// make a shallow copy
-		t = typ(0);
-		*t = *n->type;
-		n->type = t;
-
-		// make it a closed array
-		r = listfirst(&saver, &n->left);
-		if(r != N && r->op == OEMPTY)
-			r = N;
-		for(idx=0; r!=N; idx++)
-			r = listnext(&saver);
-		t->bound = idx;
-	}
-
-	var = nod(OXXX, N, N);
-	tempname(var, t);
-
-	idx = 0;
-	r = listfirst(&saver, &n->left);
-	if(r != N && r->op == OEMPTY)
-		r = N;
-
-loop:
-	if(r == N)
-		return var;
-
-	// build list of var[c] = expr
-
-	a = nodintconst(idx);
-	a = nod(OINDEX, var, a);
-	a = nod(OAS, a, r);
-	addtop = list(addtop, a);
-	idx++;
-
-	r = listnext(&saver);
-	goto loop;
-}
+//Node*
+//oldarraylit(Node *n)
+//{
+//	Iter saver;
+//	Type *t;
+//	Node *var, *r, *a;
+//	int idx;
+//
+//	t = n->type;
+//	if(t->etype != TARRAY)
+//		fatal("arraylit: not array");
+//
+//	if(t->bound < 0) {
+//		// make a shallow copy
+//		t = typ(0);
+//		*t = *n->type;
+//		n->type = t;
+//
+//		// make it a closed array
+//		r = listfirst(&saver, &n->left);
+//		if(r != N && r->op == OEMPTY)
+//			r = N;
+//		for(idx=0; r!=N; idx++)
+//			r = listnext(&saver);
+//		t->bound = idx;
+//	}
+//
+//	var = nod(OXXX, N, N);
+//	tempname(var, t);
+//
+//	idx = 0;
+//	r = listfirst(&saver, &n->left);
+//	if(r != N && r->op == OEMPTY)
+//		r = N;
+//
+//loop:
+//	if(r == N)
+//		return var;
+//
+//	// build list of var[c] = expr
+//
+//	a = nodintconst(idx);
+//	a = nod(OINDEX, var, a);
+//	a = nod(OAS, a, r);
+//	addtop = list(addtop, a);
+//	idx++;
+//
+//	r = listnext(&saver);
+//	goto loop;
+//}
  
  Node*
  arraylit(Node *n)
  {
  	Iter saver;
  	Type *t;
 -	Node *var, *r, *a, *nas, *nnew;\n+	Node *var, *r, *a, *nnew;
  	int idx, b;
  
  	t = n->type;
@@ -3571,8 +3571,26 @@ arraylit(Node *n)
  		nnew = nod(OMAKE, N, N);
  		nnew->type = t;
  
--	nas = nod(OAS, var, nnew);\n-	addtop = list(addtop, nas);\n+	a = nod(OAS, var, nnew);
+	addtop = list(addtop, a);
+	}
+
+	if(b >= 0) {
+		idx = 0;
+		r = listfirst(&saver, &n->left);
+		if(r != N && r->op == OEMPTY)
+			r = N;
+		while(r != N) {
+			// count initializers
+			idx++;
+			r = listnext(&saver);
+		}
+		// if entire array isnt initialized,
+		// then clear the array
+		if(idx < b) {
+			a = nod(OAS, var, N);
+			addtop = list(addtop, a);
+		}
  	}
  
  	idx = 0;
@@ -3581,10 +3599,6 @@ arraylit(Node *n)
  	r = N;
  	while(r != N) {
  		// build list of var[c] = expr
--	if(b >= 0 && idx >= b) {
--		yyerror("literal array initializer out of bounds");
--		break;
--	}
  		a = nodintconst(idx);
  		a = nod(OINDEX, var, a);
  		a = nod(OAS, a, r);

コアとなるコードの解説

変更の主要な部分は、arraylit関数内の以下のブロックです。

	if(b >= 0) {
		idx = 0;
		r = listfirst(&saver, &n->left);
		if(r != N && r->op == OEMPTY)
			r = N;
		while(r != N) {
			// count initializers
			idx++;
			r = listnext(&saver);
		}
		// if entire array isnt initialized,
		// then clear the array
		if(idx < b) {
			a = nod(OAS, var, N);
			addtop = list(addtop, a);
		}
	}

このコードは、配列リテラルが処理される際に、以下のステップを実行します。

  1. 配列サイズの確認: if(b >= 0)は、配列のサイズbが有効であることを確認します。bは配列の要素数を示します。
  2. 初期化子のカウント: while(r != N)ループ内で、配列リテラルで明示的に指定された初期化子の数idxをカウントします。rは現在の初期化子を表すASTノードです。
  3. ゼロクリアの条件判定: if(idx < b)は、カウントされた初期化子の数idxが、配列の宣言されたサイズbよりも少ないかどうかをチェックします。
  4. ゼロクリア処理の追加: もしidx < bが真(つまり、配列の全ての要素がリテラルで初期化されていない)であれば、以下の処理が行われます。
    • a = nod(OAS, var, N);: OASは代入操作を表すオペコードです。varは配列全体を表すASTノード、NはGoのゼロ値を表す特別なノードです。この行は、「配列変数var全体にゼロ値を代入する」という操作を表す新しいASTノードaを作成します。
    • addtop = list(addtop, a);: 作成された代入ノードaを、コンパイラが後でコード生成のために処理する命令リストaddtopに追加します。これにより、コンパイルされたプログラムが実行される際に、配列の未初期化部分が確実にゼロ値で埋められるようになります。

この変更により、Go言語の重要な特性である「ゼロ値保証」が、配列リテラルを用いた初期化のケースにおいても、コンパイラレベルで厳密に適用されるようになりました。これにより、Goプログラムの予測可能性と堅牢性が向上します。

また、以前のバージョンで存在した、配列リテラルの初期化子が配列の境界を超える場合にエラーを報告するyyerrorの呼び出しが削除されています。これは、この種のチェックがコンパイラのより早い段階(例えば型チェックフェーズ)で既に処理されるようになったため、このwalkフェーズでは不要になったことを示唆しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード(特にsrc/cmd/gc/walk.cおよび関連ファイル)
  • Go言語のゼロ値に関するブログ記事や解説
  • コンパイラ理論に関する一般的な知識