[インデックス 13841] ファイルの概要
このコミットは、Goコンパイラ (cmd/gc) におけるバグ修正を目的としています。具体的には、式を持たない switch ステートメント(いわゆる「タグなしswitch」)において、暗黙的に評価される true のブール値がインターフェース型に正しく変換されないために発生していたコード生成エラーを修正します。
コミット
commit 551e26382385a91cbe9cfc94b1327d29f030f254
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Mon Sep 17 21:29:10 2012 +0200
cmd/gc: add missing conversion from bool to interface in switches.
In switches without an expression, the compiler would not convert the implicit true to an interface, causing codegen errors.
Fixes #3980.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6497147
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/551e26382385a91cbe9cfc94b1327d29f030f254
元コミット内容
cmd/gc: add missing conversion from bool to interface in switches.
In switches without an expression, the compiler would not convert the implicit true to an interface, causing codegen errors.
Fixes #3980.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6497147
変更の背景
Go言語の switch ステートメントには、式を伴わない形式があります。この形式では、各 case 節の条件式が true と評価されるかどうかで分岐が行われます。例えば、switch { case x > 0: ... } のような記述です。
このコミットが修正する問題は、このような式を伴わない switch ステートメントにおいて、コンパイラが暗黙的に true と評価されるブール値をインターフェース型に変換する処理を適切に行っていなかったことに起因します。特に、switch i := interface{}(true); { case i: ... } のように、true がインターフェース型として扱われる必要がある場合に、コンパイラが正しいコードを生成できず、実行時エラーや予期せぬ動作を引き起こしていました。
コミットメッセージにある Fixes #3980 は、この問題がGoのIssueトラッカーで報告されていたことを示しています。ただし、現在のGoのGitHubリポジトリではこのIssue番号は存在しないため、古いIssueトラッカーの番号であるか、既に統合・クローズされたものと考えられます。
前提知識の解説
Go言語の switch ステートメント
Go言語の switch ステートメントには主に2つの形式があります。
-
式を伴う
switch(Expression Switch):switchキーワードの後に式が続き、その式の値と各case節の値を比較します。x := 10 switch x { case 5: fmt.Println("x is 5") case 10: fmt.Println("x is 10") default: fmt.Println("x is something else") } -
式を伴わない
switch(Expressionless Switch / Tagless Switch):switchキーワードの後に式がなく、各case節の条件式がブール値として評価されます。最初にtrueと評価されたcase節が実行されます。これは一連のif-else if-elseステートメントの糖衣構文(シンタックスシュガー)と考えることができます。x := 10 switch { case x < 0: fmt.Println("x is negative") case x == 0: fmt.Println("x is zero") case x > 0: fmt.Println("x is positive") // これが実行される }このコミットで問題となっていたのは、この「式を伴わない
switch」の内部処理です。
Go言語のインターフェース
Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは「暗黙的」に満たされます。つまり、ある型がインターフェースで定義されたすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。
インターフェース型の変数は、任意の基底型(スカラー型、構造体、関数など)の値を保持できます。このとき、値はインターフェース型に「変換」されます。内部的には、インターフェース値は、保持している値の型情報と値自体の2つのポインタ(または値)で構成されます。
このコミットの文脈では、bool 型の値(true)が interface{}(空インターフェース、任意の値を保持できるインターフェース)型に変換される際のコンパイラの挙動が問題となっていました。
Goコンパイラ (cmd/gc)
cmd/gc は、Go言語の公式コンパイラの一部であり、Goソースコードを機械語に変換する役割を担っています。コンパイラは、構文解析、型チェック、中間コード生成、最適化、最終的なコード生成といった複数のフェーズを経て処理を行います。
このコミットの変更は、コンパイラの型チェックおよび中間コード生成フェーズ、特に switch ステートメントの処理ロジック (src/cmd/gc/swt.c) に関連しています。
技術的詳細
このコミットの核心は、Goコンパイラの src/cmd/gc/swt.c ファイルにおける switch ステートメントの処理ロジックの修正です。
swt.c は、Goの switch ステートメントのコンパイルを担当する部分です。特に exprbsw 関数は、式を伴う switch の各 case 節の条件を処理します。しかし、式を伴わない switch の場合でも、内部的には true という暗黙の式が扱われることがあります。
問題は、case 節の式がインターフェース型である場合、例えば case i: のように、i が interface{} 型の変数であるときに発生していました。この i の値が true であるかどうかを評価する際に、コンパイラが bool 型の true を interface{} 型に正しく変換するコードを生成できていなかったのです。
修正は主に以下の2点です。
-
exprbsw関数内のOCONVIFACEチェックの追加:exprbsw関数内で、assignop関数を使って型変換の必要性をチェックしています。assignopは、ある型から別の型への代入が可能かどうか、またその際にどのような操作(変換など)が必要かを判断するコンパイラ内部の関数です。OCONVIFACEは、インターフェース型への変換が必要であることを示す内部的なオペレーションコードです。 追加されたコードは、n->left->type(case節の式の型)とexprname->type(switch式の型、式を伴わない場合は暗黙のtrueの型)の間でOCONVIFACE変換が必要な場合、goto snorm;を実行して通常の処理フローにジャンプさせます。これにより、インターフェース変換が適切に行われるパスを通るようになります。 -
式を伴わない
switchのexprnameの初期化:exprswitch関数はswitchステートメント全体を処理します。式を伴わないswitchの場合、sw->ntestはnilになります。このとき、exprname(switch式の評価結果を保持する一時変数)がnodbool(arg == Strue)で初期化されるようになりました。nodboolはブール値のリテラルノードを作成する関数です。これにより、暗黙のtrueがコンパイラ内部で正しくブール値として表現され、その後のインターフェース変換処理に引き渡されるようになります。
これらの変更により、コンパイラは bool 型の true を interface{} 型に変換する際に必要な内部的な処理(OCONVIFACE オペレーション)を認識し、適切なコードを生成できるようになりました。
テストファイル test/switch.go に追加されたテストケースは、この修正が正しく機能することを確認するためのものです。特に、switch i := interface{}(true); { case i: ... } のようなケースが追加され、以前は壊れていた(was broken: see issue 3980 とコメントされている)挙動が修正されたことを検証しています。
コアとなるコードの変更箇所
src/cmd/gc/swt.c の変更点:
--- a/src/cmd/gc/swt.c
+++ b/src/cmd/gc/swt.c
@@ -442,6 +442,10 @@ exprbsw(Case *c0, int ncase, int arg)
n = c0->node;
lno = setlineno(n);
+ if(assignop(n->left->type, exprname->type, nil) == OCONVIFACE ||
+ assignop(exprname->type, n->left->type, nil) == OCONVIFACE)
+ goto snorm;
+
switch(arg) {
case Strue:
a = nod(OIF, N, N);
@@ -457,6 +461,7 @@ exprbsw(Case *c0, int ncase, int arg)
break;
default:
+ snorm:
a = nod(OIF, N, N);
a->ntest = nod(OEQ, exprname, n->left); // if name == val
typecheck(&a->ntest, Erv);
@@ -520,6 +525,8 @@ exprswitch(Node *sw)
exprname = temp(sw->ntest->type);
cas = list1(nod(OAS, exprname, sw->ntest));
typechecklist(cas, Etop);
+ } else {
+ exprname = nodbool(arg == Strue);
}
c0 = mkcaselist(sw, arg);
test/switch.go の変更点:
--- a/test/switch.go
+++ b/test/switch.go
@@ -294,6 +294,17 @@ func main() {
assert(false, `i should be "hello"`)
}
+ // switch on implicit bool converted to interface
+ // was broken: see issue 3980
+ switch i := interface{}(true); {
+ case i:
+ assert(true, "true")
+ case false:
+ assert(false, "i should be true")
+ default:
+ assert(false, "i should be true")
+ }
+
// switch on array.
switch ar := [3]int{1, 2, 3}; ar {
case [3]int{1,2,3}:
コアとなるコードの解説
src/cmd/gc/swt.c の変更点
-
exprbsw関数内のif文とgoto snorm;:if(assignop(n->left->type, exprname->type, nil) == OCONVIFACE || assignop(exprname->type, n->left->type, nil) == OCONVIFACE) goto snorm;このコードは、
case節の式 (n->left) の型と、switch式の評価結果を保持する一時変数 (exprname) の型との間で、インターフェース変換 (OCONVIFACE) が必要かどうかをチェックしています。assignopはコンパイラ内部の関数で、型間の代入互換性や必要な変換の種類を判断します。 もしどちらかの方向でインターフェース変換が必要な場合、処理はsnormラベルにジャンプします。snormラベル以降のコードは、if name == valのような通常の比較ノードを生成する汎用的なパスです。これにより、ブール値からインターフェースへの変換が必要な特殊なケースでも、コンパイラが適切な比較ロジックを生成できるようになります。 -
snorm:ラベルの追加:default:の直前にsnorm:ラベルが追加されました。これは、上記のgoto snorm;のジャンプ先となります。この変更自体は機能的なものではなく、コードの構造を調整したものです。 -
exprswitch関数内のelseブロック:} else { exprname = nodbool(arg == Strue); }exprswitch関数はswitchステートメント全体の処理を統括します。if (sw->ntest != nil)の条件は、switchステートメントに明示的な式があるかどうかを判断します。elseブロックは、式を伴わないswitch(タグなしswitch)の場合に実行されます。 このelseブロック内で、exprname(switch式の評価結果を保持する一時変数)がnodbool(arg == Strue)で初期化されるようになりました。argはStrue(trueを意味する内部定数)と比較され、その結果に基づいてブール値のリテラルノードが作成され、exprnameに割り当てられます。これにより、式を伴わないswitchの内部で暗黙的に扱われるtrueが、コンパイラ内部で明示的なブール値として表現され、その後の型チェックやコード生成フェーズで正しく処理されるようになります。
test/switch.go の変更点
// switch on implicit bool converted to interface
// was broken: see issue 3980
switch i := interface{}(true); {
case i:
assert(true, "true")
case false:
assert(false, "i should be true")
default:
assert(false, "i should be true")
}
このテストケースは、修正されたバグを直接的に検証するものです。
switch i := interface{}(true); の部分で、true というブール値が interface{} 型の変数 i に代入されています。
その後の case i: は、i の値が true と評価されるかどうかをチェックします。
修正前は、この true がインターフェース型に変換される際に問題が発生し、case i: が正しく評価されなかったり、コード生成エラーになったりしていました。
このテストケースが assert(true, "true") を通ることで、コンパイラが bool から interface{} への暗黙的な変換を正しく処理し、switch ステートメントが期待通りに動作することが確認されます。
関連リンク
- Go CL (Change List): https://golang.org/cl/6497147
参考にした情報源リンク
- Go言語の
switchステートメントに関する公式ドキュメントやチュートリアル - Go言語のインターフェースに関する公式ドキュメントやチュートリアル
- Goコンパイラのソースコード (
src/cmd/gc/) golang issue 3980でのWeb検索結果(現在のGoリポジトリでは該当するIssueが見つからなかったことを確認)- https://github.com/grpc/grpc-go/issues/3980 (関連なし)
- https://github.com/fatedier/frp/issues/3980 (関連なし)
- このIssue番号は、GoのIssueトラッカーがGoogle CodeからGitHubに移行する前の古い番号である可能性が高いです。