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

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

このコミットは、Go言語のAPI抽出ツールである cmd/api の挙動を改善し、特に定数参照の追跡能力を向上させるものです。これにより、gccgo のような代替コンパイラとの互換性が高まり、また、定数型解決における多くの特殊ケースが削除され、コードの簡素化と堅牢化が図られています。

コミット

commit c15a42ed76370afd87aebee0be131dba713bc4f4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Feb 10 10:05:26 2012 +1100

    cmd/api: follow constant references
    
    For gccgo. Also removes bunch of special cases.
    
    Fixes #2906
    
    R=golang-dev, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/5644050

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

https://github.com/golang/go/commit/c15a42ed76370afd87aebee0be131dba713bc4f4

元コミット内容

cmd/api: follow constant references

このコミットは、cmd/api ツールが定数参照を追跡できるようにするものです。これは gccgo のために行われ、また、多くの特殊ケースを削除します。

Fixes #2906

変更の背景

このコミットの主な背景には、以下の点が挙げられます。

  1. gccgo との互換性: gccgo はGo言語の代替コンパイラであり、Goの標準コンパイラ(gc)とは異なる内部表現や挙動を持つことがあります。cmd/api ツールはGoの公開APIを抽出するために使用されますが、gccgo が定数をどのように扱うかという点で、標準ツールとの間に不一致が生じる可能性がありました。特に、定数が他の定数を参照している場合に、その参照を正しく解決できないことが問題となっていました。
  2. 定数型解決の堅牢化と簡素化: 以前の cmd/api ツールでは、特定のパッケージ(例: compress/gzip, os, path/filepath, unicode/utf8, text/scanner)の定数に対して、その型をハードコードで推測する「特殊ケース」が多数存在していました。これは、GoのAST(抽象構文木)が型情報を持たないという当時の制約に起因するものでした。しかし、このようなハードコードされたロジックは、新しい定数やパッケージが追加されるたびに更新が必要となり、保守が困難でエラーの原因にもなりやすかったのです。
  3. Issue #2906 の解決: このコミットは、GoのIssueトラッカーで報告されていた Issue 2906 を修正します。このIssueは、cmd/api が定数の型を正しく解決できない、特に iota を使用した定数や、他の定数を参照する定数において問題があることを指摘していました。

これらの背景から、cmd/api ツールがより汎用的かつ正確に定数型を解決できるよう、定数参照を追跡するメカニズムを導入し、同時に不要な特殊ケースを排除する必要がありました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. Go言語の定数 (Constants):

    • Go言語では、const キーワードを使用して定数を宣言します。定数はコンパイル時に値が決定され、実行時には変更できません。
    • 定数には型が明示的に指定されることもありますが、多くの場合、型が指定されずに宣言されます(例: const A = 1)。このような定数は「型なし定数 (untyped constant)」と呼ばれ、その値は「理想型 (ideal type)」を持ちます(例: ideal-int, ideal-float, ideal-string, ideal-bool)。
    • 型なし定数は、使用される文脈によって適切な型に変換されます。
    • 定数は他の定数を参照することができます(例: const B = A + 1)。
    • iota は、const 宣言内で使用される特別な識別子で、連続する定数に自動的にインクリメントされる値を割り当てるために使用されます。
  2. Go言語のAST (Abstract Syntax Tree):

    • Goコンパイラやツールは、Goのソースコードを解析してASTを構築します。ASTはプログラムの構造を木構造で表現したものです。
    • go/ast パッケージは、GoのASTを操作するためのAPIを提供します。
    • go/doc パッケージは、ASTからドキュメントやAPI情報を抽出するために使用されます。
  3. cmd/api ツール:

    • cmd/api は、Goの標準ライブラリやパッケージの公開API(エクスポートされた型、関数、変数、定数など)を抽出するための内部ツールです。
    • このツールは、Goの互換性を保証するために重要です。Goのバージョンアップ時にAPIの破壊的変更がないかを確認するために使用されます。
    • コミットメッセージにある BUG(bradfitz): Note that this tool is only currently suitable for use on the Go standard library, not arbitrary packages. というコメントは、当時の cmd/api がまだ汎用的なAPI抽出ツールとしては未熟であり、標準ライブラリに特化していたことを示唆しています。これは、ASTが型情報を持たないという制約と関連しています。
  4. gccgo:

    • gccgo は、GCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。Goの標準コンパイラ gc とは異なる実装であり、Goプログラムをコンパパイルする別の手段を提供します。
    • 異なるコンパイラ実装が存在する場合、それぞれのコンパイラがGo言語の仕様をどのように解釈し、実装するかに微妙な違いが生じることがあります。特に、コンパイル時の定数評価や型推論の挙動は、互換性の問題を引き起こす可能性があります。
  5. pkgSymbol 構造体:

    • このコミットで導入された pkgSymbol は、パッケージ名とシンボル名(識別子名)の組み合わせを表す構造体です。これにより、異なるパッケージに存在する同じ名前のシンボルを区別できるようになります。これは、定数参照を解決する際に、どのパッケージの定数を参照しているのかを正確に特定するために重要です。

技術的詳細

このコミットの技術的詳細は、cmd/api ツールが定数型を解決するメカニズムを根本的に変更した点にあります。

変更前のアプローチの問題点

変更前の cmd/api は、定数の型を決定する際に、主に以下の問題に直面していました。

  • ハードコードされた特殊ケース: compress/gzip, os, path/filepath, unicode/utf8, text/scanner などの特定のパッケージに対して、定数名に基づいてその型を推測する hardCodedConstantType メソッドが存在しました。これは、ASTが型情報を持たないための一時的な回避策でしたが、Goの進化とともに保守が困難になっていました。
  • 定数参照の不完全な解決: 定数が他の定数を参照している場合(例: const B = A)、cmd/api はその参照を適切に追跡し、参照先の定数の型を継承することができませんでした。特に、参照先の定数がまだ処理されていない場合や、異なるパッケージの定数を参照している場合に問題が生じました。
  • iota の特殊処理: iota はGoの定数宣言において特殊な意味を持つため、その型を ideal-int として特別に扱う必要がありました。

変更後のアプローチ

このコミットでは、これらの問題を解決するために、以下の主要な変更が導入されました。

  1. 定数依存関係の追跡 (constDepresolveConstantDeps):

    • Walker 構造体に constDep map[string]string という新しいフィールドが追加されました。これは、ある定数識別子(キー)が、まだ型が解決されていない別の定数識別子(値)に依存していることを記録するためのマップです。
    • constDepPrefix というマジックプレフィックス ("const-dependency:") が導入されました。constValueType メソッドが定数の型を解決できない場合、例えば const-dependency:SomeOtherConst のように、このプレフィックスと依存先の定数名を組み合わせた文字列を一時的な型として返します。
    • resolveConstantDeps メソッドが追加されました。このメソッドは、パッケージ内のすべてのファイルがウォークされた後に呼び出されます。constDep マップを走査し、依存関係を解決して最終的な定数型を決定します。これは、依存関係が循環している場合や、複数のレベルでネストしている場合でも、再帰的に型を解決しようとします。
    • このメカニズムにより、定数の宣言順序に依存せず、また、他の定数を参照する定数の型を正確に解決できるようになりました。
  2. pkgSymbol の導入と prevConstType の変更:

    • prevConstType マップのキーが string から pkgSymbol に変更されました。pkgSymbol{パッケージ名, シンボル名} のペアを表す構造体です。
    • これにより、prevConstType は、現在のパッケージだけでなく、インポートされた他のパッケージの定数型も正確に記録できるようになりました。これは、ast.SelectorExpr (例: flate.BestSpeed) のように、他のパッケージの定数を参照する場合の型解決に不可欠です。
  3. hardCodedConstantType の大幅な削減:

    • 新しい定数依存解決メカニズムが導入されたことで、多くのハードコードされた特殊ケースが不要になりました。コミットの差分を見ると、hardCodedConstantType メソッドから compress/gzip, compress/zlib, os, path/filepath, unicode/utf8, text/scanner に関するロジックが削除され、syscall パッケージの darwinAMD64 のみが残されています。これは、ツールがより汎用的でデータ駆動型になったことを示しています。
  4. ast.SelectorExpr の改善:

    • constValueType メソッド内の ast.SelectorExpr の処理が改善されました。以前は errTODO を返していましたが、新しいロジックでは selectorFullPkg を使用して参照先のパッケージを特定し、prevConstType からその定数の型を取得しようとします。これにより、flate.BestSpeed のようなクロスパッケージ定数参照も解決できるようになりました。
  5. エクスポートされた定数のみの出力:

    • walkConst メソッド内で、ast.IsExported(ident.Name) のチェックが追加され、エクスポートされた定数のみが emitFeature で出力されるようになりました。これにより、APIツールが公開APIのみを対象とするという本来の目的に合致するようになりました。

これらの変更により、cmd/api は定数の型をより正確に、かつ汎用的に解決できるようになり、gccgo との互換性も向上し、コードベースの保守性も高まりました。

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

このコミットにおけるコアとなるコードの変更箇所は、主に src/cmd/api/goapi.go ファイルに集中しています。

  1. Walker 構造体の変更:

    • prevConstType の型が map[string]string から map[pkgSymbol]string に変更されました。
    • constDep map[string]string という新しいフィールドが追加されました。
    type Walker struct {
        // ...
        prevConstType   map[pkgSymbol]string // identifier -> "ideal-int"
        constDep        map[string]string // key's const identifier has type of future value const identifier
        // ...
    }
    
  2. NewWalker 関数での初期化:

    • NewWalker 関数内で prevConstTypemake(map[pkgSymbol]string) で初期化されるようになりました。
    func NewWalker() *Walker {
        return &Walker{
            // ...
            prevConstType:   make(map[pkgSymbol]string),
        }
    }
    
  3. hardCodedConstantType メソッドの簡素化:

    • 多くのパッケージ(compress/gzip, os, path/filepath, unicode/utf8, text/scanner)に関するハードコードされたロジックが削除されました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -199,34 +206,10 @@ const (
     // the cases we can't handle yet.
     func (w *Walker) hardCodedConstantType(name string) (typ string, ok bool) {
     	switch w.scope[0] {
    -	case "pkg compress/gzip", "pkg compress/zlib":
    -		switch name {
    -		case "NoCompression", "BestSpeed", "BestCompression", "DefaultCompression":
    -			return "ideal-int", true
    -		}
    -	case "pkg os":
    -		switch name {
    -		case "WNOHANG", "WSTOPPED", "WUNTRACED":
    -			return "ideal-int", true
    -		}
    -	case "pkg path/filepath":
    -		switch name {
    -		case "Separator", "ListSeparator":
    -			return "char", true
    -		}
    -	case "pkg unicode/utf8":
    -		switch name {
    -		case "RuneError":
    -			return "char", true
    -		}
    -	case "pkg text/scanner":
    -		// TODO: currently this tool only resolves const types
    -		// that reference other constant types if they appear
    -		// in the right order.  the scanner package has
    -		// ScanIdents and such coming before the Ident/Int/etc
    -		// tokens, hence this hack.
    -		if strings.HasPrefix(name, "Scan") || name == "SkipComments" {
    -			return "ideal-int", true
    +	case "pkg syscall":
    +		switch name {
    +		case "darwinAMD64":
    +			return "ideal-bool", true
     		}
     	}
     	return "", false
    
  4. WalkPackage メソッドの変更:

    • w.prevConstType = map[string]string{} の行が削除され、w.constDep = map[string]string{} が追加されました。
    • w.resolveConstantDeps() の呼び出しが追加されました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -306,7 +289,7 @@ func (w *Walker) WalkPackage(name string) {
     
     	w.curPackageName = name
     	w.curPackage = apkg
    -	w.prevConstType = map[string]string{}
    +	w.constDep = map[string]string{}
     
     	for _, afile := range apkg.Files {
     		w.recordTypes(afile)
    @@ -316,6 +299,8 @@ func (w *Walker) WalkPackage(name string) {
     		w.walkFile(afile)
     	}
     
    +	w.resolveConstantDeps()
    +
     	// Now that we're done walking types, vars and consts
     	// in the *ast.Package, use go/doc to do the rest
     	// (functions and methods). This is done here because
    
  5. constValueType メソッドの変更:

    • ast.SelectorExpr の処理が改善され、他のパッケージの定数参照を解決できるようになりました。
    • ast.Ident の処理で、prevConstType のキーが pkgSymbol に変更され、未解決の定数依存を constDepPrefix を付けて返すようになりました。
    • ast.BinaryExpr の処理で、定数依存関係の型不一致を処理するロジックが追加されました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -447,8 +432,16 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     	case *ast.UnaryExpr:
     		return w.constValueType(v.X)
     	case *ast.SelectorExpr:
    -		// e.g. compress/gzip's BestSpeed == flate.BestSpeed
    -		return "", errTODO
    +		lhs := w.nodeString(v.X)
    +		rhs := w.nodeString(v.Sel)
    +		pkg, ok := w.selectorFullPkg[lhs]
    +		if !ok {
    +			return "", fmt.Errorf("unknown constant reference; unknown package in expression %s.%s", lhs, rhs)
    +		}
    +		if t, ok := w.prevConstType[pkgSymbol{pkg, rhs}]; ok {
    +			return t, nil
    +		}
    +		return "", fmt.Errorf("unknown constant reference to %s.%s", lhs, rhs)
     	case *ast.Ident:
     		if v.Name == "iota" {
     			return "ideal-int", nil // hack.
    @@ -460,10 +453,10 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     			// Hack.
     			return "ideal-int", nil
     		}
    -		if t, ok := w.prevConstType[v.Name]; ok {
    +		if t, ok := w.prevConstType[pkgSymbol{w.curPackageName, v.Name}]; ok {
     			return t, nil
     		}
    -		return "", fmt.Errorf("can't resolve existing constant %q", v.Name)
    +		return constDepPrefix + v.Name, nil
     	case *ast.BinaryExpr:
     		left, err := w.constValueType(v.X)
     		if err != nil {
    @@ -474,6 +467,8 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     		if err != nil {
     			return "", err
     		}
     		if left != right {
    +			// TODO(bradfitz): encode the real rules here,
    +			// rather than this mess.
     			if left == "ideal-int" && right == "ideal-float" {
     				return "ideal-float", nil // math.Log2E
     			}
    @@ -487,6 +482,17 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     				// Hack, for package time.
     				return "Duration", nil
     			}
    +			if left == "ideal-int" && !strings.HasPrefix(right, "ideal-") {
    +				return right, nil
    +			}
    +			if right == "ideal-int" && !strings.HasPrefix(left, "ideal-") {
    +				return left, nil
    +			}
    +			if strings.HasPrefix(left, constDepPrefix) && strings.HasPrefix(right, constDepPrefix) {
    +				// Just pick one.
    +				// e.g. text/scanner GoTokens const-dependency:ScanIdents, const-dependency:ScanFloats
    +				return left, nil
    +			}
     			return "", fmt.Errorf("in BinaryExpr, unhandled type mismatch; left=%q, right=%q", left, right)
     		}
     		return left, nil
    
  6. constDepPrefix 定数の追加:

    const constDepPrefix = "const-dependency:"
    
  7. walkConst メソッドの変更:

    • エクスポートされていない定数をスキップするロジックが削除されました(後で resolveConstantDeps で処理されるため)。
    • constDepPrefix を持つ型を constDep に記録するロジックが追加されました。
    • prevConstType への書き込みで pkgSymbol を使用するようになりました。
    • エクスポートされた定数のみ emitFeature で出力するようになりました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -601,11 +607,13 @@ func (w *Walker) resolveName(name string) (v interface{}, t interface{}, ok bool
     	return nil, nil, false
     }
     
    +// constDepPrefix is a magic prefix that is used by constValueType
    +// and walkConst to signal that a type isn't known yet. These are
    +// resolved at the end of walking of a package's files.
    +const constDepPrefix = "const-dependency:"
    +
     func (w *Walker) walkConst(vs *ast.ValueSpec) {
     	for _, ident := range vs.Names {
    -		if !ast.IsExported(ident.Name) {
    -			continue
    -		}
     		litType := ""
     		if vs.Type != nil {
     			litType = w.nodeString(vs.Type)
    @@ -627,13 +635,44 @@ func (w *Walker) walkConst(vs *ast.ValueSpec) {
     				}
     			}
     		}
    +		if strings.HasPrefix(litType, constDepPrefix) {
    +			dep := litType[len(constDepPrefix):]
    +			w.constDep[ident.Name] = dep
    +			continue
    +		}
     		if litType == "" {
     			log.Fatalf("unknown kind in const %q", ident.Name)
     		}
     		w.lastConstType = litType
     
    -		w.emitFeature(fmt.Sprintf("const %s %s", ident, litType))
    -		w.prevConstType[ident.Name] = litType
    +		w.prevConstType[pkgSymbol{w.curPackageName, ident.Name}] = litType
    +
    +		if ast.IsExported(ident.Name) {
    +			w.emitFeature(fmt.Sprintf("const %s %s", ident, litType))
    +		}
    +	}
    +}
    +
    +func (w *Walker) resolveConstantDeps() {
    +	var findConstType func(string) string
    +	findConstType = func(ident string) string {
    +		if dep, ok := w.constDep[ident]; ok {
    +			return findConstType(dep)
    +		}
    +		if t, ok := w.prevConstType[pkgSymbol{w.curPackageName, ident}]; ok {
    +			return t
    +		}
    +		return ""
    +	}
    +	for ident := range w.constDep {
    +		if !ast.IsExported(ident) {
    +			continue
    +		}
    +		t := findConstType(ident)
    +		if t == "" {
    +			log.Fatalf("failed to resolve constant %q", ident)
    +		}
    +		w.emitFeature(fmt.Sprintf("const %s %s", ident, t))
     	}
     }
    
  8. resolveConstantDeps メソッドの追加:

    • 定数依存関係を解決するための新しいメソッドが追加されました。
  9. テストデータの変更:

    • src/cmd/api/testdata/src/pkg/p1/golden.txt に新しい定数 AIsLowerAConstChase2 の出力が追加されました。
    • src/cmd/api/testdata/src/pkg/p1/p1.go に、定数参照のテストケース (ConstChase2, constChase, AIsLowerA) が追加されました。
    // src/cmd/api/testdata/src/pkg/p1/p1.go
    const (
    	ConstChase2 = constChase // forward declaration to unexported ident
    	constChase  = AIsLowerA  // forward declaration to exported ident
    
    	A         = 1
    	a         = 11
    	A64 int64 = 1
    
    	AIsLowerA = a // previously declared
    )
    

これらの変更は、cmd/api が定数の型を決定するロジックを、ハードコードされた推測から、より動的で依存関係を解決するアプローチへと移行させたことを示しています。

コアとなるコードの解説

このコミットのコアとなるコードは、src/cmd/api/goapi.go 内の Walker 構造体とその関連メソッド、特に constValueType, walkConst, そして新しく追加された resolveConstantDeps です。

Walker 構造体と pkgSymbol

  • prevConstType map[pkgSymbol]string:
    • このマップは、既に処理された定数の型を記録します。キーが pkgSymbol ({パッケージ名, シンボル名}) になったことで、異なるパッケージの同じ名前の定数を区別できるようになりました。これにより、flate.BestSpeed のような、他のパッケージの定数を参照するケースを正確に処理できます。
  • constDep map[string]string:
    • このマップは、まだ型が解決されていない定数間の依存関係を記録します。例えば、const B = A のように、B の型が A の型に依存している場合、constDep["B"] = "A" のように記録されます。これは、定数が宣言された順序に関わらず、依存関係を後から解決するための重要なメカニズムです。

constValueType メソッド

このメソッドは、定数の値からその型を推測する中心的なロジックを含んでいます。

  • ast.SelectorExpr の処理:
    • v.X (セレクタの左側、通常はパッケージ名) と v.Sel (セレクタの右側、通常は定数名) を抽出し、selectorFullPkg マップを使って完全なパッケージ名を解決します。
    • 解決されたパッケージ名と定数名を使って pkgSymbol を作成し、prevConstType からその定数の型を取得しようとします。これにより、flate.BestSpeed のようなクロスパッケージ参照が正しく解決されます。
  • ast.Ident の処理:
    • 識別子(定数名)が iota であれば ideal-int を返します。
    • 識別子が prevConstType に存在すれば、その型を返します。
    • 重要な変更点: 識別子が prevConstType に存在しない場合、以前はエラーを返していましたが、このコミットでは constDepPrefix + v.Name という形式の文字列を返します。これは、この定数の型がまだ不明であり、v.Name という別の定数に依存していることを示す「プレースホルダー」です。このプレースホルダーは後で resolveConstantDeps によって解決されます。
  • ast.BinaryExpr の処理:
    • 二項演算子(例: +, -, *)を含む定数式の場合、左右のオペランドの型を再帰的に constValueType で解決します。
    • 左右のオペランドの型が異なる場合の型推論ロジックが改善されました。特に、ideal-int と他の具体的な型(int, float64 など)の組み合わせや、両方が constDepPrefix を持つ場合の処理が追加されました。これにより、より複雑な定数式も正しく型推論できるようになりました。

walkConst メソッド

このメソッドは、const 宣言内の各定数をウォークし、その型を決定します。

  • エクスポートされていない定数の処理:
    • 以前は !ast.IsExported(ident.Name) のチェックでエクスポートされていない定数をスキップしていましたが、このコミットではそのチェックが削除されました。これは、すべての定数(エクスポートされているか否かに関わらず)の依存関係を constDep に記録し、後で resolveConstantDeps でまとめて解決するためです。最終的に emitFeature で出力されるのはエクスポートされた定数のみです。
  • constDepPrefix を持つ型の処理:
    • constValueType から返された litTypeconstDepPrefix で始まる場合、その依存関係を w.constDep マップに記録します。これにより、定数間の依存関係が明示的に追跡されます。
  • prevConstType への記録:
    • 定数の型が決定されると、w.prevConstType[pkgSymbol{w.curPackageName, ident.Name}] = litType のように、pkgSymbol をキーとして prevConstType に記録されます。
  • emitFeature の呼び出し:
    • ast.IsExported(ident.Name) のチェックがここで行われ、エクスポートされた定数のみがAPIとして出力されます。

resolveConstantDeps メソッド (新設)

このメソッドは、パッケージ内のすべてのファイルがウォークされた後に呼び出され、未解決の定数依存関係を解決します。

  • findConstType クロージャ:
    • この内部関数は、与えられた識別子 ident の最終的な型を再帰的に検索します。
    • まず w.constDep をチェックし、依存関係があればその依存先の定数の型を再帰的に findConstType で解決しようとします。
    • 次に w.prevConstType をチェックし、型が直接記録されていればそれを返します。
    • これにより、A -> B -> C のような多段階の依存関係も解決できます。
  • 依存関係の解決と出力:
    • w.constDep マップを走査し、各依存定数について findConstType を呼び出して最終的な型を決定します。
    • エクスポートされた定数のみが emitFeature で出力されます。
    • 型が解決できない場合は log.Fatalf でエラーを発生させます。

これらの変更により、cmd/api は定数の型解決において、宣言順序や複雑な依存関係に左右されない、より堅牢で正確なメカニズムを獲得しました。

関連リンク

参考にした情報源リンク

  • Go Issue 2906: 上記の関連リンクに記載。
  • Go CL 5644050: 上記の関連リンクに記載。
  • Go言語の公式ドキュメント: Go言語の定数、型システム、ASTに関する一般的な情報源。
  • gccgo のドキュメント: gccgo の特性とGo標準コンパイラとの違いに関する情報源。
  • Goのソースコード: src/cmd/api/goapi.go および関連するテストファイル。
  • Goのコミット履歴: git log コマンドやGitHubの履歴機能を用いて、このコミット前後の変更を調査。
  • Goのメーリングリストやフォーラム: 過去の議論や設計決定に関する情報源。

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

このコミットは、Go言語のAPI抽出ツールである cmd/api の挙動を改善し、特に定数参照の追跡能力を向上させるものです。これにより、gccgo のような代替コンパイラとの互換性が高まり、また、定数型解決における多くの特殊ケースが削除され、コードの簡素化と堅牢化が図られています。

コミット

commit c15a42ed76370afd87aebee0be131dba713bc4f4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Feb 10 10:05:26 2012 +1100

    cmd/api: follow constant references
    
    For gccgo. Also removes bunch of special cases.
    
    Fixes #2906
    
    R=golang-dev, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/5644050

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

https://github.com/golang/go/commit/c15a42ed76370afd87aebee0be131dba713bc4f4

元コミット内容

cmd/api: follow constant references

このコミットは、cmd/api ツールが定数参照を追跡できるようにするものです。これは gccgo のために行われ、また、多くの特殊ケースを削除します。

Fixes #2906

変更の背景

このコミットの主な背景には、以下の点が挙げられます。

  1. gccgo との互換性: gccgo はGo言語の代替コンパイラであり、Goの標準コンパイラ(gc)とは異なる内部表現や挙動を持つことがあります。cmd/api ツールはGoの公開APIを抽出するために使用されますが、gccgo が定数をどのように扱うかという点で、標準ツールとの間に不一致が生じる可能性がありました。特に、定数が他の定数を参照している場合に、その参照を正しく解決できないことが問題となっていました。
  2. 定数型解決の堅牢化と簡素化: 以前の cmd/api ツールでは、特定のパッケージ(例: compress/gzip, os, path/filepath, unicode/utf8, text/scanner)の定数に対して、その型をハードコードで推測する「特殊ケース」が多数存在していました。これは、GoのAST(抽象構文木)が型情報を持たないという当時の制約に起因するものでした。しかし、このようなハードコードされたロジックは、新しい定数やパッケージが追加されるたびに更新が必要となり、保守が困難でエラーの原因にもなりやすかったのです。
  3. Issue #2906 の解決: このコミットは、GoのIssueトラッカーで報告されていた Issue 2906 を修正します。このIssueは、cmd/api が定数の型を正しく解決できない、特に iota を使用した定数や、他の定数を参照する定数において問題があることを指摘していました。

これらの背景から、cmd/api ツールがより汎用的かつ正確に定数型を解決できるよう、定数参照を追跡するメカニズムを導入し、同時に不要な特殊ケースを排除する必要がありました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. Go言語の定数 (Constants):

    • Go言語では、const キーワードを使用して定数を宣言します。定数はコンパイル時に値が決定され、実行時には変更できません。
    • 定数には型が明示的に指定されることもありますが、多くの場合、型が指定されずに宣言されます(例: const A = 1)。このような定数は「型なし定数 (untyped constant)」と呼ばれ、その値は「理想型 (ideal type)」を持ちます(例: ideal-int, ideal-float, ideal-string, ideal-bool)。
    • 型なし定数は、使用される文脈によって適切な型に変換されます。
    • 定数は他の定数を参照することができます(例: const B = A + 1)。
    • iota は、const 宣言内で使用される特別な識別子で、連続する定数に自動的にインクリメントされる値を割り当てるために使用されます。
  2. Go言語のAST (Abstract Syntax Tree):

    • Goコンパイラやツールは、Goのソースコードを解析してASTを構築します。ASTはプログラムの構造を木構造で表現したものです。
    • go/ast パッケージは、GoのASTを操作するためのAPIを提供します。
    • go/doc パッケージは、ASTからドキュメントやAPI情報を抽出するために使用されます。
  3. cmd/api ツール:

    • cmd/api は、Goの標準ライブラリやパッケージの公開API(エクスポートされた型、関数、変数、定数など)を抽出するための内部ツールです。
    • このツールは、Goの互換性を保証するために重要です。Goのバージョンアップ時にAPIの破壊的変更がないかを確認するために使用されます。
    • コミットメッセージにある BUG(bradfitz): Note that this tool is only currently suitable for use on the Go standard library, not arbitrary packages. というコメントは、当時の cmd/api がまだ汎用的なAPI抽出ツールとしては未熟であり、標準ライブラリに特化していたことを示唆しています。これは、ASTが型情報を持たないという制約と関連しています。
    • Goプロジェクトにおける cmd/api ディレクトリは、APIサーバーとして機能するメインアプリケーションコードを整理するための一般的な慣例です。cmd ディレクトリはビルド可能なアプリケーションや実行可能ファイルを保持し、cmd/api は特にAPIサーバーの main パッケージと main() 関数を含みます。
  4. gccgo:

    • gccgo は、GCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。Goの標準コンパイラ gc とは異なる実装であり、Goプログラムをコンパイルする別の手段を提供します。
    • 異なるコンパイラ実装が存在する場合、それぞれのコンパイラがGo言語の仕様をどのように解釈し、実装するかに微妙な違いが生じることがあります。特に、コンパイル時の定数評価や型推論の挙動は、互換性の問題を引き起こす可能性があります。
  5. pkgSymbol 構造体:

    • このコミットで導入された pkgSymbol は、パッケージ名とシンボル名(識別子名)の組み合わせを表す構造体です。これにより、異なるパッケージに存在する同じ名前のシンボルを区別できるようになります。これは、定数参照を解決する際に、どのパッケージの定数を参照しているのかを正確に特定するために重要です。

技術的詳細

このコミットの技術的詳細は、cmd/api ツールが定数型を解決するメカニズムを根本的に変更した点にあります。

変更前のアプローチの問題点

変更前の cmd/api は、定数の型を決定する際に、主に以下の問題に直面していました。

  • ハードコードされた特殊ケース: compress/gzip, os, path/filepath, unicode/utf8, text/scanner などの特定のパッケージに対して、定数名に基づいてその型を推測する hardCodedConstantType メソッドが存在しました。これは、ASTが型情報を持たないための一時的な回避策でしたが、Goの進化とともに保守が困難になっていました。
  • 定数参照の不完全な解決: 定数が他の定数を参照している場合(例: const B = A)、cmd/api はその参照を適切に追跡し、参照先の定数の型を継承することができませんでした。特に、参照先の定数がまだ処理されていない場合や、異なるパッケージの定数を参照している場合に問題が生じました。
  • iota の特殊処理: iota はGoの定数宣言において特殊な意味を持つため、その型を ideal-int として特別に扱う必要がありました。

変更後のアプローチ

このコミットでは、これらの問題を解決するために、以下の主要な変更が導入されました。

  1. 定数依存関係の追跡 (constDepresolveConstantDeps):

    • Walker 構造体に constDep map[string]string という新しいフィールドが追加されました。これは、ある定数識別子(キー)が、まだ型が解決されていない別の定数識別子(値)に依存していることを記録するためのマップです。
    • constDepPrefix というマジックプレフィックス ("const-dependency:") が導入されました。constValueType メソッドが定数の型を解決できない場合、例えば const-dependency:SomeOtherConst のように、このプレフィックスと依存先の定数名を組み合わせた文字列を一時的な型として返します。
    • resolveConstantDeps メソッドが追加されました。このメソッドは、パッケージ内のすべてのファイルがウォークされた後に呼び出されます。constDep マップを走査し、依存関係を解決して最終的な定数型を決定します。これは、依存関係が循環している場合や、複数のレベルでネストしている場合でも、再帰的に型を解決しようとします。
    • このメカニズムにより、定数の宣言順序に依存せず、また、他の定数を参照する定数の型を正確に解決できるようになりました。
  2. pkgSymbol の導入と prevConstType の変更:

    • prevConstType マップのキーが string から pkgSymbol に変更されました。pkgSymbol{パッケージ名, シンボル名} のペアを表す構造体です。
    • これにより、prevConstType は、現在のパッケージだけでなく、インポートされた他のパッケージの定数型も正確に記録できるようになりました。これは、ast.SelectorExpr (例: flate.BestSpeed) のように、他のパッケージの定数を参照する場合の型解決に不可欠です。
  3. hardCodedConstantType の大幅な削減:

    • 新しい定数依存解決メカニズムが導入されたことで、多くのハードコードされた特殊ケースが不要になりました。コミットの差分を見ると、hardCodedConstantType メソッドから compress/gzip, compress/zlib, os, path/filepath, unicode/utf8, text/scanner に関するロジックが削除され、syscall パッケージの darwinAMD64 のみが残されています。これは、ツールがより汎用的でデータ駆動型になったことを示しています。
  4. ast.SelectorExpr の改善:

    • constValueType メソッド内の ast.SelectorExpr の処理が改善されました。以前は errTODO を返していましたが、新しいロジックでは selectorFullPkg を使用して参照先のパッケージを特定し、prevConstType からその定数の型を取得しようとします。これにより、flate.BestSpeed のようなクロスパッケージ定数参照も解決できるようになりました。
  5. エクスポートされた定数のみの出力:

    • walkConst メソッド内で、ast.IsExported(ident.Name) のチェックが追加され、エクスポートされた定数のみが emitFeature で出力されるようになりました。これにより、APIツールが公開APIのみを対象とするという本来の目的に合致するようになりました。

これらの変更により、cmd/api は定数の型をより正確に、かつ汎用的に解決できるようになり、gccgo との互換性も向上し、コードベースの保守性も高まりました。

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

このコミットにおけるコアとなるコードの変更箇所は、主に src/cmd/api/goapi.go ファイルに集中しています。

  1. Walker 構造体の変更:

    • prevConstType の型が map[string]string から map[pkgSymbol]string に変更されました。
    • constDep map[string]string という新しいフィールドが追加されました。
    type Walker struct {
        // ...
        prevConstType   map[pkgSymbol]string // identifier -> "ideal-int"
        constDep        map[string]string // key's const identifier has type of future value const identifier
        // ...
    }
    
  2. NewWalker 関数での初期化:

    • NewWalker 関数内で prevConstTypemake(map[pkgSymbol]string) で初期化されるようになりました。
    func NewWalker() *Walker {
        return &Walker{
            // ...
            prevConstType:   make(map[pkgSymbol]string),
        }
    }
    
  3. hardCodedConstantType メソッドの簡素化:

    • 多くのパッケージ(compress/gzip, compress/zlib, os, path/filepath, unicode/utf8, text/scanner)に関するハードコードされたロジックが削除されました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -199,34 +206,10 @@ const (
     // the cases we can't handle yet.
     func (w *Walker) hardCodedConstantType(name string) (typ string, ok bool) {
     	switch w.scope[0] {
    -	case "pkg compress/gzip", "pkg compress/zlib":
    -		switch name {
    -		case "NoCompression", "BestSpeed", "BestCompression", "DefaultCompression":
    -			return "ideal-int", true
    -		}
    -	case "pkg os":
    -		switch name {
    -		case "WNOHANG", "WSTOPPED", "WUNTRACED":
    -			return "ideal-int", true
    -		}
    -	case "pkg path/filepath":
    -		switch name {
    -		case "Separator", "ListSeparator":
    -			return "char", true
    -		}
    -	case "pkg unicode/utf8":
    -		switch name {
    -		case "RuneError":
    -			return "char", true
    -		}
    -	case "pkg text/scanner":
    -		// TODO: currently this tool only resolves const types
    -		// that reference other constant types if they appear
    -		// in the right order.  the scanner package has
    -		// ScanIdents and such coming before the Ident/Int/etc
    -		// tokens, hence this hack.
    -		if strings.HasPrefix(name, "Scan") || name == "SkipComments" {
    -			return "ideal-int", true
    +	case "pkg syscall":
    +		switch name {
    +		case "darwinAMD64":
    +			return "ideal-bool", true
     		}
     	}
     	return "", false
    
  4. WalkPackage メソッドの変更:

    • w.prevConstType = map[string]string{} の行が削除され、w.constDep = map[string]string{} が追加されました。
    • w.resolveConstantDeps() の呼び出しが追加されました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -306,7 +289,7 @@ func (w *Walker) WalkPackage(name string) {
     
     	w.curPackageName = name
     	w.curPackage = apkg
    -	w.prevConstType = map[string]string{}
    +	w.constDep = map[string]string{}
     
     	for _, afile := range apkg.Files {
     		w.recordTypes(afile)
    @@ -316,6 +299,8 @@ func (w *Walker) WalkPackage(name string) {
     		w.walkFile(afile)
     	}
     
    +	w.resolveConstantDeps()
    +
     	// Now that we're done walking types, vars and consts
     	// in the *ast.Package, use go/doc to do the rest
     	// (functions and methods). This is done here because
    
  5. constValueType メソッドの変更:

    • ast.SelectorExpr の処理が改善され、他のパッケージの定数参照を解決できるようになりました。
    • ast.Ident の処理で、prevConstType のキーが pkgSymbol に変更され、未解決の定数依存を constDepPrefix を付けて返すようになりました。
    • ast.BinaryExpr の処理で、定数依存関係の型不一致を処理するロジックが追加されました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -447,8 +432,16 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     	case *ast.UnaryExpr:
     		return w.constValueType(v.X)
     	case *ast.SelectorExpr:
    -		// e.g. compress/gzip's BestSpeed == flate.BestSpeed
    -		return "", errTODO
    +		lhs := w.nodeString(v.X)
    +		rhs := w.nodeString(v.Sel)
    +		pkg, ok := w.selectorFullPkg[lhs]
    +		if !ok {
    +			return "", fmt.Errorf("unknown constant reference; unknown package in expression %s.%s", lhs, rhs)
    +		}
    +		if t, ok := w.prevConstType[pkgSymbol{pkg, rhs}]; ok {
    +			return t, nil
    +		}
    +		return "", fmt.Errorf("unknown constant reference to %s.%s", lhs, rhs)
     	case *ast.Ident:
     		if v.Name == "iota" {
     			return "ideal-int", nil // hack.
    @@ -460,10 +453,10 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     			// Hack.
     			return "ideal-int", nil
     		}
    -		if t, ok := w.prevConstType[v.Name]; ok {
    +		if t, ok := w.prevConstType[pkgSymbol{w.curPackageName, v.Name}]; ok {
     			return t, nil
     		}
    -		return "", fmt.Errorf("can't resolve existing constant %q", v.Name)
    +		return constDepPrefix + v.Name, nil
     	case *ast.BinaryExpr:
     		left, err := w.constValueType(v.X)
     		if err != nil {
    @@ -474,6 +467,8 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     		if err != nil {
     			return "", err
     		}
     		if left != right {
    +			// TODO(bradfitz): encode the real rules here,
    +			// rather than this mess.
     			if left == "ideal-int" && right == "ideal-float" {
     				return "ideal-float", nil // math.Log2E
     			}
    @@ -487,6 +482,17 @@ func (w *Walker) constValueType(vi interface{}) (string, error) {
     				// Hack, for package time.
     				return "Duration", nil
     			}
    +			if left == "ideal-int" && !strings.HasPrefix(right, "ideal-") {
    +				return right, nil
    +			}
    +			if right == "ideal-int" && !strings.HasPrefix(left, "ideal-") {
    +				return left, nil
    +			}
    +			if strings.HasPrefix(left, constDepPrefix) && strings.HasPrefix(right, constDepPrefix) {
    +				// Just pick one.
    +				// e.g. text/scanner GoTokens const-dependency:ScanIdents, const-dependency:ScanFloats
    +				return left, nil
    +			}
     			return "", fmt.Errorf("in BinaryExpr, unhandled type mismatch; left=%q, right=%q", left, right)
     		}
     		return left, nil
    
  6. constDepPrefix 定数の追加:

    const constDepPrefix = "const-dependency:"
    
  7. walkConst メソッドの変更:

    • エクスポートされていない定数をスキップするロジックが削除されました(後で resolveConstantDeps で処理されるため)。
    • constDepPrefix を持つ型を constDep に記録するロジックが追加されました。
    • prevConstType への書き込みで pkgSymbol を使用するようになりました。
    • エクスポートされた定数のみ emitFeature で出力するようになりました。
    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -601,11 +607,13 @@ func (w *Walker) resolveName(name string) (v interface{}, t interface{}, ok bool
     	return nil, nil, false
     }
     
    +// constDepPrefix is a magic prefix that is used by constValueType
    +// and walkConst to signal that a type isn't known yet. These are
    +// resolved at the end of walking of a package's files.
    +const constDepPrefix = "const-dependency:"
    +
     func (w *Walker) walkConst(vs *ast.ValueSpec) {
     	for _, ident := range vs.Names {
    -		if !ast.IsExported(ident.Name) {
    -			continue
    -		}
     		litType := ""
     		if vs.Type != nil {
     			litType = w.nodeString(vs.Type)
    @@ -627,13 +635,44 @@ func (w *Walker) walkConst(vs *ast.ValueSpec) {
     				}
     			}
     		}
    +		if strings.HasPrefix(litType, constDepPrefix) {
    +			dep := litType[len(constDepPrefix):]
    +			w.constDep[ident.Name] = dep
    +			continue
    +		}
     		if litType == "" {
     			log.Fatalf("unknown kind in const %q", ident.Name)
     		}
     		w.lastConstType = litType
     
    -		w.emitFeature(fmt.Sprintf("const %s %s", ident, litType))
    -		w.prevConstType[ident.Name] = litType
    +		w.prevConstType[pkgSymbol{w.curPackageName, ident.Name}] = litType
    +
    +		if ast.IsExported(ident.Name) {
    +			w.emitFeature(fmt.Sprintf("const %s %s", ident, litType))
    +		}
    +	}
    +}
    +
    +func (w *Walker) resolveConstantDeps() {
    +	var findConstType func(string) string
    +	findConstType = func(ident string) string {
    +		if dep, ok := w.constDep[ident]; ok {
    +			return findConstType(dep)
    +		}
    +		if t, ok := w.prevConstType[pkgSymbol{w.curPackageName, ident}]; ok {
    +			return t
    +		}
    +		return ""
    +	}
    +	for ident := range w.constDep {
    +		if !ast.IsExported(ident) {
    +			continue
    +		}
    +		t := findConstType(ident)
    +		if t == "" {
    +			log.Fatalf("failed to resolve constant %q", ident)
    +		}
    +		w.emitFeature(fmt.Sprintf("const %s %s", ident, t))
     	}
     }
    
  8. resolveConstantDeps メソッドの追加:

    • 定数依存関係を解決するための新しいメソッドが追加されました。
  9. テストデータの変更:

    • src/cmd/api/testdata/src/pkg/p1/golden.txt に新しい定数 AIsLowerAConstChase2 の出力が追加されました。
    • src/cmd/api/testdata/src/pkg/p1/p1.go に、定数参照のテストケース (ConstChase2, constChase, AIsLowerA) が追加されました。
    // src/cmd/api/testdata/src/pkg/p1/p1.go
    const (
    	ConstChase2 = constChase // forward declaration to unexported ident
    	constChase  = AIsLowerA  // forward declaration to exported ident
    
    	A         = 1
    	a         = 11
    	A64 int64 = 1
    
    	AIsLowerA = a // previously declared
    )
    

これらの変更は、cmd/api が定数の型を決定するロジックを、ハードコードされた推測から、より動的で依存関係を解決するアプローチへと移行させたことを示しています。

コアとなるコードの解説

このコミットのコアとなるコードは、src/cmd/api/goapi.go 内の Walker 構造体とその関連メソッド、特に constValueType, walkConst, そして新しく追加された resolveConstantDeps です。

Walker 構造体と pkgSymbol

  • prevConstType map[pkgSymbol]string:
    • このマップは、既に処理された定数の型を記録します。キーが pkgSymbol ({パッケージ名, シンボル名}) になったことで、異なるパッケージの同じ名前の定数を区別できるようになりました。これにより、flate.BestSpeed のような、他のパッケージの定数を参照するケースを正確に処理できます。
  • constDep map[string]string:
    • このマップは、まだ型が解決されていない定数間の依存関係を記録します。例えば、const B = A のように、B の型が A の型に依存している場合、constDep["B"] = "A" のように記録されます。これは、定数が宣言された順序に関わらず、依存関係を後から解決するための重要なメカニズムです。

constValueType メソッド

このメソッドは、定数の値からその型を推測する中心的なロジックを含んでいます。

  • ast.SelectorExpr の処理:
    • v.X (セレクタの左側、通常はパッケージ名) と v.Sel (セレクタの右側、通常は定数名) を抽出し、selectorFullPkg マップを使って完全なパッケージ名を解決します。
    • 解決されたパッケージ名と定数名を使って pkgSymbol を作成し、prevConstType からその定数の型を取得しようとします。これにより、flate.BestSpeed のようなクロスパッケージ参照が正しく解決されます。
  • ast.Ident の処理:
    • 識別子(定数名)が iota であれば ideal-int を返します。
    • 識別子が prevConstType に存在すれば、その型を返します。
    • 重要な変更点: 識別子が prevConstType に存在しない場合、以前はエラーを返していましたが、このコミットでは constDepPrefix + v.Name という形式の文字列を返します。これは、この定数の型がまだ不明であり、v.Name という別の定数に依存していることを示す「プレースホルダー」です。このプレースホルダーは後で resolveConstantDeps によって解決されます。
  • ast.BinaryExpr の処理:
    • 二項演算子(例: +, -, *)を含む定数式の場合、左右のオペランドの型を再帰的に constValueType で解決します。
    • 左右のオペランドの型が異なる場合の型推論ロジックが改善されました。特に、ideal-int と他の具体的な型(int, float64 など)の組み合わせや、両方が constDepPrefix を持つ場合の処理が追加されました。これにより、より複雑な定数式も正しく型推論できるようになりました。

walkConst メソッド

このメソッドは、const 宣言内の各定数をウォークし、その型を決定します。

  • エクスポートされていない定数の処理:
    • 以前は !ast.IsExported(ident.Name) のチェックでエクスポートされていない定数をスキップしていましたが、このコミットではそのチェックが削除されました。これは、すべての定数(エクスポートされているか否かに関わらず)の依存関係を constDep に記録し、後で resolveConstantDeps でまとめて解決するためです。最終的に emitFeature で出力されるのはエクスポートされた定数のみです。
  • constDepPrefix を持つ型の処理:
    • constValueType から返された litTypeconstDepPrefix で始まる場合、その依存関係を w.constDep マップに記録します。これにより、定数間の依存関係が明示的に追跡されます。
  • prevConstType への記録:
    • 定数の型が決定されると、w.prevConstType[pkgSymbol{w.curPackageName, ident.Name}] = litType のように、pkgSymbol をキーとして prevConstType に記録されます。
  • emitFeature の呼び出し:
    • ast.IsExported(ident.Name) のチェックがここで行われ、エクスポートされた定数のみがAPIとして出力されます。

resolveConstantDeps メソッド (新設)

このメソッドは、パッケージ内のすべてのファイルがウォークされた後に呼び出され、未解決の定数依存関係を解決します。

  • findConstType クロージャ:
    • この内部関数は、与えられた識別子 ident の最終的な型を再帰的に検索します。
    • まず w.constDep をチェックし、依存関係があればその依存先の定数の型を再帰的に findConstType で解決しようとします。
    • 次に w.prevConstType をチェックし、型が直接記録されていればそれを返します。
    • これにより、A -> B -> C のような多段階の依存関係も解決できます。
  • 依存関係の解決と出力:
    • w.constDep マップを走査し、各依存定数について findConstType を呼び出して最終的な型を決定します。
    • エクスポートされた定数のみが emitFeature で出力されます。
    • 型が解決できない場合は log.Fatalf でエラーを発生させます。

これらの変更により、cmd/api は定数の型解決において、宣言順序や複雑な依存関係に左右されない、より堅牢で正確なメカニズムを獲得しました。

関連リンク

参考にした情報源リンク