[インデックス 14756] ファイルの概要
このコミットは、Go言語の実験的な型チェッカー (exp/types
) およびパーサー (go/parser
) における複合リテラルのキー解決に関する改善を目的としています。具体的には、複合リテラル内で識別子として使用されるキー(例: MyStruct{field: value}
の field
や MyMap{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言語のコンパイルプロセスと関連する概念についての知識が必要です。
-
Go言語のコンパイルプロセス:
- 字句解析 (Lexical Analysis): ソースコードをトークン(最小単位の単語)に分解します。
- 構文解析 (Parsing): トークンの列を解析し、抽象構文木 (AST: Abstract Syntax Tree) を構築します。この段階で、コードの構造がツリー形式で表現されます。Go言語では、
go/parser
パッケージがこの役割を担います。 - 型チェック (Type Checking): ASTを走査し、各式の型が正しいか、型変換が適切かなどを検証します。この段階で、識別子がどの宣言に対応するか(解決)も行われます。このコミットで言及されている
exp/types
パッケージは、Goの型チェッカーの実験的な実装でした。 - コード生成 (Code Generation): 型チェックが完了したASTから、実行可能なバイナリコードを生成します。
-
抽象構文木 (AST: Abstract Syntax Tree):
- ソースコードの構造を木構造で表現したものです。各ノードは、式、文、宣言などのコード要素に対応します。
go/ast
パッケージは、GoのASTのデータ構造を定義しています。
-
複合リテラル (Composite Literals):
- Go言語で構造体、配列、スライス、マップなどの複合型の値を直接初期化するための構文です。
TypeName{key: value, ...}
の形式を取ります。- キーは省略することもできますが、指定する場合は、構造体のフィールド名、マップのキー、配列/スライスのインデックスなどが使われます。
-
識別子の解決 (Identifier Resolution):
- ソースコード中の識別子(変数名、関数名、型名など)が、どの宣言に対応するかを特定するプロセスです。
- これは通常、スコープ(識別子が有効な範囲)の概念と密接に関連しています。
- パーサーは構文的な構造を理解しますが、識別子が具体的に何を指すのか(例えば、
x
がローカル変数なのか、グローバル変数なのか、パッケージレベルの定数なのか)を完全に解決するには、型情報やスコープ情報が必要になります。
-
exp/types
パッケージ:- Go言語の型チェッカーの実験的な実装でした。Go 1.0のリリース後、より堅牢で正確な型チェッカーを開発するために導入されました。最終的には、このパッケージの成果が
go/types
パッケージとして標準ライブラリに統合されました。 - このパッケージは、ASTを受け取り、型情報を付与し、型エラーを検出する役割を担っていました。
- Go言語の型チェッカーの実験的な実装でした。Go 1.0のリリース後、より堅牢で正確な型チェッカーを開発するために導入されました。最終的には、このパッケージの成果が
-
go/parser
パッケージ:- Go言語のソースコードを解析し、ASTを生成する標準パッケージです。
- このコミットでは、パーサーが複合リテラルのキーを処理する方法が変更されています。
このコミットの核心は、パーサーと型チェッカーの間の責任分担の調整にあります。パーサーは構文的な構造を構築しますが、意味的な解決(識別子が何を指すか、型が何か)は型チェッカーの役割です。しかし、複合リテラルのキーのように、構文解析の段階で「ヒント」として識別子を解決しようとすることで、後続の型チェックプロセスを効率化し、より良いエラー報告を可能にしようとしています。
技術的詳細
このコミットの技術的な変更は、主に src/pkg/exp/types/
と src/pkg/go/parser/
の2つのパッケージにまたがっています。
go/parser
の変更点
最も重要な変更は、go/parser/parser.go
の parseElement
メソッドと、新しいヘルパーメソッド tryResolve
の導入です。
-
tryResolve
メソッドの導入:- 以前は
resolve
メソッドが識別子の解決と、解決できなかった場合のunresolved
リストへの追加を同時に行っていました。 tryResolve(x ast.Expr, collectUnresolved bool)
は、識別子x
を解決しようとしますが、collectUnresolved
がfalse
の場合、解決できなかった識別子をunresolved
リストに追加しません。- これは、複合リテラルのキーのように、パーサーが「試行的に」解決を試みるが、失敗してもそれが必ずしもエラーではない(型チェッカーが後で解決する可能性がある、または構造体フィールド名であるためパーサーが解決する必要がない)場合に有用です。
- 以前は
-
parseElement
メソッドの変更:- 複合リテラルの要素(キーと値のペア)を解析するメソッドです。
- 以前は、キーが識別子である場合、パーサーはそれを解決しようとせず、型チェッカーに任せていました。コメントにも「The parser cannot resolve a key expression... Leave this to type-checking phase.」と明記されていました。
- 変更後、キーが
KeyValueExpr
のKey
である場合、p.tryResolve(x, false)
が呼び出されます。- これは、「キーが識別子である場合、試行的に解決を試みるが、もし解決できなくても
unresolved
リストには追加しない」という挙動を意味します。 - この「試行的な解決」の意図は、コミットメッセージのコメントに詳しく書かれています。
- パーサーは複合リテラルの型を知らないため、キーが構造体フィールド名なのか、それとも値を示す識別子なのかを区別できません。
- もしキーが解決できれば、それは正しく解決されたか、あるいはたまたま別の識別子と名前が一致した構造体フィールド名であるかのどちらかです。後者の場合でも、型チェッカーが最終的にフィールドルックアップを行うため問題ありません。
- もしキーが解決できなければ、それはパッケージ内の別のファイルで定義されているか、未宣言であるか、あるいは構造体フィールド名であるかのいずれかです。型チェッカーは、トップレベルのルックアップやフィールドルックアップを行うことで、これらを適切に処理できます。
- この変更により、パーサーはより多くの情報を型チェッカーに渡すことができ、型チェッカーはより効率的に、かつ正確にエラーを報告できるようになります。
- これは、「キーが識別子である場合、試行的に解決を試みるが、もし解決できなくても
exp/types
の変更点
exp/types/check.go
と exp/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"
のようなパッケージレベルの変数をマップキーとして使用するケース。index3
やkey3
のように未宣言の識別子を使用した場合に、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
) である場合に、その識別子を現在のスコープで解決しようと試みます。 collectUnresolved
がtrue
の場合、解決できなかった識別子はp.unresolved
リストに追加され、後で型チェッカーが処理できるようにマークされます。collectUnresolved
がfalse
の場合、解決できなかったとしてもp.unresolved
リストには追加されません。これは、複合リテラルのキーのように、パーサーが解決できなくても型チェッカーが後で別の方法(例: 構造体フィールドのルックアップ)で解決できる可能性がある場合に利用されます。
-
parseElement
関数内のtryResolve
の呼び出し:- 複合リテラル内で
key: value
の形式で要素が指定されている場合、key
部分のASTノードx
に対してp.tryResolve(x, false)
が呼び出されます。 - これにより、パーサーはキーが識別子である場合に、それが既存の変数や定数として解決できるかどうかを「試行的に」確認します。
- もし解決できれば、その識別子には対応する
ast.Object
が設定されます。 - 解決できなかった場合でも、
collectUnresolved
がfalse
のため、未解決の識別子としてマークされません。これは、キーが構造体のフィールド名である可能性があり、その場合は型チェッカーが構造体の型情報に基づいて解決するため、パーサーが未解決として扱う必要がないためです。
- 複合リテラル内で
exp/types
の変更
-
checker
構造体へのpkgscope
フィールドの追加:pkgscope *ast.Scope
- 型チェッカーが、現在チェックしているパッケージのトップレベルスコープ(パッケージスコープ)にアクセスできるようにします。これにより、パッケージレベルで宣言された識別子をルックアップできるようになります。
-
compositeLitKey
関数の追加:func (check *checker) compositeLitKey(key ast.Expr)
- この関数は、型チェッカーの段階で、複合リテラルのキーが識別子であり、かつパーサーによってまだ解決されていない (
ident.Obj == nil
) 場合に呼び出されます。 check.pkgscope.Lookup(ident.Name)
を使用して、パッケージスコープ内でその識別子をルックアップします。- もし識別子が見つかれば、その
ast.Object
をident.Obj
に設定し、解決済みとします。 - もし見つからなければ、その識別子は未宣言であると判断し、
undeclared name
エラーを報告します。 - この関数は、配列/スライスリテラルのインデックスやマップリテラルのキーのチェック時に呼び出され、パーサーが試行的に解決できなかった識別子を、型チェッカーがパッケージスコープで最終的に解決しようとします。
これらの変更により、複合リテラルのキー解決のロジックが、パーサーと型チェッカーの間でより適切に分担されるようになりました。パーサーは構文解析の段階で可能な限り解決を試み、型チェッカーはより詳細な型情報とスコープ情報に基づいて最終的な解決とエラー報告を行います。これにより、コンパイル時のエラーメッセージの精度が向上し、開発者が問題を特定しやすくなります。
関連リンク
- Go言語の複合リテラルに関する公式ドキュメント (Go言語仕様):
- Go言語のパーサー (
go/parser
) に関するドキュメント: - Go言語のAST (
go/ast
) に関するドキュメント: - Go言語の型チェッカー (
go/types
) に関するドキュメント (このコミットのexp/types
の後継):
参考にした情報源リンク
- https://github.com/golang/go/commit/d0428379e79d8a3a868fda9509963563e73e10b2 (本コミットのGitHubページ)
- https://golang.org/cl/6994047 (本コミットに対応するGerrit Code Reviewページ)
- Go言語の公式ドキュメントおよび仕様書
- Go言語のコンパイラソースコード (特に
go/parser
とgo/types
パッケージ)