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

[インデックス 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つの形式があります。

  1. 式を伴う 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")
    }
    
  2. 式を伴わない 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: のように、iinterface{} 型の変数であるときに発生していました。この i の値が true であるかどうかを評価する際に、コンパイラが bool 型の trueinterface{} 型に正しく変換するコードを生成できていなかったのです。

修正は主に以下の2点です。

  1. exprbsw 関数内の OCONVIFACE チェックの追加: exprbsw 関数内で、assignop 関数を使って型変換の必要性をチェックしています。assignop は、ある型から別の型への代入が可能かどうか、またその際にどのような操作(変換など)が必要かを判断するコンパイラ内部の関数です。 OCONVIFACE は、インターフェース型への変換が必要であることを示す内部的なオペレーションコードです。 追加されたコードは、n->left->typecase 節の式の型)と exprname->typeswitch 式の型、式を伴わない場合は暗黙の true の型)の間で OCONVIFACE 変換が必要な場合、goto snorm; を実行して通常の処理フローにジャンプさせます。これにより、インターフェース変換が適切に行われるパスを通るようになります。

  2. 式を伴わない switchexprname の初期化: exprswitch 関数は switch ステートメント全体を処理します。式を伴わない switch の場合、sw->ntestnil になります。このとき、exprnameswitch 式の評価結果を保持する一時変数)が nodbool(arg == Strue) で初期化されるようになりました。nodbool はブール値のリテラルノードを作成する関数です。これにより、暗黙の true がコンパイラ内部で正しくブール値として表現され、その後のインターフェース変換処理に引き渡されるようになります。

これらの変更により、コンパイラは bool 型の trueinterface{} 型に変換する際に必要な内部的な処理(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 の変更点

  1. 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 のような通常の比較ノードを生成する汎用的なパスです。これにより、ブール値からインターフェースへの変換が必要な特殊なケースでも、コンパイラが適切な比較ロジックを生成できるようになります。

  2. snorm: ラベルの追加: default: の直前に snorm: ラベルが追加されました。これは、上記の goto snorm; のジャンプ先となります。この変更自体は機能的なものではなく、コードの構造を調整したものです。

  3. exprswitch 関数内の else ブロック:

    } else {
        exprname = nodbool(arg == Strue);
    }
    

    exprswitch 関数は switch ステートメント全体の処理を統括します。 if (sw->ntest != nil) の条件は、switch ステートメントに明示的な式があるかどうかを判断します。 else ブロックは、式を伴わない switch(タグなしswitch)の場合に実行されます。 この else ブロック内で、exprnameswitch 式の評価結果を保持する一時変数)が nodbool(arg == Strue) で初期化されるようになりました。 argStruetrue を意味する内部定数)と比較され、その結果に基づいてブール値のリテラルノードが作成され、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言語の switch ステートメントに関する公式ドキュメントやチュートリアル
  • Go言語のインターフェースに関する公式ドキュメントやチュートリアル
  • Goコンパイラのソースコード (src/cmd/gc/)
  • golang issue 3980 でのWeb検索結果(現在のGoリポジトリでは該当するIssueが見つからなかったことを確認)