[インデックス 13170] ファイルの概要
このコミットは、Go言語のコンパイラ(cmd/gc
)におけるrange
ループの並行代入のバグ修正に関するものです。具体的には、for expr1, expr2 = range slice
のような形式のrange
ループにおいて、expr1
とexpr2
への値の代入が逐次的に行われていた問題を、本来あるべき並行代入に修正しています。この修正は、for i, x[i] = range slice
のように、ループ変数とインデックス付きの要素が同時に更新されるようなケースで特に重要となります。
コミット
commit 51072eb1fb2c380284cd0f87e61d1589201c3eea
Author: Russ Cox <rsc@golang.org>
Date: Thu May 24 23:05:36 2012 -0400
cmd/gc: fix parallel assignment in range
for expr1, expr2 = range slice
was assigning to expr1 and expr2 in sequence
instead of in parallel. Now it assigns in parallel,
as it should. This matters for things like
for i, x[i] = range slice.
Fixes #3464.
R=ken2
CC=golang-dev
https://golang.org/cl/6252048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/51072eb1fb2c380284cd0f87e61d1589201c3eea
元コミット内容
cmd/gc: fix parallel assignment in range
for expr1, expr2 = range slice
was assigning to expr1 and expr2 in sequence
instead of in parallel. Now it assigns in parallel,
as it should. This matters for things like
for i, x[i] = range slice.
Fixes #3464.
変更の背景
Go言語のfor...range
ループは、スライス、配列、文字列、マップ、チャネルなどのコレクションをイテレートするための強力な構文です。このループは、イテレーションごとに2つの値を返すことができます。例えば、スライスの場合、最初の値はインデックス、2番目の値はそのインデックスに対応する要素です。
このコミットが修正している問題は、for index, value = range collection
のような形式で複数の変数に値を代入する際に、コンパイラがその代入を「並行」ではなく「逐次」として処理していたことに起因します。並行代入とは、代入の右辺の式がすべて評価されてから、その結果が左辺の変数に同時に代入されることを意味します。一方、逐次代入では、左辺の変数が一つずつ順番に更新されます。
この違いは、特にfor i, x[i] = range y
のようなケースで顕著な問題を引き起こします。もし代入が逐次的に行われると、i
が更新された直後にx[i]
が評価されるため、x[i]
のi
が意図しない新しい値になってしまう可能性があります。本来のrange
ループのセマンティクスでは、イテレーションの開始時点でのインデックスと値が同時に取得され、それらが変数に並行して代入されるべきです。
このバグはGo言語のIssue #3464として報告されており、このコミットはその修正として作成されました。
前提知識の解説
Go言語のfor...range
ループ
Go言語のfor...range
ループは、コレクションの要素をイテレートするための構文です。
基本的な形式は以下の通りです。
for index, value := range collection {
// index と value を使った処理
}
- スライスと配列:
index
は要素のインデックス、value
はそのインデックスの要素のコピーです。 - 文字列:
index
はUnicodeコードポイントの開始バイトオフセット、value
は対応するルーン(Unicodeコードポイント)です。 - マップ:
index
はキー、value
は値のコピーです。マップのイテレーション順序は保証されません。 - チャネル:
value
はチャネルから受信した値です。チャネルが閉じられるまでループは続きます。
並行代入 (Parallel Assignment)
Go言語には、複数の変数に同時に値を代入する「並行代入」の機能があります。
a, b = b, a // aとbの値を交換
この例では、右辺のb
とa
がまず評価され、その結果が左辺のa
とb
に同時に代入されます。これにより、一時変数を使わずに値を交換することができます。並行代入は、複数の関数戻り値を受け取る際や、複数の変数を初期化する際にも使用されます。
for...range
ループにおけるindex, value := range collection
も、この並行代入のセマンティクスに従うべきです。つまり、イテレーションごとに生成されるインデックスと値のペアが、同時にindex
とvalue
変数に代入される必要があります。
Goコンパイラの構造(cmd/gc
)
Goコンパイラは、Go言語のソースコードを機械語に変換するツールチェーンの一部です。cmd/gc
は、Go言語のフロントエンドコンパイラであり、構文解析、型チェック、中間コード生成、最適化などの主要な処理を担当します。
- AST (Abstract Syntax Tree): ソースコードはまず抽象構文木に変換されます。
- Walk (AST Traversal): ASTは様々なフェーズで走査(walk)され、意味解析や変換が行われます。
range.c
やsubr.c
のようなファイルは、このASTの走査と変換に関連する処理を実装しています。 - Nodes: コンパイラ内部では、ASTの各要素は
Node
構造体として表現されます。nod(OP, ...)
のような関数は、特定の操作(OP
)を表す新しいノードを作成するために使用されます。
技術的詳細
このコミットは、Goコンパイラのcmd/gc
ディレクトリ内のrange.c
とsubr.c
の2つのファイルに修正を加えています。
range.c
の変更
range.c
は、for...range
ループのコンパイル処理を担当するファイルです。
修正前のコードでは、for expr1, expr2 = range slice
のような2変数形式のrange
ループにおいて、expr1
とexpr2
への代入が以下のように逐次的に行われていました。
// 修正前 (概念的な表現)
body = list1(nod(OAS, v1, hv1)); // v1 = hv1 (インデックスの代入)
if(v2) {
body = list(body, nod(OAS, v2, nod(OIND, hp, N))); // v2 = *hp (値の代入)
}
ここで、v1
はインデックス変数、v2
は値変数、hv1
は現在のインデックス、hp
は現在の要素へのポインタ(または値)を表す一時変数です。この逐次的な代入では、v1
が更新された後、v2
の評価が行われるため、v2
の評価式にv1
が含まれる場合(例: x[i]
のi
)、意図しない結果を招く可能性がありました。
修正後のコードでは、この代入を明示的に並行代入として扱うように変更されています。
// 修正後 (概念的な表現)
if(v2 == N) // v2がない場合(1変数形式)
body = list1(nod(OAS, v1, hv1));
else { // v2がある場合(2変数形式)
a = nod(OAS2, N, N); // OAS2は並行代入を表すノード
a->list = list(list1(v1), v2); // 左辺: v1, v2
a->rlist = list(list1(hv1), nod(OIND, hp, N)); // 右辺: hv1, *hp
body = list1(a);
}
OAS2
はGoコンパイラ内部で「2つの値の代入」(つまり並行代入)を表すオペレーションコードです。この変更により、hv1
とnod(OIND, hp, N)
(現在の要素の値)がまず評価され、その結果が同時にv1
とv2
に代入されるようになります。これにより、for i, x[i] = range y
のようなケースで、x[i]
の評価がi
の古い値に基づいて行われることが保証されます。
subr.c
の変更
subr.c
は、コンパイラのサブルーチンやユーティリティ関数を含むファイルです。
このコミットでは、safeexpr
関数に以下の変更が加えられています。
// 修正後
if(n->ninit) {
walkstmtlist(n->ninit);
*init = concat(*init, n->ninit);
n->ninit = nil;
}
safeexpr
関数は、式が副作用を持たないか、または安全に評価できるかを判断し、必要に応じて初期化ステートメントを抽出する役割を担っています。この追加されたコードは、ノードn
が初期化ステートメント(n->ninit
)を持っている場合、それらを処理(walkstmtlist
)し、現在の初期化リスト(*init
)に連結(concat
)しています。そして、ノード自身の初期化リストをクリア(n->ninit = nil
)しています。
この変更は、range.c
でのOAS2
ノードの導入と関連している可能性があります。並行代入の右辺の式が評価される際に、その式がさらに初期化を必要とするような複雑な式である場合、その初期化ステートメントが適切に処理されるようにするためのものです。これにより、コンパイラが生成するコードの正確性と安全性が向上します。
コアとなるコードの変更箇所
src/cmd/gc/range.c
--- a/src/cmd/gc/range.c
+++ b/src/cmd/gc/range.c
@@ -152,9 +152,14 @@ walkrange(Node *n)
n->ntest = nod(OLT, hv1, hn);
n->nincr = nod(OASOP, hv1, nodintconst(1));
n->nincr->etype = OADD;
- body = list1(nod(OAS, v1, hv1));
- if(v2) {
- body = list(body, nod(OAS, v2, nod(OIND, hp, N)));
+ if(v2 == N)
+ body = list1(nod(OAS, v1, hv1));
+ else {
+ a = nod(OAS2, N, N);
+ a->list = list(list1(v1), v2);
+ a->rlist = list(list1(hv1), nod(OIND, hp, N));
+ body = list1(a);
+
tmp = nod(OADD, hp, nodintconst(t->type->width));
tmp->type = hp->type;
tmp->typecheck = 1;
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1950,6 +1950,12 @@ safeexpr(Node *n, NodeList **init)
if(n == N)
return N;
+ if(n->ninit) {
+ walkstmtlist(n->ninit);
+ *init = concat(*init, n->ninit);
+ n->ninit = nil;
+ }
+
switch(n->op) {
case ONAME:
case OLITERAL:
test/range.go
--- a/test/range.go
+++ b/test/range.go
@@ -58,6 +58,17 @@ func testslice() {
println("wrong sum ranging over makeslice")
panic("fail")
}
+
+ x := []int{10, 20}
+ y := []int{99}
+ i := 1
+ for i, x[i] = range y {
+ break
+ }
+ if i != 0 || x[0] != 10 || x[1] != 99 {
+ println("wrong parallel assignment", i, x[0], x[1])
+ panic("fail")
+ }
}
func testslice1() {
コアとなるコードの解説
src/cmd/gc/range.c
の変更点
このファイルの変更は、for...range
ループの2変数形式(for v1, v2 = range ...
)における代入処理の核心です。
-
修正前:
body = list1(nod(OAS, v1, hv1));
でまずインデックス変数v1
にhv1
(現在のインデックス) を代入します。- その後に
if(v2)
ブロック内でbody = list(body, nod(OAS, v2, nod(OIND, hp, N)));
で値変数v2
に*hp
(現在の要素の値) を代入します。 - これは明らかに逐次的な代入であり、
v2
の評価がv1
の新しい値に依存してしまう可能性がありました。
-
修正後:
if(v2 == N)
: これは1変数形式(for v1 = range ...
)の場合で、以前と同様にv1
への代入を行います。else
: これは2変数形式(for v1, v2 = range ...
)の場合です。a = nod(OAS2, N, N);
:OAS2
オペレーションコードを持つ新しいノードa
を作成します。OAS2
はGoコンパイラが並行代入を表現するために使用する内部的なノードタイプです。a->list = list(list1(v1), v2);
:a
の左辺(代入先)のリストを設定します。ここにはv1
とv2
が含まれます。a->rlist = list(list1(hv1), nod(OIND, hp, N));
:a
の右辺(代入元)のリストを設定します。ここにはhv1
(インデックス) とnod(OIND, hp, N)
(値) が含まれます。body = list1(a);
: ループ本体のステートメントリストに、この並行代入ノードa
を追加します。
この変更により、コンパイラはrange
ループのイテレーションごとに、インデックスと値のペアを並行して(同時に)変数に代入するようになります。これにより、for i, x[i] = range y
のようなコードが期待通りに動作し、x[i]
の評価がそのイテレーションの開始時点でのi
の値に基づいて行われることが保証されます。
src/cmd/gc/subr.c
の変更点
safeexpr
関数は、コンパイラが式を評価する際に、その式が副作用を持つかどうか、または安全に評価できるかどうかを判断するために使用されます。この関数は、式が評価される前に実行されるべき初期化ステートメントを抽出する役割も持っています。
- 追加されたコード:
このコードブロックは、if(n->ninit) { walkstmtlist(n->ninit); *init = concat(*init, n->ninit); n->ninit = nil; }
safeexpr
が処理している現在のノードn
が、それ自身に関連付けられた初期化ステートメントのリスト (n->ninit
) を持っている場合に実行されます。walkstmtlist(n->ninit);
:n->ninit
内の各ステートメントを再帰的に走査し、必要に応じて変換や最適化を行います。*init = concat(*init, n->ninit);
:n->ninit
に含まれる初期化ステートメントを、safeexpr
の呼び出し元に渡された全体の初期化リスト (*init
) に連結します。これにより、これらの初期化ステートメントが、式が評価される前に実行されるべきコードとして適切に収集されます。n->ninit = nil;
: 処理が完了したため、ノードn
から初期化ステートメントのリストをクリアします。
この変更は、range.c
で導入されたOAS2
ノードのような、より複雑な式や構造がコンパイラによって生成されるようになったことと関連しています。並行代入の右辺の式が、さらに内部的な初期化を必要とするような場合(例えば、関数呼び出しや複雑な式)、safeexpr
がそれらの初期化を正しく抽出し、コンパイルされたコードに含めることを保証します。これにより、コンパイラが生成するコードの健全性が保たれます。
test/range.go
の追加テストケース
このテストケースは、修正された並行代入の動作を検証するために追加されました。
x := []int{10, 20}
y := []int{99}
i := 1
for i, x[i] = range y {
break
}
if i != 0 || x[0] != 10 || x[1] != 99 {
println("wrong parallel assignment", i, x[0], x[1])
panic("fail")
}
- 初期状態:
x = [10, 20]
,y = [99]
,i = 1
for i, x[i] = range y
ループが実行されます。y
は1つの要素99
を持ちます。- イテレーションが開始される際、
y
の最初の要素のインデックス0
と値99
が取得されます。 - 並行代入が正しく機能する場合:
i
に0
が代入されます。x[i]
(この時点でのi
はループ開始前の1
です) に99
が代入されます。つまりx[1]
が99
になります。
- ループは
break
で即座に終了します。 - 期待される結果:
i
は0
になり、x
は[10, 99]
になります。 if i != 0 || x[0] != 10 || x[1] != 99
の条件が、この期待される結果と一致するかを検証しています。もし並行代入が機能せず、i
が先に更新されてからx[i]
が評価されると、x[0]
が99
になってしまうなどの誤った結果になる可能性があります。
このテストケースは、range
ループにおける並行代入の修正が正しく機能していることを明確に示しています。
関連リンク
- Go Issue #3464: https://github.com/golang/go/issues/3464
- Gerrit Change-Id:
6252048
(Goのコードレビューシステムにおける変更セットのID)
参考にした情報源リンク
- Go言語の公式ドキュメント:
for
ステートメント (特にfor...range
セクション) - Goコンパイラのソースコード (特に
src/cmd/gc/
ディレクトリ内のファイル) - Go言語のIssueトラッカー (GitHub Issues)
- Go言語のGerritコードレビューシステム
- 並行代入に関する一般的なプログラミングの概念
- コンパイラの内部構造とAST (抽象構文木) の処理に関する一般的な知識
[インデックス 13170] ファイルの概要
このコミットは、Go言語のコンパイラ(cmd/gc
)におけるrange
ループの並行代入のバグ修正に関するものです。具体的には、for expr1, expr2 = range slice
のような形式のrange
ループにおいて、expr1
とexpr2
への値の代入が逐次的に行われていた問題を、本来あるべき並行代入に修正しています。この修正は、for i, x[i] = range slice
のように、ループ変数とインデックス付きの要素が同時に更新されるようなケースで特に重要となります。
コミット
commit 51072eb1fb2c380284cd0f87e61d1589201c3eea
Author: Russ Cox <rsc@golang.org>
Date: Thu May 24 23:05:36 2012 -0400
cmd/gc: fix parallel assignment in range
for expr1, expr2 = range slice
was assigning to expr1 and expr2 in sequence
instead of in parallel. Now it assigns in parallel,
as it should. This matters for things like
for i, x[i] = range slice.
Fixes #3464.
R=ken2
CC=golang-dev
https://golang.org/cl/6252048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/51072eb1fb2c380284cd0f87e61d1589201c3eea
元コミット内容
cmd/gc: fix parallel assignment in range
for expr1, expr2 = range slice
was assigning to expr1 and expr2 in sequence
instead of in parallel. Now it assigns in parallel,
as it should. This matters for things like
for i, x[i] = range slice.
Fixes #3464.
変更の背景
Go言語のfor...range
ループは、スライス、配列、文字列、マップ、チャネルなどのコレクションをイテレートするための強力な構文です。このループは、イテレーションごとに2つの値を返すことができます。例えば、スライスの場合、最初の値はインデックス、2番目の値はそのインデックスに対応する要素です。
このコミットが修正している問題は、for index, value = range collection
のような形式で複数の変数に値を代入する際に、コンパイラがその代入を「並行」ではなく「逐次」として処理していたことに起因します。並行代入とは、代入の右辺の式がすべて評価されてから、その結果が左辺の変数に同時に代入されることを意味します。一方、逐次代入では、左辺の変数が一つずつ順番に更新されます。
この違いは、特にfor i, x[i] = range y
のようなケースで顕著な問題を引き起こします。もし代入が逐次的に行われると、i
が更新された直後にx[i]
が評価されるため、x[i]
のi
が意図しない新しい値になってしまう可能性があります。本来のrange
ループのセマンティクスでは、イテレーションの開始時点でのインデックスと値が同時に取得され、それらが変数に並行して代入されるべきです。
このバグはGo言語のIssue #3464として報告されており、このコミットはその修正として作成されました。
前提知識の解説
Go言語のfor...range
ループ
Go言語のfor...range
ループは、コレクションの要素をイテレートするための構文です。
基本的な形式は以下の通りです。
for index, value := range collection {
// index と value を使った処理
}
- スライスと配列:
index
は要素のインデックス、value
はそのインデックスの要素のコピーです。 - 文字列:
index
はUnicodeコードポイントの開始バイトオフセット、value
は対応するルーン(Unicodeコードポイント)です。 - マップ:
index
はキー、value
は値のコピーです。マップのイテレーション順序は保証されません。 - チャネル:
value
はチャネルから受信した値です。チャネルが閉じられるまでループは続きます。
並行代入 (Parallel Assignment)
Go言語には、複数の変数に同時に値を代入する「並行代入」の機能があります。
a, b = b, a // aとbの値を交換
この例では、右辺のb
とa
がまず評価され、その結果が左辺のa
とb
に同時に代入されます。これにより、一時変数を使わずに値を交換することができます。並行代入は、複数の関数戻り値を受け取る際や、複数の変数を初期化する際にも使用されます。
for...range
ループにおけるindex, value := range collection
も、この並行代入のセマンティクスに従うべきです。つまり、イテレーションごとに生成されるインデックスと値のペアが、同時にindex
とvalue
変数に代入される必要があります。
Goコンパイラの構造(cmd/gc
)
Goコンパイラは、Go言語のソースコードを機械語に変換するツールチェーンの一部です。cmd/gc
は、Go言語のフロントエンドコンパイラであり、構文解析、型チェック、中間コード生成、最適化などの主要な処理を担当します。
- AST (Abstract Syntax Tree): ソースコードはまず抽象構文木に変換されます。
- Walk (AST Traversal): ASTは様々なフェーズで走査(walk)され、意味解析や変換が行われます。
range.c
やsubr.c
のようなファイルは、このASTの走査と変換に関連する処理を実装しています。 - Nodes: コンパイラ内部では、ASTの各要素は
Node
構造体として表現されます。nod(OP, ...)
のような関数は、特定の操作(OP
)を表す新しいノードを作成するために使用されます。
技術的詳細
このコミットは、Goコンパイラのcmd/gc
ディレクトリ内のrange.c
とsubr.c
の2つのファイルに修正を加えています。
range.c
の変更
range.c
は、for...range
ループのコンパイル処理を担当するファイルです。
修正前のコードでは、for expr1, expr2 = range slice
のような2変数形式のrange
ループにおいて、expr1
とexpr2
への代入が以下のように逐次的に行われていました。
// 修正前 (概念的な表現)
body = list1(nod(OAS, v1, hv1)); // v1 = hv1 (インデックスの代入)
if(v2) {
body = list(body, nod(OAS, v2, nod(OIND, hp, N))); // v2 = *hp (値の代入)
}
ここで、v1
はインデックス変数、v2
は値変数、hv1
は現在のインデックス、hp
は現在の要素へのポインタ(または値)を表す一時変数です。この逐次的な代入では、v1
が更新された後、v2
の評価が行われるため、v2
の評価式にv1
が含まれる場合(例: x[i]
のi
)、意図しない結果を招く可能性がありました。
修正後のコードでは、この代入を明示的に並行代入として扱うように変更されています。
// 修正後 (概念的な表現)
if(v2 == N) // v2がない場合(1変数形式)
body = list1(nod(OAS, v1, hv1));
else { // v2がある場合(2変数形式)
a = nod(OAS2, N, N); // OAS2は並行代入を表すノード
a->list = list(list1(v1), v2); // 左辺: v1, v2
a->rlist = list(list1(hv1), nod(OIND, hp, N)); // 右辺: hv1, *hp
body = list1(a);
}
OAS2
はGoコンパイラ内部で「2つの値の代入」(つまり並行代入)を表すオペレーションコードです。この変更により、hv1
とnod(OIND, hp, N)
(現在の要素の値)がまず評価され、その結果が同時にv1
とv2
に代入されるようになります。これにより、for i, x[i] = range y
のようなケースで、x[i]
の評価がi
の古い値に基づいて行われることが保証されます。
subr.c
の変更
subr.c
は、コンパイラのサブルーチンやユーティリティ関数を含むファイルです。
このコミットでは、safeexpr
関数に以下の変更が加えられています。
// 修正後
if(n->ninit) {
walkstmtlist(n->ninit);
*init = concat(*init, n->ninit);
n->ninit = nil;
}
safeexpr
関数は、式が副作用を持たないか、または安全に評価できるかを判断し、必要に応じて初期化ステートメントを抽出する役割を担っています。この追加されたコードは、ノードn
が初期化ステートメント(n->ninit
)を持っている場合、それらを処理(walkstmtlist
)し、現在の初期化リスト(*init
)に連結(concat
)しています。そして、ノード自身の初期化リストをクリア(n->ninit = nil
)しています。
この変更は、range.c
でのOAS2
ノードの導入と関連している可能性があります。並行代入の右辺の式が評価される際に、その式がさらに初期化を必要とするような複雑な式である場合、その初期化ステートメントが適切に処理されるようにするためのものです。これにより、コンパイラが生成するコードの正確性と安全性が向上します。
コアとなるコードの変更箇所
src/cmd/gc/range.c
--- a/src/cmd/gc/range.c
+++ b/src/cmd/gc/range.c
@@ -152,9 +152,14 @@ walkrange(Node *n)
n->ntest = nod(OLT, hv1, hn);
n->nincr = nod(OASOP, hv1, nodintconst(1));
n->nincr->etype = OADD;
- body = list1(nod(OAS, v1, hv1));
- if(v2) {
- body = list(body, nod(OAS, v2, nod(OIND, hp, N)));
+ if(v2 == N)
+ body = list1(nod(OAS, v1, hv1));
+ else {
+ a = nod(OAS2, N, N);
+ a->list = list(list1(v1), v2);
+ a->rlist = list(list1(hv1), nod(OIND, hp, N));
+ body = list1(a);
+
tmp = nod(OADD, hp, nodintconst(t->type->width));
tmp->type = hp->type;
tmp->typecheck = 1;
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1950,6 +1950,12 @@ safeexpr(Node *n, NodeList **init)
if(n == N)
return N;
+ if(n->ninit) {
+ walkstmtlist(n->ninit);
+ *init = concat(*init, n->ninit);
+ n->ninit = nil;
+ }
+
switch(n->op) {
case ONAME:
case OLITERAL:
test/range.go
--- a/test/range.go
+++ b/test/range.go
@@ -58,6 +58,17 @@ func testslice() {
println("wrong sum ranging over makeslice")
panic("fail")
}
+
+ x := []int{10, 20}
+ y := []int{99}
+ i := 1
+ for i, x[i] = range y {
+ break
+ }
+ if i != 0 || x[0] != 10 || x[1] != 99 {
+ println("wrong parallel assignment", i, x[0], x[1])
+ panic("fail")
+ }
}
func testslice1() {
コアとなるコードの解説
src/cmd/gc/range.c
の変更点
このファイルの変更は、for...range
ループの2変数形式(for v1, v2 = range ...
)における代入処理の核心です。
-
修正前:
body = list1(nod(OAS, v1, hv1));
でまずインデックス変数v1
にhv1
(現在のインデックス) を代入します。- その後に
if(v2)
ブロック内でbody = list(body, nod(OAS, v2, nod(OIND, hp, N)));
で値変数v2
に*hp
(現在の要素の値) を代入します。 - これは明らかに逐次的な代入であり、
v2
の評価がv1
の新しい値に依存してしまう可能性がありました。
-
修正後:
if(v2 == N)
: これは1変数形式(for v1 = range ...
)の場合で、以前と同様にv1
への代入を行います。else
: これは2変数形式(for v1, v2 = range ...
)の場合です。a = nod(OAS2, N, N);
:OAS2
オペレーションコードを持つ新しいノードa
を作成します。OAS2
はGoコンパイラが並行代入を表現するために使用する内部的なノードタイプです。a->list = list(list1(v1), v2);
:a
の左辺(代入先)のリストを設定します。ここにはv1
とv2
が含まれます。a->rlist = list(list1(hv1), nod(OIND, hp, N));
:a
の右辺(代入元)のリストを設定します。ここにはhv1
(インデックス) とnod(OIND, hp, N)
(値) が含まれます。body = list1(a);
: ループ本体のステートメントリストに、この並行代入ノードa
を追加します。
この変更により、コンパイラはrange
ループのイテレーションごとに、インデックスと値のペアを並行して(同時に)変数に代入するようになります。これにより、for i, x[i] = range y
のようなコードが期待通りに動作し、x[i]
の評価がそのイテレーションの開始時点でのi
の値に基づいて行われることが保証されます。
src/cmd/gc/subr.c
の変更点
safeexpr
関数は、コンパイラが式を評価する際に、その式が副作用を持つかどうか、または安全に評価できるかを判断するために使用されます。この関数は、式が評価される前に実行されるべき初期化ステートメントを抽出する役割も持っています。
- 追加されたコード:
このコードブロックは、if(n->ninit) { walkstmtlist(n->ninit); *init = concat(*init, n->ninit); n->ninit = nil; }
safeexpr
が処理している現在のノードn
が、それ自身に関連付けられた初期化ステートメントのリスト (n->ninit
) を持っている場合に実行されます。walkstmtlist(n->ninit);
:n->ninit
内の各ステートメントを再帰的に走査し、必要に応じて変換や最適化を行います。*init = concat(*init, n->ninit);
:n->ninit
に含まれる初期化ステートメントを、safeexpr
の呼び出し元に渡された全体の初期化リスト (*init
) に連結します。これにより、これらの初期化ステートメントが、式が評価される前に実行されるべきコードとして適切に収集されます。n->ninit = nil;
: 処理が完了したため、ノードn
から初期化ステートメントのリストをクリアします。
この変更は、range.c
で導入されたOAS2
ノードのような、より複雑な式や構造がコンパイラによって生成されるようになったことと関連しています。並行代入の右辺の式が、さらに内部的な初期化を必要とするような場合(例えば、関数呼び出しや複雑な式)、safeexpr
がそれらの初期化を正しく抽出し、コンパイルされたコードに含めることを保証します。これにより、コンパイラが生成するコードの健全性が保たれます。
test/range.go
の追加テストケース
このテストケースは、修正された並行代入の動作を検証するために追加されました。
x := []int{10, 20}
y := []int{99}
i := 1
for i, x[i] = range y {
break
}
if i != 0 || x[0] != 10 || x[1] != 99 {
println("wrong parallel assignment", i, x[0], x[1])
panic("fail")
}
- 初期状態:
x = [10, 20]
,y = [99]
,i = 1
for i, x[i] = range y
ループが実行されます。y
は1つの要素99
を持ちます。- イテレーションが開始される際、
y
の最初の要素のインデックス0
と値99
が取得されます。 - 並行代入が正しく機能する場合:
i
に0
が代入されます。x[i]
(この時点でのi
はループ開始前の1
です) に99
が代入されます。つまりx[1]
が99
になります。
- ループは
break
で即座に終了します。 - 期待される結果:
i
は0
になり、x
は[10, 99]
になります。 if i != 0 || x[0] != 10 || x[1] != 99
の条件が、この期待される結果と一致するかを検証しています。もし並行代入が機能せず、i
が先に更新されてからx[i]
が評価されると、x[0]
が99
になってしまうなどの誤った結果になる可能性があります。
このテストケースは、range
ループにおける並行代入の修正が正しく機能していることを明確に示しています。
関連リンク
- Go Issue #3464: https://github.com/golang/go/issues/3464
- Gerrit Change-Id:
6252048
(Goのコードレビューシステムにおける変更セットのID)
参考にした情報源リンク
- Go言語の公式ドキュメント:
for
ステートメント (特にfor...range
セクション) - Goコンパイラのソースコード (特に
src/cmd/gc/
ディレクトリ内のファイル) - Go言語のIssueトラッカー (GitHub Issues)
- Go言語のGerritコードレビューシステム
- 並行代入に関する一般的なプログラミングの概念
- コンパイラの内部構造とAST (抽象構文木) の処理に関する一般的な知識