[インデックス 14877] ファイルの概要
このコミットは、Go言語のコンパイラツールチェーンの一部である go/types
パッケージにおける重要なリファクタリングの第2段階を示しています。具体的には、抽象構文木(AST)のノードを表現する *go/ast.Object
から、型チェックシステム内部で利用される go/types.Object
への移行を完了させることを目的としています。これにより、go/types
APIが外部に *go/ast.Object
を公開しないようになり、型チェックの内部実装がASTの生成方法に依存しない、より柔軟な設計へと進化します。
コミット
commit 94878070af6f7efe1aa002089b800fe9393f9923
Author: Robert Griesemer <gri@golang.org>
Date: Sun Jan 13 10:33:08 2013 -0800
go/types: Moving from *ast.Objects to types.Objects (step 2).
Completely removed *ast.Objects from being exposed by the
types API. *ast.Objects are still required internally for
resolution, but now the door is open for an internal-only
rewrite of identifier resolution entirely at type-check
time. Once that is done, ASTs can be type-checked whether
they have been created via the go/parser or otherwise,
and type-checking does not require *ast.Object or scope
invariants to be maintained externally.
R=adonovan
CC=golang-dev
https://golang.org/cl/7096048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/94878070af6f7efe1aa002089b800fe9393f9923
元コミット内容
上記の「コミット」セクションに記載されている内容が元コミット内容です。
変更の背景
Go言語のコンパイラは、ソースコードを解析してAST(抽象構文木)を構築し、そのASTを基に型チェックやコード生成を行います。初期のGoコンパイラでは、ASTノードが直接型情報を持つ *ast.Object
を利用していました。しかし、これは go/parser
パッケージによって生成されたASTに強く依存するという制約がありました。
このコミットの背景には、go/types
パッケージをより汎用的な型チェッカーとして独立させるという設計思想があります。go/types
は、Goプログラムの型情報を解析し、セマンティックなエラーを検出するためのライブラリです。このライブラリが *ast.Object
に直接依存していると、以下のような問題が生じます。
- 結合度の高さ:
go/types
がgo/ast
の内部構造に密接に結合してしまい、それぞれのパッケージの独立性が損なわれる。 - 柔軟性の欠如:
go/parser
以外の方法で生成されたAST(例えば、プログラム的に構築されたASTや、異なるパーサーからのAST)をgo/types
で型チェックすることが困難になる。 - 内部状態の管理:
*ast.Object
が持つスコープ情報や解決されたオブジェクトへの参照をgo/types
の外部で維持する必要があり、複雑さが増す。
このコミットは、これらの問題を解決するための「ステップ2」として位置づけられています。最終的な目標は、go/types
が独自の types.Object
構造体とスコープ管理システムを持つことで、ASTの具体的な表現形式から完全に独立し、より堅牢で再利用可能な型チェッカーとなることです。これにより、Goのツールエコシステム全体で型チェック機能をより柔軟に利用できるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語のコンパイラ関連の概念とパッケージに関する知識が必要です。
1. 抽象構文木 (AST) と go/ast
パッケージ
- AST: ソースコードの構文構造を木構造で表現したものです。Go言語では、
go/parser
パッケージがソースコードを解析してASTを生成します。 go/ast
パッケージ: Go言語のASTを定義するパッケージです。ast.Node
インターフェースを実装する様々な構造体(ast.Ident
、ast.Expr
、ast.Stmt
など)が含まれます。*ast.Object
:go/ast
パッケージで定義される構造体で、識別子(変数名、関数名、型名など)が参照する実体(オブジェクト)を表します。Kind
フィールドでそのオブジェクトの種類(定数、変数、型、関数など)を示し、Decl
フィールドでそのオブジェクトが宣言されたASTノードへの参照を持ちます。また、Data
フィールドは任意のデータを保持するために使用され、型チェックの過程で型情報などが一時的に格納されることがありました。
2. 型システムと go/types
パッケージ
go/types
パッケージ: Go言語の型システムを実装し、Goプログラムの型チェックを行うためのライブラリです。ソースコードのセマンティックな正当性を検証し、型エラーを検出します。- 型チェック: プログラムが型規則に準拠しているかを確認するプロセスです。例えば、関数呼び出しの引数の型が期待される型と一致するか、変数の代入が正しい型で行われているかなどを検証します。
types.Object
(このコミットで強化される概念):go/types
パッケージが内部で管理する、Go言語のプログラム要素(パッケージ、定数、型、変数、関数など)を表す抽象的なオブジェクトです。*ast.Object
とは異なり、go/types
の内部表現に特化しており、ASTの具体的な構造から独立しています。
3. 識別子の解決 (Identifier Resolution)
- 識別子の解決: ソースコード中の識別子(変数名、関数名など)が、どの宣言された実体(オブジェクト)を指しているかを特定するプロセスです。これはスコープの概念と密接に関連しています。
- スコープ: プログラム中で識別子が有効な範囲を定義します。Go言語には、ユニバーススコープ、パッケージスコープ、ファイルスコープ、ブロックスコープなどがあります。
4. token.Pos
と token.FileSet
go/token
パッケージ: ソースコード中の位置情報(行番号、列番号、ファイルオフセット)を扱うためのパッケージです。token.Pos
: ソースコード内の特定の位置を表す型です。token.FileSet
: 複数のファイルにまたがる位置情報を管理するための構造体です。エラーメッセージの表示などで、token.Pos
から具体的なファイル名、行番号、列番号を取得するために使用されます。
このコミットは、go/types
が *ast.Object
に直接依存するのではなく、独自の types.Object
とスコープ管理メカニズムを持つことで、型チェックの独立性と柔軟性を高めるという、Goコンパイラの設計思想の進化を反映しています。
技術的詳細
このコミットの主要な技術的変更点は、go/types
パッケージが *ast.Object
への直接的な依存を排除し、独自の types.Object
インターフェースとその具象型(Package
, Const
, TypeName
, Var
, Func
)を導入・強化したことです。これにより、型チェッカーの内部ロジックがASTの具体的な表現から切り離され、より抽象的な types.Object
を中心に処理が行われるようになります。
具体的な変更は以下の多岐にわたります。
1. go/types/objects.go
の大幅な変更
Object
インターフェースの定義:GetName() string
,GetType() Type
,GetPos() token.Pos
,anObject()
メソッドを持つObject
インターフェースが導入されました。これにより、すべての型システムオブジェクトが共通のインターフェースを介して扱えるようになります。- 具象型への
implementsObject
の削除: 以前はimplementsObject
という空の構造体を埋め込むことでObject
インターフェースを実装していましたが、これは削除され、各具象型が直接anObject()
メソッドを実装する形に変更されました。 Package
,Const
,TypeName
,Var
,Func
構造体の変更:- これらの構造体は、
*ast.Object
の代わりに、それぞれが関連するASTノードへの参照(例:spec *ast.ValueSpec
forConst
,decl *ast.FuncDecl
forFunc
)を持つようになりました。これにより、types.Object
がASTノードから必要な情報を取得できるようになりますが、types.Object
自体はASTの内部構造から独立した存在となります。 GetPos()
メソッドが追加され、各types.Object
が自身の宣言位置をtoken.Pos
で返すようになりました。これはエラー報告などで重要です。Const
にはVal
フィールドが追加され、定数の値が直接格納されるようになりました。以前は*ast.Object.Data
に格納されていました。Var
にはvisited bool
フィールドが追加され、初期化サイクルの検出に利用されます。
- これらの構造体は、
newObj
関数の導入:*ast.Object
から対応するtypes.Object
を生成するためのヘルパー関数newObj
が追加されました。これは、ASTから型システムへの橋渡しを担います。
2. go/types/scope.go
の新規追加
Scope
構造体の導入:go/types
パッケージ独自のスコープ管理システムが導入されました。以前は*ast.Scope
を利用していましたが、このコミットでtypes.Scope
が独立しました。Scope
構造体はOuter *Scope
(外側のスコープへの参照) とEntries []Object
(スコープ内のオブジェクトのリスト) を持ちます。Lookup(name string) Object
とInsert(obj Object) Object
メソッドが提供され、スコープ内でのオブジェクトの検索と挿入を管理します。これにより、go/types
が独自の識別子解決ロジックを持つ基盤が確立されます。
3. go/types/check.go
の変更
checker
構造体の変更:pkgscope *ast.Scope
フィールドが削除され、pkg *Package
とmethods map[*TypeName]*Scope
が追加されました。これは、型チェッカーがtypes.Object
とtypes.Scope
を直接扱うようになったことを示します。idents map[*ast.Ident]Object
とobjects map[*ast.Object]Object
が追加され、*ast.Ident
や*ast.Object
とtypes.Object
のマッピングを内部で管理するようになりました。これは、ASTを走査しながらtypes.Object
を構築する際の橋渡し役となります。
lookup
メソッドの導入:*ast.Ident
から対応するtypes.Object
を取得するためのlookup
メソッドが追加されました。このメソッドは、idents
マップとobjects
マップを利用して、識別子解決を行います。object
メソッドの変更:*ast.Object
を引数にとっていたobject
メソッドがtypes.Object
を引数にとるように変更され、各types.Object
の種類に応じた型チェックロジックが実装されました。定数、変数、型名、関数などの型チェックがtypes.Object
を中心に行われます。declare
関数の削除とdeclareIdent
の導入:*ast.Object
を宣言するdeclare
関数が削除され、types.Object
をスコープに宣言するdeclareIdent
関数が導入されました。Check
関数のシグネチャ変更:Check
関数が*ast.Package
を返さなくなり、*Package
(つまりtypes.Package
) のみを返すようになりました。これは、go/types
APIが*ast.Object
を外部に公開しないという目標を達成したことを意味します。assocInitvalsOrMethod
の削除とassocMethod
の変更: 初期化式の関連付けとメソッドの関連付けのロジックが変更され、types.Object
とtypes.Scope
を利用するようになりました。
4. go/types/resolve.go
の変更
resolve
関数のシグネチャ変更:resolve
関数が*ast.Package
を返さなくなり、*Package
(types.Package) と[]*ast.FuncDecl
(メソッドのAST宣言) を返すようになりました。- スコープ管理の変更:
ast.NewScope
の代わりにnew(Scope)
(types.Scope) を使用するようになりました。 - 識別子解決ロジックの変更:
ast.Object
ではなくtypes.Object
をスコープに挿入し、解決するようになりました。
5. その他のファイルへの影響
src/pkg/exp/gotype/gotype.go
:types.Check
の呼び出しが変更されました。src/pkg/go/types/api.go
:Check
関数のシグネチャが変更され、*ast.Package
の戻り値が削除されました。src/pkg/go/types/builtins.go
: 組み込み関数の定義がtypes.Func
を使用するように変更されました。src/pkg/go/types/errors.go
:NamedType
のAstObj
フィールドへの参照が削除されました。src/pkg/go/types/expr.go
: 式の型チェックロジックがtypes.Object
を使用するように変更されました。src/pkg/go/types/gcimporter.go
: GCインポーターがtypes.TypeName
を使用するように変更されました。src/pkg/go/types/operand.go
: オペランドの処理がtypes.Object
を使用するように変更されました。src/pkg/go/types/predicates.go
: 型の同一性チェックがtypes.Object
を使用するように変更されました。src/pkg/go/types/resolver_test.go
: テストコードが一時的に無効化または変更されました。src/pkg/go/types/stmt.go
: ステートメントの型チェックロジックがtypes.Object
を使用するように変更されました。src/pkg/go/types/testdata/decls2a.src
: テストデータが更新されました。src/pkg/go/types/types.go
:Type
インターフェースの実装方法が変更され、NamedType
からAstObj
フィールドが削除されました。src/pkg/go/types/types_test.go
: テストコードがtypes.Package
を使用するように変更されました。src/pkg/go/types/universe.go
: ユニバーススコープの初期化がtypes.Scope
とtypes.Object
を使用するように変更されました。
これらの変更は、go/types
パッケージの内部構造を go/ast
から完全に分離し、よりクリーンで独立した型チェックエンジンを構築するための重要なステップです。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、go/types
パッケージ内のオブジェクト表現とスコープ管理の根本的な見直しです。特に以下のファイルがその中心となります。
-
src/pkg/go/types/objects.go
:Object
インターフェースの定義と、それを実装する具象型 (Package
,Const
,TypeName
,Var
,Func
) の構造が変更されました。*ast.Object
への依存を排除し、各types.Object
が自身のAST宣言への参照を持つようになりました。GetPos()
メソッドが追加され、オブジェクトの宣言位置を取得できるようになりました。newObj
関数が導入され、*ast.Object
からtypes.Object
を生成する役割を担います。
--- a/src/pkg/go/types/objects.go +++ b/src/pkg/go/types/objects.go @@ -4,51 +4,65 @@ package types +import ( + "go/ast" + "go/token" +) + // An Object describes a named language entity such as a package, // constant, type, variable, function (incl. methods), or label. // All objects implement the Object interface. // type Object interface { - anObject() GetName() string + GetType() Type + GetPos() token.Pos + + anObject() } // A Package represents the contents (objects) of a Go package. type Package struct { - implementsObject Name string Path string // import path, "" for current (non-imported) package - Scope *Scope // nil for current (non-imported) package for now - Imports map[string]*Package // map of import paths to packages + Scope *Scope // package-level scope + Imports map[string]*Package // map of import paths to imported packages + + spec *ast.ImportSpec } // A Const represents a declared constant. type Const struct { - implementsObject Name string Type Type Val interface{} +\ +\tspec *ast.ValueSpec } // A TypeName represents a declared type. type TypeName struct { - implementsObject Name string Type Type // *NamedType or *Basic +\ +\tspec *ast.TypeSpec } // A Variable represents a declared variable (including function parameters and results). type Var struct { - implementsObject Name string Type Type +\ +\tvisited bool // for initialization cycle detection +\tdecl interface{} } // A Func represents a declared function. type Func struct { - implementsObject Name string Type Type // *Signature or *Builtin +\ +\tdecl *ast.FuncDecl } func (obj *Package) GetName() string { return obj.Name } @@ -57,64 +71,84 @@ func (obj *TypeName) GetName() string { return obj.Name } func (obj *Var) GetName() string { return obj.Name } func (obj *Func) GetName() string { return obj.Name } -func (obj *Package) GetType() Type { return nil } +func (obj *Package) GetType() Type { return Typ[Invalid] } func (obj *Const) GetType() Type { return obj.Type } func (obj *TypeName) GetType() Type { return obj.Type } func (obj *Var) GetType() Type { return obj.Type } func (obj *Func) GetType() Type { return obj.Type } -// All concrete objects embed implementsObject which -// ensures that they all implement the Object interface.\ -type implementsObject struct{} - -func (*implementsObject) anObject() {} - -// A Scope maintains the set of named language entities declared -// in the scope and a link to the immediately surrounding (outer) -// scope.\ -// -type Scope struct { - Outer *Scope - Elems []Object // scope entries in insertion order - large map[string]Object // for fast lookup - only used for larger scopes -} - -// Lookup returns the object with the given name if it is -// found in scope s, otherwise it returns nil. Outer scopes -// are ignored.\ -// -func (s *Scope) Lookup(name string) Object { - if s.large != nil { - return s.large[name] - } - for _, obj := range s.Elems { - if obj.GetName() == name { - return obj - } - } - return nil -} +func (obj *Package) GetPos() token.Pos { return obj.spec.Pos() } +func (obj *Const) GetPos() token.Pos { + for _, n := range obj.spec.Names { + if n.Name == obj.Name { + return n.Pos() + } + } + return token.NoPos +} +func (obj *TypeName) GetPos() token.Pos { return obj.spec.Pos() } +func (obj *Var) GetPos() token.Pos { + switch d := obj.decl.(type) { + case *ast.Field: + for _, n := range d.Names { + if n.Name == obj.Name { + return n.Pos() + } + } + case *ast.ValueSpec: + for _, n := range d.Names { + if n.Name == obj.Name { + return n.Pos() + } + } + case *ast.AssignStmt: + for _, x := range d.Lhs { + if ident, isIdent := x.(*ast.Ident); isIdent && ident.Name == obj.Name { + return ident.Pos() + } + } + } + return token.NoPos +} +func (obj *Func) GetPos() token.Pos { return obj.decl.Name.Pos() } + +func (*Package) anObject() {} +func (*Const) anObject() {} +func (*TypeName) anObject() {} +func (*Var) anObject() {} +func (*Func) anObject() {} -// Insert attempts to insert an object obj into scope s. -// If s already contains an object with the same name, -// Insert leaves s unchanged and returns that object. -// Otherwise it inserts obj and returns nil.\ -// -func (s *Scope) Insert(obj Object) Object { - name := obj.GetName() - if alt := s.Lookup(name); alt != nil { - return alt - } - s.Elems = append(s.Elems, obj) - if len(s.Elems) > 20 { - if s.large == nil { - m := make(map[string]Object, len(s.Elems)) - for _, obj := range s.Elems { - m[obj.GetName()] = obj - } - s.large = m - } - s.large[name] = obj - } - return nil -} +func newObj(astObj *ast.Object) Object { + name := astObj.Name + typ, _ := astObj.Type.(Type) + switch astObj.Kind { + case ast.Bad: + // ignore + case ast.Pkg: + unreachable() + case ast.Con: + return &Const{Name: name, Type: typ, Val: astObj.Data, spec: astObj.Decl.(*ast.ValueSpec)} + case ast.Typ: + return &TypeName{Name: name, Type: typ, spec: astObj.Decl.(*ast.TypeSpec)} + case ast.Var: + switch astObj.Decl.(type) { + case *ast.Field, *ast.ValueSpec, *ast.AssignStmt: // these are ok + default: + unreachable() + } + return &Var{Name: name, Type: typ, decl: astObj.Decl} + case ast.Fun: + return &Func{Name: name, Type: typ, decl: astObj.Decl.(*ast.FuncDecl)} + case ast.Lbl: + unreachable() // for now + } + unreachable() + return nil +}
-
src/pkg/go/types/scope.go
(新規ファイル):go/types
パッケージ専用のScope
構造体と、その操作メソッド (Lookup
,Insert
) が定義されました。これにより、go/types
がASTのスコープ管理から独立した独自のスコープ階層を持つことが可能になります。
// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package types // A Scope maintains the set of named language entities declared // in the scope and a link to the immediately surrounding (outer) // scope. // type Scope struct { Outer *Scope Entries []Object // scope entries in insertion order large map[string]Object // for fast lookup - only used for larger scopes } // Lookup returns the object with the given name if it is // found in scope s, otherwise it returns nil. Outer scopes // are ignored. // func (s *Scope) Lookup(name string) Object { if s.large != nil { return s.large[name] } for _, obj := range s.Entries { if obj.GetName() == name { return obj } } return nil } // Insert attempts to insert an object obj into scope s. // If s already contains an object with the same name, // Insert leaves s unchanged and returns that object. // Otherwise it inserts obj and returns nil. // func (s *Scope) Insert(obj Object) Object { name := obj.GetName() if alt := s.Lookup(name); alt != nil { return alt } s.Entries = append(s.Entries, obj) // If the scope size reaches a threshold, use a map for faster lookups. const threshold = 20 if len(s.Entries) > threshold { if s.large == nil { m := make(map[string]Object, len(s.Entries)) for _, obj := range s.Entries { m[obj.GetName()] = obj } s.large = m } s.large[name] = obj } return nil }
-
src/pkg/go/types/check.go
:checker
構造体からpkgscope *ast.Scope
が削除され、idents map[*ast.Ident]Object
とobjects map[*ast.Object]Object
が追加されました。lookup
メソッドが導入され、*ast.Ident
からtypes.Object
を取得する中心的な役割を担います。object
メソッドの引数が*ast.Object
からtypes.Object
に変更され、内部の型チェックロジックがtypes.Object
を直接扱うようになりました。Check
関数の戻り値から*ast.Package
が削除されました。
--- a/src/pkg/go/types/check.go +++ b/src/pkg/go/types/check.go @@ -21,17 +21,50 @@ type checker struct { files []*ast.File // lazily initialized - pkg *Package - pkgscope *ast.Scope - firsterr error - initspec map[*ast.ValueSpec]*ast.ValueSpec // "inherited" type and initialization expressions for constant declarations - funclist []function // list of functions/methods with correct signatures and non-empty bodies - funcsig *Signature // signature of currently typechecked function - pos []token.Pos // stack of expr positions; debugging support, used if trace is set + pkg *Package // current package + firsterr error // first error encountered + idents map[*ast.Ident]Object // maps identifiers to their unique object + objects map[*ast.Object]Object // maps *ast.Objects to their unique object + initspecs map[*ast.ValueSpec]*ast.ValueSpec // "inherited" type and initialization expressions for constant declarations + methods map[*TypeName]*Scope // maps type names to associated methods + funclist []function // list of functions/methods with correct signatures and non-empty bodies + funcsig *Signature // signature of currently typechecked function + pos []token.Pos // stack of expr positions; debugging support, used if trace is set +} + +// 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 +// uses the checker.objects map. +// +// TODO(gri) Once identifier resolution is done entirely by +// the typechecker, only the idents map is needed. +// +func (check *checker) lookup(ident *ast.Ident) Object { + astObj := ident.Obj + obj := check.idents[ident] + + if obj != nil { + assert(astObj == nil || check.objects[astObj] == nil || check.objects[astObj] == obj) + return obj + } + + if astObj == nil { + return nil + } + + obj = check.objects[astObj] + if obj == nil { + obj = newObj(astObj) + check.idents[ident] = obj + check.objects[astObj] = obj + } + + return obj } type function struct { - obj *ast.Object // for debugging/tracing only + obj *Func // for debugging/tracing only sig *Signature body *ast.BlockStmt } @@ -40,32 +73,20 @@ type function struct { // that need to be processed after all package-level declarations // are typechecked. // -func (check *checker) later(obj *ast.Object, sig *Signature, body *ast.BlockStmt) { +func (check *checker) later(f *Func, sig *Signature, body *ast.BlockStmt) { // functions implemented elsewhere (say in assembly) have no body if body != nil { - check.funclist = append(check.funclist, function{obj, sig, body}) + check.funclist = append(check.funclist, function{f, sig, body}) } } -// declare declares an object of the given kind and name (ident) in scope; -// decl is the corresponding declaration in the AST. An error is reported -// if the object was declared before. -// -// TODO(gri) This is very similar to the declare function in go/parser; it -// is only used to associate methods with their respective receiver base types. -// In a future version, it might be simpler and cleaner to do all the resolution -// in the type-checking phase. It would simplify the parser, AST, and also -// reduce some amount of code duplication. -// -func (check *checker) declare(scope *ast.Scope, kind ast.ObjKind, ident *ast.Ident, decl ast.Decl) { - assert(ident.Obj == nil) // identifier already declared or resolved - obj := ast.NewObj(kind, ident.Name) - obj.Decl = decl - ident.Obj = obj +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 if ident.Name != "_" { if alt := scope.Insert(obj); alt != nil { prevDecl := "" - if pos := alt.Pos(); pos.IsValid() { + if pos := alt.GetPos(); pos.IsValid() { prevDecl = fmt.Sprintf("\n\tprevious declaration at %s", check.fset.Position(pos)) } check.errorf(ident.Pos(), fmt.Sprintf("%s redeclared in this block%s", ident.Name, prevDecl)) @@ -73,7 +94,7 @@ func (check *checker) declare(scope *ast.Scope, kind ast.ObjKind, ident *ast.Ide } } -func (check *checker) valueSpec(pos token.Pos, obj *ast.Object, lhs []*ast.Ident, typ ast.Expr, rhs []ast.Expr, iota int) { +func (check *checker) valueSpec(pos token.Pos, obj Object, lhs []*ast.Ident, spec *ast.ValueSpec, iota int) { if len(lhs) == 0 { check.invalidAST(pos, "missing lhs in declaration") return @@ -81,38 +102,53 @@ func (check *checker) valueSpec(pos token.Pos, obj *ast.Object, lhs []*ast.Ident // determine type for all of lhs, if any // (but only set it for the object we typecheck!) - var t Type - if typ != nil { - t = check.typ(typ, false) + var typ Type + if spec.Type != nil { + typ = check.typ(spec.Type, false) } // len(lhs) > 0 + rhs := spec.Values if len(lhs) == len(rhs) { // check only lhs and rhs corresponding to obj var l, r ast.Expr for i, name := range lhs { - if name.Obj == obj { + if check.lookup(name) == obj { l = lhs[i] r = rhs[i] break } } assert(l != nil) - obj.Type = t + switch obj := obj.(type) { + case *Const: + obj.Type = typ + case *Var: + obj.Type = typ + default: + unreachable() + } check.assign1to1(l, r, nil, true, iota) return } // there must be a type or initialization expressions - if t == nil && len(rhs) == 0 { + if typ == nil && len(rhs) == 0 { check.invalidAST(pos, "missing type or initialization expression") - t = Typ[Invalid] + typ = Typ[Invalid] } // if we have a type, mark all of lhs - if t != nil { + if typ != nil { for _, name := range lhs { - name.Obj.Type = t + switch obj := check.lookup(name).(type) { + case *Const: + obj.Type = typ + case *Var: + obj.Type = typ + default: + unreachable() + } } } @@ -127,82 +163,97 @@ func (check *checker) valueSpec(pos token.Pos, obj *ast.Object, lhs []*ast.Ident } } -// object typechecks an object by assigning it a type; obj.Type must be nil. -// Callers must check obj.Type before calling object; this eliminates a call -// for each identifier that has been typechecked already, a common scenario. +// object typechecks an object by assigning it a type. // -func (check *checker) object(obj *ast.Object, cycleOk bool) { - assert(obj.Type == nil) - - switch obj.Kind { - case ast.Bad, ast.Pkg: +func (check *checker) object(obj Object, cycleOk bool) { + switch obj := obj.(type) { + case *Package: // nothing to do - - case ast.Con, ast.Var: - // The obj.Data field for constants and variables is initialized - // to the respective (hypothetical, for variables) iota value by - // the parser. The object's fields can be in one of the following - // states: - // Type != nil => the constant value is Data - // Type == nil => the object is not typechecked yet, and Data can be: - // Data is int => Data is the value of iota for this declaration - // Data == nil => the object's expression is being evaluated - if obj.Data == nil { - check.errorf(obj.Pos(), "illegal cycle in initialization of %s", obj.Name) + case *Const: + if obj.Type != nil { + return // already checked + } + // The obj.Val field for constants is initialized to its respective + // iota value by the parser. + // The object's fields can be in one of the following states: + // Type != nil => the constant value is Val + // Type == nil => the constant is not typechecked yet, and Val can be: + // Val is int => Val is the value of iota for this declaration + // Val == nil => the object's expression is being evaluated + if obj.Val == nil { + check.errorf(obj.GetPos(), "illegal cycle in initialization of %s", obj.Name) obj.Type = Typ[Invalid] return } - spec := obj.Decl.(*ast.ValueSpec) - iota := obj.Data.(int) - obj.Data = nil + spec := obj.spec + iota := obj.Val.(int) + obj.Val = nil // mark obj as "visited" for cycle detection // determine spec for type and initialization expressions init := spec - if len(init.Values) == 0 && obj.Kind == ast.Con { - init = check.initspec[spec] + if len(init.Values) == 0 { + init = check.initspecs[spec] } - check.valueSpec(spec.Pos(), obj, spec.Names, init.Type, init.Values, iota) + check.valueSpec(spec.Pos(), obj, spec.Names, init, iota) - case ast.Typ: - typ := &NamedType{AstObj: obj} + case *Var: + if obj.Type != nil { + return // already checked + } + if obj.visited { + check.errorf(obj.GetPos(), "illegal cycle in initialization of %s", obj.Name) + obj.Type = Typ[Invalid] + return + } + spec := obj.decl.(*ast.ValueSpec) + obj.visited = true + check.valueSpec(spec.Pos(), obj, spec.Names, spec, 0) + + case *TypeName: + if obj.Type != nil { + return // already checked + } + typ := &NamedType{Obj: obj} obj.Type = typ // "mark" object so recursion terminates - typ.Underlying = underlying(check.typ(obj.Decl.(*ast.TypeSpec).Type, cycleOk)) + typ.Underlying = underlying(check.typ(obj.spec.Type, cycleOk)) // typecheck associated method signatures - if obj.Data != nil { - scope := obj.Data.(*ast.Scope) + if scope := check.methods[obj]; scope != nil { switch t := typ.Underlying.(type) { case *Struct: // struct fields must not conflict with methods for _, f := range t.Fields { if m := scope.Lookup(f.Name); m != nil { - check.errorf(m.Pos(), "type %s has both field and method named %s", obj.Name, f.Name) + check.errorf(m.GetPos(), "type %s has both field and method named %s", obj.Name, f.Name) // ok to continue } } case *Interface: // methods cannot be associated with an interface type - for _, m := range scope.Objects { - recv := m.Decl.(*ast.FuncDecl).Recv.List[0].Type + for _, m := range scope.Entries { + recv := m.(*Func).decl.Recv.List[0].Type check.errorf(recv.Pos(), "invalid receiver type %s (%s is an interface type)", obj.Name, obj.Name) // ok to continue } } // typecheck method signatures var methods []*Method - for _, obj := range scope.Objects { - mdecl := obj.Decl.(*ast.FuncDecl) - sig := check.typ(mdecl.Type, cycleOk).(*Signature) - params, _ := check.collectParams(mdecl.Recv, false) + for _, obj := range scope.Entries { + m := obj.(*Func) + sig := check.typ(m.decl.Type, cycleOk).(*Signature) + params, _ := check.collectParams(m.decl.Recv, false) sig.Recv = params[0] // the parser/assocMethod ensure there is exactly one parameter - obj.Type = sig - methods = append(methods, &Method{QualifiedName{nil, obj.Name}, sig}) - check.later(obj, sig, mdecl.Body) + m.Type = sig + methods = append(methods, &Method{QualifiedName{nil, m.Name}, sig}) + check.later(m, sig, m.decl.Body) } typ.Methods = methods - obj.Data = nil // don't use obj.Data later, accidentally + delete(check.methods, obj) // we don't need this scope anymore } - case ast.Fun: - fdecl := obj.Decl.(*ast.FuncDecl) + case *Func: + if obj.Type != nil { + return // already checked + } + fdecl := obj.decl // methods are typechecked when their receivers are typechecked if fdecl.Recv == nil { sig := check.typ(fdecl.Type, cycleOk).(*Signature) @@ -215,12 +266,12 @@ func (check *checker) object(obj *ast.Object, cycleOk bool) { } default: - panic("unreachable") + unreachable() } } // assocInitvals associates "inherited" initialization expressions -// with the corresponding *ast.ValueSpec in the check.initspec map +// with the corresponding *ast.ValueSpec in the check.initspecs map // for constant declarations without explicit initialization expressions. // func (check *checker) assocInitvals(decl *ast.GenDecl) { @@ -230,7 +281,7 @@ func (check *checker) assocInitvals(decl *ast.GenDecl) {\ if len(s.Values) > 0 { last = s } else { - check.initspec[s] = last + check.initspecs[s] = last } } } @@ -251,48 +302,36 @@ func (check *checker) assocMethod(meth *ast.FuncDecl) {\ if ptr, ok := typ.(*ast.StarExpr); ok { typ = ptr.X } + // determine receiver base type name + ident, ok := typ.(*ast.Ident) + if !ok { + // not an identifier - parser reported error already + return // ignore this method + } // determine receiver base type object - var obj *ast.Object - if ident, ok := typ.(*ast.Ident); ok && ident.Obj != nil { - obj = ident.Obj - if obj.Kind != ast.Typ { + var tname *TypeName + if obj := check.lookup(ident); obj != nil { + obj, ok := obj.(*TypeName) + if !ok { check.errorf(ident.Pos(), "%s is not a type", ident.Name) return // ignore this method } - // TODO(gri) determine if obj was defined in this package - /* - if check.notLocal(obj) { - check.errorf(ident.Pos(), "cannot define methods on non-local type %s", ident.Name) - return // ignore this method - } - */ + if obj.spec == nil { + check.errorf(ident.Pos(), "cannot define method on non-local type %s", ident.Name) + return // ignore this method + } + tname = obj } else { - // If it's not an identifier or the identifier wasn't declared/resolved, - // the parser/resolver already reported an error. Nothing to do here. + // identifier not declared/resolved - parser reported error already return // ignore this method } // declare method in receiver base type scope - var scope *ast.Scope - if obj.Data != nil { - scope = obj.Data.(*ast.Scope) - } else { - scope = ast.NewScope(nil) - obj.Data = scope - } - check.declare(scope, ast.Fun, meth.Name, meth) -} -
-func (check *checker) assocInitvalsOrMethod(decl ast.Decl) {
-
switch d := decl.(type) {
-
case *ast.GenDecl:
-
if d.Tok == token.CONST {
-
check.assocInitvals(d)
-
}
-
case *ast.FuncDecl:
-
if d.Recv != nil {
-
check.assocMethod(d)
-
}
- scope := check.methods[tname]
- if scope == nil {
-
scope = new(Scope)
-
check.methods[tname] = scope
- }
- check.declareIdent(scope, meth.Name, &Func{Name: meth.Name.Name, decl: meth}) }
func (check *checker) decl(decl ast.Decl) { @@ -303,17 +342,13 @@ func (check *checker) decl(decl ast.Decl) {
for _, spec := range d.Specs { switch s := spec.(type) { case *ast.ImportSpec:-
// nothing to do (handled by ast.NewPackage)
-
// nothing to do (handled by check.resolve) case *ast.ValueSpec: for _, name := range s.Names {
-
if obj := name.Obj; obj.Type == nil {
-
check.object(obj, false)
-
}
-
check.object(check.lookup(name), false) } case *ast.TypeSpec:
-
if obj := s.Name.Obj; obj.Type == nil {
-
check.object(obj, false)
-
}
-
check.object(check.lookup(s.Name), false) default: check.invalidAST(s.Pos(), "unknown ast.Spec node %T", s) }
@@ -323,42 +358,33 @@ func (check *checker) decl(decl ast.Decl) {
if d.Recv != nil { return }-
obj := d.Name.Obj
-
obj := check.lookup(d.Name) // Initialization functions don't have an object associated with them // since they are not in any scope. Create a dummy object for them. if d.Name.Name == "init" { assert(obj == nil) // all other functions should have an object
-
obj = ast.NewObj(ast.Fun, d.Name.Name)
-
obj.Decl = d
-
d.Name.Obj = obj
-
}
-
if obj.Type == nil {
-
check.object(obj, false)
-
obj = &Func{Name: d.Name.Name, decl: d}
-
check.idents[d.Name] = obj }
-
default: check.invalidAST(d.Pos(), "unknown ast.Decl node %T", d) } }check.object(obj, false)
-// iterate calls f for each package-level declaration. -func (check *checker) iterate(f func(*checker, ast.Decl)) {
- for _, file := range check.files {
-
for _, decl := range file.Decls {
-
f(check, decl)
-
}
- } -}
-
// A bailout panic is raised to indicate early termination. type bailout struct{}
-func check(ctxt *Context, fset *token.FileSet, files []*ast.File) (astpkg *ast.Package, pkg *Package, err error) { +func check(ctxt *Context, fset *token.FileSet, files []*ast.File) (pkg *Package, err error) { // initialize checker check := checker{
-
ctxt: ctxt,
-
fset: fset,
-
files: files,
-
initspec: make(map[*ast.ValueSpec]*ast.ValueSpec),
-
ctxt: ctxt,
-
fset: fset,
-
files: files,
-
idents: make(map[*ast.Ident]Object),
-
objects: make(map[*ast.Object]Object),
-
initspecs: make(map[*ast.ValueSpec]*ast.ValueSpec),
-
methods: make(map[*TypeName]*Scope),
}
// handle panics @@ -369,20 +395,20 @@ func check(ctxt *Context, fset *token.FileSet, files []*ast.File) (astpkg *ast.P default: // unexpected panic: don't crash clients
-
// panic(p) // enable for debugging
-
panic(p) // enable for debugging // TODO(gri) add a test case for this scenario err = fmt.Errorf("types internal error: %v", p) }
}\
// resolve identifiers
- astpkg, pkg = check.resolve(imp)
- // Imported packages and all types refer to types.Objects,
- // the current package files' AST uses ast.Objects.
- // Use an ast.Scope for the current package scope.
- pkg, methods := check.resolve(imp) check.pkg = pkg
-
check.pkgscope = astpkg.Scope
-
// determine missing constant initialization expressions
-
// and associate methods with types
-
check.iterate((*checker).assocInitvalsOrMethod)
-
// associate methods with types
-
for _, m := range methods {
-
check.assocMethod(m)
-
}
// typecheck all declarations
- check.iterate((*checker).decl)
-
for _, f := range check.files {
-
for _, d := range f.Decls {
-
check.decl(d)
-
}
-
}
// typecheck all function/method bodies // (funclist may grow when checking statements - do not use range clause!)
-
コアとなるコードの解説
このコミットの核心は、Goの型チェッカーがASTの具体的な表現から独立し、独自の型システムオブジェクト (types.Object
) とスコープ (types.Scope
) を中心に動作するように再構築された点にあります。
1. types.Object
の導入と役割
以前は、go/ast
パッケージの *ast.Object
が、識別子の解決結果や型情報を直接保持していました。しかし、これは go/ast
と go/types
の間に強い結合を生み、go/parser
以外の方法で生成されたASTを型チェックする際の柔軟性を損なっていました。
このコミットでは、go/types
パッケージ内に Object
インターフェースが導入され、Package
, Const
, TypeName
, Var
, Func
といった具象型がこれを実装します。これらの types.Object
は、Go言語のプログラム要素を型システムが理解しやすい形で抽象化したものです。
objects.go
の変更: 各types.Object
構造体は、対応するASTノードへの参照(例:Const
のspec *ast.ValueSpec
)を持つようになりました。これにより、types.Object
は必要に応じてASTから情報を取得できますが、types.Object
自体はASTの内部構造から独立した、型システム独自のオブジェクトとして振る舞います。GetPos()
メソッドの追加は、エラー報告時にtypes.Object
から直接ソースコード上の位置情報を取得できるようにするためのものです。newObj
関数の役割:newObj
関数は、*ast.Object
を受け取り、それに対応する新しいtypes.Object
を生成します。これは、go/parser
が生成したASTをgo/types
が処理する際に、ASTのオブジェクトを型システム独自のオブジェクトに変換する「アダプター」のような役割を果たします。
2. types.Scope
による独立したスコープ管理
以前の go/types
は、go/ast
の *ast.Scope
を利用して識別子解決を行っていました。しかし、*ast.Scope
もまたASTの内部構造に密接に結合しており、go/types
の独立性を妨げていました。
scope.go
の新規追加:go/types
パッケージ内にScope
構造体が新しく定義されました。このtypes.Scope
は、Outer *Scope
フィールドで外側のスコープへの参照を持ち、Entries []Object
でそのスコープ内で宣言されたtypes.Object
のリストを保持します。Lookup
とInsert
メソッドにより、types.Scope
は独自の識別子検索と挿入ロジックを提供します。check.go
とresolve.go
の変更:checker
構造体からpkgscope *ast.Scope
が削除され、types.Scope
を直接利用するようになりました。resolve
関数もtypes.Scope
を構築し、types.Object
をスコープに挿入するようになりました。これにより、go/types
はASTのスコープ管理から完全に独立し、自身のセマンティックなスコープ階層を維持できるようになります。
3. 型チェックロジックの抽象化
check.go
の object
メソッドは、以前は *ast.Object
を引数にとり、その Kind
フィールドに基づいて処理を分岐していました。このコミットでは、object
メソッドの引数が types.Object
に変更され、Goの型アサーション (switch obj := obj.(type)
) を利用して、各 types.Object
の具象型に応じた型チェックロジックを実行するようになりました。
checker.idents
とchecker.objects
マップ:checker
構造体に追加されたこれらのマップは、*ast.Ident
や*ast.Object
と、それに対応するtypes.Object
の間のマッピングを一時的に保持します。これは、ASTを走査しながらtypes.Object
を構築し、それらを型チェックのコンテキストで利用するための橋渡し役です。最終的には、識別子解決が完全にgo/types
内部で行われるようになれば、これらのマップは不要になる可能性があります(コミットメッセージのTODO(gri)
に示唆されています)。
4. APIのクリーンアップ
src/pkg/go/types/api.go
の Check
関数のシグネチャから *ast.Package
の戻り値が削除されました。これは、go/types
パッケージが外部に *ast.Object
を公開しないという設計目標を達成したことを明確に示しています。これにより、go/types
はよりクリーンで、内部実装の詳細に依存しないAPIを提供するようになりました。
これらの変更は、Goの型チェッカーがよりモジュール化され、柔軟で、将来の拡張に対応しやすい基盤を構築するための重要な一歩です。ASTの具体的な表現から独立することで、go/types
は様々なソースからのGoコードの型チェックに利用できるようになり、Goのツールエコシステムの発展に貢献します。
関連リンク
- Go言語の
go/ast
パッケージ: https://pkg.go.dev/go/ast - Go言語の
go/types
パッケージ: https://pkg.go.dev/go/types - Go言語の
go/token
パッケージ: https://pkg.go.dev/go/token - このコミットの変更リスト (Gerrit): https://golang.org/cl/7096048
参考にした情報源リンク
- Go言語の公式ドキュメント (
go/ast
,go/types
,go/token
パッケージ) - Go言語のソースコード (
src/pkg/go/types
ディレクトリ内の関連ファイル) - Gerrit のコードレビューコメント (コミットメッセージに記載されている
https://golang.org/cl/7096048
を参照) - Go言語のコンパイラ設計に関する一般的な情報源 (例: Goコンパイラの内部構造に関するブログ記事やプレゼンテーション)
これらの情報源は、Go言語のコンパイラがどのように動作し、特に型チェックとASTの間の関係がどのように進化してきたかを理解する上で役立ちました。