[インデックス 14885] ファイルの概要
このコミットは、Go言語の型チェッカー(go/types
パッケージ)における重要な改善を導入しています。具体的には、抽象構文木(AST)内の識別子(*ast.Ident
)と、それが参照するオブジェクト(Object
)とのマッピングを処理するためのコールバックメカニズムが追加されました。これにより、型チェックプロセス中に識別子がどの実体(変数、関数、型など)を指しているかをより柔軟に追跡できるようになります。また、関連するリゾルバーテストが再有効化され、この変更が正しく機能することを確認しています。
コミット
commit 7f18f8119257d42e5702b56c246c930904aac15a
Author: Robert Griesemer <gri@golang.org>
Date: Mon Jan 14 09:43:27 2013 -0800
go/types: callback for *ast.Ident -> Object mapping
Also re-enabled resolver test.
R=adonovan
CC=golang-dev
https://golang.org/cl/7107043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7f18f8119257d42e5702b56c246c930904aac15a
元コミット内容
go/types: callback for *ast.Ident -> Object mapping
Also re-enabled resolver test.
R=adonovan
CC=golang-dev
https://golang.org/cl/7107043
変更の背景
このコミットは、Go言語のコンパイラツールチェーンの一部であるgo/types
パッケージの機能強化の一環として行われました。go/types
パッケージは、Goプログラムの型チェックとセマンティック解析を担当します。プログラムの正確性を保証するために、各識別子(変数名、関数名など)がどの宣言されたエンティティ(オブジェクト)を指しているかを正確に解決する必要があります。
以前のバージョンでは、識別子からオブジェクトへのマッピングの追跡が内部的に行われていましたが、このコミットでは、そのマッピングが確立された際に外部からフックできるコールバックメカニズムを導入しています。これにより、型チェッカーの外部ツールや分析ツールが、型チェックの過程で識別子の解決情報をリアルタイムで取得できるようになります。これは、IDEのコード補完、リファクタリングツール、静的解析ツールなど、Goのコードベースを理解し操作する高度なツールを構築する上で非常に有用です。
また、この変更は、以前無効化されていたリゾルバーテストを再有効化する目的も持っています。これは、型チェッカーの識別子解決ロジックが正しく機能していることを確認するための重要なステップです。テストの再有効化は、この新しいコールバックメカニズムが既存の機能に悪影響を与えず、むしろ安定性を向上させることを示唆しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。
- 抽象構文木 (Abstract Syntax Tree - AST): プログラムのソースコードを木構造で表現したものです。Go言語では、
go/ast
パッケージがASTの構造を定義しています。*ast.Ident
はASTノードの一種で、識別子(変数名、関数名、型名など)を表します。 - 型チェッカー (Type Checker): プログラムがGo言語の型システム規則に準拠しているかを検証するコンパイラのフェーズです。
go/types
パッケージがこの役割を担います。型チェッカーは、各識別子が正しい型を持ち、適切な文脈で使用されているかを検証します。 - オブジェクト (Object): Go言語のプログラムにおいて、名前を持つエンティティ(変数、関数、型、パッケージなど)を抽象的に表現する概念です。
go/types
パッケージでは、これらのオブジェクトをtypes.Object
インターフェース(またはその具象型)として表現します。型チェッカーは、AST内の*ast.Ident
がどのObject
を指しているかを解決(resolve)します。 - 識別子解決 (Identifier Resolution): ソースコード中の識別子(例:
myVar
)が、プログラム内で宣言されたどの実体(例:var myVar int
で宣言された変数)を指しているかを特定するプロセスです。これは型チェックの重要なステップであり、スコープ規則に基づいて行われます。 - コールバック (Callback): あるイベントが発生したときに呼び出される関数です。このコミットでは、
*ast.Ident
がObject
に解決されたというイベントが発生した際に、ユーザーが定義した関数を呼び出すメカニズムが導入されています。 go/token
パッケージ: ソースコードの位置情報(ファイル名、行番号、列番号)を扱うためのパッケージです。token.Pos
はソースコード内の特定の位置を表します。go/parser
パッケージ: Goのソースコードを解析し、ASTを生成するためのパッケージです。
技術的詳細
このコミットの主要な技術的変更点は、go/types
パッケージのContext
構造体にIdent func(id *ast.Ident, obj Object)
という新しいコールバックフィールドが追加されたことです。
Context
構造体:go/types
パッケージにおける型チェックのコンテキストを保持する構造体です。エラーハンドリングのためのError
コールバックなどが既に存在していました。Ident
コールバックの追加:Ident func(id *ast.Ident, obj Object)
: この関数は、型チェッカーが*ast.Ident
(識別子)を対応するObject
(宣言された実体)に解決するたびに呼び出されます。id
: 解決された識別子のASTノードです。obj
:id
が参照するObject
です。- このコールバックは、型チェックの過程で識別子とオブジェクトのマッピングを外部に公開するためのフックを提供します。
このコールバックを実際に呼び出すために、checker
構造体(型チェックの主要なロジックを実装する内部構造体)にregister
メソッドが追加されました。
register(id *ast.Ident, obj Object)
メソッド:- このメソッドは、
checker
の内部マップ(check.idents
)に*ast.Ident
とObject
のマッピングを記録します。 - 重要なのは、このメソッド内で
check.ctxt.Ident
(つまり、Context
に設定されたIdent
コールバック)がnil
でなければ呼び出される点です。これにより、外部から提供されたコールバック関数が、識別子解決のたびに実行されるようになります。 stmt.go
のコメントにあるように、一部の式(例: 関数呼び出しの引数)は型チェック中に複数回評価される可能性があり、その場合、register
メソッドが複数回呼び出される可能性があります。register
メソッドは、既に登録済みのマッピングであれば何もしないように冪等性(idempotency)が考慮されています。
- このメソッドは、
これらの変更により、型チェッカーの内部状態を外部から監視し、識別子解決の情報を利用することが可能になります。これは、Goのツールエコシステムを強化する上で非常に重要な基盤となります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードスニペットは以下の通りです。
-
src/pkg/go/types/api.go
:Context
構造体にIdent
コールバックが追加されました。--- a/src/pkg/go/types/api.go +++ b/src/pkg/go/types/api.go @@ -20,9 +20,16 @@ type Context struct { PtrSize int64 // size in bytes of pointers // If Error is not nil, it is called with each error found - // during type checking. + // during type checking. Most error messages have accurate + // position information; those error strings are formatted + // filename:line:column: message. Error func(err error) + // If Ident is not nil, it is called for each identifier + // id that is type-checked: obj is the object denoted by + // the identifier. + Ident func(id *ast.Ident, obj Object) + // If Expr is not nil, it is called for each expression x that is // type-checked: typ is the expression type, and val is the value // if x is constant, val is nil otherwise.
-
src/pkg/go/types/check.go
:checker
構造体にregister
メソッドが追加され、既存の識別子解決ロジック(lookup
,declareIdent
,decl
など)からこのregister
メソッドが呼び出されるようになりました。--- a/src/pkg/go/types/check.go +++ b/src/pkg/go/types/check.go @@ -32,6 +32,21 @@ type checker struct { pos []token.Pos // stack of expr positions; debugging support, used if trace is set }\n +func (check *checker) register(id *ast.Ident, obj Object) { + // When an expression is evaluated more than once (happens + // in rare cases, e.g. for statement expressions, see + // comment in stmt.go), the object has been registered + // before. Don't do anything in that case. + if alt := check.idents[id]; alt != nil { + assert(alt == obj) + return + } + check.idents[id] = obj + if f := check.ctxt.Ident; f != nil { + f(id, obj) + } +} + // lookup returns the unique Object denoted by the identifier. // For identifiers without assigned *ast.Object, it uses the // checker.idents map; for identifiers with an *ast.Object it @@ -41,8 +56,8 @@ type checker struct { // the typechecker, only the idents map is needed. // func (check *checker) lookup(ident *ast.Ident) Object { - astObj := ident.Obj obj := check.idents[ident] + astObj := ident.Obj if obj != nil { assert(astObj == nil || check.objects[astObj] == nil || check.objects[astObj] == obj) @@ -53,10 +68,9 @@ func (check *checker) lookup(ident *ast.Ident) Object { return nil }\n - obj = check.objects[astObj] - if obj == nil { + if obj = check.objects[astObj]; obj == nil { obj = newObj(astObj) - check.idents[ident] = obj + check.register(ident, obj) check.objects[astObj] = obj }\n @@ -82,7 +96,7 @@ func (check *checker) later(f *Func, sig *Signature, body *ast.BlockStmt) { func (check *checker) declareIdent(scope *Scope, ident *ast.Ident, obj Object) { assert(check.lookup(ident) == nil) // identifier already declared or resolved - check.idents[ident] = obj + check.register(ident, obj) if ident.Name != "_" { if alt := scope.Insert(obj); alt != nil { prevDecl := "" @@ -364,7 +378,7 @@ func (check *checker) decl(decl ast.Decl) { if d.Name.Name == "init" { assert(obj == nil) // all other functions should have an object obj = &Func{Name: d.Name.Name, decl: d} - check.idents[d.Name] = obj + check.register(d.Name, obj) } check.object(obj, false) default:
-
src/pkg/go/types/expr.go
: 式(expression)の型チェックに関連する箇所でcheck.register
が呼び出されるようになりました。特に、複合リテラル(composite literal)のキーやセレクタ式(selector expression)の解決時に使用されます。--- a/src/pkg/go/types/expr.go +++ b/src/pkg/go/types/expr.go @@ -511,7 +511,7 @@ func (check *checker) index(index ast.Expr, length int64, iota int) int64 { func (check *checker) compositeLitKey(key ast.Expr) { if ident, ok := key.(*ast.Ident); ok && ident.Obj == nil { if obj := check.pkg.Scope.Lookup(ident.Name); obj != nil { - check.idents[ident] = obj + check.register(ident, obj) } else { check.errorf(ident.Pos(), "undeclared name: %s", ident.Name) } @@ -871,6 +871,7 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle check.errorf(e.Sel.Pos(), "cannot refer to unexported %s", sel) goto Error } + check.register(e.Sel, exp) // Simplified version of the code for *ast.Idents: // - imported packages use types.Scope and types.Objects // - imported objects are always fully initialized
-
src/pkg/go/types/resolve.go
: 識別子解決のロジックでcheck.register
が呼び出されるようになりました。--- a/src/pkg/go/types/resolve.go +++ b/src/pkg/go/types/resolve.go @@ -29,7 +29,7 @@ func (check *checker) declareObj(scope, altScope *Scope, obj Object) { func (check *checker) resolveIdent(scope *Scope, ident *ast.Ident) bool { for ; scope != nil; scope = scope.Outer { if obj := scope.Lookup(ident.Name); obj != nil { - check.idents[ident] = obj + check.register(ident, obj) return true } }
-
src/pkg/go/types/resolver_test.go
: リゾルバーテストが大幅に修正され、再有効化されました。特に、TestResolveQualifiedIdents
関数内でContext
のIdent
コールバックを設定し、それを利用して識別子解決が正しく行われているかを検証するようになりました。--- a/src/pkg/go/types/resolver_test.go +++ b/src/pkg/go/types/resolver_test.go @@ -5,10 +5,8 @@ package types import ( - "fmt" "go/ast" - //"go/parser" - "go/scanner" + "go/parser" "go/token" "testing" ) @@ -30,13 +28,9 @@ var sources = []string{ }\n \tfunc g() (x int) { return }\n \t`,\n - // TODO(gri) fix this - // cannot handle dot-import at the moment - /* - \t\t`package p - \t\timport . "go/parser" - \t\tfunc g() Mode { return ImportsOnly }`,\n - */ + `package p + import . "go/parser" + func g() Mode { return ImportsOnly }`,\n }\n \n var pkgnames = []string{ @@ -44,88 +38,52 @@ var pkgnames = []string{ "math", }\n \n -// ResolveQualifiedIdents resolves the selectors of qualified -// identifiers by associating the correct ast.Object with them. -// TODO(gri): Eventually, this functionality should be subsumed -// by Check. -// -func ResolveQualifiedIdents(fset *token.FileSet, pkg *ast.Package) error { - var errors scanner.ErrorList - - findObj := func(pkg *ast.Object, name *ast.Ident) *ast.Object { - scope := pkg.Data.(*ast.Scope) - obj := scope.Lookup(name.Name) - if obj == nil { - errors.Add(fset.Position(name.Pos()), fmt.Sprintf("no %s in package %s", name.Name, pkg.Name)) - } - return obj - } - - ast.Inspect(pkg, func(n ast.Node) bool { - if s, ok := n.(*ast.SelectorExpr); ok { - if x, ok := s.X.(*ast.Ident); ok && x.Obj != nil && x.Obj.Kind == ast.Pkg { - // find selector in respective package - s.Sel.Obj = findObj(x.Obj, s.Sel) - } - return false - } - return true - }) - - return errors.Err() -} - func TestResolveQualifiedIdents(t *testing.T) { - return - // disabled for now - /* - \t\t// parse package files - \t\tfset := token.NewFileSet() - \t\tfiles := make([]*ast.File, len(sources)) - \t\tfor i, src := range sources { - \t\t\tf, err := parser.ParseFile(fset, "", src, parser.DeclarationErrors) - \t\t\tif err != nil { - \t\t\t\tt.Fatal(err) - \t\t\t}\n - \t\t\tfiles[i] = f - \t\t}\n - - \t\t// resolve package AST - \t\tastpkg, pkg, err := Check(fset, files) - \t\tif err != nil { - \t\t\tt.Fatal(err) - \t\t}\n - - \t\t// check that all packages were imported - \t\tfor _, name := range pkgnames { - \t\t\tif pkg.Imports[name] == nil { - \t\t\t\tt.Errorf("package %s not imported", name) - \t\t\t}\n - \t\t}\n - - \t\t// TODO(gri) fix this - \t\t// unresolved identifiers are not collected at the moment - \t\t// check that there are no top-level unresolved identifiers - \t\tfor _, f := range astpkg.Files { - \t\t\tfor _, x := range f.Unresolved { - \t\t\t\tt.Errorf("%s: unresolved global identifier %s", fset.Position(x.Pos()), x.Name) - \t\t\t}\n - \t\t}\n - - \t\t// resolve qualified identifiers - \t\tif err := ResolveQualifiedIdents(fset, astpkg); err != nil { - \t\t\tt.Error(err) - \t\t}\n - - \t\t// check that qualified identifiers are resolved - \t\tast.Inspect(astpkg, func(n ast.Node) bool { + // parse package files + fset := token.NewFileSet() + var files []*ast.File + for _, src := range sources { + f, err := parser.ParseFile(fset, "", src, parser.DeclarationErrors) + if err != nil { + t.Fatal(err) + } + files = append(files, f) + } + + // resolve and type-check package AST + idents := make(map[*ast.Ident]Object) + ctxt := Default + ctxt.Ident = func(id *ast.Ident, obj Object) { idents[id] = obj } + pkg, err := ctxt.Check(fset, files) + if err != nil { + t.Fatal(err) + } + + // check that all packages were imported + for _, name := range pkgnames { + if pkg.Imports[name] == nil { + t.Errorf("package %s not imported", name) + } + } + + // check that there are no top-level unresolved identifiers + for _, f := range files { + for _, x := range f.Unresolved { + t.Errorf("%s: unresolved global identifier %s", fset.Position(x.Pos()), x.Name) + } + } + + // check that qualified identifiers are resolved + for _, f := range files { + ast.Inspect(f, func(n ast.Node) bool { if s, ok := n.(*ast.SelectorExpr); ok { if x, ok := s.X.(*ast.Ident); ok { - if x.Obj == nil { + obj := idents[x] + if obj == nil { t.Errorf("%s: unresolved qualified identifier %s", fset.Position(x.Pos()), x.Name) return false } - if x.Obj.Kind == ast.Pkg && s.Sel != nil && s.Sel.Obj == nil { + if _, ok := obj.(*Package); ok && idents[s.Sel] == nil { t.Errorf("%s: unresolved selector %s", fset.Position(s.Sel.Pos()), s.Sel.Name) return false } @@ -135,5 +93,5 @@ func TestResolveQualifiedIdents(t *testing.T) {\n }\n return true })\n - */ + } }
-
src/pkg/go/types/stmt.go
: ステートメント(statement)の型チェックに関連する箇所でcheck.register
が呼び出されるようになりました。--- a/src/pkg/go/types/stmt.go +++ b/src/pkg/go/types/stmt.go @@ -307,6 +307,9 @@ func (check *checker) stmt(s ast.Stmt) {\n // function calls are permitted used = true // but some builtins are excluded + // (Caution: This evaluates e.Fun twice, once here and once + // below as part of s.X. This has consequences for + // check.register. Perhaps this can be avoided.) check.expr(&x, e.Fun, nil, -1) if x.mode != invalid { if b, ok := x.typ.(*builtin); ok && !b.isStatement { @@ -431,7 +434,7 @@ func (check *checker) stmt(s ast.Stmt) {\n }\n name := ast.NewIdent(res.Name)\n name.NamePos = s.Pos()\n - check.idents[name] = &Var{Name: res.Name, Type: res.Type}\n + check.register(name, &Var{Name: res.Name, Type: res.Type})\n lhs[i] = name }\n if len(s.Results) > 0 || !named {\n @@ -465,7 +468,7 @@ func (check *checker) stmt(s ast.Stmt) {\n if tag == nil {\n // use fake true tag value and position it at the opening { of the switch ident := &ast.Ident{NamePos: s.Body.Lbrace, Name: "true"}\n - check.idents[ident] = Universe.Lookup("true") + check.register(ident, Universe.Lookup("true")) tag = ident }\n check.expr(&x, tag, nil, -1)
コアとなるコードの解説
このコミットの核心は、go/types
パッケージが識別子を解決する際に、その解決結果を外部に通知するメカニズムを提供することです。
以前は、checker
内部のidents
マップが*ast.Ident
からObject
へのマッピングを管理していました。このコミットでは、このマップへの書き込み操作がregister
という新しいヘルパーメソッドに集約されました。
register
メソッドの定義は以下の通りです。
func (check *checker) register(id *ast.Ident, obj Object) {
// When an expression is evaluated more than once (happens
// in rare cases, e.g. for statement expressions, see
// comment in stmt.go), the object has been registered
// before. Don't do anything in that case.
if alt := check.idents[id]; alt != nil {
assert(alt == obj)
return
}
check.idents[id] = obj
if f := check.ctxt.Ident; f != nil {
f(id, obj)
}
}
このメソッドの重要な部分は、if f := check.ctxt.Ident; f != nil { f(id, obj) }
の行です。
これは、Context
構造体(型チェックの全体的な設定と状態を保持する)にIdent
という関数フィールドが設定されている場合、その関数を呼び出すことを意味します。このIdent
関数は、解決された識別子id
とそれに対応するオブジェクトobj
を引数として受け取ります。
これにより、go/types
パッケージを利用する外部のツール(例えば、GoLandのようなIDEや、go vet
のような静的解析ツール)は、Context.Ident
フィールドに独自のコールバック関数を設定することで、型チェックの過程でどの識別子がどのオブジェクトに解決されたかという情報をリアルタイムで取得できるようになります。これは、コードのシンボル解決情報に基づいて高度な機能(例: 定義へのジャンプ、参照の検索、リファクタリング)を実装するための強力なフックとなります。
また、resolver_test.go
の変更は、この新しいコールバックメカニズムが実際に機能し、識別子解決が期待通りに行われていることを検証するために、テストフレームワークがこのコールバックを利用するように更新されたことを示しています。これにより、型チェッカーの正確性と安定性が保証されます。
関連リンク
- Go言語の
go/types
パッケージのドキュメント: https://pkg.go.dev/go/types - Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージのドキュメント: https://pkg.go.dev/go/token - Go言語の
go/parser
パッケージのドキュメント: https://pkg.go.dev/go/parser - Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージにある
golang.org/cl/7107043
はGerritのチェンジリストへのリンクです)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
go/types
パッケージ) - Go言語のコンパイラ設計に関する一般的な情報
- 抽象構文木(AST)に関する一般的な情報
- 型システムと型チェックに関する一般的な情報
- Gerrit Code Review (golang.org/cl/7107043)