[インデックス 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言語の重要な設計原則である「ゼロ値保証」を、配列リテラルを用いた初期化のケースにおいても徹底するために導入されたと考えられます。これにより、開発者は配列の初期状態について常に予測可能な挙動を期待できるようになります。
前提知識の解説
このコミットの理解には、以下の技術的知識が役立ちます。
-
Go言語のゼロ値 (Zero Value): Go言語の設計思想の根幹をなす概念の一つです。変数を宣言した際に明示的に初期化しなくても、その型に応じたデフォルト値(ゼロ値)が自動的に割り当てられます。例えば、
var i int
と宣言するとi
は自動的に0
に、var s string
と宣言するとs
は自動的に""
(空文字列)になります。配列の場合、var a [3]int
と宣言するとa
は[0 0 0]
となります。このゼロ値保証は、Goプログラムの安全性と予測可能性を高める上で非常に重要です。 -
Goコンパイラの構造とフェーズ: Goコンパイラ(
gc
)は、ソースコードを機械語に変換する過程で複数のフェーズを経ます。- 字句解析 (Lexing): ソースコードをトークンに分解します。
- 構文解析 (Parsing): トークンから抽象構文木 (AST: Abstract Syntax Tree) を構築します。
- 型チェック (Type Checking): ASTの各ノードの型を検証し、型の一貫性を保証します。
- ASTウォーク/最適化 (AST Walking/Optimization): ASTを走査し、高レベルなGoの構文をより低レベルな中間表現に変換したり、最適化を行ったりします。このコミットが関連する
walk.c
ファイルはこのフェーズの一部です。 - コード生成 (Code Generation): 中間表現から最終的な機械語コードを生成します。
-
抽象構文木 (AST): コンパイラがソースコードを内部的に表現するためのツリー構造です。ソースコードの各要素(変数、関数呼び出し、演算子など)がノードとして表現され、それらの関係がツリーの枝で示されます。コンパイラはASTを走査(ウォーク)しながら、型チェックや最適化、コード生成を行います。
-
C言語とコンパイラ開発: Goコンパイラの初期バージョンはC言語で書かれており、
src/cmd/gc/walk.c
もC言語のコードです。コンパイラ開発では、ASTノードを表す構造体(例:Node
)、型情報を表す構造体(例:Type
)、そしてそれらを操作する関数(例:nod
,list
)が頻繁に用いられます。 -
配列リテラル (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); // そのノードをコンパイラの命令リストに追加
}
}
このコードブロックは、配列リテラルが処理される際に実行されます。
b >= 0
は、配列のサイズが確定していることを確認します(...
でサイズが推論される場合も含む)。idx
は、配列リテラルで明示的に指定された初期化子の数をカウントします。if(idx < b)
の条件は、初期化子の数が配列の宣言されたサイズよりも少ない場合に真となります。- この条件が真の場合、
a = nod(OAS, var, N);
という行が実行されます。nod
は新しいASTノードを作成するコンパイラ内部の関数です。OAS
は「代入 (Assignment)」操作を表すオペコードです。var
は現在処理している配列リテラルに対応する配列変数のASTノードです。N
は、このコンテキストではGoのゼロ値を表す特別なASTノード(またはNULLポインタ)を意味します。- したがって、この行は「
var
(配列全体)にゼロ値を代入する」という操作を表すASTノードを生成します。
addtop = list(addtop, a);
は、生成された代入ノードを、コンパイラが後で処理する命令のリスト(addtop
)に追加します。これにより、コンパイル時に配列全体がゼロ値で初期化されるコードが生成されることが保証されます。
この変更により、Goのゼロ値保証が配列リテラルに対しても厳密に適用されるようになりました。例えば、var arr [5]int = [5]int{1, 2}
というコードがあった場合、arr[0]
とarr[1]
はそれぞれ1
と2
に初期化され、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);
}
}
このコードは、配列リテラルが処理される際に、以下のステップを実行します。
- 配列サイズの確認:
if(b >= 0)
は、配列のサイズb
が有効であることを確認します。b
は配列の要素数を示します。 - 初期化子のカウント:
while(r != N)
ループ内で、配列リテラルで明示的に指定された初期化子の数idx
をカウントします。r
は現在の初期化子を表すASTノードです。 - ゼロクリアの条件判定:
if(idx < b)
は、カウントされた初期化子の数idx
が、配列の宣言されたサイズb
よりも少ないかどうかをチェックします。 - ゼロクリア処理の追加: もし
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言語のゼロ値: https://go.dev/blog/go-zero
- Goコンパイラの内部構造に関する一般的な情報(Goのバージョンによって詳細は異なりますが、基本的なフェーズは共通です):
- Goコンパイラのソースコード: https://github.com/golang/go/tree/master/src/cmd/compile
- Goコンパイラの設計に関するドキュメント(古いものも含まれますが、概念理解に役立ちます): https://go.dev/doc/articles/go_compiler.html
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード(特に
src/cmd/gc/walk.c
および関連ファイル) - Go言語のゼロ値に関するブログ記事や解説
- コンパイラ理論に関する一般的な知識