[インデックス 1432] ファイルの概要
このコミットは、Goコンパイラのsrc/cmd/gc/go.h
とsrc/cmd/gc/walk.c
ファイルに対する変更を含んでいます。具体的には、配列リテラル(特に[...]
形式でサイズが推論される配列)の処理におけるバグ修正と、それに伴う型システム関連のヘルパー関数の追加が行われています。
コミット
commit c458c9838863bd93e6124764c52a015d67804090
Author: Ken Thompson <ken@golang.org>
Date: Wed Jan 7 13:20:10 2009 -0800
[...] bug
R=r
OCL=22218
CL=22218
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c458c9838863bd93e6124764c52a015d67804090
元コミット内容
[...] bug
R=r
OCL=22218
CL=22218
変更の背景
このコミットは、Go言語の初期段階におけるコンパイラのバグ修正の一環として行われました。特に、Go言語の配列リテラルにおいて、要素数(長さ)をコンパイラに推論させる記法である[...]Type{...}
形式の配列が正しく処理されないバグが存在したと考えられます。
Go言語では、配列の長さは型の一部であり、コンパイル時に固定されます。[...]
記法は、プログラマが明示的に長さを指定する代わりに、初期化子の数からコンパイラが長さを決定することを許可します。この機能は便利ですが、コンパイラ内部での型の表現や、配列の初期化処理において、特定の条件下で誤った動作を引き起こす可能性がありました。
このバグは、おそらく配列リテラルの初期化時に、コンパイラが配列の実際の長さと初期化子の数を正しく比較・管理できていなかったことに起因すると推測されます。その結果、配列が適切にゼロクリアされなかったり、不正な長さで扱われたりする問題が発生していた可能性があります。
前提知識の解説
Go言語の配列とスライス
- 配列 (Array): Go言語の配列は、固定長で同じ型の要素のシーケンスです。配列の長さは型の一部であり、コンパイル時に決定されます。例えば、
[5]int
と[10]int
は異なる型として扱われます。 - スライス (Slice): スライスは、配列の一部を参照する動的なビューです。スライスは長さと容量を持ち、実行時に長さを変更できます。スライスは配列の上に構築されており、より柔軟なデータ構造として利用されます。
配列リテラルと[...]
記法
Go言語では、配列を初期化する際に配列リテラルを使用します。
例: var a [3]int = [3]int{1, 2, 3}
また、配列の長さをコンパイラに推論させるために[...]
記法を使用できます。
例: var a [...]int = [...]int{1, 2, 3}
(この場合、a
の型は[3]int
となる)
Goコンパイラの構造 (gc)
Goコンパイラ(gc
)は、Go言語のソースコードを機械語に変換するツールチェーンの主要部分です。コンパイラは複数のフェーズに分かれており、このコミットで変更されているsrc/cmd/gc/go.h
とsrc/cmd/gc/walk.c
は、それぞれコンパイラ内部のデータ構造の定義と、抽象構文木(AST)の走査("walking")および変換を担当する部分に関連しています。
src/cmd/gc/go.h
: コンパイラ内部で使用される型、構造体、定数、関数プロトタイプなどが定義されています。src/cmd/gc/walk.c
: ASTを走査し、最適化やコード生成のための変換を行う「ウォーカー」のロジックが含まれています。arraylit
関数は、配列リテラルを処理するウォーカーの一部です。
型のbound
とetype
Goコンパイラ内部では、型はType
構造体で表現されます。
t->etype
: 型の種類(例:TARRAY
は配列型、TSLICE
はスライス型など)を示します。t->bound
: 配列型の場合、その配列の長さを表すフィールドです。スライス型の場合、この値は負の値(通常は-1)になります。[...]
形式の配列リテラルの場合、初期段階では特別な負の値(このコミットでは-100
)が設定され、後で実際の長さに更新されます。
技術的詳細
このコミットの主要な変更点は、src/cmd/gc/walk.c
内のarraylit
関数における配列リテラルの処理ロジックの修正です。特に、[...]
形式の配列リテラル(コンパイラが長さを推論する配列)の扱いが改善されています。
arraylit
関数の役割
arraylit
関数は、Goソースコード中の配列リテラル(例: {1, 2, 3}
)をコンパイラが処理する際に呼び出されます。この関数は、リテラルから配列の型と値を構築し、必要に応じて初期化処理を行います。
変更点の詳細
-
shallow
関数の追加:src/cmd/gc/go.h
にType* shallow(Type*);
という新しい関数プロトタイプが追加されました。- この関数は、既存の型から「浅いコピー」を作成するために使用されると推測されます。特に、
[...]
配列のように、初期段階では長さが未定だが、後で確定するような型を扱う際に、元の型を変更せずに新しい型インスタンスを作成するために利用されます。
-
arraylit
関数のロジック変更:- 初期化子のカウント: 変更前は、配列の初期化子をカウントするロジックが、配列の長さが確定している場合とそうでない場合で重複していました。変更後は、
ninit
という変数で初期化子の数を最初に一度だけカウントするように統一されました。 [...]
配列の処理改善:- 変更前は、
b == -100
([...]
配列のフラグ)の場合に、初期化子をカウントし、その数でt->bound
を更新するロジックがarraylit
関数の後半にありました。 - 変更後は、初期化子をカウントした後、すぐに
b == -100
のチェックを行い、その場合はshallow(t)
を呼び出して新しい型を作成し、その型のbound
をninit
(初期化子の数)で更新するように変更されました。これにより、配列の長さが早期に確定し、後続の処理で正しい長さが使用されるようになります。
- 変更前は、
- スライスと配列の分岐の整理:
- 変更前は、
b < 0 && b != -100
でスライスを、b >= 0
で固定長配列を処理していました。 - 変更後は、
if(b < 0)
でスライスを処理し、else
ブロックで固定長配列(および[...]
配列で長さが確定したもの)を処理するように簡潔化されました。これにより、コードの可読性と保守性が向上しています。
- 変更前は、
- ゼロクリアロジックの修正:
- 配列全体が初期化されていない場合に配列をゼロクリアするロジックにおいて、変更前は
idx < b
(idx
は初期化子のカウント)で比較していましたが、変更後はninit < b
を使用するように修正されました。これは、ninit
が初期化子の正確な数を表すため、より正確な比較となります。
- 配列全体が初期化されていない場合に配列をゼロクリアするロジックにおいて、変更前は
- 冗長なコードの削除:
arraylit
関数の最後にあったif(b == -100)
のブロックが削除されました。これは、[...]
配列の長さ確定ロジックが関数の冒頭に移動したため、不要になったためです。
- 初期化子のカウント: 変更前は、配列の初期化子をカウントするロジックが、配列の長さが確定している場合とそうでない場合で重複していました。変更後は、
これらの変更により、[...]
形式の配列リテラルがコンパイラによって正しく解釈され、適切な長さで配列が生成されるようになりました。特に、配列の長さが初期化子の数によって決定されるケースにおいて、コンパイラが正確な型情報を持ち、それに従ってメモリを確保し、初期化を行うことが保証されます。
コアとなるコードの変更箇所
src/cmd/gc/go.h
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -836,3 +836,4 @@ void argspace(int32);
Node* nodarg(Type*, int);
void nodconst(Node*, Type*, vlong);
Type* deep(Type*);
+Type* shallow(Type*);
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -3556,38 +3556,44 @@ arraylit(Node *n)
Iter saver;
Type *t;
Node *var, *r, *a, *nnew;
- int idx, b;
+ int idx, ninit, b;
t = n->type;
if(t->etype != TARRAY)
fatal("arraylit: not array");
+ // count initializers
+ ninit = 0;
+ r = listfirst(&saver, &n->left);
+ if(r != N && r->op == OEMPTY)
+ r = N;
+ while(r != N) {
+ ninit++;
+ r = listnext(&saver);
+ }
+
+ b = t->bound;
+ if(b == -100) {
+ // flag for [...]
+ b = ninit;
+ t = shallow(t);
+ t->bound = b;
+ }
+
var = nod(OXXX, N, N);
tempname(var, t);
- b = t->bound;
- if(b < 0 && b != -100) {
+ if(b < 0) {
// slice
nnew = nod(OMAKE, N, N);
nnew->type = t;
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);
- }
+ } else {
// if entire array isnt initialized,
// then clear the array
- if(idx < b) {
+ if(ninit < b) {
a = nod(OAS, var, N);
addtop = list(addtop, a);
}
@@ -3606,11 +3612,6 @@ arraylit(Node *n)
idx++;
r = listnext(&saver);
}
- if(b == -100) {
- // compiler counted closed array
- b = idx;
- t->bound = b;
- }
if(b < 0)
nnew->left = nodintconst(idx);
return var;
コアとなるコードの解説
src/cmd/gc/go.h
の変更
Type* shallow(Type*);
の追加: この行は、Goコンパイラの内部型システムに新しい関数shallow
のプロトタイプを追加しています。この関数は、既存の型オブジェクトの「浅いコピー」を作成するために使用されると推測されます。Goの型システムでは、配列の長さは型の一部であるため、[...]
配列のように長さが後から確定するような場合、元の型を変更せずに新しい長さを持つ型を効率的に作成するためにこのような関数が必要になります。
src/cmd/gc/walk.c
のarraylit
関数の変更
-
初期化子のカウントの統一:
- int idx, b; + int idx, ninit, b;
ninit
という新しい変数が導入され、配列リテラルの初期化子の数をカウントするために使用されます。これにより、初期化子のカウントロジックが関数の冒頭に集約され、冗長性が排除されました。// count initializers ninit = 0; r = listfirst(&saver, &n->left); if(r != N && r->op == OEMPTY) r = N; while(r != N) { ninit++; r = listnext(&saver); }
このブロックは、配列リテラルの初期化子リストを走査し、
ninit
にその数を格納します。 -
[...]
配列の長さ確定ロジックの変更:- b = t->bound; - if(b < 0 && b != -100) { + b = t->bound; + if(b == -100) { + // flag for [...] + b = ninit; + t = shallow(t); + t->bound = b; + }
この変更は、
[...]
形式の配列リテラル(t->bound
が-100
でマークされている)の処理方法を根本的に改善しています。- 以前は、
b == -100
の処理が関数の後半にありました。 - 新しいコードでは、初期化子をカウントした直後にこの処理が行われます。
b = ninit;
で、配列の長さが初期化子の数に設定されます。t = shallow(t);
は、shallow
関数を呼び出して、新しい長さを持つ型オブジェクトを作成します。これにより、元の型オブジェクトが不変に保たれつつ、新しい長さが適用された型が後続の処理で使用されるようになります。t->bound = b;
で、新しい型オブジェクトのbound
フィールドが確定した長さに設定されます。
- 以前は、
-
スライスと配列の分岐の整理:
- if(b < 0 && b != -100) { + if(b < 0) { // slice nnew = nod(OMAKE, N, N); nnew->type = t; 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); - } + } else { // if entire array isnt initialized, // then clear the array - if(idx < b) { + if(ninit < b) { a = nod(OAS, var, N); addtop = list(addtop, a); }
この変更により、スライスと固定長配列(および長さが確定した
[...]
配列)の処理分岐がより明確になりました。if(b < 0)
:b
が負の値の場合、それはスライス型であることを意味します(-100
は既にshallow
で処理されているため、ここには到達しない)。スライスの作成(OMAKE
)と初期化が行われます。else
:b
が0以上の値の場合、それは固定長配列であることを意味します。このブロックでは、配列が完全に初期化されていない場合に、配列をゼロクリアする処理が行われます。
-
ゼロクリアロジックの修正:
- if(idx < b) { + if(ninit < b) { a = nod(OAS, var, N); addtop = list(addtop, a); }
配列のゼロクリア条件が
idx < b
からninit < b
に変更されました。ninit
は初期化子の正確な数を表すため、この変更により、配列のゼロクリアがより正確に行われるようになります。 -
冗長なコードの削除:
- if(b == -100) { - // compiler counted closed array - b = idx; - t->bound = b; - }
このブロックは、
[...]
配列の長さ確定ロジックが関数の冒頭に移動したため、不要になり削除されました。これにより、コードが簡潔になりました。
これらの変更は、Goコンパイラが[...]
形式の配列リテラルをより堅牢かつ正確に処理できるようにするための重要な改善です。特に、型の不変性を保ちつつ、動的に長さを確定させるためのshallow
関数の導入と、arraylit
関数内のロジックの整理がポイントです。
関連リンク
- Go言語の配列とスライスに関する公式ドキュメント: https://go.dev/tour/moretypes/6
- Go言語の配列リテラルに関する公式ドキュメント: https://go.dev/ref/spec#Array_literals
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc/
ディレクトリ): https://github.com/golang/go - Goコンパイラの内部構造に関する一般的な情報源 (Goの初期の設計に関する議論など)
- Go言語の型システムに関する議論 (GoのメーリングリストやIssueトラッカーなど)
- Go言語の
[...]
配列リテラルに関する情報 (Goのブログ記事やチュートリアルなど) I have generated the commit explanation and outputted it to standard output as requested.