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

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

このコミットは、Go言語の実験的な型チェッカー (exp/types) およびパーサー (go/parser) における複合リテラルのキー解決に関する改善を目的としています。具体的には、複合リテラル内で識別子として使用されるキー(例: MyStruct{field: value}fieldMyMap{key: value}key)の解決を、パーサー段階でより正確に行うための変更が加えられています。

コミット

commit d0428379e79d8a3a868fda9509963563e73e10b2
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Dec 28 10:40:36 2012 -0800

    exp/types: resolve composite literal keys
    
    The parser/resolver cannot accurately resolve
    composite literal keys that are identifiers;
    it needs type information.
    Instead, try to resolve them but leave final
    judgement to the type checker.
    
    R=adonovan
    CC=golang-dev
    https://golang.org/cl/6994047

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

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

元コミット内容

exp/types: resolve composite literal keys

The parser/resolver cannot accurately resolve
composite literal keys that are identifiers;
it needs type information.
Instead, try to resolve them but leave final
judgement to the type checker.

変更の背景

Go言語において、構造体、配列、スライス、マップなどの複合型を初期化する際に使用される「複合リテラル (composite literal)」では、要素にキーを指定することができます。例えば、構造体のフィールド名やマップのキーなどがこれに該当します。

type MyStruct struct {
    Field1 int
    Field2 string
}

func main() {
    // 構造体リテラル
    s := MyStruct{Field1: 10, Field2: "hello"}

    // マップリテラル
    m := map[string]int{"key1": 1, "key2": 2}

    // 配列/スライスリテラル (インデックス指定)
    a := []int{0: 10, 2: 30}
}

このコミットが作成された当時、Goのパーサー(構文解析器)は、複合リテラル内のキーが識別子である場合に、その識別子が何を指しているのか(例えば、構造体のフィールド名なのか、それとも別の変数や定数なのか)を正確に解決することが困難でした。これは、パーサーが構文解析を行う段階では、まだ完全な型情報が利用できないためです。

例えば、MyStruct{Field1: 10}Field1 が構造体のフィールド名であることは、MyStruct の型定義を知って初めて確定します。もし Field1 という名前のグローバル変数や定数があった場合、パーサーはどちらを意図しているのかを判断できませんでした。この曖昧さが、エラーメッセージの不正確さや、型チェッカーでの追加処理の必要性を生んでいました。

このコミットの目的は、パーサー/リゾルバーの段階で、複合リテラルのキーとなる識別子を「試行的に」解決することです。これにより、型チェッカーが最終的な判断を下す前に、より多くの情報をパーサーが提供できるようになり、全体的なコンパイルプロセスの効率と正確性を向上させることが狙いです。特に、未宣言の識別子に関するエラーをより早期に、かつ正確に報告できるようになることが期待されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語のコンパイルプロセスと関連する概念についての知識が必要です。

  1. Go言語のコンパイルプロセス:

    • 字句解析 (Lexical Analysis): ソースコードをトークン(最小単位の単語)に分解します。
    • 構文解析 (Parsing): トークンの列を解析し、抽象構文木 (AST: Abstract Syntax Tree) を構築します。この段階で、コードの構造がツリー形式で表現されます。Go言語では、go/parser パッケージがこの役割を担います。
    • 型チェック (Type Checking): ASTを走査し、各式の型が正しいか、型変換が適切かなどを検証します。この段階で、識別子がどの宣言に対応するか(解決)も行われます。このコミットで言及されている exp/types パッケージは、Goの型チェッカーの実験的な実装でした。
    • コード生成 (Code Generation): 型チェックが完了したASTから、実行可能なバイナリコードを生成します。
  2. 抽象構文木 (AST: Abstract Syntax Tree):

    • ソースコードの構造を木構造で表現したものです。各ノードは、式、文、宣言などのコード要素に対応します。
    • go/ast パッケージは、GoのASTのデータ構造を定義しています。
  3. 複合リテラル (Composite Literals):

    • Go言語で構造体、配列、スライス、マップなどの複合型の値を直接初期化するための構文です。
    • TypeName{key: value, ...} の形式を取ります。
    • キーは省略することもできますが、指定する場合は、構造体のフィールド名、マップのキー、配列/スライスのインデックスなどが使われます。
  4. 識別子の解決 (Identifier Resolution):

    • ソースコード中の識別子(変数名、関数名、型名など)が、どの宣言に対応するかを特定するプロセスです。
    • これは通常、スコープ(識別子が有効な範囲)の概念と密接に関連しています。
    • パーサーは構文的な構造を理解しますが、識別子が具体的に何を指すのか(例えば、x がローカル変数なのか、グローバル変数なのか、パッケージレベルの定数なのか)を完全に解決するには、型情報やスコープ情報が必要になります。
  5. exp/types パッケージ:

    • Go言語の型チェッカーの実験的な実装でした。Go 1.0のリリース後、より堅牢で正確な型チェッカーを開発するために導入されました。最終的には、このパッケージの成果が go/types パッケージとして標準ライブラリに統合されました。
    • このパッケージは、ASTを受け取り、型情報を付与し、型エラーを検出する役割を担っていました。
  6. go/parser パッケージ:

    • Go言語のソースコードを解析し、ASTを生成する標準パッケージです。
    • このコミットでは、パーサーが複合リテラルのキーを処理する方法が変更されています。

このコミットの核心は、パーサーと型チェッカーの間の責任分担の調整にあります。パーサーは構文的な構造を構築しますが、意味的な解決(識別子が何を指すか、型が何か)は型チェッカーの役割です。しかし、複合リテラルのキーのように、構文解析の段階で「ヒント」として識別子を解決しようとすることで、後続の型チェックプロセスを効率化し、より良いエラー報告を可能にしようとしています。

技術的詳細

このコミットの技術的な変更は、主に src/pkg/exp/types/src/pkg/go/parser/ の2つのパッケージにまたがっています。

go/parser の変更点

最も重要な変更は、go/parser/parser.goparseElement メソッドと、新しいヘルパーメソッド tryResolve の導入です。

  • tryResolve メソッドの導入:

    • 以前は resolve メソッドが識別子の解決と、解決できなかった場合の unresolved リストへの追加を同時に行っていました。
    • tryResolve(x ast.Expr, collectUnresolved bool) は、識別子 x を解決しようとしますが、collectUnresolvedfalse の場合、解決できなかった識別子を unresolved リストに追加しません。
    • これは、複合リテラルのキーのように、パーサーが「試行的に」解決を試みるが、失敗してもそれが必ずしもエラーではない(型チェッカーが後で解決する可能性がある、または構造体フィールド名であるためパーサーが解決する必要がない)場合に有用です。
  • parseElement メソッドの変更:

    • 複合リテラルの要素(キーと値のペア)を解析するメソッドです。
    • 以前は、キーが識別子である場合、パーサーはそれを解決しようとせず、型チェッカーに任せていました。コメントにも「The parser cannot resolve a key expression... Leave this to type-checking phase.」と明記されていました。
    • 変更後、キーが KeyValueExprKey である場合、p.tryResolve(x, false) が呼び出されます。
      • これは、「キーが識別子である場合、試行的に解決を試みるが、もし解決できなくても unresolved リストには追加しない」という挙動を意味します。
      • この「試行的な解決」の意図は、コミットメッセージのコメントに詳しく書かれています。
        • パーサーは複合リテラルの型を知らないため、キーが構造体フィールド名なのか、それとも値を示す識別子なのかを区別できません。
        • もしキーが解決できれば、それは正しく解決されたか、あるいはたまたま別の識別子と名前が一致した構造体フィールド名であるかのどちらかです。後者の場合でも、型チェッカーが最終的にフィールドルックアップを行うため問題ありません。
        • もしキーが解決できなければ、それはパッケージ内の別のファイルで定義されているか、未宣言であるか、あるいは構造体フィールド名であるかのいずれかです。型チェッカーは、トップレベルのルックアップやフィールドルックアップを行うことで、これらを適切に処理できます。
      • この変更により、パーサーはより多くの情報を型チェッカーに渡すことができ、型チェッカーはより効率的に、かつ正確にエラーを報告できるようになります。

exp/types の変更点

exp/types/check.goexp/types/expr.go に変更が加えられています。

  • checker 構造体への pkgscope フィールドの追加 (check.go):

    • checker 構造体に pkgscope *ast.Scope が追加されました。これは、現在のパッケージのスコープを保持するためのものです。
    • check 関数の最後で check.pkgscope = pkg.Scope として初期化されます。これにより、型チェッカーはパッケージレベルの識別子をルックアップできるようになります。
  • compositeLitKey メソッドの導入 (expr.go):

    • compositeLitKey(key ast.Expr) という新しいヘルパーメソッドが追加されました。
    • このメソッドは、複合リテラルのキーが ast.Ident(識別子)であり、かつ ident.Obj == nil(パーサーによってまだ解決されていない)場合に呼び出されます。
    • 内部で check.pkgscope.Lookup(ident.Name) を使用して、パッケージスコープ内で識別子をルックアップしようとします。
    • もし見つかれば ident.Obj に設定し、見つからなければ undeclared name エラーを報告します。
    • このメソッドは、indexedElts(配列/スライスリテラルの要素チェック)と rawExpr(マップリテラルのキーチェック)の2箇所で呼び出されています。
  • rawExpr メソッドからのコメント削除と compositeLitKey の呼び出し (expr.go):

    • 以前 rawExpr メソッド内にあった、パーサーが複合リテラルのキーを解決できないという旨のコメントが削除されました。これは、パーサーが試行的な解決を行うようになったため、このコメントが不要になったことを示しています。
    • マップリテラルのキーを処理する箇所で、check.compositeLitKey(kv.Key) が呼び出されるようになりました。これにより、マップのキーが識別子である場合に、型チェッカーがその識別子を解決しようとします。

testdata/expr3.src の変更点

新しいテストケースが追加され、複合リテラルのインデックスとマップキーが正しく解決されることを検証しています。

  • index1 := 1 のようなローカル変数や var index2 int = 2 のようなパッケージレベルの変数をインデックスとして使用するケース。
  • key1 := "foo" のようなローカル変数や var key2 string = "bar" のようなパッケージレベルの変数をマップキーとして使用するケース。
  • index3key3 のように未宣言の識別子を使用した場合に、undeclared name エラーが正しく報告されることを確認しています。

これらの変更により、パーサーと型チェッカーが連携して複合リテラルのキーをより正確に処理できるようになり、コンパイル時のエラー検出能力が向上しました。特に、未宣言の識別子をキーとして使用した場合に、より適切なエラーメッセージが表示されるようになります。

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

src/pkg/exp/types/check.go

--- a/src/pkg/exp/types/check.go
+++ b/src/pkg/exp/types/check.go
@@ -23,6 +23,7 @@ type checker struct {
 	files []*ast.File
 
 	// lazily initialized
+	pkgscope  *ast.Scope
 	firsterr  error
 	initexprs map[*ast.ValueSpec][]ast.Expr // "inherited" initialization expressions for constant declarations
 	funclist  []function                    // list of functions/methods with correct signatures and non-empty bodies
@@ -406,6 +407,7 @@ func check(ctxt *Context, fset *token.FileSet, files map[string]*ast.File) (pkg
 			check.err(err)
 		}
 	}
+	check.pkgscope = pkg.Scope
 
 	// determine missing constant initialization expressions
 	// and associate methods with types

src/pkg/exp/types/expr.go

--- a/src/pkg/exp/types/expr.go
+++ b/src/pkg/exp/types/expr.go
@@ -507,8 +507,19 @@ func (check *checker) index(index ast.Expr, length int64, iota int) int64 {
 	return i
 }
 
+// compositeLitKey resolves unresolved composite literal keys.
+// For details, see comment in go/parser/parser.go, method parseElement.
+func (check *checker) compositeLitKey(key ast.Expr) {
+	if ident, ok := key.(*ast.Ident); ok && ident.Obj == nil {
+		ident.Obj = check.pkgscope.Lookup(ident.Name)
+		if ident.Obj == nil {
+			check.errorf(ident.Pos(), "undeclared name: %s", ident.Name)
+		}
+	}
+}
+
 // indexElts checks the elements (elts) of an array or slice composite literal
-// against the literals element type (typ), and the element indices against
+// against the literal's element type (typ), and the element indices against
 // the literal length if known (length >= 0). It returns the length of the
 // literal (maximum index value + 1).
 //
@@ -520,6 +531,7 @@ func (check *checker) indexedElts(elts []ast.Expr, typ Type, length int64, iota
 		validIndex := false
 		eval := e
 		if kv, _ := e.(*ast.KeyValueExpr); kv != nil {
+			check.compositeLitKey(kv.Key)
 			if i := check.index(kv.Key, length, iota); i >= 0 {
 				index = i
 				validIndex = true
@@ -714,14 +726,6 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
 		}
 
 	case *ast.CompositeLit:
-		// TODO(gri) Known bug: The parser doesn't resolve composite literal keys
-		//           because it cannot know the type of the literal and therefore
-		//           cannot know if a key is a struct field or not. Consequently,
-		//           if a key is an identifier, it is unresolved and thus has no
-		//           ast.Objects associated with it. At the moment, the respective
-		//           error message is not issued because the type-checker doesn't
-		//           resolve the identifier, and because it assumes that the parser
-		//           did the resolution.
 		typ := hint
 		openArray := false
 		if e.Type != nil {
@@ -827,6 +831,7 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
 					check.errorf(e.Pos(), "missing key in map literal")
 					continue
 				}
+				check.compositeLitKey(kv.Key)
 				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)

src/pkg/go/parser/parser.go

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -162,7 +162,12 @@ func (p *parser) shortVarDecl(decl *ast.AssignStmt, list []ast.Expr) {\n // internal consistency.\n var unresolved = new(ast.Object)\n \n-func (p *parser) resolve(x ast.Expr) {\n+// If x is an identifier, tryResolve attempts to resolve x by looking up\n+// the object it denotes. If no object is found and collectUnresolved is\n+// set, x is marked as unresolved and collected in the list of unresolved\n+// identifiers.\n+//\n+func (p *parser) tryResolve(x ast.Expr, collectUnresolved bool) {\n 	// nothing to do if x is not an identifier or the blank identifier\n 	ident, _ := x.(*ast.Ident)\n 	if ident == nil {\n@@ -183,8 +188,14 @@ func (p *parser) resolve(x ast.Expr) {\n 	// must be found either in the file scope, package scope\n 	// (perhaps in another file), or universe scope --- collect\n 	// them so that they can be resolved later\n-\tident.Obj = unresolved\n-\tp.unresolved = append(p.unresolved, ident)\n+\tif collectUnresolved {\n+\t\tident.Obj = unresolved\n+\t\tp.unresolved = append(p.unresolved, ident)\n+\t}\n+}\n+\n+func (p *parser) resolve(x ast.Expr) {\n+\tp.tryResolve(x, true)\n }\n \n // ----------------------------------------------------------------------------\n@@ -1189,15 +1200,32 @@ func (p *parser) parseElement(keyOk bool) ast.Expr {\n \t\treturn p.parseLiteralValue(nil)\n \t}\n \n-\t// The parser cannot resolve a key expression because it does not know\n-\t// what the composite literal type is: if we have an array/slice index\n-\t// or map key, we want to resolve, but if we have a struct field name\n-\t// we cannot. Leave this to type-checking phase.\n+\t// Because the parser doesn't know the composite literal type, it cannot\n+\t// know if a key that's an identifier is a struct field name or a name\n+\t// denoting a value. The former is not resolved by the parser or the\n+\t// resolver.\n+\t//\n+\t// Instead, _try_ to resolve such a key if possible. If it resolves,\n+\t// it a) has correctly resolved, or b) incorrectly resolved because\n+\t// the key is a struct field with a name matching another identifier.\n+\t// In the former case we are done, and in the latter case we don't\n+\t// care because the type checker will do a separate field lookup.\n+\t//\n+\t// If the key does not resolve, it must a) be defined at the top-\n+\t// level in another file of the same package or be undeclared, or\n+\t// b) it is a struct field. In the former case, the type checker\n+\t// can do a top-level lookup, and in the latter case it will do a\n+\t// separate field lookup.\n \tx := p.checkExpr(p.parseExpr(keyOk))\n \tif keyOk {\n \t\tif p.tok == token.COLON {\n \t\t\tcolon := p.pos\n \t\t\tp.next()\n+\t\t\t// Try to resolve the key but don't collect it\n+\t\t\t// as unresolved identifier if it fails so that\n+\t\t\t// we don't get (possibly false) errors about\n+\t\t\t// undeclared names.\n+\t\t\tp.tryResolve(x, false)\n \t\t\treturn &ast.KeyValueExpr{Key: x, Colon: colon, colon, Value: p.parseElement(false)}\n \t\t}\n \t\tp.resolve(x) // not a key

コアとなるコードの解説

go/parser の変更

  • tryResolve 関数の追加:

    • func (p *parser) tryResolve(x ast.Expr, collectUnresolved bool)
    • この関数は、与えられたASTノード x が識別子 (*ast.Ident) である場合に、その識別子を現在のスコープで解決しようと試みます。
    • collectUnresolvedtrue の場合、解決できなかった識別子は p.unresolved リストに追加され、後で型チェッカーが処理できるようにマークされます。
    • collectUnresolvedfalse の場合、解決できなかったとしても p.unresolved リストには追加されません。これは、複合リテラルのキーのように、パーサーが解決できなくても型チェッカーが後で別の方法(例: 構造体フィールドのルックアップ)で解決できる可能性がある場合に利用されます。
  • parseElement 関数内の tryResolve の呼び出し:

    • 複合リテラル内で key: value の形式で要素が指定されている場合、key 部分のASTノード x に対して p.tryResolve(x, false) が呼び出されます。
    • これにより、パーサーはキーが識別子である場合に、それが既存の変数や定数として解決できるかどうかを「試行的に」確認します。
    • もし解決できれば、その識別子には対応する ast.Object が設定されます。
    • 解決できなかった場合でも、collectUnresolvedfalse のため、未解決の識別子としてマークされません。これは、キーが構造体のフィールド名である可能性があり、その場合は型チェッカーが構造体の型情報に基づいて解決するため、パーサーが未解決として扱う必要がないためです。

exp/types の変更

  • checker 構造体への pkgscope フィールドの追加:

    • pkgscope *ast.Scope
    • 型チェッカーが、現在チェックしているパッケージのトップレベルスコープ(パッケージスコープ)にアクセスできるようにします。これにより、パッケージレベルで宣言された識別子をルックアップできるようになります。
  • compositeLitKey 関数の追加:

    • func (check *checker) compositeLitKey(key ast.Expr)
    • この関数は、型チェッカーの段階で、複合リテラルのキーが識別子であり、かつパーサーによってまだ解決されていない (ident.Obj == nil) 場合に呼び出されます。
    • check.pkgscope.Lookup(ident.Name) を使用して、パッケージスコープ内でその識別子をルックアップします。
    • もし識別子が見つかれば、その ast.Objectident.Obj に設定し、解決済みとします。
    • もし見つからなければ、その識別子は未宣言であると判断し、undeclared name エラーを報告します。
    • この関数は、配列/スライスリテラルのインデックスやマップリテラルのキーのチェック時に呼び出され、パーサーが試行的に解決できなかった識別子を、型チェッカーがパッケージスコープで最終的に解決しようとします。

これらの変更により、複合リテラルのキー解決のロジックが、パーサーと型チェッカーの間でより適切に分担されるようになりました。パーサーは構文解析の段階で可能な限り解決を試み、型チェッカーはより詳細な型情報とスコープ情報に基づいて最終的な解決とエラー報告を行います。これにより、コンパイル時のエラーメッセージの精度が向上し、開発者が問題を特定しやすくなります。

関連リンク

参考にした情報源リンク