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

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

このコミットは、Go言語のコンパイラ(cmd/gc)におけるインライン化のバグを修正するものです。具体的には、ifforswitchなどの制御フロー文の条件式内で複合リテラル(composite literals)が使用された際に発生するインライン化の不具合を解消します。この修正は、Go Issue #4230 に対応しています。

修正は主にコンパイラの内部的なフォーマット処理 (src/cmd/gc/fmt.c) に変更を加え、複合リテラルの表現が正しくなるように括弧を追加しています。また、このバグを再現し、修正が正しく機能することを確認するための新しいテストケースが追加されています。

コミット

commit 319131f295fecb687787d89b1441a6ea9222d5e4
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Oct 22 08:38:23 2012 +0200

    cmd/gc: fix inlining bug for composite literals in if statements.

    Fixes #4230.

    R=golang-dev, rsc
    CC=golang-dev, remy
    https://golang.org/cl/6640056
---
 src/cmd/gc/fmt.c               | 10 ++++---
 test/fixedbugs/bug465.dir/a.go | 61 ++++++++++++++++++++++++++++++++++++++++++
 test/fixedbugs/bug465.dir/b.go | 17 ++++++++++++\n test/fixedbugs/bug465.go       | 10 +++++++
 4 files changed, 94 insertions(+), 4 deletions(-)

diff --git a/src/cmd/gc/fmt.c b/src/cmd/gc/fmt.c
index ec6d3d0cdf..6945e9c8e3 100644
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1161,7 +1161,7 @@ exprfmt(Fmt *f, Node *n, int prec)
  	case OCOMPLIT:
  	\tif(fmtmode == FErr)
  	\t\treturn fmtstrcpy(f, "composite literal");
-\t\treturn fmtprint(f, "%N{ %,H }", n->right, n->list);
+\t\treturn fmtprint(f, "(%N{ %,H })", n->right, n->list);

  	case OPTRLIT:
  	\tif(fmtmode == FExp && n->left->implicit)
@@ -1172,8 +1172,8 @@ exprfmt(Fmt *f, Node *n, int prec)
  	\tif(fmtmode == FExp) {   // requires special handling of field names
  	\t\tif(n->implicit)
  	\t\t\tfmtstrcpy(f, "{");
-\t\t\telse 
-\t\t\t\tfmtprint(f, "%T{", n->type);
+\t\t\telse
+\t\t\t\tfmtprint(f, "(%T{", n->type);
  	\t\tfor(l=n->list; l; l=l->next) {
  	\t\t\t// another special case: if n->left is an embedded field of builtin type,
  	\t\t\t// it needs to be non-qualified.  Can't figure that out in %S, so do it here
@@ -1190,6 +1190,8 @@ exprfmt(Fmt *f, Node *n, int prec)
  	\t\t\t\telse
  	\t\t\t\t\tfmtstrcpy(f, " ");
  	\t\t\t}\n+\t\t\tif(!n->implicit)\n+\t\t\t\treturn fmtstrcpy(f, "})");
  	\t\t\treturn fmtstrcpy(f, "}");
  	\t\t}\n \t\t// fallthrough
@@ -1200,7 +1202,7 @@ exprfmt(Fmt *f, Node *n, int prec)
  	\t\t\treturn fmtprint(f, "%T literal", n->type);
  	\tif(fmtmode == FExp && n->implicit)
  	\t\treturn fmtprint(f, "{ %,H }", n->list);
-\t\treturn fmtprint(f, "%T{ %,H }", n->type, n->list);
+\t\treturn fmtprint(f, "(%T{ %,H })", n->type, n->list);\n \n \tcase OKEY:\
  \t\tif(n->left && n->right)
diff --git a/test/fixedbugs/bug465.dir/a.go b/test/fixedbugs/bug465.dir/a.go
new file mode 100644
index 0000000000..c5d410de47
--- /dev/null
+++ b/test/fixedbugs/bug465.dir/a.go
@@ -0,0 +1,61 @@
+// 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.
+
+package a
+
+type T struct{ A, B int }
+
+type A []int
+
+type M map[int]int
+
+func F1() int {
+\tif (T{1, 2}) == (T{3, 4}) {
+\t\treturn 1
+\t}
+\treturn 0
+}
+
+func F2() int {
+\tif (M{1: 2}) == nil {
+\t\treturn 1
+\t}
+\treturn 0
+}
+
+func F3() int {
+\tif nil == (A{}) {
+\t\treturn 1
+\t}
+\treturn 0
+}
+
+func F4() int {
+\tif a := (A{}); a == nil {
+\t\treturn 1
+\t}
+\treturn 0
+}
+
+func F5() int {
+\tfor k, v := range (M{1: 2}) {
+\t\treturn v - k
+\t}
+\treturn 0
+}
+
+func F6() int {
+\tswitch a := (T{1, 1}); a == (T{1, 2}) {
+\tdefault:\
+\t\treturn 1
+\t}
+\treturn 0
+}
+
+func F7() int {
+\tfor m := (M{}); len(m) < (T{1, 2}).A; m[1] = (A{1})[0] {
+\t\treturn 1
+\t}
+\treturn 0
+}
diff --git a/test/fixedbugs/bug465.dir/b.go b/test/fixedbugs/bug465.dir/b.go
new file mode 100644
index 0000000000..0f4909f4db
--- /dev/null
+++ b/test/fixedbugs/bug465.dir/b.go
@@ -0,0 +1,17 @@
+// 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.
+
+package main
+
+import "./a"
+
+func main() {
+\tfor _, f := range []func() int{
+\t\ta.F1, a.F2, a.F3, a.F4,
+\t\ta.F5, a.F6, a.F7} {
+\t\tif f() > 1 {
+\t\t\tpanic("f() > 1")
+\t\t}\n+\t}\n+}\ndiff --git a/test/fixedbugs/bug465.go b/test/fixedbugs/bug465.go
new file mode 100644
index 0000000000..a6ef5876ab
--- /dev/null
+++ b/test/fixedbugs/bug465.go
@@ -0,0 +1,10 @@
+// rundir
+
+// 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 4230: inlining bug for composite literal in
+// if, for, switch statements.
+
+package ignored

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

https://github.com/golang/go/commit/319131f295fecb687787d89b1441a6ea9222d5e4

元コミット内容

このコミットの目的は、Goコンパイラ(cmd/gc)におけるインライン化のバグを修正することです。具体的には、if文などの条件式内で複合リテラルが使用された場合に発生する問題に対処しています。このバグはGo Issue #4230として報告されており、今回の修正によってその問題が解決されます。

変更の背景

Go言語のコンパイラgcは、プログラムの実行性能を向上させるために様々な最適化を行います。その一つに「インライン化」があります。インライン化とは、関数呼び出しをその関数の本体のコードで直接置き換える最適化手法です。これにより、関数呼び出しのオーバーヘッドが削減され、プログラム全体の実行速度が向上することが期待されます。

しかし、このコミットが修正しようとしている問題は、特定の条件下でインライン化が正しく行われない、あるいは誤ったコードが生成されるというバグでした。具体的には、ifforswitchといった制御フロー文の条件式や初期化ステートメント内で複合リテラルが使用された場合に、コンパイラがその複合リテラルを正しく解釈できず、結果としてインライン化の過程で誤ったコードが生成される可能性がありました。

Go Issue #4230では、このような状況でコンパイラがパニックを起こしたり、誤った実行結果を返したりするケースが報告されていました。これは、コンパイラの内部表現において、複合リテラルが特定の文脈で曖昧に解釈されるか、あるいはその表現がインライン化のロジックと整合性が取れていなかったことが原因と考えられます。このバグは、Goプログラムの安定性と信頼性に直接影響を与えるため、早急な修正が必要とされていました。

前提知識の解説

Go言語のコンパイラ (gc)

gcは、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。gcは、単にコードを変換するだけでなく、プログラムの実行効率を高めるための様々な最適化(例: インライン化、エスケープ解析など)も行います。src/cmd/gcディレクトリには、このコンパイラのソースコードが含まれており、fmt.cのようなファイルは、コンパイラのフロントエンドや中間表現の処理に関連する部分です。

インライン化 (Inlining)

インライン化は、コンパイラ最適化の重要な手法の一つです。関数呼び出しが発生する箇所で、呼び出される関数のコードを直接埋め込むことで、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を排除し、実行速度を向上させます。また、インライン化によって、呼び出し元のコンテキストでさらに多くの最適化(例: 定数伝播、デッドコード削除)が可能になることがあります。しかし、インライン化はコードサイズを増加させる可能性があり、過度なインライン化はキャッシュの効率を低下させることもあります。

複合リテラル (Composite Literals)

Go言語における複合リテラルは、構造体、配列、スライス、マップなどの複合型を初期化するための簡潔な構文です。 例:

  • 構造体: Person{Name: "Alice", Age: 30}
  • 配列: [3]int{1, 2, 3}
  • スライス: []string{"apple", "banana"}
  • マップ: map[string]int{"one": 1, "two": 2}

これらのリテラルは、プログラム内で直接値を表現するために頻繁に使用されます。

制御フロー文 (Control Flow Statements)

Go言語の制御フロー文には、if(条件分岐)、for(ループ)、switch(多分岐)などがあります。これらの文は、プログラムの実行パスを決定するために不可欠です。複合リテラルがこれらの文の条件式や初期化ステートメント内で使用される場合、コンパイラはそれらを正しく解析し、適切な中間表現に変換する必要があります。

Issue #4230

Go Issue #4230は、「inlining bug for composite literal in if, for, switch statements.」と題されたバグ報告です。この問題は、コンパイラが複合リテラルをインライン化する際に、特にそれがifforswitchといった制御フロー文のコンテキストで使用されている場合に、誤ったコードを生成するというものでした。具体的な症状としては、コンパイルエラー、ランタイムパニック、または予期せぬプログラムの振る舞いが挙げられます。このバグは、コンパイラの内部的なAST(抽象構文木)の表現や、その後の最適化パスでの処理に起因していると考えられます。

技術的詳細

このバグの根本原因は、Goコンパイラgcの内部で、複合リテラルが特定の文脈(特に制御フロー文の条件式など)で文字列として表現される際に、その表現が曖昧であったり、構文的に不完全であったりしたことにあります。

src/cmd/gc/fmt.cファイルは、コンパイラのデバッグ出力や内部的なノードの文字列表現を生成する役割を担っています。exprfmt関数は、抽象構文木(AST)のノードを文字列にフォーマットするために使用されます。

問題は、OCOMPLIT(複合リテラル)やOPTRLIT(ポインタリテラル)などのノードがフォーマットされる際に、生成される文字列が、コンパイラの他の部分(特にインライン化パス)が期待する正確な構文表現を欠いていた点にありました。具体的には、複合リテラルが括弧で囲まれていない場合、コンパイラがその構造を正しく識別できず、インライン化の際に誤ったAST変換やコード生成を引き起こす可能性がありました。

例えば、if (T{1, 2}) == (T{3, 4})のようなコードがあった場合、コンパイラが内部的に(T{1, 2})を文字列として表現する際に、括弧が欠落していると、T{1, 2}が単なる式として扱われ、その後の比較演算子==との結合順序や、インライン化の際の式の評価順序が意図しないものになることが考えられます。これにより、コンパイラが生成する中間コードや最終的な機械語が、元のGoコードのセマンティクスと異なるものとなり、バグとして顕在化していました。

この修正は、exprfmt関数において、複合リテラルやポインタリテラルを文字列として表現する際に、明示的に括弧を追加することで、この曖昧さを解消し、コンパイラの他の部分が常に正しい構文構造を認識できるようにします。これにより、インライン化の過程で発生していた誤った最適化やコード生成が防止されます。

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

このコミットのコアとなるコード変更は、src/cmd/gc/fmt.cファイル内のexprfmt関数に集中しています。

具体的には、以下の3箇所でfmtprint関数の呼び出しにおいて、複合リテラルの出力形式に括弧が追加されています。

  1. case OCOMPLIT: の箇所:

    --- a/src/cmd/gc/fmt.c
    +++ b/src/cmd/gc/fmt.c
    @@ -1161,7 +1161,7 @@ exprfmt(Fmt *f, Node *n, int prec)
      	case OCOMPLIT:
      	\tif(fmtmode == FErr)
      	\t\treturn fmtstrcpy(f, "composite literal");
    -\t\treturn fmtprint(f, "%N{ %,H }", n->right, n->list);
    +\t\treturn fmtprint(f, "(%N{ %,H })", n->right, n->list);
    

    %N{ %,H } の形式で出力されていた複合リテラルが、(%N{ %,H }) のように全体が括弧で囲まれるようになりました。

  2. case OPTRLIT: 内の fmtmode == FExp の箇所:

    --- a/src/cmd/gc/fmt.c
    +++ b/src/cmd/gc/fmt.c
    @@ -1172,8 +1172,8 @@ exprfmt(Fmt *f, Node *n, int prec)
      	\tif(fmtmode == FExp) {   // requires special handling of field names
      	\t\tif(n->implicit)
      	\t\t\tfmtstrcpy(f, "{");
    -\t\t\telse 
    -\t\t\t\tfmtprint(f, "%T{", n->type);
    +\t\t\telse
    +\t\t\t\tfmtprint(f, "(%T{", n->type);
    

    ポインタリテラルが型情報と共に表示される際に、%T{ だったものが (%T{ となり、開始括弧が追加されました。

  3. case OPTRLIT: 内の !n->implicit の条件分岐の追加:

    --- a/src/cmd/gc/fmt.c
    +++ b/src/cmd/gc/fmt.c
    @@ -1190,6 +1190,8 @@ exprfmt(Fmt *f, Node *n, int prec)
      	\t\t\t\telse
      	\t\t\t\t\tfmtstrcpy(f, " ");
      	\t\t\t}\n+\t\t\tif(!n->implicit)\n+\t\t\t\treturn fmtstrcpy(f, "})");
      	\t\t\treturn fmtstrcpy(f, "}");
      	\t\t}\n \t\t// fallthrough
    

    これは、OPTRLITの処理において、暗黙的でない(!n->implicit)場合に、閉じ括弧と波括弧の組み合わせ }) を追加するものです。これにより、開始括弧 ( と対応する閉じ括弧 ) がペアになります。

  4. case OLITERAL: の箇所:

    --- a/src/cmd/gc/fmt.c
    +++ b/src/cmd/gc/fmt.c
    @@ -1200,7 +1202,7 @@ exprfmt(Fmt *f, Node *n, int prec)
      	\t\t\treturn fmtprint(f, "%T literal", n->type);
      	\tif(fmtmode == FExp && n->implicit)
      	\t\treturn fmtprint(f, "{ %,H }", n->list);
    -\t\treturn fmtprint(f, "%T{ %,H }", n->type, n->list);
    +\t\treturn fmtprint(f, "(%T{ %,H })", n->type, n->list);
    

    こちらも複合リテラルの出力形式が (%T{ %,H }) となり、全体が括弧で囲まれるようになりました。

これらの変更は、コンパイラが複合リテラルを内部的に表現する際の「文字列化」のルールを厳密にし、常に明確な構文境界を持つようにすることで、インライン化などの最適化パスが誤った解釈をしないようにするためのものです。

また、以下のテストファイルが追加され、修正が正しく機能することを確認しています。

  • test/fixedbugs/bug465.dir/a.go: 複合リテラルがifforswitch文の条件式や初期化ステートメントで使用される様々なケースを網羅したテスト関数群。
  • test/fixedbugs/bug465.dir/b.go: a.goで定義されたテスト関数を呼び出し、結果を検証するmain関数。
  • test/fixedbugs/bug465.go: rundirディレクティブを使用して、bug465.dir内のテストを実行するように指示するファイル。

コアとなるコードの解説

src/cmd/gc/fmt.cexprfmt関数は、Goコンパイラgcの内部で、抽象構文木(AST)のノードを人間が読める形式、またはコンパイラの他の部分が処理しやすい形式の文字列に変換する役割を担っています。この関数は、コンパイラのデバッグ出力や、中間コード生成の前の段階でASTの構造を検証する際などに利用されます。

このコミットにおける変更の核心は、exprfmt関数が複合リテラル(OCOMPLIT)やポインタリテラル(OPTRLIT)を文字列として表現する際に、明示的に括弧 () を追加するようになった点です。

なぜ括弧が必要だったのか?

Go言語の構文では、複合リテラルはそれ自体が式として機能します。しかし、コンパイラの内部処理、特にインライン化のような最適化フェーズでは、式の結合性や評価順序が非常に重要になります。

例えば、if T{1, 2} == T{3, 4} のようなコードがあった場合、コンパイラが内部的にT{1, 2}を文字列として表現する際に、もし括弧がないと、T{1, 2}が単なる値として扱われ、その後の==演算子との結合が曖昧になる可能性がありました。特に、インライン化の際にASTが変換される過程で、この曖昧さが原因で、コンパイラが意図しないAST構造を生成したり、式の評価順序を誤って最適化したりすることが考えられます。

括弧 () は、プログラミング言語において式の評価順序を明示的に指定するための最も基本的な構文要素です。exprfmt関数が複合リテラルを文字列化する際に、常に括弧で囲むようにすることで、以下の効果が期待されます。

  1. 明確な構文境界の提供: 複合リテラルが常に独立した式として認識されるようになり、その周囲の演算子や制御フロー構造との結合が明確になります。
  2. インライン化の正確性向上: インライン化のパスが、複合リテラルを単一のまとまりとして扱い、その内部構造やセマンティクスを損なうことなく、呼び出し元のコンテキストに展開できるようになります。これにより、インライン化によるバグの発生を防ぎます。
  3. コンパイラ内部の一貫性: コンパイラの異なるフェーズ(構文解析、型チェック、最適化、コード生成)間で、複合リテラルの表現に関する一貫性が保たれ、誤った解釈や処理が減少します。

この修正は、Goコンパイラの堅牢性を高め、特定の複雑な構文パターンにおけるインライン化の信頼性を向上させるための重要なステップでした。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go言語の構文、コンパイラ、最適化に関する一般的な情報)
  • Go言語のソースコード (特にsrc/cmd/gcディレクトリ内の関連ファイル)
  • Go Issue Tracker (Issue #4230の議論内容)
  • コンパイラ最適化に関する一般的な知識 (インライン化、ASTなど)
  • C言語のfmtライブラリの一般的な概念 (Goコンパイラのfmt.cの理解のため)
  • Go言語の複合リテラルに関する仕様