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

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

このコミットは、Go言語のコンパイラ(cmd/gc)におけるrangeループの並行代入のバグ修正に関するものです。具体的には、for expr1, expr2 = range sliceのような形式のrangeループにおいて、expr1expr2への値の代入が逐次的に行われていた問題を、本来あるべき並行代入に修正しています。この修正は、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の値を交換

この例では、右辺のbaがまず評価され、その結果が左辺のabに同時に代入されます。これにより、一時変数を使わずに値を交換することができます。並行代入は、複数の関数戻り値を受け取る際や、複数の変数を初期化する際にも使用されます。

for...rangeループにおけるindex, value := range collectionも、この並行代入のセマンティクスに従うべきです。つまり、イテレーションごとに生成されるインデックスと値のペアが、同時にindexvalue変数に代入される必要があります。

Goコンパイラの構造(cmd/gc

Goコンパイラは、Go言語のソースコードを機械語に変換するツールチェーンの一部です。cmd/gcは、Go言語のフロントエンドコンパイラであり、構文解析、型チェック、中間コード生成、最適化などの主要な処理を担当します。

  • AST (Abstract Syntax Tree): ソースコードはまず抽象構文木に変換されます。
  • Walk (AST Traversal): ASTは様々なフェーズで走査(walk)され、意味解析や変換が行われます。range.csubr.cのようなファイルは、このASTの走査と変換に関連する処理を実装しています。
  • Nodes: コンパイラ内部では、ASTの各要素はNode構造体として表現されます。nod(OP, ...)のような関数は、特定の操作(OP)を表す新しいノードを作成するために使用されます。

技術的詳細

このコミットは、Goコンパイラのcmd/gcディレクトリ内のrange.csubr.cの2つのファイルに修正を加えています。

range.cの変更

range.cは、for...rangeループのコンパイル処理を担当するファイルです。 修正前のコードでは、for expr1, expr2 = range sliceのような2変数形式のrangeループにおいて、expr1expr2への代入が以下のように逐次的に行われていました。

// 修正前 (概念的な表現)
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つの値の代入」(つまり並行代入)を表すオペレーションコードです。この変更により、hv1nod(OIND, hp, N)(現在の要素の値)がまず評価され、その結果が同時にv1v2に代入されるようになります。これにより、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)); でまずインデックス変数 v1hv1 (現在のインデックス) を代入します。
    • その後に 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 の左辺(代入先)のリストを設定します。ここには v1v2 が含まれます。
      • 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) を持っている場合に実行されます。
    1. walkstmtlist(n->ninit);: n->ninit内の各ステートメントを再帰的に走査し、必要に応じて変換や最適化を行います。
    2. *init = concat(*init, n->ninit);: n->ninitに含まれる初期化ステートメントを、safeexprの呼び出し元に渡された全体の初期化リスト (*init) に連結します。これにより、これらの初期化ステートメントが、式が評価される前に実行されるべきコードとして適切に収集されます。
    3. 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 が取得されます。
  • 並行代入が正しく機能する場合:
    • i0 が代入されます。
    • x[i] (この時点での i はループ開始前の 1 です) に 99 が代入されます。つまり x[1]99 になります。
  • ループは break で即座に終了します。
  • 期待される結果: i0 になり、x[10, 99] になります。
  • if i != 0 || x[0] != 10 || x[1] != 99 の条件が、この期待される結果と一致するかを検証しています。もし並行代入が機能せず、i が先に更新されてから x[i] が評価されると、x[0]99 になってしまうなどの誤った結果になる可能性があります。

このテストケースは、rangeループにおける並行代入の修正が正しく機能していることを明確に示しています。

関連リンク

参考にした情報源リンク

  • 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ループにおいて、expr1expr2への値の代入が逐次的に行われていた問題を、本来あるべき並行代入に修正しています。この修正は、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の値を交換

この例では、右辺のbaがまず評価され、その結果が左辺のabに同時に代入されます。これにより、一時変数を使わずに値を交換することができます。並行代入は、複数の関数戻り値を受け取る際や、複数の変数を初期化する際にも使用されます。

for...rangeループにおけるindex, value := range collectionも、この並行代入のセマンティクスに従うべきです。つまり、イテレーションごとに生成されるインデックスと値のペアが、同時にindexvalue変数に代入される必要があります。

Goコンパイラの構造(cmd/gc

Goコンパイラは、Go言語のソースコードを機械語に変換するツールチェーンの一部です。cmd/gcは、Go言語のフロントエンドコンパイラであり、構文解析、型チェック、中間コード生成、最適化などの主要な処理を担当します。

  • AST (Abstract Syntax Tree): ソースコードはまず抽象構文木に変換されます。
  • Walk (AST Traversal): ASTは様々なフェーズで走査(walk)され、意味解析や変換が行われます。range.csubr.cのようなファイルは、このASTの走査と変換に関連する処理を実装しています。
  • Nodes: コンパイラ内部では、ASTの各要素はNode構造体として表現されます。nod(OP, ...)のような関数は、特定の操作(OP)を表す新しいノードを作成するために使用されます。

技術的詳細

このコミットは、Goコンパイラのcmd/gcディレクトリ内のrange.csubr.cの2つのファイルに修正を加えています。

range.cの変更

range.cは、for...rangeループのコンパイル処理を担当するファイルです。 修正前のコードでは、for expr1, expr2 = range sliceのような2変数形式のrangeループにおいて、expr1expr2への代入が以下のように逐次的に行われていました。

// 修正前 (概念的な表現)
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つの値の代入」(つまり並行代入)を表すオペレーションコードです。この変更により、hv1nod(OIND, hp, N)(現在の要素の値)がまず評価され、その結果が同時にv1v2に代入されるようになります。これにより、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)); でまずインデックス変数 v1hv1 (現在のインデックス) を代入します。
    • その後に 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 の左辺(代入先)のリストを設定します。ここには v1v2 が含まれます。
      • 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) を持っている場合に実行されます。
    1. walkstmtlist(n->ninit);: n->ninit内の各ステートメントを再帰的に走査し、必要に応じて変換や最適化を行います。
    2. *init = concat(*init, n->ninit);: n->ninitに含まれる初期化ステートメントを、safeexprの呼び出し元に渡された全体の初期化リスト (*init) に連結します。これにより、これらの初期化ステートメントが、式が評価される前に実行されるべきコードとして適切に収集されます。
    3. 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 が取得されます。
  • 並行代入が正しく機能する場合:
    • i0 が代入されます。
    • x[i] (この時点での i はループ開始前の 1 です) に 99 が代入されます。つまり x[1]99 になります。
  • ループは break で即座に終了します。
  • 期待される結果: i0 になり、x[10, 99] になります。
  • if i != 0 || x[0] != 10 || x[1] != 99 の条件が、この期待される結果と一致するかを検証しています。もし並行代入が機能せず、i が先に更新されてから x[i] が評価されると、x[0]99 になってしまうなどの誤った結果になる可能性があります。

このテストケースは、rangeループにおける並行代入の修正が正しく機能していることを明確に示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: for ステートメント (特に for...range セクション)
  • Goコンパイラのソースコード (特に src/cmd/gc/ ディレクトリ内のファイル)
  • Go言語のIssueトラッカー (GitHub Issues)
  • Go言語のGerritコードレビューシステム
  • 並行代入に関する一般的なプログラミングの概念
  • コンパイラの内部構造とAST (抽象構文木) の処理に関する一般的な知識