[インデックス 13989] ファイルの概要
このコミットは、Goコンパイラのrange
キーワードの挙動、特に2番目のイテレーション変数がブランク識別子(_
)である場合の処理に関するバグ修正とテスト追加を含んでいます。具体的には、src/cmd/gc/range.c
におけるコンパイラのrange
処理ロジックの調整と、test/fixedbugs/bug454.go
という新しいテストファイルの追加が行われました。
コミット
commit 9a3bc51c8119cde353da5c304b4c52f348ad7c46
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Sat Sep 29 23:23:56 2012 +0800
test/fixedbugs/bug454.go: add a test for CL 6564052
Also mention that ignoring second blank identifier of range is required by the spec in the code.
Fixes #4173.
R=daniel.morsing, remyoudompheng, r
CC=golang-dev
https://golang.org/cl/6594043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9a3bc51c8119cde353da5c304b4c52f348ad7c46
元コミット内容
このコミットは、Go言語のrange
キーワードに関する特定のバグ(Issue 4173)を修正するために、新しいテストケースtest/fixedbugs/bug454.go
を追加しています。また、src/cmd/gc/range.c
内のコメントを更新し、range
文の2番目のイテレーション変数がブランク識別子である場合に、その変数を無視することがGo言語の仕様によって要求されていることを明記しています。これは、以前の変更(CL 6564052)に関連するテストの追加でもあります。
変更の背景
この変更の背景には、Go言語のrange
キーワードの特定の挙動、特に配列やスライスに対してrange
を使用し、2番目のイテレーション変数をブランク識別子(_
)で受け取る場合に発生する可能性のあるパニック(実行時エラー)がありました。
Go言語の仕様では、range
文において2番目のイテレーション変数がブランク識別子である場合、そのrange
句は最初の変数のみが存在する場合と同じであると規定されています。これは、インデックスのみが必要で値が不要な場合に、コンパイラが値の計算を最適化してスキップできることを意味します。
しかし、特定の条件下(例えば、nil
の配列ポインタに対してrange
を使用し、2番目の変数をブランク識別子で受け取る場合)において、コンパイラがこの最適化を適切に行わず、存在しない要素にアクセスしようとしてパニックを引き起こすバグが存在しました(Issue 4173)。
このコミットは、このバグを再現し、修正された挙動を検証するためのテストケースを追加するとともに、コンパイラコード内のコメントを更新して、この仕様要件を明確にすることで、将来的な同様のバグの発生を防ぐことを目的としています。
前提知識の解説
Go言語のrange
キーワード
Go言語のfor ... range
文は、スライス、配列、文字列、マップ、チャネルなどのコレクションをイテレート(反復処理)するために使用されます。
基本的な構文は以下の通りです。
for index, value := range collection {
// index と value を使用した処理
}
- スライスと配列:
index
は要素のインデックス、value
はそのインデックスに対応する要素の値になります。 - 文字列:
index
はUnicodeコードポイントの開始バイトオフセット、value
は対応するルーン(Unicodeコードポイント)になります。 - マップ:
index
はキー、value
は値になります。マップのイテレーション順序は保証されません。 - チャネル:
value
はチャネルから受信した値になります。チャネルが閉じられるまでイテレートを続けます。
ブランク識別子(_
)
Go言語では、変数を宣言したが使用しない場合にコンパイルエラーを避けるために、ブランク識別子(_
)を使用できます。これは、特定の値を破棄したい場合や、関数の戻り値の一部を無視したい場合によく使われます。
range
文においても、ブランク識別子は重要な役割を果たします。
- 値のみが必要な場合:
for _, value := range collection
この場合、インデックスは不要なので_
で破棄し、value
のみを使用します。 - インデックスのみが必要な場合:
for index, _ := range collection
この場合、値は不要なので_
で破棄し、index
のみを使用します。
Go言語の仕様とrange
の挙動
Go言語の仕様(The Go Programming Language Specification)には、range
文の挙動について明確な規定があります。特に、2番目のイテレーション変数がブランク識別子である場合について、以下のように述べられています。
If the second iteration variable is the blank identifier, the range clause is equivalent to the same clause with only the first variable present. (2番目のイテレーション変数がブランク識別子である場合、その
range
句は、最初の変数のみが存在する場合と同じ句に相当する。)
これは、コンパイラがこのケースを特別に扱い、2番目の変数を計算したり、その値にアクセスしたりするコードを生成しないように最適化できることを意味します。この最適化は、単なるパフォーマンス向上だけでなく、今回のバグのように、存在しない値へのアクセスを試みることで発生するパニックを防ぐための「要件」でもあります。
Goコンパイラ(gc
)
Go言語の公式コンパイラはgc
(Go Compiler)と呼ばれます。gc
はGoのソースコードを機械語にコンパイルする役割を担っています。src/cmd/gc
ディレクトリには、コンパイラのフロントエンド、型チェック、コード生成など、コンパイルプロセスの様々な段階を処理するソースコードが含まれています。
このコミットで変更されたsrc/cmd/gc/range.c
は、range
文の型チェックとコード生成に関連するロジックを処理する部分です。
技術的詳細
このコミットの技術的詳細は、Goコンパイラがrange
文をどのように処理するか、特に2番目のイテレーション変数がブランク識別子である場合の最適化と仕様遵守に焦点を当てています。
src/cmd/gc/range.c
の役割
src/cmd/gc/range.c
ファイルは、Goコンパイラの型チェックフェーズにおいて、range
文のセマンティクスを処理する役割を担っています。具体的には、typecheckrange
関数がrange
文のAST(抽象構文木)ノードを受け取り、イテレーション変数、range
の対象となる式、およびそれらの型を検証します。
変更前の問題点(Issue 4173)
Issue 4173は、nil
の配列ポインタに対してfor i, _ := range arr
のような形式でrange
を使用した場合に、コンパイラが誤ってarr[i]
のような要素アクセスコードを生成しようとし、結果として実行時にパニックを引き起こすという問題でした。
本来、Goの仕様によれば、2番目のイテレーション変数がブランク識別子である場合、値は不要であり、コンパイラは値へのアクセスコードを生成すべきではありません。しかし、この最適化(または仕様遵守)が特定のケースで欠けていたため、問題が発生していました。
変更による修正
このコミットでは、src/cmd/gc/range.c
のtypecheckrange
関数内に以下のコメントとロジックが追加されました。
// this is not only a optimization but also a requirement in the spec.
// "if the second iteration variable is the blank identifier, the range
// clause is equivalent to the same clause with only the first variable
// present."
if(isblank(v2)) {
n->list = list1(v1);
v2 = N;
}
- コメントの追加: 「これは最適化であるだけでなく、仕様の要件でもある」というコメントが追加され、Go言語の仕様からの引用が示されています。これにより、このコードパスの重要性と目的が明確になりました。
- ロジックの変更:
if(isblank(v2))
という条件が追加され、2番目のイテレーション変数v2
がブランク識別子であるかどうかをチェックします。- もし
v2
がブランク識別子であれば、n->list = list1(v1);
によって、range
文のイテレーション変数のリストが最初の変数v1
のみを含むように変更されます。 - そして、
v2 = N;
によって、2番目の変数v2
がN
(nilノード、つまり存在しないことを示す)に設定されます。
- もし
この変更により、コンパイラは2番目のイテレーション変数がブランク識別子である場合に、その変数を完全に無視し、値へのアクセスコードを生成しないように強制されます。これにより、nil
の配列ポインタに対するrange
でパニックが発生する問題が解決されます。
test/fixedbugs/bug454.go
の追加
このコミットでは、上記の修正が正しく機能することを検証するために、新しいテストファイルtest/fixedbugs/bug454.go
が追加されました。このテストは、nil
の配列ポインタに対してfor i, _ := range arr
を使用するシナリオを再現し、パニックが発生しないことを確認します。
テストコードは、var arr *[10]int
というnil
の配列ポインタを宣言し、その上でfor i, _ := range arr
を実行します。ループ内でs += i
のようにインデックスi
のみを使用し、値にはアクセスしません。もし修正が正しくなければ、このrange
ループ内でパニックが発生するはずですが、修正後はパニックが発生せず、s
の値が期待通り(0から9までの合計である45)になることを検証します。
コアとなるコードの変更箇所
src/cmd/gc/range.c
--- a/src/cmd/gc/range.c
+++ b/src/cmd/gc/range.c
@@ -71,7 +71,11 @@ typecheckrange(Node *n)
v2 = N;
if(n->list->next)
v2 = n->list->next->n;
-
+
+ // this is not only a optimization but also a requirement in the spec.
+ // "if the second iteration variable is the blank identifier, the range
+ // clause is equivalent to the same clause with only the first variable
+ // present."
if(isblank(v2)) {
n->list = list1(v1);
v2 = N;
test/fixedbugs/bug454.go
--- /dev/null
+++ b/test/fixedbugs/bug454.go
@@ -0,0 +1,21 @@
+// run
+
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Issue 4173
+
+package main
+
+func main() {
+ var arr *[10]int
+ s := 0
+ for i, _ := range arr {
+ // used to panic trying to access arr[i]
+ s += i
+ }
+ if s != 45 {
+ println("BUG")
+ }
+}
コアとなるコードの解説
src/cmd/gc/range.c
の変更
この変更は、typecheckrange
関数内で行われています。この関数は、for ... range
文の型チェックとセマンティック分析を担当します。
v1
はrange
の最初のイテレーション変数(通常はインデックス)、v2
は2番目のイテレーション変数(通常は値)を表すノードです。- 追加されたコードブロックは、
v2
がブランク識別子(_
)であるかどうかをisblank(v2)
でチェックします。 - もし
v2
がブランク識別子であれば、以下の処理が行われます。n->list = list1(v1);
:range
文のイテレーション変数のリストを、v1
(最初の変数)のみを含むように再構築します。これにより、コンパイラは2番目の変数v2
を考慮しなくなります。v2 = N;
:v2
ノードをN
(nilノード)に設定します。これは、2番目の変数が存在しないことを明示的に示します。
- このロジックにより、Goの仕様で定められている「2番目のイテレーション変数がブランク識別子である場合、その
range
句は最初の変数のみが存在する場合と同じ」という要件がコンパイラレベルで強制されます。これにより、値への不要なアクセス試行が回避され、Issue 4173のようなパニックが防止されます。
test/fixedbugs/bug454.go
の追加
この新しいテストファイルは、Issue 4173で報告されたバグを再現し、修正が正しく適用されたことを検証するためのものです。
// run
: このコメントは、Goのテストフレームワークに対して、このファイルをテストとして実行するよう指示します。var arr *[10]int
:nil
の配列ポインタarr
を宣言します。これがバグをトリガーする重要な条件でした。s := 0
: 合計値を格納するための変数s
を初期化します。for i, _ := range arr
: ここがテストの核心部分です。nil
の配列ポインタarr
に対してrange
を使用し、インデックスi
とブランク識別子_
で値を受け取ります。// used to panic trying to access arr[i]
: コメントは、この行が以前はarr[i]
へのアクセスを試みてパニックを引き起こしていたことを示しています。s += i
: ループ内でインデックスi
のみを使用し、値にはアクセスしません。
if s != 45 { println("BUG") }
: ループが正常に完了した場合、s
は0から9までの合計である45になるはずです。もしs
が45でなければ、テストは「BUG」を出力し、失敗を示します。
このテストは、コンパイラの修正が、nil
の配列ポインタに対するrange
で2番目の変数がブランク識別子である場合に、値へのアクセスを試みないことを保証します。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/9a3bc51c8119cde353da5c304b4c52f348ad7c46
- Go Gerrit Change-ID (CL 6594043): https://golang.org/cl/6594043
- Go Issue 4173: 検索結果からは直接的なGoプロジェクトのIssue 4173は見つかりませんでしたが、コミットメッセージに
Fixes #4173
と明記されているため、Goプロジェクトの内部的なIssueトラッカーに存在したと考えられます。
参考にした情報源リンク
- Go Gerrit Code Review (CL 6594043) の要約情報
- Go言語の仕様 (The Go Programming Language Specification) -
for
statements (range clause) - Go言語のブランク識別子に関する一般的な知識
- Goコンパイラ(
gc
)の構造に関する一般的な知識 goautodial.org
(CL 6564052に関する検索結果の一部)github.com
(Go Issue 4173に関する検索結果の一部)apache.org
(Go Issue 4173に関する検索結果の一部)vertexaisearch.cloud.google.com
(Web検索結果のソース)