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

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

このコミットは、gccgo コンパイラが特定の有効なGoコードをコンパイルする際にクラッシュするバグ(bug436)を修正するために、そのバグを再現するテストケース test/fixedbugs/bug436.go を追加するものです。このテストケースは、Go言語の多値戻り値と変数初期化の挙動が gccgo で正しく処理されていなかった問題を示しています。

コミット

commit df644489322609ef3555eda8d6e8a1f4ccd95672
Author: Ian Lance Taylor <iant@golang.org>
Date:   Thu May 3 10:33:10 2012 -0700

    test: add bug436, valid code that crashed gccgo
    
    R=golang-dev, bsiegert, rsc
    CC=golang-dev
    https://golang.org/cl/6158046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/df644489322609ef3555eda8d6e8a1f4ccd95672

元コミット内容

test: add bug436, valid code that crashed gccgo

R=golang-dev, bsiegert, rsc
CC=golang-dev
https://golang.org/cl/6158046

変更の背景

この変更の背景には、Go言語のコンパイラの一つである gccgo が、特定の有効なGoコードに対してコンパイル時にクラッシュするという深刻なバグが存在したことがあります。このバグは bug436 として追跡されており、Goの言語仕様に準拠したコードであるにもかかわらず、gccgo がそのコードを正しく処理できないという問題でした。

コンパイラのクラッシュは、開発者にとって非常に大きな問題です。なぜなら、それはコンパイラ自体の信頼性を損ない、開発プロセスを中断させるからです。このコミットは、この特定のクラッシュを再現する最小限のテストケースを追加することで、バグが修正されたことを検証し、将来的に同様の回帰が発生しないようにするためのものです。テストケースの追加は、ソフトウェア開発における品質保証の重要なステップであり、特にコンパイラのような基盤ソフトウェアにおいては不可欠です。

前提知識の解説

このコミットと関連するバグを理解するためには、以下のGo言語の概念とコンパイラに関する基本的な知識が必要です。

  1. Go言語の多値戻り値 (Multiple Return Values): Go言語の関数は、複数の値を返すことができます。これは他の多くのプログラミング言語にはない特徴で、エラーハンドリングや複数の関連する結果を一度に返す際に非常に便利です。 例: func foo() (int, int) { return 1, 2 } は2つの int 型の値を返します。

  2. Go言語の変数宣言と初期化: Goでは、変数を宣言し、同時に初期化することができます。特に、多値戻り値を持つ関数の結果を複数の変数に一度に代入する構文が特徴的です。 例: var a, b = foo() は、foo() から返される2つの値をそれぞれ変数 ab に代入します。

  3. 変数初期化の順序: Go言語では、パッケージレベルの変数は宣言順に初期化されます。これは、ある変数の初期化が別の変数の値に依存する場合に重要です。

  4. gccgo: gccgo は、GCC (GNU Compiler Collection) のフロントエンドの一つで、Go言語のコードをコンパイルするために使用されます。Go言語の公式コンパイラである gc とは異なる実装であり、GCCの最適化バックエンドを利用できるという特徴があります。コンパイラは、ソースコードを機械語に変換するソフトウェアであり、その過程で構文解析、意味解析、最適化、コード生成などの複数のフェーズを経ます。

  5. コンパイラのクラッシュ: コンパイラがクラッシュするとは、コンパイル中に予期せぬエラーが発生し、プログラムが異常終了することです。これは通常、コンパイラ自身のバグ、つまり特定の入力コードを正しく処理できない場合に起こります。

技術的詳細

bug436 の技術的な詳細は、Go言語の多値戻り値と、それを受け取る変数の初期化順序の複雑な相互作用に起因していました。問題のコードは以下のようになっています。

package main

func foo() (int, int) {
	return 1, 2
}

var c = b
var a, b = foo()
var d = b + 1

func main() {
	// ... 検証コード ...
}

このコードのポイントは、パッケージレベル変数 a, b, c, d の宣言と初期化の順序です。

  1. var c = b: ここで cb の値で初期化されようとします。しかし、この時点では b はまだ初期化されていません。Go言語の仕様では、このような場合、b はそのゼロ値(int 型の場合は 0)で初期化されるべきです。
  2. var a, b = foo(): 次に abfoo() の戻り値 (1, 2) で初期化されます。この時点で b の値は 2 になります。
  3. var d = b + 1: 最後に db の現在の値(2)に 1 を加えた 3 で初期化されます。

gccgo は、この初期化の順序、特に c = b の行で b がまだ foo() の結果で初期化されていない段階で参照されるケースを正しく処理できませんでした。Go言語の仕様では、パッケージレベル変数の初期化は宣言順に行われ、初期化式で参照される変数がまだ初期化されていない場合は、その変数のゼロ値が使用されることになっています。gccgo はこのセマンティクスを誤って解釈し、内部的な状態が不正になり、結果としてコンパイル時にクラッシュしていました。

このバグは、コンパイラが変数のライフサイクル、特に初期化の依存関係を追跡する際の複雑さを示しています。多値戻り値と、それらが複数の変数に同時に代入される構文は、コンパイラにとって特別な処理が必要となる領域であり、gccgo の実装に欠陥があったことを示唆しています。

コアとなるコードの変更箇所

diff --git a/test/fixedbugs/bug436.go b/test/fixedbugs/bug436.go
new file mode 100644
index 0000000000..e848eaeba2
--- /dev/null
+++ b/test/fixedbugs/bug436.go
@@ -0,0 +1,32 @@
+// 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.
+
+// Gccgo used to crash compiling this.
+
+package main
+
+func foo() (int, int) {
+	return 1, 2
+}
+
+var c = b
+var a, b = foo()
+var d = b + 1
+
+func main() {
+	if a != 1 {
+		panic(a)
+	}
+	if b != 2 {
+		panic(b)
+	}
+	if c != 2 {
+		panic(c)
+	}
+	if d != 3 {
+		panic(d)
+	}
+}

コアとなるコードの解説

追加された test/fixedbugs/bug436.go ファイルは、gccgo のクラッシュを引き起こした特定のコードパターンを再現するためのものです。

  • // run: このコメントは、Goのテストシステムに対して、このファイルが実行可能なテストであることを示します。
  • // Gccgo used to crash compiling this.: このコメントは、このテストケースの目的、すなわち gccgo が以前このコードでクラッシュしたことを明確に示しています。
  • package main: 実行可能なプログラムであることを示します。
  • func foo() (int, int) { return 1, 2 }: 2つの整数値を返すシンプルな関数です。これが多値戻り値の例となります。
  • var c = b: ここがバグの核心部分です。cb で初期化されますが、この時点では b はまだ foo() の結果で初期化されていません。Goの仕様では、この場合 b はそのゼロ値(0)で初期化されるべきです。しかし、gccgo はこの状況を正しく処理できませんでした。
  • var a, b = foo(): abfoo() の戻り値 (1, 2) で初期化されます。これにより、a1b2 になります。
  • var d = b + 1: db の現在の値(2)に 1 を加えた 3 で初期化されます。
  • func main(): プログラムのエントリポイントです。
  • if a != 1 { panic(a) }
  • if b != 2 { panic(b) }
  • if c != 2 { panic(c) }
  • if d != 3 { panic(d) }: これらの if 文は、各変数が期待通りの値で初期化されているかを検証します。
    • afoo() からの最初の戻り値なので 1 であるべきです。
    • bfoo() からの2番目の戻り値なので 2 であるべきです。
    • cbfoo() の結果で初期化される前に参照されるため、b のゼロ値である 0 で初期化されると考えるかもしれませんが、Goの仕様では、パッケージレベル変数の初期化は宣言順に行われ、初期化式で参照される変数がまだ初期化されていない場合は、その変数のゼロ値が使用されます。しかし、var a, b = foo() の行で b2 に更新された後、c の初期化が完了する前に b の値が確定するため、c は最終的に 2 になります。これはGoの初期化順序の複雑な側面です。
    • db2 になった後に b + 1 で初期化されるので 3 であるべきです。

このテストケースは、gccgo がこの特定の初期化シーケンスを正しく処理できるようになったことを確認するために使用されます。もし gccgo がまだバグを抱えていれば、コンパイル時にクラッシュするか、実行時に panic が発生するでしょう。

関連リンク

参考にした情報源リンク