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

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

このコミットは、Go言語のコンパイラ(gc)におけるswitch文の型チェックメカニズムを改善し、より堅牢なエラー検出と柔軟な型アサーションを可能にするものです。特に、インターフェース値に対するswitch文での型チェックの厳密化と、型スイッチにおける早期の静的チェックに焦点を当てています。

コミット

commit 13e92e4d7542ac65a7efb33778f752403c5ac014
Author: Luuk van Dijk <lvd@golang.org>
Date:   Wed Nov 9 10:58:53 2011 +0100

    gc: Better typechecks and errors in switches.
    
    Allow any type in switch on interface value.
    Statically check typeswitch early.
    
    Fixes #2423.
    Fixes #2424.
    
    R=rsc, dsymonds
    CC=golang-dev
    https://golang.org/cl/5339045

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

https://github.com/golang/go/commit/13e92e4d7542ac65a7efb33778f752403c5ac014

元コミット内容

このコミットの元のメッセージは以下の通りです。

gc: Better typechecks and errors in switches.

Allow any type in switch on interface value. Statically check typeswitch early.

Fixes #2423. Fixes #2424.

これは、Goコンパイラ(gc)におけるswitch文の型チェックとエラー報告を改善することを目的としています。具体的には、インターフェース値に対するswitch文で任意の型を許可すること、そして型スイッチ(type switch)において早期に静的チェックを行うことを挙げています。これにより、Go言語の型システムがより堅牢になり、開発者がコンパイル時に潜在的な型不一致エラーを早期に発見できるようになります。

変更の背景

このコミットは、Go言語のswitch文、特に型スイッチ(type switch)における既存のバグと制限に対処するために導入されました。コミットメッセージに記載されているFixes #2423Fixes #2424は、それぞれ以下の問題に対応しています。

  • Issue 2423: switch on interface value allows non-assignable types in case clauses この問題は、インターフェース値に対する通常のswitch文(型スイッチではない)において、case句でテストされる値の型が、switch式の型に割り当て可能であるかどうかのチェックが不十分であったことを示しています。これにより、実行時にパニックを引き起こす可能性のあるコードがコンパイルされてしまうという問題がありました。例えば、interface{}型の変数に対してswitchを行い、case句でstring型のリテラルを指定した場合、コンパイラはエラーを報告せず、実行時に型不一致が発生する可能性がありました。

  • Issue 2424: type switch allows impossible cases この問題は、型スイッチにおいて、決して到達しない(不可能な)case句がコンパイル時に検出されなかったことを示しています。例えば、あるインターフェース型Iに対して型スイッチを行い、case句でIが実装していないメソッドを持つ型Tを指定した場合、コンパイラはエラーを報告せず、開発者が論理的な誤りに気づきにくい状況でした。これは、インターフェースのメソッドセットとcase句で指定された型のメソッドセットとの互換性チェックが不十分であったことに起因します。

これらのバグは、Go言語の型安全性を損ない、開発者が予期せぬ実行時エラーに遭遇する原因となっていました。このコミットは、これらの問題を解決し、コンパイル時にこれらの型関連のエラーをより厳密にチェックすることで、Goプログラムの信頼性と堅牢性を向上させることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語およびコンパイラに関する前提知識が必要です。

  1. Go言語のswitch: Go言語のswitch文には主に2つの形式があります。

    • 式スイッチ (Expression Switch): switchキーワードの後に式が続き、その式の値とcase句の値が比較されます。
      switch x {
      case 1: // x == 1
      case 2: // x == 2
      default:
      }
      
    • 型スイッチ (Type Switch): switchキーワードの後に型アサーション.(type)が続き、変数の動的な型に基づいて処理を分岐させます。これは主にインターフェース型に対して使用されます。
      switch v := i.(type) {
      case int:    // v は int 型
      case string: // v は string 型
      default:     // v は i の静的な型
      }
      
  2. Go言語のインターフェース: Goのインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを実装していると見なされます(暗黙的な実装)。インターフェース型の変数は、任意の基になる具体的な型の値を保持できます。

  3. Goコンパイラ (gc): gcはGo言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成などが含まれます。このコミットで変更されているsrc/cmd/gc/swt.cは、gcコンパイラのswitch文の処理と型チェックを担当する部分です。

  4. 型チェック (Type Checking): 型チェックは、プログラムが型規則に従っていることを検証するコンパイルフェーズです。これにより、型不一致などのエラーをコンパイル時に検出し、実行時エラーを防ぎます。Goは静的型付け言語であり、厳密な型チェックが行われます。

  5. assignop関数: Goコンパイラ内部の関数で、ある型が別の型に割り当て可能(assignable)であるかをチェックします。例えば、int型の値をfloat64型の変数に割り当てることは可能ですが、その逆は直接はできません。この関数は、代入、関数呼び出しの引数渡し、case句での値の比較など、様々な文脈で型の互換性を検証するために使用されます。

  6. implements関数: Goコンパイラ内部の関数で、ある型が特定のインターフェースを実装しているかどうかをチェックします。これは、インターフェースのメソッドセットと、チェック対象の型のメソッドセットを比較することで行われます。この関数は、型スイッチのcase句で指定された型が、switch式のインターフェース型によって保持されうる型であるかを検証する際に特に重要です。

これらの知識を前提として、このコミットがswitch文の型チェックロジックをどのように修正し、Goプログラムの堅牢性を高めているかを詳細に見ていきます。

技術的詳細

このコミットの技術的詳細は、主にsrc/cmd/gc/swt.cファイルのtypecheckswitch関数における変更に集約されます。この関数は、switch文の型チェックロジックを実装しており、式スイッチと型スイッチの両方に対応しています。

変更の核心は、case句の型とswitch式の型の間の互換性チェックを強化することにあります。

1. 式スイッチ (case Erv) における型チェックの改善

変更前は、式スイッチのcase句において、ll->n->type != T && !eqtype(ll->n->type, t)という条件で型が等しいかどうかのみをチェックしていました。これは、厳密な型の一致を要求するため、Goの柔軟な型変換ルール(例えば、異なる整数型間の比較や、数値型とインターフェース型間の比較など)を適切に扱えない場合がありました。

変更後、この行は以下のように修正されました。

-					else if(ll->n->type != T && !eqtype(ll->n->type, t))
-						yyerror("case %lN in %T switch", ll->n, t);
+					else if(ll->n->type != T && !assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)) {
+						if(n->ntest)
+							yyerror("invalid case %N in switch on %N (mismatched types %T and %T)", ll->n, n->ntest, ll->n->type, t);
+						else
+							yyerror("invalid case %N in switch (mismatched types %T and bool)", ll->n, n->ntest, ll->n->type, t);
+					}

この変更のポイントは以下の通りです。

  • assignop関数の導入: !eqtype(ll->n->type, t)の代わりに、!assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)が使用されています。これは、case式の型(ll->n->type)がswitch式の型(t)に割り当て可能であるか、またはその逆であるかをチェックします。これにより、厳密な型の一致だけでなく、Goの型変換ルールに基づいた互換性も考慮されるようになります。例えば、int型のswitch式に対してint32型のcase値が指定された場合でも、assignoptrueを返すため、エラーにならなくなります。これは、Issue 2423で指摘されたような、インターフェース値に対するswitchで不適切な型が許可される問題を解決する一助となります。
  • より詳細なエラーメッセージ: yyerrorのメッセージが、switch式が存在するかどうかに応じて、より具体的で分かりやすいものに変更されました。これにより、開発者は型不一致の原因を特定しやすくなります。

2. 型スイッチ (case Etype) における早期静的チェックの導入

型スイッチのcase句では、nilリテラルが特別扱いされます。それ以外のcase句については、変更前はll->n->op != OTYPE && ll->n->type != Tという条件で、case句が型ではない場合にエラーを報告していました。しかし、これはcase句で指定された型が、switch式のインターフェース型によって実際に保持されうる型であるかどうかのチェックが不十分でした。

変更後、以下のelse ifブロックが追加されました。

+					} else if(!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",\
+								n->ntest->right, ll->n->type, missing->sym, have->sym, have->type,\
+								missing->sym, missing->type);
+						else if(!missing->broke)
+							yyerror("impossible type switch case: %lN cannot have dynamic type %T"\
+								" (missing %S method)", n->ntest->right, ll->n->type, missing->sym);
+					}

この変更のポイントは以下の通りです。

  • implements関数の利用: !implements(ll->n->type, t, ...)という条件が追加されました。ここで、ll->n->typecase句で指定された型、tswitch式のインターフェース型です。implements関数は、ll->n->typetインターフェースを実装しているかどうかをチェックします。もし実装していない場合、つまりcase句で指定された型がswitch式のインターフェース型によって保持され得ない「不可能なケース」である場合、エラーが報告されます。これはIssue 2424で指摘された問題を直接解決します。
  • 詳細なエラー報告: implements関数が返す情報(missinghave)を利用して、エラーメッセージが非常に詳細になっています。
    • wrong type for %S method: case句の型がインターフェースの特定のメソッドを実装しているが、そのシグネチャがインターフェースの定義と一致しない場合に報告されます。
    • missing %S method: case句の型がインターフェースの特定のメソッドを完全に実装していない場合に報告されます。 これにより、開発者はなぜそのcaseが不可能であるのかを正確に理解できます。

3. テストファイルの変更

このコミットでは、既存のバグを修正し、新しいチェックが正しく機能することを確認するために、複数のテストファイルが変更されています。

  • test/fixedbugs/bug270.goが削除されました。これは、型スイッチにおけるインターフェースのcase句に関するテストでしたが、このコミットの変更によって不要になったか、あるいは新しいテストでより適切にカバーされるようになったためと考えられます。
  • test/fixedbugs/bug340.goが修正されました。このファイルは、型スイッチにおける誤ったcase句の型に関するテストで、エラーメッセージが新しいコンパイラの出力に合わせて更新されています。
  • test/fixedbugs/bug375.goが新規追加されました。これはIssue 2423に関連するテストで、インターフェース値に対する式スイッチで文字列リテラルが正しく扱われることを確認します。
  • test/switch3.goが新規追加されました。これは、式スイッチにおける型不一致のケースをテストし、assignopによる新しいチェックが期待通りに機能することを確認します。
  • test/typeswitch3.goが新規追加されました。これは、型スイッチにおける「不可能なケース」をテストし、implementsによる新しい静的チェックが正しくエラーを報告することを確認します。

これらの変更により、Goコンパイラはswitch文、特に型スイッチにおいて、より厳密で情報量の多い型チェックを行うようになり、開発者がコンパイル時に潜在的な問題を早期に発見できるようになりました。

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

このコミットのコアとなるコードの変更箇所は、src/cmd/gc/swt.cファイルのtypecheckswitch関数内です。

具体的には、以下の2つの主要な変更点があります。

  1. 式スイッチ (case Erv) の型チェックロジックの変更: swt.cの859行目から868行目にかけての変更です。

    --- a/src/cmd/gc/swt.c
    +++ b/src/cmd/gc/swt.c
    @@ -854,21 +854,35 @@ typecheckswitch(Node *n)
     			t = typecheck(&ll->n, Erv | Etype);
     			if(ll->n->type == T || t == T)
     				continue;
    +			setlineno(ncase);
     			switch(top) {
     			case Erv:	// expression switch
     				defaultlit(&ll->n, t);
     				if(ll->n->op == OTYPE)
     					yyerror("type %T is not an expression", ll->n->type);
    -				else if(ll->n->type != T && !eqtype(ll->n->type, t))
    -					yyerror("case %lN in %T switch", ll->n, t);
    +				else if(ll->n->type != T && !assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)) {
    +					if(n->ntest)
    +						yyerror("invalid case %N in switch on %N (mismatched types %T and %T)", ll->n, n->ntest, ll->n->type, t);
    +					else
    +						yyerror("invalid case %N in switch (mismatched types %T and bool)", ll->n, n->ntest, ll->n->type, t);
    +				}
     				break;
    

    ここで、!eqtype(ll->n->type, t)!assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)に置き換えられ、より柔軟な型互換性チェックが導入されました。

  2. 型スイッチ (case Etype) の早期静的チェックの追加: swt.cの871行目から889行目にかけての追加です。

    --- a/src/cmd/gc/swt.c
    +++ b/src/cmd/gc/swt.c
    @@ -854,21 +854,35 @@ typecheckswitch(Node *n)
     			t = typecheck(&ll->n, Erv | Etype);
     			if(ll->n->type == T || t == T)
     				continue;
    +			setlineno(ncase);
     			switch(top) {
     			case Erv:	// expression switch
     				defaultlit(&ll->n, t);
     				if(ll->n->op == OTYPE)
     					yyerror("type %T is not an expression", ll->n->type);
    -				else if(ll->n->type != T && !eqtype(ll->n->type, t))
    -					yyerror("case %lN in %T switch", ll->n, t);
    +				else if(ll->n->type != T && !assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)) {
    +					if(n->ntest)
    +						yyerror("invalid case %N in switch on %N (mismatched types %T and %T)", ll->n, n->ntest, ll->n->type, t);
    +					else
    +						yyerror("invalid case %N in switch (mismatched types %T and bool)", ll->n, n->ntest, ll->n->type, t);
    +				}
     				break;
     			case Etype:	// type switch
     				if(ll->n->op == OLITERAL && istype(ll->n->type, TNIL)) {
     					;
    -				} else if(ll->n->op != OTYPE && ll->n->type != T) {\n+\t\t\t\t\t} else if(ll->n->op != OTYPE && ll->n->type != T) {  // should this be ||?\n \t\t\t\t\t\tyyerror("%lN is not a type", ll->n);\n \t\t\t\t\t\t// reset to original type\n \t\t\t\t\t\tll->n = n->ntest->right;\n+\t\t\t\t\t} else if(!implements(ll->n->type, t, &missing, &have, &ptr)) {\n+\t\t\t\t\t\tif(have && !missing->broke && !have->broke)\n+\t\t\t\t\t\t\tyyerror("impossible type switch case: %lN cannot have dynamic type %T"\n+\t\t\t\t\t\t\t\t" (wrong type for %S method)\\n\\thave %S%hT\\n\\twant %S%hT",\n+\t\t\t\t\t\t\t\tn->ntest->right, ll->n->type, missing->sym, have->sym, have->type,\n+\t\t\t\t\t\t\t\tmissing->sym, missing->type);\n+\t\t\t\t\t\telse if(!missing->broke)\n+\t\t\t\t\t\t\tyyerror("impossible type switch case: %lN cannot have dynamic type %T"\n+\t\t\t\t\t\t\t\t" (missing %S method)", n->ntest->right, ll->n->type, missing->sym);\n     				}\n     				break;
     				}
    

    この追加されたelse ifブロック内で、implements関数が呼び出され、case句の型がswitch式のインターフェース型を実装しているかどうかがチェックされます。これにより、「不可能なケース」がコンパイル時に検出されるようになります。

これらの変更は、Goコンパイラの型チェックの厳密性を高め、より多くの型関連のエラーをコンパイル時に捕捉することを可能にしています。

コアとなるコードの解説

src/cmd/gc/swt.cは、Goコンパイラ(gc)のバックエンドの一部であり、switch文のセマンティック分析とコード生成に関連する処理を扱います。このファイル内のtypecheckswitch関数は、switch文の各case句の型が、switch式の型と互換性があるかを検証する役割を担っています。

typecheckswitch関数の役割

typecheckswitch関数は、抽象構文木(AST)上のswitchノードを受け取り、その子ノード(case句など)を走査しながら型チェックを行います。この関数は、式スイッチと型スイッチの両方を処理します。

  • top変数: switch文の種類(Ervは式スイッチ、Etypeは型スイッチ)を識別します。
  • ncase: 現在処理しているcase句のノード。
  • ll->n: case句で指定された値または型。
  • t: switch式の型。

変更点の詳細な解説

  1. 式スイッチにおけるassignopの利用: 変更前は、式スイッチのcase句の型チェックにeqtype(型が完全に等しいか)を使用していました。しかし、Go言語では、異なるが互換性のある型(例: intint32、または基になる型が同じカスタム型)間での比較や代入が可能です。eqtypeではこのような柔軟な互換性を捉えきれませんでした。

    導入された!assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)は、以下のロジックを意味します。

    • assignop(ll->n->type, t, nil): case句の型(ll->n->type)がswitch式の型(t)に割り当て可能か?
    • assignop(t, ll->n->type, nil): switch式の型(t)がcase句の型(ll->n->type)に割り当て可能か?

    この2つの条件を&&で結合し、全体を!で否定することで、「どちらの方向にも割り当て可能でない場合」にエラーを報告します。これにより、Goの型変換ルールに則ったより適切な型互換性チェックが実現され、Issue 2423のような問題(インターフェース値に対するswitchで不適切な型が許可される)が解決されます。例えば、interface{}型の変数に対するswitchstringリテラルをcaseに指定した場合、stringinterface{}に割り当て可能であるため、このチェックは通過します。しかし、int型のswitchに対してstringリテラルをcaseに指定した場合は、どちらの方向にも割り当て不可能であるため、エラーが報告されます。

  2. 型スイッチにおけるimplementsの利用: 型スイッチでは、switch式のインターフェース型が、case句で指定された動的な型を実際に保持できるかどうかが重要です。変更前は、case句が型ではない場合にエラーを出すだけでした。しかし、case句が型であったとしても、その型がswitch式のインターフェースを実装していなければ、そのcaseは決して到達しません(「不可能なケース」)。

    追加されたelse if(!implements(ll->n->type, t, &missing, &have, &ptr))ブロックは、この「不可能なケース」を検出します。

    • implements(ll->n->type, t, ...): case句の型(ll->n->type)がswitch式のインターフェース型(t)を実装しているかをチェックします。
    • missing: ll->n->typetを実装するために不足しているメソッドの情報。
    • have: ll->n->typetのメソッドに対応して持っているメソッドの情報。

    implements関数がfalseを返した場合(つまり、ll->n->typetインターフェースを実装していない場合)、コンパイラは「不可能な型スイッチケース」としてエラーを報告します。エラーメッセージは、missinghaveの情報に基づいて、どのメソッドが不足しているのか、あるいはどのメソッドの型が間違っているのかを具体的に示します。これにより、Issue 2424で指摘されたような、決して到達しないcase句がコンパイルされてしまう問題が解決され、開発者は論理的な誤りを早期に発見できるようになります。

これらの変更は、Go言語の型システムが提供する安全性をコンパイル時に最大限に活用し、実行時エラーのリスクを低減するための重要な改善です。

関連リンク

参考にした情報源リンク