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

[インデックス 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.IdentObjectに解決されたというイベントが発生した際に、ユーザーが定義した関数を呼び出すメカニズムが導入されています。
  • 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.IdentObjectのマッピングを記録します。
    • 重要なのは、このメソッド内でcheck.ctxt.Ident(つまり、Contextに設定されたIdentコールバック)がnilでなければ呼び出される点です。これにより、外部から提供されたコールバック関数が、識別子解決のたびに実行されるようになります。
    • stmt.goのコメントにあるように、一部の式(例: 関数呼び出しの引数)は型チェック中に複数回評価される可能性があり、その場合、registerメソッドが複数回呼び出される可能性があります。registerメソッドは、既に登録済みのマッピングであれば何もしないように冪等性(idempotency)が考慮されています。

これらの変更により、型チェッカーの内部状態を外部から監視し、識別子解決の情報を利用することが可能になります。これは、Goのツールエコシステムを強化する上で非常に重要な基盤となります。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

  1. 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.
    
  2. 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:
    
  3. 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
    
  4. 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
      		}
      	}
    
  5. src/pkg/go/types/resolver_test.go: リゾルバーテストが大幅に修正され、再有効化されました。特に、TestResolveQualifiedIdents関数内でContextIdentコールバックを設定し、それを利用して識別子解決が正しく行われているかを検証するようになりました。

    --- 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    -	*/
    +	}
     }
    
  6. 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言語のソースコード(特にgo/typesパッケージ)
  • Go言語のコンパイラ設計に関する一般的な情報
  • 抽象構文木(AST)に関する一般的な情報
  • 型システムと型チェックに関する一般的な情報
  • Gerrit Code Review (golang.org/cl/7107043)