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

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

このコミットは、Goコンパイラの型スイッチにおける静的なimplementsチェックの挙動を修正するものです。具体的には、型スイッチのcase句でインターフェース型が指定された場合に、コンパイラが誤って「到達不可能な型スイッチケース」と判断してしまうバグ(Issue 2700)を修正しています。この変更により、コンパイラは具象型(concrete type)に対してのみimplementsチェックを適用するようになります。

コミット

commit 0e919ff2c978294c9b0472055b96bb1a09606934
Author: Luuk van Dijk <lvd@golang.org>
Date:   Tue Jan 24 13:53:00 2012 +0100

    gc: static implements check on typeswitches only applies to concrete case types.

    Fixes #2700.

    R=rsc
    CC=golang-dev
    https://golang.org/cl/5574046

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

https://github.com/golang/go/commit/0e919ff2c978294c9b0472055b96bb1a09606934

元コミット内容

gc: static implements check on typeswitches only applies to concrete case types.

Fixes #2700.

R=rsc
CC=golang-dev
https://golang.org/cl/5574046

変更の背景

Go言語のコンパイラ(gc)は、型スイッチ(type switch)のcase句において、指定された型がスイッチ対象のインターフェース型を実装しているかどうかを静的にチェックする機能を持っています。このチェックは、例えばinterface{}型の変数に対してcase string:のような具象型を指定した場合に、その具象型がインターフェースを実装しているか(この場合は常に真)を検証し、もし実装していない場合はコンパイル時に「impossible type switch case」(到達不可能な型スイッチケース)というエラーを出すことで、開発者に潜在的なバグを知らせる役割があります。

しかし、この静的チェックには問題がありました。case句でインターフェース型が指定された場合にも、コンパイラが誤ってimplementsチェックを適用してしまい、結果として不適切なエラーを報告することがありました。

具体的には、Issue 2700で報告された問題は、以下のようなシナリオで発生しました。

package main

import (
	"io"
)

func main() {
	var r io.Reader

	// この行はコンパイルエラーにならない
	_, _ = r.(io.Writer) // rがio.Writerインターフェースを実装しているかどうかの型アサーション

	switch r.(type) {
	case io.Writer: // ここでコンパイルエラーが発生していた
		// rがio.Writerインターフェースを実装している場合に実行される
	}
}

io.Reader型の変数rに対して、io.Writerインターフェースへの型アサーション(r.(io.Writer))はGoの仕様上合法であり、実行時にrio.Writerを実装していれば成功し、そうでなければパニックまたは第二戻り値でfalseが返されます。しかし、同じロジックを型スイッチのcase io.Writer:として記述すると、コンパイラが「io.Readerio.Writerを実装できない」と誤解し、「impossible type switch case」エラーを出力していました。

これは、インターフェース型はそれ自体がメソッドセットを持つため、別のインターフェース型を「実装する」という概念が具象型とは異なるためです。インターフェース型Aがインターフェース型Bを実装するということは、AのメソッドセットがBのメソッドセットを完全に含むことを意味します。この関係は実行時に動的に評価されるべきであり、静的なimplementsチェックの対象とはなりませんでした。

このコミットは、この誤った静的チェックの適用範囲を修正し、型スイッチのcase句が具象型である場合にのみimplementsチェックを行うようにすることで、この問題を解決することを目的としています。

前提知識の解説

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

  1. インターフェース (Interfaces):

    • Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。
    • ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを「実装している」とみなされます(暗黙的な実装)。
    • インターフェース型の変数は、そのインターフェースを実装する任意の具象型の値を保持できます。
    • インターフェース型は、それ自体がメソッドセットを持つため、別のインターフェース型を「実装する」という概念も存在します。これは、あるインターフェースのメソッドセットが別のインターフェースのメソッドセットを完全に含む場合に成立します。
  2. 型スイッチ (Type Switch):

    • switch x.(type)構文は、インターフェース型の変数が保持している具象型に基づいて異なるコードパスを実行するために使用されます。
    • case T:Tは、具象型(string, int, MyStructなど)またはインターフェース型(io.Reader, errorなど)のいずれかを指定できます。
    • コンパイラは、型スイッチのcase句が到達可能かどうかを静的に分析しようとします。
  3. 型アサーション (Type Assertion):

    • x.(T)構文は、インターフェース型の変数xが特定の型Tを保持しているかどうかをチェックし、もしそうであればその型Tの値として抽出するために使用されます。
    • Tが具象型の場合、xがその具象型を保持していなければパニックが発生するか、ok変数がfalseになります(v, ok := x.(T)の場合)。
    • Tがインターフェース型の場合、xがそのインターフェース型を実装していれば成功します。これは、xが保持する具象型がTのメソッドセットをすべて実装している場合に真となります。
  4. Goコンパイラ (gc):

    • Goの公式コンパイラはgcと呼ばれます。
    • コンパイラは、ソースコードを解析し、構文チェック、型チェック、最適化などを行い、実行可能なバイナリを生成します。
    • 型チェックの段階で、静的に検出可能なエラー(例: 未定義の変数、型不一致、到達不可能なコードパス)を報告します。
  5. implements関数 (コンパイラ内部):

    • Goコンパイラの内部には、ある型が別のインターフェースを実装しているかどうかをチェックするimplementsのような関数が存在します。
    • この関数は、主に具象型がインターフェースを実装しているかを静的に判断するために使用されます。

このコミットの核心は、コンパイラがimplementsチェックを適用する際に、case句の型が「具象型」であるか「インターフェース型」であるかを適切に区別する必要があるという点です。インターフェース型に対するimplementsチェックは、具象型に対するそれとは異なるセマンティクスを持ち、静的に「不可能」と判断すべきではないケースが存在します。

技術的詳細

このコミットの技術的詳細は、Goコンパイラの型チェックフェーズにおけるimplementsチェックのロジックにあります。

Goコンパイラのsrc/cmd/gc/swt.cファイルは、型スイッチの処理を担当する部分です。typecheckswitch関数は、型スイッチの各case句を型チェックする際に呼び出されます。

変更前のコードでは、typecheckswitch関数内で、case句の型(ll->n->type)がスイッチ対象のインターフェース型(t)を実装しているかどうかをimplements関数でチェックしていました。このチェックは、case string:のように具象型が指定された場合には適切に機能し、例えばinterface{}型の変数に対してcase string:と書かれた場合、string型はinterface{}を常に実装するため問題ありません。しかし、もしcase句の型がスイッチ対象のインターフェース型を実装できない具象型であれば、「impossible type switch case」エラーが報告されます。

問題は、case io.Writer:のようにcase句にインターフェース型が指定された場合にも、このimplementsチェックがそのまま適用されてしまっていた点です。io.Reader型の変数がio.Writerインターフェースを実装しているかどうかは、そのio.Readerが保持している具象型に依存します。例えば、io.Reader*bytes.Bufferを保持している場合、*bytes.Bufferio.Writerも実装しているため、case io.Writer:は到達可能です。しかし、io.Reader*os.Fileを保持している場合、*os.Fileio.Writerを実装していないため、case io.Writer:は到達不可能です。

このように、インターフェース型に対する型スイッチのcaseは、そのインターフェースが保持する動的な具象型によって到達可能性が変化するため、静的に「不可能」と断定することはできませんでした。

このコミットでは、この問題を解決するために、implements関数を呼び出す前にll->n->type->etype != TINTERという条件を追加しています。

  • ll->n->type:型スイッチのcase句で指定された型。
  • etype:Goコンパイラ内部で型を表す列挙型(TINTERはインターフェース型を意味します)。

この条件は、「case句の型がインターフェース型ではない場合(つまり、具象型である場合)にのみ、implementsチェックを実行する」という意味になります。

もしcase句の型がインターフェース型(TINTER)であれば、implementsチェックはスキップされます。これにより、コンパイラはインターフェース型に対する型スイッチのcaseを静的に「不可能」と誤って判断することがなくなり、実行時の動的な型チェックに委ねられるようになります。

テストファイルtest/typeswitch3.goの変更は、この修正の意図を明確にするものです。変更前は、case io.Writer:の行でコンパイルエラーが発生することを期待していましたが、修正後はエラーが発生しないことを期待するようになっています。これは、インターフェース型に対する型スイッチのcaseは、静的に「不可能」と判断されるべきではないという新しい挙動を反映しています。

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

src/cmd/gc/swt.c

--- a/src/cmd/gc/swt.c
+++ b/src/cmd/gc/swt.c
@@ -889,7 +889,7 @@ typecheckswitch(Node *n)
 					tyyerror("%lN is not a type", ll->n);
 					// reset to original type
 					ll->n = n->ntest->right;
-				} else if(!implements(ll->n->type, t, &missing, &have, &ptr)) {
+				} else if(ll->n->type->etype != TINTER && !implements(ll->n->type, t, &missing, &have, &ptr)) {
 					if(have && !missing->broke && !have->broke)
 						yyerror("impossible type switch case: %lN cannot have dynamic type %T"
 							" (wrong type for %S method)\n\thave %S%hT\n\twant %S%hT",

test/typeswitch3.go

--- a/test/typeswitch3.go
+++ b/test/typeswitch3.go
@@ -6,15 +6,30 @@
 
 package main
 
+import (
+	"io"
+)
+
 
 type I interface {
-       M()
+	M()
 }
 
 func main(){
-       var x I
-       switch x.(type) {
-       case string:	// ERROR "impossible"
-               println("FAIL")
-       }
+	var x I
+	switch x.(type) {
+	case string:	// ERROR "impossible"
+		println("FAIL")
+	}
+	
+	// Issue 2700: if the case type is an interface, nothing is impossible
+	
+	var r io.Reader
+	
+	_, _ = r.(io.Writer)
+	
+	switch r.(type) {
+	case io.Writer:
+	}
 }
+
+

コアとなるコードの解説

src/cmd/gc/swt.c の変更

変更の中心は、typecheckswitch関数内の以下の行です。

-				} else if(!implements(ll->n->type, t, &missing, &have, &ptr)) {
+				} else if(ll->n->type->etype != TINTER && !implements(ll->n->type, t, &missing, &have, &ptr)) {
  • ll->n->type: これは、型スイッチのcase句で指定された型を表すコンパイラ内部の型オブジェクトです。
  • t: これは、型スイッチの対象となっているインターフェース変数の型(例: var x II)を表すコンパイラ内部の型オブジェクトです。
  • implements(ll->n->type, t, ...): この関数は、ll->n->typetを実装しているかどうかを静的にチェックします。もし実装していない場合、falseを返します。
  • ll->n->type->etype != TINTER: この新しい条件が追加されました。etypeはGoコンパイラ内部で型を分類するための列挙型であり、TINTERはインターフェース型を示します。
    • この条件は、「case句の型がインターフェース型ではない場合」を意味します。
    • つまり、case句の型が具象型(string, int, 構造体など)である場合にのみ、続くimplementsチェックが実行されます。
    • もしcase句の型がインターフェース型(例: io.Writer)であれば、この条件はfalseとなり、implements関数は呼び出されず、静的な「impossible」エラーの判定がスキップされます。

この変更により、コンパイラはインターフェース型がcase句に指定された場合に、その到達可能性を静的に判断しようとすることをやめ、実行時の動的な型アサーションのセマンティクスに合致するようになりました。

test/typeswitch3.go の変更

このテストファイルは、修正の意図と効果を明確にするために更新されました。

  1. import ("io") の追加: io.Readerio.Writerを使用するためにioパッケージがインポートされました。
  2. 既存のテストケースの維持:
    var x I
    switch x.(type) {
    case string:	// ERROR "impossible"
    	println("FAIL")
    }
    
    この部分は変更されていません。IインターフェースはM()メソッドを要求しますが、string型はM()メソッドを実装していません。したがって、xstring型であることは静的に不可能であり、コンパイラは引き続きERROR "impossible"を報告することが期待されます。これは、具象型に対するimplementsチェックが引き続き正しく機能することを示しています。
  3. Issue 2700 のテストケースの追加:
    // Issue 2700: if the case type is an interface, nothing is impossible
    
    var r io.Reader
    
    _, _ = r.(io.Writer)
    
    switch r.(type) {
    case io.Writer:
    }
    
    この新しいテストケースが追加されました。
    • var r io.Readerio.Reader型の変数を宣言します。
    • _, _ = r.(io.Writer):これは、io.Reader型の変数rio.Writerインターフェースを実装しているかどうかをチェックする型アサーションです。これはGoの仕様上合法であり、実行時にrが保持する具象型がio.Writerを実装していれば成功します。
    • switch r.(type) { case io.Writer: }:これがIssue 2700で問題となっていた部分です。修正前はここで「impossible type switch case」エラーが発生していましたが、修正後はエラーが発生しないことが期待されます。コメント// Issue 2700: if the case type is an interface, nothing is impossibleは、このテストケースの目的を明確に示しています。つまり、case句がインターフェース型である場合、コンパイラは静的に「不可能」と判断すべきではない、という新しい挙動を検証しています。

これらの変更により、コンパイラは型スイッチのcase句における静的な型チェックの挙動がより正確になり、Go言語のインターフェースの動的な性質と整合性が取れるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (インターフェース、型スイッチ、型アサーションに関するセクション)
  • Goコンパイラのソースコード (src/cmd/gc/swt.cの関連部分)
  • Go言語のIssueトラッカー (Issue 2700の議論)
  • Go言語のコードレビューシステム (CL 5574046の議論)
  • Go言語の型システムに関する一般的な解説記事やブログポスト