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

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

このコミットは、Go言語の実験的な型チェッカーパッケージ exp/types における重要な改善を含んでいます。主に、フィールドおよびメソッドのルックアップロジックの修正、複合リテラルのチェック機能の強化、パラメータ・メソッド・フィールドチェックのクリーンアップ、そして型チェッカーからのパニックの漏洩防止に焦点を当てています。

変更されたファイルは以下の通りです。

  • src/pkg/exp/types/check.go: 型チェックの主要ロジックとエラーハンドリングに関連する変更。
  • src/pkg/exp/types/check_test.go: 型チェッカーのテストファイル。新しいテストケースの追加。
  • src/pkg/exp/types/expr.go: 式の型チェックに関連する主要ロジック。特に複合リテラルとセレクタ式の処理が大幅に修正。
  • src/pkg/exp/types/operand.go: オペランド(式の結果)の表現と操作に関連するロジック。フィールド/メソッドのルックアップロジックが変更。
  • src/pkg/exp/types/testdata/decls0.src: 宣言に関するテストデータ。エラーメッセージの調整。
  • src/pkg/exp/types/testdata/decls3.src: 新しく追加された宣言に関するテストデータ。特に埋め込み型とフィールドの競合に関するテスト。
  • src/pkg/exp/types/testdata/expr3.src: 式に関するテストデータ。複合リテラルに関する新しいテストケースの追加。
  • src/pkg/exp/types/types.go: 型システムの基本定義。構造体のフィールドインデックスルックアップメソッドの追加。

コミット

commit d7b0271065b043487c98e42617ff2ab53cfbdbed
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Nov 26 12:49:04 2012 -0800

    exp/types: fixed field/method lookup
    
    also:
    
    - composite literal checking close to complete
    - cleaned up parameter, method, field checking
    - don't let panics escape type checker
    - more TODOs eliminated
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/6816083

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

https://github.com/golang/go/commit/d7b0271065b043487c98e42617ff2ab53cfbdbed

元コミット内容

exp/types: fixed field/method lookup

also:

- composite literal checking close to complete
- cleaned up parameter, method, field checking
- don't let panics escape type checker
- more TODOs eliminated

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

変更の背景

このコミットは、Go言語の型チェッカー exp/types パッケージの堅牢性と正確性を向上させることを目的としています。当時の exp/types はまだ実験的な段階であり、Go言語の仕様に厳密に準拠した型チェックを実装するための継続的な作業の一環でした。

特に、以下の点が課題となっていました。

  1. フィールドおよびメソッドのルックアップの不正確さ: 構造体の埋め込みフィールドやインターフェースの埋め込みメソッドにおいて、名前の競合(シャドーイング)や多重定義のルールが正しく処理されていない可能性がありました。Go言語の仕様では、同じレベルで同じ名前のフィールドやメソッドが複数存在する場合、それらは「競合」とみなされ、アクセスできなくなります。この挙動を正確に反映する必要がありました。
  2. 複合リテラルの不完全なチェック: 構造体、配列、スライス、マップなどの複合リテラル(T{...} の形式で値を初期化する構文)の型チェックが不完全であり、不正な構文や型不一致を見落とす可能性がありました。特に、キー付き要素とキーなし要素の混在、未知のフィールド名、重複するキーなどのエラーを検出する必要がありました。
  3. 型チェッカー内部のパニック: 型チェック処理中に予期せぬパニックが発生した場合、それが型チェッカーの外部に漏洩し、クライアントアプリケーションをクラッシュさせる可能性がありました。これは、型チェッカーがライブラリとして利用される際に、堅牢性を損なう要因となります。
  4. コードの整理とTODOの解消: 開発途上であったため、コードベースには多くのTODOコメントや改善の余地がありました。これらを解消し、コードの可読性と保守性を向上させる必要がありました。

これらの課題に対処することで、exp/types パッケージはより正確で信頼性の高い型チェッカーへと進化し、将来的にGoコンパイラの型チェック部分を置き換える基盤となることを目指していました。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語およびコンパイラ関連の概念を理解しておく必要があります。

Go言語の型システム

  • 型 (Type): Go言語における値の性質を定義するもの。int, string, struct, interface, func など。
  • 基底型 (Underlying Type): 型定義によって新しい型が作られた場合でも、その型の「実体」となる型。例えば type MyInt intMyInt の基底型は int です。型チェッカーは、多くの場合、型の互換性を判断する際に基底型を考慮します。underlying(typ) のような関数がこれに該当します。
  • 構造体 (Struct): 複数のフィールドをまとめた複合型。
    • 埋め込みフィールド (Embedded Fields): 構造体内に型名のみでフィールドを宣言することで、その型のフィールドやメソッドを「昇格」させる機能。これにより、外側の構造体から埋め込まれた型のフィールドやメソッドに直接アクセスできるようになります。
    • フィールドのルックアップルール: 埋め込みフィールドがある場合、フィールドのルックアップは多段階で行われます。同じレベルで同じ名前のフィールドが複数存在する場合、それらは競合し、アクセスできなくなります。
  • インターフェース (Interface): メソッドのシグネチャの集合を定義する型。
    • 埋め込みインターフェース (Embedded Interfaces): インターフェース内に別のインターフェースを埋め込むことで、そのインターフェースのメソッドをすべて含むことができます。
    • メソッドセット (Method Set): 特定の型が実装するメソッドの集合。型チェッカーは、インターフェースの実装をチェックする際にメソッドセットを比較します。ポインタレシーバと値レシーバのメソッドセットの違いも重要です。
  • 関数シグネチャ (Function Signature): 関数のパラメータと戻り値の型を定義するもの。
  • 複合リテラル (Composite Literals): 構造体、配列、スライス、マップなどの複合型の値を初期化するための構文。
    • StructType{Field: Value, ...} (キー付き要素)
    • StructType{Value, Value, ...} (キーなし要素)
    • ArrayType{Index: Value, ...} (配列のキー付き要素)
    • ArrayType{Value, Value, ...} (配列のキーなし要素)
    • MapType{Key: Value, ...} (マップのキー付き要素)

Goコンパイラの内部構造 (ASTと型チェック)

  • AST (Abstract Syntax Tree): ソースコードを解析して得られる、プログラムの構造を木構造で表現したもの。go/ast パッケージで定義されます。
    • ast.Object: 識別子(変数、関数、型など)を表す抽象的なオブジェクト。
    • ast.FieldList: 構造体のフィールド、関数のパラメータや戻り値のリスト。
    • ast.CompositeLit: 複合リテラルを表すASTノード。
    • ast.KeyValueExpr: 複合リテラル内のキーと値のペアを表すASTノード。
  • token.Token: Go言語のキーワード、演算子、識別子などの字句要素を表す定数。
  • 型チェッカー (exp/types パッケージ): ASTを走査し、Go言語の型ルールに従ってプログラムの型が正しいか検証するコンポーネント。
    • checker 構造体: 型チェックのコンテキストを保持する主要な構造体。
    • operand: 式の結果を表す内部構造体。mode (値、型、定数など) と typ (型) を持つ。
    • operandMode: オペランドのモード(例: value, typexpr, constant, invalid)。
    • lookupResult: フィールドやメソッドのルックアップ結果を保持する構造体。
  • パニックとリカバリ (panicrecover): Go言語の例外処理メカニズム。
    • panic: プログラムの実行を停止し、パニックシーケンスを開始する。
    • recover: defer 関数内で呼び出され、パニックシーケンスを停止し、パニックの引数を返す。これにより、プログラムがクラッシュするのを防ぎ、エラーを適切に処理できる。型チェッカーのようなライブラリでは、内部エラーが外部に漏洩しないように recover を使用することが一般的です。
    • bailout: このコミットで導入された内部的なパニックマーカー。型チェック中に致命的なエラーが発生した場合に、通常のパニックとは区別して処理を中断するために使用されます。

その他の概念

  • strconv.Unquote: 文字列リテラルのクォートを外し、エスケープシーケンスを解釈する関数。構造体タグの解析に使用されます。
  • assert: 開発中に予期せぬ状態を検出するためのアサーション。本番コードでは通常削除されるか、より堅牢なエラーハンドリングに置き換えられます。

これらの概念を理解することで、コミットがGo言語の型チェックのどの側面を改善し、どのように内部的に動作するのかを深く把握することができます。

技術的詳細

このコミットは、Go言語の型チェッカー exp/types パッケージに複数の重要な改善を導入しています。

1. フィールド/メソッドのルックアップの修正と強化

最も重要な変更点の一つは、構造体のフィールドやインターフェースのメソッドのルックアップロジックの修正です。特に、埋め込み型(anonymous fields)が存在する場合のルックアップがよりGo言語の仕様に厳密に準拠するように変更されました。

  • lookupFieldRecursive から lookupFieldBreadthFirst への変更:
    • 以前の lookupFieldRecursive は、再帰的にフィールドを探索していましたが、埋め込み型による名前の競合(シャドーイング)のルールを完全にカバーしていませんでした。
    • 新しい lookupFieldBreadthFirst 関数は、reflect パッケージの FieldByNameFunc の構造に似ており、幅優先探索(Breadth-First Search)の考え方でフィールドやメソッドを探索します。
    • 競合の検出: 同じレベルで同じ名前のフィールドやメソッドが複数見つかった場合、それらは競合とみなされ、lookupResultmodeinvalid に設定されます。これにより、"no single field or method %s" というエラーメッセージが生成されるようになります。これは、Go言語の仕様で「曖昧なセレクタ」として扱われるケースを正確に反映しています。
    • embeddedType 構造体の導入: 埋め込み型とその多重度(同じ型が同じレベルで複数回埋め込まれているか)を追跡するために、embeddedType という新しい内部構造体が導入されました。これにより、競合の検出がより正確になります。
    • visited マップの改善: 探索済みの型を追跡する visited マップが、無限ループを防ぎ、効率的な探索を保証します。
  • エラーメッセージの具体化: "%s has no field or method %s" という一般的なエラーメッセージが、埋め込み型による競合の場合に "%s has no single field or method %s" とより具体的に変更されました。

2. 複合リテラルチェックのほぼ完了

複合リテラル(ast.CompositeLit)の型チェックが大幅に強化され、Go言語の仕様に準拠した厳密な検証が行われるようになりました。

  • 構造体リテラル:
    • キー付き要素とキーなし要素の混在の禁止: T{a: 1, 2} のような混在はエラーとして検出されます。
    • 未知のフィールド名: T{unknownField: 1} のような存在しないフィールド名への割り当てはエラーとなります。
    • 重複するフィールド名: T{a: 1, a: 2} のような重複するフィールド名への割り当てはエラーとなります。
    • 値の型の互換性チェック: フィールドの型と割り当てられる値の型が互換性があるか (isAssignable) が厳密にチェックされます。
    • 要素数のチェック: キーなし要素の場合、構造体のフィールド数とリテラルの要素数が一致するか (too few values, too many values) がチェックされます。
  • 配列/スライスリテラル:
    • 要素の型と割り当てられる値の型の互換性 (isAssignable) がチェックされます。
    • インデックスのチェック(TODOとして残っている部分もありますが、基本的な枠組みが導入されました)。
  • マップリテラル:
    • キーの欠落: M{1} のようなキーのない要素はエラーとなります。
    • キーと値の型の互換性チェック: マップのキー型/値型と割り当てられるキー/値の型が互換性があるか (isAssignable) が厳密にチェックされます。
    • 重複するキー: M{"a": 1, "a": 2} のような重複するキーはエラーとなります。定数キーの場合に特に検出されます。
  • ast.KeyValueExpr の処理: rawExpr 関数内で ast.KeyValueExpr が直接処理されることはなくなり、複合リテラル内で適切に処理されるようになりました。これにより、"no key:value expected" というエラーが生成されるようになりました。

3. パラメータ、メソッド、フィールドチェックのクリーンアップ

check.goexpr.go 内の、関数パラメータ、インターフェースメソッド、構造体フィールドを収集するロジックが整理されました。

  • collectFields の分割: 以前は collectFields という単一の関数が、インターフェースメソッド、関数パラメータ、構造体フィールドの収集を兼ねていました。これが、より特化した関数に分割されました。
    • collectParams(list *ast.FieldList): 関数パラメータと戻り値を収集。可変長引数 (...) の処理も担当。
    • collectMethods(list *ast.FieldList): インターフェースメソッドを収集。埋め込みインターフェースのメソッドも再帰的に収集し、重複するメソッド名のチェックも行います。
    • collectFields(list *ast.FieldList): 構造体フィールドを収集。構造体タグの処理も担当。
  • tag 関数の分離: 構造体タグを解析するロジックが tag(t *ast.BasicLit) という独立した関数に分離され、再利用性と可読性が向上しました。
  • check.goobject メソッドの変更: 関数宣言 (ast.FuncDecl) のレシーバの型チェックが check.collectParams(fdecl.Recv) を呼び出すように変更され、より一貫した処理になりました。

4. 型チェッカーからのパニックの漏洩防止

check.gocheck 関数に deferrecover を使用したパニックハンドリングが導入されました。

  • 以前は、型チェッカー内部で発生したパニックがそのまま外部に漏洩し、クライアントアプリケーションをクラッシュさせる可能性がありました。
  • 新しい実装では、recover() を使用してパニックを捕捉し、それが型チェッカー内部で意図的に発生させた bailout パニックである場合は適切に処理し、それ以外の予期せぬパニックの場合は、より一般的なエラーメッセージ ("types.check internal error: %v") を返すように変更されました。これにより、型チェッカーの堅牢性が向上し、ライブラリとしての利用がより安全になりました。

5. その他の改善

  • TODOコメントの削除: コードベースから多くのTODOコメントが削除され、実装が完了したことを示しています。
  • operand.isAssignable の改善: 型なし定数の割り当て可能性チェックがより正確になりました。
  • Struct.fieldIndex メソッドの追加: 構造体内で指定された名前のフィールドのインデックスを効率的に検索するためのヘルパーメソッドが追加されました。

これらの変更は、exp/types パッケージをGo言語の型システムにさらに厳密に準拠させ、より堅牢で正確な型チェック機能を提供するための重要なステップでした。

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

1. フィールド/メソッドルックアップの変更 (src/pkg/exp/types/operand.go)

--- a/src/pkg/exp/types/operand.go
+++ b/src/pkg/exp/types/operand.go
@@ -199,35 +222,50 @@ type lookupResult struct {
 	typ  Type
 }
 
-// lookupFieldRecursive is similar to FieldByNameFunc in reflect/type.go
-// TODO(gri): FieldByNameFunc seems more complex - what are we missing?
-func lookupFieldRecursive(list []*NamedType, name string) (res lookupResult) {
-	// visited records the types that have been searched already
-	visited := make(map[Type]bool)
+type embeddedType struct {
+	typ       *NamedType
+	multiples bool // if set, typ is embedded multiple times at the same level
+}
+
+// lookupFieldBreadthFirst searches all types in list for a single entry (field
+// or method) of the given name. If such a field is found, the result describes
+// the field mode and type; otherwise the result mode is invalid.
+// (This function is similar in structure to FieldByNameFunc in reflect/type.go)
+//
+func lookupFieldBreadthFirst(list []embeddedType, name string) (res lookupResult) {
+	// visited records the types that have been searched already.
+	visited := make(map[*NamedType]bool)
 
 	// embedded types of the next lower level
-	var next []*NamedType
+	var next []embeddedType
 
-	potentialMatch := func(mode operandMode, typ Type) bool {
-		if res.mode != invalid {
-			// name appeared multiple times at this level - annihilate
+	// potentialMatch is invoked every time a match is found.
+	potentialMatch := func(multiples bool, mode operandMode, typ Type) bool {
+		if multiples || res.mode != invalid {
+			// name appeared already at this level - annihilate
 			res.mode = invalid
 			return false
 		}
+		// first appearance of name
 		res.mode = mode
 		res.typ = typ
 		return true
 	}
 
-	// look for name in all types of this level
+	// Search the current level if there is any work to do and collect
+	// embedded types of the next lower level in the next list.
 	for len(list) > 0 {
+		// The res.mode indicates whether we have found a match already
+		// on this level (mode != invalid), or not (mode == invalid).
 		assert(res.mode == invalid)
-		for _, typ := range list {
+
+		// start with empty next list (don't waste underlying array)
+		next = next[:0]
+
+		// look for name in all types at this level
+		for _, e := range list {
+			typ := e.typ
 			if visited[typ] {
-				// We have seen this type before, at a higher level.
-				// That higher level shadows the lower level we are
-				// at now, and either we would have found or not
-				// found the field before. Ignore this type now.
 				continue
 			}
 			visited[typ] = true
@@ -236,7 +274,7 @@ func lookupFieldRecursive(list []*NamedType, name string) (res lookupResult) {
 			if data := typ.Obj.Data; data != nil {
 				if obj := data.(*ast.Scope).Lookup(name); obj != nil {
 					assert(obj.Type != nil)
-					if !potentialMatch(value, obj.Type.(Type)) {
+					if !potentialMatch(e.multiples, value, obj.Type.(Type)) {
 						return // name collision
 					}
 				}
@@ -244,21 +282,26 @@ func lookupFieldRecursive(list []*NamedType, name string) (res lookupResult) {
 
 			switch typ := underlying(typ).(type) {
 			case *Struct:
-				// look for a matching fieldm and collect embedded types
+				// look for a matching field and collect embedded types
 				for _, f := range typ.Fields {
 					if f.Name == name {
 						assert(f.Type != nil)
-						if !potentialMatch(variable, f.Type) {
+						if !potentialMatch(e.multiples, variable, f.Type) {
 							return // name collision
 						}
 						continue
 					}
 					// Collect embedded struct fields for searching the next
-					// lower level, but only if we have not seen a match yet.
+					// lower level, but only if we have not seen a match yet
+					// (if we have a match it is either the desired field or
+					// we have a name collision on the same level; in either
+					// case we don't need to look further).
 					// Embedded fields are always of the form T or *T where
-					// T is a named type.
+					// T is a named type. If typ appeared multiple times at
+					// this level, f.Type appears multiple times at the next
+					// level.
 					if f.IsAnonymous && res.mode == invalid {
-						next = append(next, deref(f.Type).(*NamedType))
+						next = append(next, embeddedType{deref(f.Type).(*NamedType), e.multiples})
 					}
 				}
 
@@ -267,17 +310,41 @@ func lookupFieldRecursive(list []*NamedType, name string) (res lookupResult) {\
 				for _, obj := range typ.Methods {
 					if obj.Name == name {
 						assert(obj.Type != nil)
-						if !potentialMatch(value, obj.Type.(Type)) {
+						if !potentialMatch(e.multiples, value, obj.Type.(Type)) {
 							return // name collision
 						}
 					}
 				}
 
 		}
 
 		if res.mode != invalid {
-			// we found a match on this level
+			// we found a single match on this level
 			return
 		}
 
-		// search the next level
-		list = append(list[:0], next...) // don't waste underlying arrays
-		next = next[:0]
+		// No match and no collision so far.
+		// Compute the list to search for the next level.
+		list = list[:0] // don't waste underlying array
+		for _, e := range next {
+			// Instead of adding the same type multiple times, look for
+			// it in the list and mark it as multiple if it was added
+			// before.
+			// We use a sequential search (instead of a map for next)
+			// because the lists tend to be small, can easily be reused,
+			// and explicit search appears to be faster in this case.
+			if alt := findType(list, e.typ); alt != nil {
+				alt.multiples = true
+			} else {
+				list = append(list, e)
+			}
+		}
+
 	}
+
 	return
 }
 
+func findType(list []embeddedType, typ *NamedType) *embeddedType {
+	for i := range list {
+		if p := &list[i]; p.typ == typ {
+			return p
+		}
+	}
+	return nil
+}
+
 func lookupField(typ Type, name string) (operandMode, Type) {
 	typ = deref(typ)
 
@@ -301,17 +368,20 @@ func lookupField(typ Type, name string) (operandMode, Type) {
 
 	switch typ := underlying(typ).(type) {
 	case *Struct:
-		var list []*NamedType
+		var next []embeddedType
 		for _, f := range typ.Fields {
 			if f.Name == name {
 				return variable, f.Type
 			}
 			if f.IsAnonymous {
-				list = append(list, deref(f.Type).(*NamedType))
+				// Possible optimization: If the embedded type
+				// is a pointer to the current type we could
+				// ignore it.
+				next = append(next, embeddedType{typ: deref(f.Type).(*NamedType)})
 			}
 		}
-		if len(list) > 0 {
-			res := lookupFieldRecursive(list, name)
+		if len(next) > 0 {
+			res := lookupFieldBreadthFirst(next, name)
 			return res.mode, res.typ
 		}
 

2. 複合リテラルチェックの強化 (src/pkg/exp/types/expr.go)

rawExpr 関数内の case *ast.CompositeLit: ブロックが大幅に拡張されました。

--- a/src/pkg/exp/types/expr.go
+++ b/src/pkg/exp/types/expr.go
@@ -537,27 +562,155 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
 		}
 
 	case *ast.FuncLit:
-		x.mode = value
-		x.typ = check.typ(e.Type, false)
-		// TODO(gri) handle errors (e.g. x.typ is not a *Signature)
-		check.function(x.typ.(*Signature), e.Body)
+		if typ, ok := check.typ(e.Type, false).(*Signature); ok {
+			x.mode = value
+			x.typ = typ
+			check.function(typ, e.Body)
+		} else {
+			check.invalidAST(e.Pos(), "invalid function literal %s", e)
+			goto Error
+		}
 
 	case *ast.CompositeLit:
-		// TODO(gri)
-		//	- determine element type if nil
-		//	- deal with map elements
-		var typ Type
+		typ := hint
 		if e.Type != nil {
-			// TODO(gri) Fix this - just to get going for now
 			typ = check.typ(e.Type, false)
 		}
-		for _, e := range e.Elts {
-			var x operand
-			check.expr(&x, e, hint, iota)
-			// TODO(gri) check assignment compatibility to element type
+		if typ == nil {
+			check.errorf(e.Pos(), "missing type in composite literal")
+			goto Error
 		}
-		// TODO(gri) this is not correct - leave for now to get going
-		x.mode = variable
-		x.typ = typ
+
+		// TODO(gri) try to factor code below better
+
+		switch utyp := underlying(deref(typ)).(type) {
+		case *Struct:
+			if len(e.Elts) == 0 {
+				break
+			}
+			fields := utyp.Fields
+			if _, ok := e.Elts[0].(*ast.KeyValueExpr); ok {
+				// all elements must have keys
+				visited := make([]bool, len(fields))
+				for _, e := range e.Elts {
+					kv, _ := e.(*ast.KeyValueExpr)
+					if kv == nil {
+						check.errorf(e.Pos(), "mixture of field:value and value elements in struct literal")
+						continue
+					}
+					key, _ := kv.Key.(*ast.Ident)
+					if key == nil {
+						check.errorf(kv.Pos(), "invalid field name %s in struct literal", kv.Key)
+						continue
+					}
+					i := utyp.fieldIndex(key.Name)
+					if i < 0 {
+						check.errorf(kv.Pos(), "unknown field %s in struct literal", key.Name)
+						continue
+					}
+					// 0 <= i < len(fields)
+					if visited[i] {
+						check.errorf(kv.Pos(), "duplicate field name %s in struct literal", key.Name)
+						continue
+					}
+					visited[i] = true
+					check.expr(x, kv.Value, nil, iota)
+					etyp := fields[i].Type
+					if !x.isAssignable(etyp) {
+						check.errorf(x.pos(), "cannot use %s as %s value in struct literal", x, etyp)
+						continue
+					}
+				}
+			} else {
+				// no element must have a key
+				for i, e := range e.Elts {
+					if kv, _ := e.(*ast.KeyValueExpr); kv != nil {
+						check.errorf(kv.Pos(), "mixture of field:value and value elements in struct literal")
+						continue
+					}
+					check.expr(x, e, nil, iota)
+					if i >= len(fields) {
+						check.errorf(x.pos(), "too many values in struct literal")
+						goto Error
+					}
+					etyp := fields[i].Type
+					if !x.isAssignable(etyp) {
+						check.errorf(x.pos(), "cannot use %s as an element of type %s in struct literal", x, etyp)
+						continue
+					}
+				}
+				if len(e.Elts) < len(fields) {
+					check.errorf(e.Rbrace, "too few values in struct literal")
+					goto Error
+				}
+			}
+
+		case *Array:
+			var index int64
+			for _, e := range e.Elts {
+				eval := e
+				if kv, _ := e.(*ast.KeyValueExpr); kv != nil {
+					check.index(kv.Key, -1, iota)
+					eval = kv.Value
+				}
+				// TODO(gri) missing index range & duplicate check
+				check.expr(x, eval, utyp.Elt, iota)
+				if !x.isAssignable(utyp.Elt) {
+					check.errorf(x.pos(), "cannot use %s as %s value in array literal", x, utyp.Elt)
+				}
+				index++
+			}
+
+		case *Slice:
+			var index int64
+			for _, e := range e.Elts {
+				eval := e
+				if kv, _ := e.(*ast.KeyValueExpr); kv != nil {
+					// TODO(gri) check key
+					check.index(kv.Key, -1, iota)
+					eval = kv.Value
+				}
+				// TODO(gri) missing index range & duplicate check
+				check.expr(x, eval, utyp.Elt, iota)
+				if !x.isAssignable(utyp.Elt) {
+					check.errorf(x.pos(), "cannot use %s as %s value in slice literal", x, utyp.Elt)
+				}
+				index++
+			}
+
+		case *Map:
+			visited := make(map[interface{}]bool, len(e.Elts))
+			for _, e := range e.Elts {
+				kv, _ := e.(*ast.KeyValueExpr)
+				if kv == nil {
+					check.errorf(e.Pos(), "missing key in map literal")
+					continue
+				}
+				check.expr(x, kv.Key, nil, iota)
+				if !x.isAssignable(utyp.Key) {
+					check.errorf(x.pos(), "cannot use %s as %s key in map literal", x, utyp.Key)
+					continue
+				}
+				if x.mode == constant {
+					if visited[x.val] {
+						check.errorf(x.pos(), "duplicate key %s in map literal", x.val)
+						continue
+					}
+					visited[x.val] = true
+				}
+				check.expr(x, kv.Value, utyp.Elt, iota)
+				if !x.isAssignable(utyp.Elt) {
+					check.errorf(x.pos(), "cannot use %s as %s value in map literal", x, utyp.Elt)
+					continue
+					}
+				}
+
+		default:
+			check.errorf(e.Pos(), "%s is not a valid composite literal type", typ)
+			goto Error
+		}
+
+		x.mode = variable // TODO(gri) mode is really a value - keep for now to get going
+		x.typ = typ
 
 	case *ast.ParenExpr:
 		check.expr(x, e.X, hint, iota)
@@ -827,7 +985,9 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
 		check.binary(x, &y, e.Op, hint)
 
 	case *ast.KeyValueExpr:
-		unimplemented()
+		// key:value expressions are handled in composite literals
+		check.invalidAST(e.Pos(), "no key:value expected")
+		goto Error
 
 	case *ast.ArrayType:
 		if e.Len != nil {
@@ -862,19 +1022,17 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
 
 	case *ast.StructType:
 		x.mode = typexpr
-		x.typ = &Struct{Fields: check.collectStructFields(e.Fields, cycleOk)}
+		x.typ = &Struct{Fields: check.collectFields(e.Fields, cycleOk)}
 
 	case *ast.FuncType:
-		params, _, isVariadic := check.collectFields(token.FUNC, e.Params, true)
-		results, _, _ := check.collectFields(token.FUNC, e.Results, true)
+		params, isVariadic := check.collectParams(e.Params)
+		results, _ := check.collectParams(e.Results)
 		x.mode = typexpr
 		x.typ = &Signature{Recv: nil, Params: params, Results: results, IsVariadic: isVariadic}
 
 	case *ast.InterfaceType:
-		methods, _, _ := check.collectFields(token.INTERFACE, e.Methods, cycleOk)
-		methods.Sort()
 		x.mode = typexpr
-		x.typ = &Interface{Methods: methods}
+		x.typ = &Interface{Methods: check.collectMethods(e.Methods)}
 
 	case *ast.MapType:
 		x.mode = typexpr

3. パラメータ、メソッド、フィールドチェックのクリーンアップ (src/pkg/exp/types/expr.go)

collectFieldscollectParams, collectMethods, collectFields に分割され、tag 関数が独立しました。

--- a/src/pkg/exp/types/expr.go
+++ b/src/pkg/exp/types/expr.go
@@ -17,70 +17,98 @@ import (
 // - simplify invalid handling: maybe just use Typ[Invalid] as marker, get rid of invalid Mode for values?
 // - rethink error handling: should all callers check if x.mode == valid after making a call?
 
-func (check *checker) tag(field *ast.Field) string {
-	if t := field.Tag; t != nil {
-		assert(t.Kind == token.STRING)
-		if tag, err := strconv.Unquote(t.Value); err == nil {
-			return tag
+func (check *checker) collectParams(list *ast.FieldList) (params ObjList, isVariadic bool) {
+	if list == nil {
+		return
+	}
+	for _, field := range list.List {
+		ftype := field.Type
+		if t, ok := ftype.(*ast.Ellipsis); ok {
+			ftype = t.Elt
+			isVariadic = true
 		}
-		check.invalidAST(t.Pos(), "incorrect tag syntax: %q", t.Value)
+		// the parser ensures that f.Tag is nil and we don't
+		// care if a constructed AST contains a non-nil tag
+		typ := check.typ(ftype, true)
+		if len(field.Names) > 0 {
+			// named parameter
+			for _, name := range field.Names {
+				obj := name.Obj
+				obj.Type = typ
+				params = append(params, obj)
+			}
+		} else {
+			// anonymous parameter
+			obj := ast.NewObj(ast.Var, "")
+			obj.Type = typ
+			params = append(params, obj)
+		}
 	}
-	return ""
+	return
 }
 
-// collectFields collects interface methods (tok = token.INTERFACE), and function arguments/results (tok = token.FUNC).
-func (check *checker) collectFields(tok token.Token, list *ast.FieldList, cycleOk bool) (fields ObjList, tags []string, isVariadic bool) {
-	if list != nil {
-		for _, field := range list.List {
-			ftype := field.Type
-			if t, ok := ftype.(*ast.Ellipsis); ok {
-				ftype = t.Elt
-				isVariadic = true
+func (check *checker) collectMethods(list *ast.FieldList) (methods ObjList) {
+	if list == nil {
+		return
+	}
+	for _, f := range list.List {
+		typ := check.typ(f.Type, len(f.Names) > 0) // cycles are not ok for embedded interfaces
+		// the parser ensures that f.Tag is nil and we don't
+		// care if a constructed AST contains a non-nil tag
+		if len(f.Names) > 0 {
+			// methods (the parser ensures that there's only one
+			// and we don't care if a constructed AST has more)
+			if _, ok := typ.(*Signature); !ok {
+				check.invalidAST(f.Type.Pos(), "%s is not a method signature", typ)
+				continue
 			}
-			typ := check.typ(ftype, cycleOk)
-			tag := check.tag(field)
-			if len(field.Names) > 0 {
-				// named fields
-				for _, name := range field.Names {
-					obj := name.Obj
-					obj.Type = typ
-					fields = append(fields, obj)
-					if tok == token.STRUCT {
-						tags = append(tags, tag)
-					}
-				}
-			} else {
-				// anonymous field
-				switch tok {
-				case token.FUNC:
-					obj := ast.NewObj(ast.Var, "")
-					obj.Type = typ
-					fields = append(fields, obj)
-				case token.INTERFACE:
-					utyp := underlying(typ)
-					if typ, ok := utyp.(*Interface); ok {
-						// TODO(gri) This is not good enough. Check for double declarations!
-						fields = append(fields, typ.Methods...)
-					} else if utyp != Typ[Invalid] {
-						// if utyp is invalid, don't complain (the root cause was reported before)
-						check.errorf(ftype.Pos(), "interface contains embedded non-interface type")
-					}
-				default:
-					panic("unreachable")
-				}
+			for _, name := range f.Names {
+				obj := name.Obj
+				obj.Type = typ
+				methods = append(methods, obj)
 			}
+		} else {
+			// embedded interface
+			utyp := underlying(typ)
+			if ityp, ok := utyp.(*Interface); ok {
+				methods = append(methods, ityp.Methods...)
+			} else if utyp != Typ[Invalid] {
+				// if utyp is invalid, don't complain (the root cause was reported before)
+				check.errorf(f.Type.Pos(), "%s is not an interface type", typ)
+			}
 		}
 	}
+	// check for double declarations
+	methods.Sort()
+	prev := ""
+	for _, obj := range methods {
+		if obj.Name == prev {
+			check.errorf(list.Pos(), "multiple methods named %s", prev)
+			return // keep multiple entries, lookup will only return the first entry
+		}
+	}
 	return
 }
 
-func (check *checker) collectStructFields(list *ast.FieldList, cycleOk bool) (fields []*StructField) {\n+func (check *checker) tag(t *ast.BasicLit) string {\n+\tif t != nil {\n+\t\tif t.Kind == token.STRING {\n+\t\t\tif val, err := strconv.Unquote(t.Value); err == nil {\n+\t\t\t\treturn val\n+\t\t\t}\n+\t\t}\n+\t\tcheck.invalidAST(t.Pos(), "incorrect tag syntax: %q", t.Value)\n+\t}\n+\treturn ""\n+}\n+\n+func (check *checker) collectFields(list *ast.FieldList, cycleOk bool) (fields []*StructField) {\n 	if list == nil {
 		return
 	}
 	for _, f := range list.List {
 		typ := check.typ(f.Type, cycleOk)
-		tag := check.tag(f)
+		tag := check.tag(f.Tag)
 		if len(f.Names) > 0 {
 			// named fields
 			for _, name := range f.Names {

4. 型チェッカーからのパニックの漏洩防止 (src/pkg/exp/types/check.go)

check 関数内の defer ブロックが変更されました。

--- a/src/pkg/exp/types/check.go
+++ b/src/pkg/exp/types/check.go
@@ -355,12 +351,19 @@ func check(fset *token.FileSet, pkg *ast.Package, errh func(token.Pos, string),\n 	check.mapf = f
 	check.initexprs = make(map[*ast.ValueSpec][]ast.Expr)\n 
-	// handle bailouts
+	// handle panics
 	defer func() {
-		if p := recover(); p != nil {
-			_ = p.(bailout) // re-panic if not a bailout
+		switch p := recover().(type) {
+		case nil:
+			// normal return - nothing to do
+		case bailout:
+			// early exit
+			err = check.firsterr
+		default:
+			// unexpected panic: don't crash clients
+			// panic(p) // enable for debugging
+			err = fmt.Errorf("types.check internal error: %v", p)
 		}
-		err = check.firsterr
 	}()
 
 	// determine missing constant initialization expressions

コアとなるコードの解説

1. フィールド/メソッドルックアップの変更 (lookupFieldBreadthFirst)

lookupFieldBreadthFirst 関数は、Go言語の構造体におけるフィールドやメソッドのルックアップの核心部分です。特に、埋め込み型(匿名フィールド)が存在する場合の複雑なルールを処理します。

  • list []embeddedType: 探索対象となる型のリストです。embeddedType は、型 (*NamedType) と、その型が現在のレベルで複数回埋め込まれているかを示す multiples フラグを持ちます。この multiples フラグが、同じレベルでの名前の競合を検出するために重要です。
  • visited := make(map[*NamedType]bool): 既に探索した型を記録するためのマップです。これにより、無限ループを防ぎ、効率的な探索を保証します。
  • potentialMatch 関数: 内部ヘルパー関数で、フィールドやメソッドが見つかった際に呼び出されます。
    • multiples || res.mode != invalid: この条件が重要です。
      • multiplestrue の場合(同じ型が複数回埋め込まれている場合)、その型から見つかったフィールド/メソッドは競合とみなされます。
      • res.mode != invalid の場合(既に現在のレベルで別のフィールド/メソッドが見つかっている場合)、それも競合とみなされます。
    • いずれかの条件が真であれば、res.mode = invalid となり、競合が発生したことを示します。これにより、"no single field or method" エラーがトリガーされます。
  • 幅優先探索のループ:
    • for len(list) > 0: 現在のレベルに探索すべき型がある限りループを続けます。
    • next = next[:0]: 次のレベルの埋め込み型を収集するためのスライスをリセットします。
    • for _, e := range list: 現在のレベルの各埋め込み型 e を処理します。
    • if visited[typ] { continue }: 既に探索済みの型であればスキップします。
    • 直接のフィールド/メソッドの検索: typ.Obj.Data.(*ast.Scope).Lookup(name) を使って、現在の型に直接定義されているフィールドやメソッドを検索します。
    • 構造体内のフィールド/メソッドの検索: switch typ := underlying(typ).(type) { case *Struct: ... case *Interface: ... } ブロックで、構造体やインターフェースの基底型を調べて、その中のフィールドやメソッドを検索します。
      • 構造体の場合、f.Name == name で直接のフィールドをチェックし、f.IsAnonymous で匿名フィールド(埋め込み型)を next リストに追加します。この際、e.multiples を引き継ぐことで、埋め込み元の多重度を次のレベルに伝播させます。
      • インターフェースの場合、obj.Name == name でメソッドをチェックします。
    • 競合の処理: if !potentialMatch(...) { return } で、競合が発生した場合はすぐに探索を終了します。
    • 次のレベルへの移行: 現在のレベルの探索が終わり、競合がなければ、list = list[:0] で現在のリストをクリアし、for _, e := range nextnext リスト(次のレベルの埋め込み型)を list にコピーします。この際、findType を使って、既に list に存在する型であれば multiples フラグを true に設定します。

この複雑なロジックにより、Go言語のフィールド/メソッドルックアップの厳密なルール(特に埋め込み型による競合)が正確に実装されています。

2. 複合リテラルチェックの強化 (rawExpr 内の ast.CompositeLit 処理)

rawExpr 関数は、Go言語の様々な式を型チェックする主要な関数です。このコミットでは、特に ast.CompositeLit(複合リテラル)の処理が大幅に改善されました。

  • 型推論とエラーハンドリング:
    • typ := hintif e.Type != nil { typ = check.typ(e.Type, false) } により、複合リテラルの型が明示的に指定されている場合はそれを使用し、そうでない場合はコンテキストからのヒント (hint) を利用します。
    • if typ == nil { check.errorf(e.Pos(), "missing type in composite literal"); goto Error } は、型が特定できない場合にエラーを報告します。
  • 基底型による分岐: switch utyp := underlying(deref(typ)).(type) を使用して、複合リテラルの基底型(構造体、配列、スライス、マップ)に応じて異なるチェックロジックを適用します。
    • *Struct (構造体リテラル):
      • if _, ok := e.Elts[0].(*ast.KeyValueExpr); ok: 最初の要素がキー付きかどうかで、リテラル全体がキー付き形式かキーなし形式かを判断します。Go言語ではこれらを混在させることはできません。
      • キー付き要素の場合:
        • visited := make([]bool, len(fields)): 既に初期化されたフィールドを追跡し、重複を検出します。
        • kv, _ := e.(*ast.KeyValueExpr): 各要素が KeyValueExpr であることを確認します。
        • key, _ := kv.Key.(*ast.Ident): キーが識別子であることを確認します。
        • i := utyp.fieldIndex(key.Name): フィールド名からインデックスを取得します。
        • if i < 0 { check.errorf(kv.Pos(), "unknown field %s in struct literal", key.Name) }: 未知のフィールド名を検出します。
        • if visited[i] { check.errorf(kv.Pos(), "duplicate field name %s in struct literal", key.Name) }: 重複するフィールド名を検出します。
        • if !x.isAssignable(etyp) { check.errorf(x.pos(), "cannot use %s as %s value in struct literal", x, etyp) }: 値の型がフィールドの型に割り当て可能かチェックします。
      • キーなし要素の場合:
        • if kv, _ := e.(*ast.KeyValueExpr); kv != nil { check.errorf(kv.Pos(), "mixture of field:value and value elements in struct literal") }: キー付き要素が混在していないことを確認します。
        • if i >= len(fields) { check.errorf(x.pos(), "too many values in struct literal") }: 要素が多すぎる場合を検出します。
        • if len(e.Elts) < len(fields) { check.errorf(e.Rbrace, "too few values in struct literal") }: 要素が少なすぎる場合を検出します。
        • if !x.isAssignable(etyp) { check.errorf(x.pos(), "cannot use %s as an element of type %s in struct literal", x, etyp) }: 値の型がフィールドの型に割り当て可能かチェックします。
    • *Array (配列リテラル):
      • if kv, _ := e.(*ast.KeyValueExpr); kv != nil { check.index(kv.Key, -1, iota) }: キー付き要素の場合、キー(インデックス)をチェックします。
      • if !x.isAssignable(utyp.Elt) { check.errorf(x.pos(), "cannot use %s as %s value in array literal", x, utyp.Elt) }: 値の型が要素の型に割り当て可能かチェックします。
    • *Slice (スライスリテラル): 配列リテラルと同様のチェックを行います。
    • *Map (マップリテラル):
      • if kv == nil { check.errorf(e.Pos(), "missing key in map literal") }: キーが欠落している場合を検出します。
      • check.expr(x, kv.Key, nil, iota): キーの型チェックを行います。
      • if !x.isAssignable(utyp.Key) { check.errorf(x.pos(), "cannot use %s as %s key in map literal", x, utyp.Key) }: キーの型がマップのキー型に割り当て可能かチェックします。
      • if x.mode == constant { if visited[x.val] { check.errorf(x.pos(), "duplicate key %s in map literal", x.val) } }: 定数キーの場合、重複するキーを検出します。
      • check.expr(x, kv.Value, utyp.Elt, iota): 値の型チェックを行います。
      • if !x.isAssignable(utyp.Elt) { check.errorf(x.pos(), "cannot use %s as %s value in map literal", x, utyp.Elt) }: 値の型がマップの値型に割り当て可能かチェックします。
    • default: 上記以外の型が複合リテラルとして使用された場合にエラーを報告します。

この詳細なチェックにより、Go言語の複合リテラルの構文と型ルールが厳密に適用され、コンパイル時のエラー検出能力が大幅に向上しました。

3. パラメータ、メソッド、フィールドチェックのクリーンアップ (collectParams, collectMethods, collectFields)

以前は collectFields という単一の関数が多岐にわたる役割を担っていましたが、このコミットで役割が明確に分割され、コードの可読性と保守性が向上しました。

  • collectParams(list *ast.FieldList):
    • 関数のパラメータや戻り値を収集するために特化しています。
    • ast.Ellipsis をチェックすることで、可変長引数 (...) を正しく処理します。
    • field.Names の有無で、名前付きパラメータと匿名パラメータを区別して ObjList に追加します。
  • collectMethods(list *ast.FieldList):
    • インターフェースのメソッドを収集するために特化しています。
    • check.typ(f.Type, len(f.Names) > 0): メソッドの型をチェックします。埋め込みインターフェースの場合、サイクル検出のために len(f.Names) > 0false になることで cycleOkfalse に渡されます。
    • if _, ok := typ.(*Signature); !ok { check.invalidAST(...) }: メソッドの型が Signature であることを確認します。
    • utyp := underlying(typ); if ityp, ok := utyp.(*Interface); ok { methods = append(methods, ityp.Methods...) }: 埋め込みインターフェースの場合、そのメソッドを再帰的に収集します。
    • methods.Sort(); prev := ""; for _, obj := range methods { if obj.Name == prev { check.errorf(...) } }: 収集したメソッドをソートし、同じ名前のメソッドが複数存在しないか(重複宣言)をチェックします。これは、インターフェースのメソッドセットにおける競合ルールを適用するためです。
  • collectFields(list *ast.FieldList, cycleOk bool):
    • 構造体のフィールドを収集するために特化しています。
    • tag := check.tag(f.Tag): 構造体タグを解析するために、新しく分離された tag 関数を呼び出します。
    • if len(f.Names) > 0: 名前付きフィールドを処理します。
    • この関数は、以前の collectFields から構造体フィールド収集のロジックのみを引き継ぎ、他の役割は新しい関数に委譲されています。
  • tag(t *ast.BasicLit):
    • ast.BasicLit から構造体タグの文字列を安全に抽出するヘルパー関数です。
    • strconv.Unquote を使用して、クォートを外し、エスケープシーケンスを解釈します。
    • 不正なタグ構文を検出した場合にエラーを報告します。

これらの関数分割と責任の明確化により、型チェッカーのコードベースはよりモジュール化され、理解しやすくなりました。

4. 型チェッカーからのパニックの漏洩防止 (check.godefer ブロック)

check 関数は型チェックの全体を統括する関数であり、その堅牢性は非常に重要です。このコミットでは、deferrecover を使用して、型チェッカー内部で発生したパニックが外部に漏洩しないようにするメカニズムが導入されました。

	defer func() {
		switch p := recover().(type) {
		case nil:
			// normal return - nothing to do
		case bailout:
			// early exit
			err = check.firsterr
		default:
			// unexpected panic: don't crash clients
			// panic(p) // enable for debugging
			err = fmt.Errorf("types.check internal error: %v", p)
		}
	}()
  • defer func() { ... }(): check 関数が終了する直前にこの無名関数が実行されることを保証します。
  • p := recover().(type): recover() はパニックが発生した場合にそのパニックの引数を返します。パニックが発生していない場合は nil を返します。.(type) は型アサーションで、p の実際の型に基づいて処理を分岐させます。
  • case nil:: パニックが発生しなかった通常の終了パスです。何もする必要はありません。
  • case bailout:: bailout は、型チェッカー内部で意図的に発生させる「早期終了」のためのカスタムパニック型です。例えば、致命的な型エラーが検出され、それ以上型チェックを続ける意味がない場合に panic(bailout{}) のように使用されます。この場合、check.firsterr に記録された最初のエラーを err に設定し、正常に終了します。これにより、型チェッカーはエラーを報告しつつ、クラッシュせずに制御を呼び出し元に戻すことができます。
  • default:: nil でも bailout でもない、予期せぬパニックが発生した場合の処理です。
    • // panic(p) // enable for debugging: デバッグ時には元のパニックを再発生させることで、問題の特定を容易にできます。
    • err = fmt.Errorf("types.check internal error: %v", p): 本番環境では、予期せぬパニックを捕捉し、より一般的な「内部エラー」としてラップして返します。これにより、型チェッカーの利用者が、内部実装の詳細を知ることなく、堅牢なエラーハンドリングを行うことができます。

このパニックハンドリングの導入により、exp/types パッケージはより安定したライブラリとなり、Goコンパイラや他のツールからの利用において、予期せぬクラッシュを防ぐことができるようになりました。

関連リンク

参考にした情報源リンク

  • reflect パッケージの FieldByNameFunc の実装 (Go標準ライブラリ): src/reflect/type.go
  • reflect パッケージのテストケース (Go標準ライブラリ): src/reflect/all_test.go
  • Go言語の panicrecover に関するドキュメント: https://go.dev/blog/defer-panic-and-recover
  • Go言語のAST (Abstract Syntax Tree) に関するドキュメント: https://pkg.go.dev/go/ast
  • Go言語の型システムに関する一般的な情報源 (例: Effective Go, Go Tourなど)