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

[インデックス 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.ctypecheckrange関数内に以下のコメントとロジックが追加されました。

		// 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番目の変数v2N(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文の型チェックとセマンティック分析を担当します。

  • v1rangeの最初のイテレーション変数(通常はインデックス)、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番目の変数がブランク識別子である場合に、値へのアクセスを試みないことを保証します。

関連リンク

参考にした情報源リンク

  • 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検索結果のソース)