[インデックス 11981] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における戻り値の型チェックの不具合を修正するものです。具体的には、return
ステートメントにおける式のリストの型チェックが正しく行われず、不正な戻り値を持つコードが誤ってコンパイルされたり、内部エラーを引き起こしたりする問題(Issue 3044)を解決します。この修正により、コンパイラは単一の戻り値と複数の戻り値を適切に区別し、より厳密な型チェックを適用するようになります。
コミット
- コミットハッシュ:
1d3ca9236e93c8e0ba3cd7f14b758fc2c791ad34
- Author: Rémy Oudompheng oudomphe@phare.normalesup.org
- Date: Thu Feb 16 23:42:19 2012 +0100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d3ca9236e93c8e0ba3cd7f14b758fc2c791ad34
元コミット内容
cmd/gc: correctly typecheck expression lists in returns.
Invalid return statements were accidentally compiling or
triggering internal errors.
Fixes #3044.
R=golang-dev, rsc
CC=golang-dev, remy
https://golang.org/cl/5673074
変更の背景
Go言語では、関数が複数の値を返すことができます。例えば、func foo() (int, string)
のように定義された関数は、整数と文字列の2つの値を返します。return
ステートメントでは、これらの戻り値の型と数が関数の定義と一致している必要があります。
このコミットが修正する問題は、Goコンパイラ(cmd/gc
)の型チェックロジックに存在していました。具体的には、return
ステートメントで複数の式が指定された場合(例: return Two(), 0
のように、Two()
が複数の値を返す関数である場合)、コンパイラがその式のリストを正しく型チェックできていませんでした。これにより、以下のような問題が発生していました。
- 不正なコードのコンパイル: 本来であればコンパイルエラーとなるべき、関数の戻り値の型や数と一致しない
return
ステートメントが、誤ってコンパイルされてしまうケースがありました。 - 内部コンパイラエラー (ICE): 特定の不正な
return
ステートメントが、コンパイラの内部で予期せぬエラー(パニックなど)を引き起こし、コンパイルプロセスが中断してしまうことがありました。
これらの問題は、Go言語の型安全性を損ない、開発者が予期せぬランタイムエラーに遭遇する可能性がありました。特に、Issue 3044として報告されたバグは、このような状況を具体的に示していました。このコミットは、これらの問題を解決し、コンパイラの堅牢性と正確性を向上させることを目的としています。
前提知識の解説
Goコンパイラ (cmd/gc
)
Go言語の公式コンパイラは、通常 gc
と呼ばれます。cmd/gc
は、Goソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成などの段階が含まれます。このコミットで変更される typecheck.c
は、コンパイルプロセスの「型チェック」フェーズを担当する部分です。
型チェック (Type Checking)
型チェックは、プログラムが型システム(Go言語の場合は静的型付け)の規則に従っていることを検証するプロセスです。これにより、型に関するエラー(例: 整数型変数に文字列を代入しようとするなど)をコンパイル時に検出できます。Go言語では、関数の引数、戻り値、変数への代入など、あらゆる場所で厳密な型チェックが行われます。
Go言語の多値戻り値 (Multiple Return Values)
Go言語の大きな特徴の一つは、関数が複数の値を返すことができる点です。これはエラーハンドリングや、複数の関連する結果を一度に返す際に非常に便利です。 例:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
この機能は強力ですが、コンパイラは return
ステートメントで提供される値の数と型が、関数の定義と正確に一致していることを保証する必要があります。
typecheck.c
と typechecklist
src/cmd/gc/typecheck.c
は、Goコンパイラの型チェックロジックの主要部分を実装しているC言語のファイルです。このファイルには、Goプログラムの様々な構文要素(式、ステートメント、関数呼び出しなど)の型を検証するための関数が含まれています。
typechecklist
は、このファイル内で定義されている重要な関数の一つで、式のリスト(例えば、関数呼び出しの引数リストや return
ステートメントの戻り値リスト)の型チェックを行います。この関数は、型チェックの際に適用すべきルールを示すフラグ(例: Erv
、Efnstruct
)を受け取ります。
Erv
: "expression list for return value" の略で、戻り値の式リストであることを示します。Efnstruct
: "function structure" の略で、関数の戻り値の構造(複数の戻り値を持つ場合など)を考慮した型チェックが必要であることを示します。
技術的詳細
このコミットの技術的な核心は、return
ステートメントにおける式のリストの型チェックロジックを改善することにあります。以前のコンパイラでは、return
ステートメントの式のリストに対して、常に Erv | Efnstruct
というフラグを付けて typechecklist
を呼び出していました。これは、単一の戻り値を持つ関数と複数の戻り値を持つ関数の両方に対して、一律に「関数の構造を考慮した戻り値の式リスト」として扱っていたことを意味します。
しかし、この一律の扱いは、特に複数の値を返す関数呼び出しが return
ステートメント内で単一の式として扱われるべき場合に問題を引き起こしました。例えば、func Two() (a, b int)
のような関数があり、func F() (x interface{}, y int)
の中で return Two(), 0
と記述された場合、Two()
は2つの値を返しますが、F
の戻り値の型は (interface{}, int)
であり、Two()
の戻り値が x
に、0
が y
にそれぞれ対応すると解釈されるべきです。しかし、Two()
自体が単一の式として扱われるべき文脈(例えば、単一の戻り値しか期待されない場所)では、Two()
のような多値関数呼び出しはエラーとなるべきです。
このコミットでは、src/cmd/gc/typecheck.c
の ORETURN
ケースにおいて、return
ステートメントの式のリストに含まれる要素の数をチェックするロジックが追加されました。
変更前:
typechecklist(n->list, Erv | Efnstruct);
変更後:
if(count(n->list) == 1)
typechecklist(n->list, Erv | Efnstruct);
else
typechecklist(n->list, Erv);
この変更のポイントは以下の通りです。
count(n->list) == 1
の条件:return
ステートメントの式のリスト(n->list
)に含まれる要素が1つだけの場合、以前と同様にErv | Efnstruct
フラグを使用してtypechecklist
を呼び出します。これは、単一の式が返される場合(例:return x
)や、多値関数呼び出しが単一の式として扱われるべき文脈(例:return Two()
の結果が単一のインターフェース値に代入される場合など)に対応します。else
ブロック: 式のリストに含まれる要素が1つではない場合(つまり、複数の式が明示的に指定されている場合、例:return x, y
)、Erv
フラグのみを使用してtypechecklist
を呼び出します。Efnstruct
フラグが取り除かれることで、コンパイラは「関数の構造を考慮した」型チェックではなく、より一般的な「戻り値の式リスト」としての型チェックを行います。これにより、多値関数呼び出しが単一の戻り値しか期待されない文脈で使われた場合に、正しくエラーを検出できるようになります。
この修正により、test/fixedbugs/bug418.go
で示されているような、多値関数呼び出しが単一の戻り値しか期待されない場所で使用された場合に「single-value context」エラーが正しく報告されるようになり、また、再帰的な呼び出しが内部コンパイラエラーを引き起こす問題も解決されました。
コアとなるコードの変更箇所
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -1410,7 +1410,10 @@ reswitch:
case ORETURN:
ok |= Etop;
- typechecklist(n->list, Erv | Efnstruct);
+ if(count(n->list) == 1)
+ typechecklist(n->list, Erv | Efnstruct);
+ else
+ typechecklist(n->list, Erv);
if(curfn == N) {
yyerror("return outside function");
goto error;
コアとなるコードの解説
上記のコードスニペットは、Goコンパイラの src/cmd/gc/typecheck.c
ファイル内の ORETURN
(return
ステートメントを表すASTノード)を処理する部分の変更を示しています。
case ORETURN:
: これは、現在処理している抽象構文木(AST)ノードがreturn
ステートメントであることを示します。ok |= Etop;
: これは、現在のコンテキストがトップレベルのステートメントであることを示すフラグを設定しています。if(count(n->list) == 1)
: ここが今回の修正の核心です。n->list
はreturn
ステートメントの後に続く式のリストを表します。count(n->list)
はそのリストに含まれる式の数を返します。- もし式の数が1つであれば(例:
return x
またはreturn Two()
のように、Two()
が多値を返す関数であっても、return
の直後には1つの式しか書かれていない場合)、typechecklist(n->list, Erv | Efnstruct);
が呼び出されます。Erv | Efnstruct
は、戻り値の式リストであり、かつ関数の構造(多値戻り値など)を考慮して型チェックを行う必要があることをコンパイラに伝えます。 - もし式の数が1つでなければ(例:
return x, y
のように、複数の式がカンマで区切られて明示的に指定されている場合)、typechecklist(n->list, Erv);
が呼び出されます。この場合、Efnstruct
フラグが取り除かれています。これは、コンパイラが個々の式を独立した戻り値として扱い、それぞれの型が関数の定義と一致するかどうかをチェックすることを意味します。これにより、例えばfunc F() (x interface{}, y int)
の中でreturn Two(), 0
と書かれた場合に、Two()
が単一のinterface{}
型に変換され、0
がint
型に変換されるという、Go言語の多値戻り値のセマンティクスに沿った正しい型チェックが行われるようになります。
- もし式の数が1つであれば(例:
if(curfn == N) { yyerror("return outside function"); goto error; }
: これは、return
ステートメントが関数の外で使用されていないかをチェックする既存のロジックです。
この変更により、コンパイラは return
ステートメントの文脈に応じて、より適切な型チェックルールを適用できるようになり、Issue 3044で報告されたような不正なコードのコンパイルや内部エラーの発生を防ぐことができます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/1d3ca9236e93c8e0ba3cd7f14b758fc2c791ad34
- Go Gerrit Change-List: https://golang.org/cl/5673074
- Go Issue 3044 (関連するバグ報告): https://go.dev/issue/3044 (このコミットが修正した具体的なGoコンパイラのバグ報告)
参考にした情報源リンク
- https://h-da.de/fileadmin/personal/f.fischer/go-compiler-internals/go-compiler-internals.pdf (Goコンパイラの内部に関する資料、Issue 3044が言及されている)
- Go言語の公式ドキュメント (多値戻り値、型システムに関する一般的な情報)
- Go言語のソースコード (
src/cmd/gc/typecheck.c
の周辺コード) - Go言語のテストコード (
test/fixedbugs/bug418.go
)