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

[インデックス 14830] ファイルの概要

このコミットは、Go言語の型チェッカーの一部である src/pkg/go/types/check.go ファイルに対する変更です。このファイルは、Goプログラムの構文木(AST)を走査し、型の一貫性や宣言の正当性を検証する役割を担っています。具体的には、定数宣言における型の推論と初期化式の処理に関するバグ修正が含まれています。

コミット

  • コミットハッシュ: 65cb1904c1f74acf3161a3271d1f248e5aaf7dfa
  • 作者: Robert Griesemer gri@golang.org
  • コミット日時: 2013年1月8日 火曜日 15:03:30 -0800

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/65cb1904c1f74acf3161a3271d1f248e5aaf7dfa

元コミット内容

go/types: "inherit" type in constant declarations w/o initialization expressions (bug fix)

R=adonovan
CC=golang-dev
https://golang.org/cl/7060054

変更の背景

Go言語では、定数宣言において初期化式を省略した場合、前の定数宣言の初期化式と型を「継承」する動作が定義されています。これは、複数の定数をまとめて宣言する際にコードを簡潔にするための機能です。例えば、以下のようなコードが考えられます。

const (
    A = 1
    B // BはAの初期化式と型を継承し、1となる
    C = "hello"
    D // DはCの初期化式と型を継承し、"hello"となる
)

しかし、このコミットが修正しようとしているバグは、初期化式を持たない定数宣言が、前の定数宣言から「型」を正しく継承しないという問題でした。特に、型が明示的に指定されていない定数宣言において、その値が前の定数から推論されるべき場合に、型情報が正しく引き継がれないことが原因で、型チェックエラーが発生する可能性がありました。

このバグは、Goコンパイラの型システムが、定数宣言の連鎖において、初期化式だけでなく型情報も適切に伝播させる必要があるという設計上の課題を示しています。go/types パッケージはGo言語のセマンティック分析を担当しており、このような型推論のルールを厳密に適用する必要があります。この修正は、Go言語の仕様に準拠し、より堅牢な型チェックを実現するために不可欠でした。

前提知識の解説

Go言語の定数宣言と型推論

Go言語の定数宣言は const キーワードを使用します。定数には数値、真偽値、文字列のみが使用でき、実行時に値が変化することはありません。

  • 明示的な型指定: const x int = 10 のように型を明示的に指定できます。
  • 型推論: const x = 10 のように型を省略した場合、初期化式の値から型が推論されます。例えば 10int 型、3.14float64 型と推論されます。
  • 定数グループと継承: 複数の定数をまとめて宣言する際、初期化式を省略すると、直前の定数宣言の初期化式と型が「継承」されます。
    const (
        A = 10       // Aはint型、値は10
        B            // BはAを継承し、int型、値は10
        C = "string" // Cはstring型、値は"string"
        D            // DはCを継承し、string型、値は"string"
    )
    
    この「継承」のメカニズムが、今回のバグ修正の核心です。

go/types パッケージ

go/types パッケージは、Go言語のコンパイラツールチェーンの一部であり、Goプログラムの型チェックとセマンティック分析を行います。主な機能は以下の通りです。

  • 型情報の構築: ソースコードから抽象構文木(AST)を読み込み、各識別子(変数、関数、定数など)の型情報を決定します。
  • 型の一貫性チェック: 演算子のオペランドの型が適切か、関数の引数と戻り値の型が一致するかなどを検証します。
  • スコープ管理: 識別子の可視性(スコープ)を管理し、名前解決を行います。
  • 定数評価: コンパイル時に定数式を評価し、その値を決定します。

このパッケージは、Goプログラムが言語仕様に準拠していることを保証し、実行時エラーを未然に防ぐための重要な役割を担っています。

ast パッケージと ast.ValueSpec

Go言語のコンパイラは、ソースコードを解析して抽象構文木(AST: Abstract Syntax Tree)を生成します。go/ast パッケージは、このASTを表現するためのデータ構造を提供します。

  • ast.File: Goのソースファイル全体を表すASTのルートノード。
  • ast.GenDecl: import, const, var, type などの一般的な宣言を表します。
  • ast.ValueSpec: constvar 宣言における個々の値の仕様(名前、型、初期化式)を表します。例えば、const A, B = 1, 2 の場合、AB それぞれが ast.ValueSpec に対応します。

型チェッカーは、これらのASTノードを走査し、各ノードが表すコード要素の型情報を決定し、検証します。

ast.Objectast.Scope

  • ast.Object: Goプログラム内の名前付きエンティティ(変数、関数、型、定数など)を表す抽象的なオブジェクトです。各 ast.Object は、そのエンティティの種類(Kind)、名前(Name)、宣言された位置(Pos)、そして宣言されたASTノードへの参照(Decl)を持ちます。定数宣言の場合、ast.Objectast.Con (Constant) の Kind を持ちます。
  • ast.Scope: スコープは、プログラム内で識別子が有効な領域を定義します。ast.Scope は、そのスコープ内で宣言された ast.Object のマップを保持します。型チェッカーは、名前解決のためにスコープツリーを辿ります。

iota

iota はGo言語の特別な識別子で、const 宣言ブロック内で使用され、連続する定数に自動的に値を割り当てるために使われます。iotaconst ブロック内で0から始まり、新しい const 宣言ごとに1ずつ増加します。

const (
    A = iota // A = 0
    B        // B = 1 (iotaが1に増加)
    C = iota // C = 2 (iotaが2に増加)
    D        // D = 3 (iotaが3に増加)
)

今回のバグ修正は iota の使用とは直接関係ありませんが、定数宣言の文脈で iota が使われる可能性があるため、関連する概念として理解しておくことが重要です。

技術的詳細

このバグは、Go言語の型チェッカーが定数宣言の「継承」ロジックを処理する際に、初期化式だけでなく「型」の継承も考慮する必要があるという点に起因していました。

go/types パッケージ内の checker 構造体は、型チェックのコンテキストを保持します。修正前は、この構造体内に initexprs map[*ast.ValueSpec][]ast.Expr というフィールドがありました。これは、初期化式を持たない定数宣言に対して、前の定数宣言から「初期化式」を継承するために使用されていました。しかし、このマップは []ast.Expr (初期化式のスライス) のみを保持しており、型情報そのものは保持していませんでした。

問題は、定数宣言が初期化式を省略した場合に、その定数の型が前の定数宣言から推論されるべきであるにもかかわらず、initexprs マップが型情報を提供していなかった点です。これにより、型チェッカーが正しい型を決定できず、誤った型チェック結果やコンパイルエラーを引き起こす可能性がありました。

修正の核心は、initexprs マップを initspec map[*ast.ValueSpec]*ast.ValueSpec に変更したことです。

  • initexprs (旧): map[*ast.ValueSpec][]ast.Expr

    • キー: *ast.ValueSpec (現在の定数宣言のASTノード)
    • 値: []ast.Expr (継承すべき初期化式のスライス)
    • 問題点: 型情報そのものを保持していない。
  • initspec (新): map[*ast.ValueSpec]*ast.ValueSpec

    • キー: *ast.ValueSpec (現在の定数宣言のASTノード)
    • 値: *ast.ValueSpec (継承元となる定数宣言のASTノード)
    • 利点: 継承元となる ast.ValueSpec 全体を保持することで、その ValueSpec が持つ Type (明示的な型指定) や Values (初期化式) の両方を参照できるようになる。

この変更により、型チェッカーは初期化式を省略した定数宣言を処理する際に、継承元の ast.ValueSpec からその型情報(init.Type)と初期化式(init.Values)の両方を正確に取得できるようになりました。これにより、Go言語の定数宣言における型推論のセマンティクスが正しく実装され、バグが修正されました。

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

src/pkg/go/types/check.go ファイルにおいて、以下の変更が行われました。

  1. checker 構造体のフィールド名変更と型変更:

    --- a/src/pkg/go/types/check.go
    +++ b/src/pkg/go/types/check.go
    @@ -23,12 +23,12 @@ type checker struct {
      	files []*ast.File
    
      	// lazily initialized
    -	pkgscope  *ast.Scope
    -	firsterr  error
    -	initexprs map[*ast.ValueSpec][]ast.Expr // "inherited" initialization expressions for constant declarations
    -	funclist  []function                    // list of functions/methods with correct signatures and non-empty bodies
    -	funcsig   *Signature                    // signature of currently typechecked function
    -	pos       []token.Pos                   // stack of expr positions; debugging support, used if trace is set
    +	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
     }
    

    initexprsinitspec に変更され、型が map[*ast.ValueSpec][]ast.Expr から map[*ast.ValueSpec]*ast.ValueSpec に変更されました。

  2. object メソッド内のロジック変更:

    --- a/src/pkg/go/types/check.go
    +++ b/src/pkg/go/types/check.go
    @@ -156,12 +156,12 @@ func (check *checker) object(obj *ast.Object, cycleOk bool) {
      		spec := obj.Decl.(*ast.ValueSpec)
      		iota := obj.Data.(int)
      		obj.Data = nil
    -		// determine initialization expressions
    -		values := spec.Values
    -		if len(values) == 0 && obj.Kind == ast.Con {
    -			values = check.initexprs[spec]
    +		// determine spec for type and initialization expressions
    +		init := spec
    +		if len(init.Values) == 0 && obj.Kind == ast.Con {
    +			init = check.initspec[spec]
      		}
    -		check.valueSpec(spec.Pos(), obj, spec.Names, spec.Type, values, iota)
    +		check.valueSpec(spec.Pos(), obj, spec.Names, init.Type, init.Values, iota)
    

    values 変数に直接初期化式を代入する代わりに、init という *ast.ValueSpec 型の変数を導入し、継承元の ValueSpec を参照するように変更されました。check.valueSpec の呼び出しも、spec.Typevalues ではなく、init.Typeinit.Values を使うように変更されました。

  3. assocInitvals メソッド内のロジック変更:

    --- a/src/pkg/go/types/check.go
    +++ b/src/pkg/go/types/check.go
    @@ -217,21 +217,21 @@ func (check *checker) object(obj *ast.Object, cycleOk bool) {
     }
    
     // assocInitvals associates "inherited" initialization expressions
    -// with the corresponding *ast.ValueSpec in the check.initexprs map
    +// with the corresponding *ast.ValueSpec in the check.initspec map
     // for constant declarations without explicit initialization expressions.
     //
     func (check *checker) assocInitvals(decl *ast.GenDecl) {
    -	var values []ast.Expr
    +	var last *ast.ValueSpec
     	for _, s := range decl.Specs {
     		if s, ok := s.(*ast.ValueSpec); ok {
     			if len(s.Values) > 0 {
    -				values = s.Values
    +				last = s
     			} else {
    -				check.initexprs[s] = values
    +				check.initspec[s] = last
     			}
     		}
     	}
    -	if len(values) == 0 {
    +	if last == nil {
     		check.invalidAST(decl.Pos(), "no initialization values provided")
     	}
     }
    

    values []ast.Expr を保持していたロジックが、last *ast.ValueSpec を保持するように変更されました。これにより、初期化式を持つ最後の ValueSpec 全体を記憶し、初期化式を持たない定数宣言がその ValueSpec を継承できるようにします。マップへの代入も check.initexprs[s] = values から check.initspec[s] = last に変更されました。

  4. check 関数内の checker 初期化:

    --- a/src/pkg/go/types/check.go
    +++ b/src/pkg/go/types/check.go
    @@ -370,10 +370,10 @@ type bailout struct{}\
     func check(ctxt *Context, fset *token.FileSet, files map[string]*ast.File) (pkg *ast.Package, err error) {
     	// initialize checker
     	check := checker{
    -		ctxt:      ctxt,\
    -		fset:      fset,\
    -		files:     sortedFiles(files),\
    -		initexprs: make(map[*ast.ValueSpec][]ast.Expr),\
    +		ctxt:     ctxt,\
    +		fset:     fset,\
    +		files:    sortedFiles(files),\
    +		initspec: make(map[*ast.ValueSpec]*ast.ValueSpec),\
      	}
    

    checker 構造体の初期化時に、initexprs の代わりに initspec マップが初期化されるように変更されました。

コアとなるコードの解説

checker 構造体の変更

checker 構造体は型チェックの全体的な状態を管理します。initexprs から initspec への変更は、単に初期化式([]ast.Expr)だけを継承するのではなく、初期化式を持つ ast.ValueSpec 全体(*ast.ValueSpec)を継承するように設計思想が変更されたことを示しています。これにより、継承される情報が初期化式だけでなく、その ValueSpec が持つ型情報も含まれるようになります。

object メソッド内の変更

object メソッドは、ASTノードが表すオブジェクト(この場合は定数)の型チェックを行います。 変更前は、初期化式を持たない定数(len(values) == 0 && obj.Kind == ast.Con)の場合、check.initexprs[spec] から継承された初期化式 values を取得していました。 変更後は、init := spec で現在の ValueSpecinit に代入し、もし初期化式がなければ init = check.initspec[spec] として、継承元の ValueSpecinit に設定します。 そして、check.valueSpec を呼び出す際に、spec.Typevalues の代わりに init.Typeinit.Values を渡すことで、継承元の ValueSpec から型と初期化式の両方を正しく取得して型チェックを行うようになりました。これにより、初期化式を省略した定数宣言でも、前の定数宣言の型が正しく推論されるようになります。

assocInitvals メソッド内の変更

assocInitvals メソッドは、定数宣言ブロック内で初期化式を省略した定数に対して、継承すべき初期化式(と型)を関連付ける役割を担います。 変更前は var values []ast.Expr を使用し、初期化式を持つ定数宣言が見つかるたびに values を更新し、初期化式を持たない定数宣言に対しては check.initexprs[s] = values でその values を関連付けていました。 変更後は var last *ast.ValueSpec を使用し、初期化式を持つ定数宣言が見つかるたびに last = s でその ValueSpec 全体を記憶します。そして、初期化式を持たない定数宣言に対しては check.initspec[s] = last で、記憶しておいた last (継承元の ValueSpec) を関連付けます。 これにより、object メソッドが check.initspec[spec] を参照した際に、継承元の ValueSpec から型情報と初期化式の両方をまとめて取得できるようになり、定数宣言の継承セマンティクスが完全に実装されました。

check 関数内の初期化

check 関数は型チェック処理のエントリポイントであり、checker 構造体を初期化します。ここで initexprs の代わりに initspec マップが初期化されることで、新しい継承ロジックがシステム全体で有効になります。

これらの変更は、Go言語の型システムにおける定数宣言のセマンティクスを正確に実装し、初期化式を省略した場合の型推論のバグを修正するために不可欠でした。

関連リンク

参考にした情報源リンク