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

[インデックス 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 に直接依存していると、以下のような問題が生じます。

  1. 結合度の高さ: go/typesgo/ast の内部構造に密接に結合してしまい、それぞれのパッケージの独立性が損なわれる。
  2. 柔軟性の欠如: go/parser 以外の方法で生成されたAST(例えば、プログラム的に構築されたASTや、異なるパーサーからのAST)を go/types で型チェックすることが困難になる。
  3. 内部状態の管理: *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.Identast.Exprast.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.Postoken.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 for Const, decl *ast.FuncDecl for Func)を持つようになりました。これにより、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) ObjectInsert(obj Object) Object メソッドが提供され、スコープ内でのオブジェクトの検索と挿入を管理します。これにより、go/types が独自の識別子解決ロジックを持つ基盤が確立されます。

3. go/types/check.go の変更

  • checker 構造体の変更:
    • pkgscope *ast.Scope フィールドが削除され、pkg *Packagemethods map[*TypeName]*Scope が追加されました。これは、型チェッカーが types.Objecttypes.Scope を直接扱うようになったことを示します。
    • idents map[*ast.Ident]Objectobjects map[*ast.Object]Object が追加され、*ast.Ident*ast.Objecttypes.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.Objecttypes.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: NamedTypeAstObj フィールドへの参照が削除されました。
  • 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.Scopetypes.Object を使用するように変更されました。

これらの変更は、go/types パッケージの内部構造を go/ast から完全に分離し、よりクリーンで独立した型チェックエンジンを構築するための重要なステップです。

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

このコミットのコアとなる変更は、go/types パッケージ内のオブジェクト表現とスコープ管理の根本的な見直しです。特に以下のファイルがその中心となります。

  1. 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
    +}
    
  2. 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
    }
    
  3. src/pkg/go/types/check.go:

    • checker 構造体から pkgscope *ast.Scope が削除され、idents map[*ast.Ident]Objectobjects 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
        }
      
    •   check.object(obj, false)
      
      default: check.invalidAST(d.Pos(), "unknown ast.Decl node %T", d) } }

    -// 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/astgo/types の間に強い結合を生み、go/parser 以外の方法で生成されたASTを型チェックする際の柔軟性を損なっていました。

このコミットでは、go/types パッケージ内に Object インターフェースが導入され、Package, Const, TypeName, Var, Func といった具象型がこれを実装します。これらの types.Object は、Go言語のプログラム要素を型システムが理解しやすい形で抽象化したものです。

  • objects.go の変更: 各 types.Object 構造体は、対応するASTノードへの参照(例: Constspec *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 のリストを保持します。LookupInsert メソッドにより、types.Scope は独自の識別子検索と挿入ロジックを提供します。
  • check.goresolve.go の変更: checker 構造体から pkgscope *ast.Scope が削除され、types.Scope を直接利用するようになりました。resolve 関数も types.Scope を構築し、types.Object をスコープに挿入するようになりました。これにより、go/types はASTのスコープ管理から完全に独立し、自身のセマンティックなスコープ階層を維持できるようになります。

3. 型チェックロジックの抽象化

check.goobject メソッドは、以前は *ast.Object を引数にとり、その Kind フィールドに基づいて処理を分岐していました。このコミットでは、object メソッドの引数が types.Object に変更され、Goの型アサーション (switch obj := obj.(type)) を利用して、各 types.Object の具象型に応じた型チェックロジックを実行するようになりました。

  • checker.identschecker.objects マップ: checker 構造体に追加されたこれらのマップは、*ast.Ident*ast.Object と、それに対応する types.Object の間のマッピングを一時的に保持します。これは、ASTを走査しながら types.Object を構築し、それらを型チェックのコンテキストで利用するための橋渡し役です。最終的には、識別子解決が完全に go/types 内部で行われるようになれば、これらのマップは不要になる可能性があります(コミットメッセージの TODO(gri) に示唆されています)。

4. APIのクリーンアップ

src/pkg/go/types/api.goCheck 関数のシグネチャから *ast.Package の戻り値が削除されました。これは、go/types パッケージが外部に *ast.Object を公開しないという設計目標を達成したことを明確に示しています。これにより、go/types はよりクリーンで、内部実装の詳細に依存しないAPIを提供するようになりました。

これらの変更は、Goの型チェッカーがよりモジュール化され、柔軟で、将来の拡張に対応しやすい基盤を構築するための重要な一歩です。ASTの具体的な表現から独立することで、go/types は様々なソースからのGoコードの型チェックに利用できるようになり、Goのツールエコシステムの発展に貢献します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (go/ast, go/types, go/token パッケージ)
  • Go言語のソースコード (src/pkg/go/types ディレクトリ内の関連ファイル)
  • Gerrit のコードレビューコメント (コミットメッセージに記載されている https://golang.org/cl/7096048 を参照)
  • Go言語のコンパイラ設計に関する一般的な情報源 (例: Goコンパイラの内部構造に関するブログ記事やプレゼンテーション)

これらの情報源は、Go言語のコンパイラがどのように動作し、特に型チェックとASTの間の関係がどのように進化してきたかを理解する上で役立ちました。