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

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

このコミットは、Go言語の型システムを扱う go/types パッケージと、SSA (Static Single Assignment) 形式のコード生成を扱う実験的な exp/ssa パッケージにおける変更を記述しています。主な目的は、NamedType.String() メソッドの出力にパッケージのインポートパスを含めることで、診断の曖昧さを解消し、gc コンパイラ(Goの公式コンパイラ)の出力に近づけることです。また、exp/ssa パッケージ内のいくつかのコスメティックな変更も含まれています。

コミット

commit a17c46169f59579788d0ffb82eab875e4f15fc00
Author: Alan Donovan <adonovan@google.com>
Date:   Tue Feb 19 14:42:05 2013 -0500

    go/types: include package import path in NamedType.String().
    
    This avoids ambiguity and makes the diagnostics closer to
    those issued by gc, but it is more verbose since it qualifies
    intra-package references.
    
    Without extra context---e.g. a 'from *Package' parameter to
    Type.String()---we are forced to err on one side or the other.
    
    Also, cosmetic changes to exp/ssa:
    - Remove package-qualification workaround in Function.FullName.
    - Always set go/types.Package.Path field to the import path,
      since we know the correct path at this point.
    - In Function.DumpTo, show variadic '...' and result type info,
      and delete now-redundant "# Type: " line.
    
    R=gri
    CC=golang-dev
    https://golang.org/cl/7325051

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

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

元コミット内容

このコミットの元の内容は以下の通りです。

  • go/types: NamedType.String() にパッケージのインポートパスを含める。
    • これにより曖昧さが解消され、gc コンパイラが出力する診断に近くなるが、パッケージ内の参照も修飾されるため冗長になる。
    • Type.String()from *Package のような追加のコンテキストがない場合、どちらかの側でエラーを出すしかない。
  • exp/ssa のコスメティックな変更:
    • Function.FullName からパッケージ修飾の回避策を削除。
    • go/types.Package.Path フィールドを常にインポートパスに設定する。これは、この時点で正しいパスが分かっているため。
    • Function.DumpTo で可変長引数 ... と結果の型情報を表示し、冗長になった # Type: 行を削除。

変更の背景

この変更の背景には、Go言語のコンパイラやツールチェーンにおける型情報の表現と診断の改善という目的があります。

Go言語では、異なるパッケージで同じ名前の型が定義されることがあります。例えば、package Atype MyType int があり、package B にも type MyType string がある場合、単に MyType とだけ表示されると、どちらのパッケージの MyType を指しているのかが曖昧になります。特に、コンパイラがエラーメッセージやデバッグ情報を出力する際に、この曖昧さがユーザーの混乱を招く可能性がありました。

gc コンパイラは、型名を表示する際にその型が属するパッケージのインポートパスを修飾することが一般的です(例: fmt.Stringer)。go/types パッケージは、Goのソースコードを解析して型情報を構築するためのライブラリであり、コンパイラやリンター、IDEなどのツールで利用されます。この go/types パッケージが生成する型情報の文字列表現が gc コンパイラの出力と異なる場合、ツールの出力と公式コンパイラの出力との間に乖離が生じ、ユーザー体験を損なう可能性があります。

このコミットの主な動機は、go/types パッケージが提供する NamedType.String() メソッドの出力を gc コンパイラの診断出力に近づけることで、ツールの診断メッセージの一貫性と明確性を向上させることにあります。これにより、ユーザーはより正確で理解しやすい型情報を受け取ることができるようになります。

また、exp/ssa パッケージは、GoコンパイラのSSAバックエンドの実験的な実装であり、コンパイラの最適化やコード生成の過程を可視化・デバッグするために使用されます。このパッケージ内の変更は、SSA形式の関数表現のデバッグ出力(Function.DumpTo)をより情報豊富にし、かつ冗長な情報を削除することで、開発者がSSAコードを分析しやすくすることを目的としています。特に、Function.FullName からパッケージ修飾の回避策を削除したことは、go/types 側の変更によって型名の修飾が適切に行われるようになったため、exp/ssa 側での特別な処理が不要になったことを示唆しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と関連パッケージについての知識が必要です。

  1. Go言語のパッケージとインポートパス: Go言語のコードはパッケージに分割されます。各パッケージは、そのパッケージを一意に識別するための「インポートパス」を持ちます。例えば、標準ライブラリの fmt パッケージは fmt というインポートパスを持ち、GitHub上のリポジトリであれば github.com/user/repo/package_name のような形式になります。型や関数を参照する際には、通常、package_name.TypeName のようにパッケージ名を修飾して参照します。

  2. go/types パッケージ: go/types はGoの標準ライブラリの一部であり、Goのソースコードを解析して型情報を構築するためのパッケージです。これは、Goのコンパイラ、リンター、静的解析ツール、IDEなどが、Goプログラムのセマンティクス(意味)を理解するために利用します。

    • types.Package: Goのパッケージを表す構造体です。インポートパスやパッケージ名などの情報を含みます。
    • types.Type インターフェース: Goのあらゆる型(基本型、構造体、インターフェース、関数など)を表すインターフェースです。
    • types.NamedType: type MyType int のように、名前が付けられた型(定義型)を表す構造体です。これは、基底型(int)と、その型が定義されたオブジェクト(MyType)への参照を持ちます。
    • String() メソッド: Goの多くの型は String() メソッドを実装しており、その型の文字列表現を返します。これはデバッグ出力やエラーメッセージで頻繁に利用されます。
  3. exp/ssa パッケージ: exp/ssa は、GoコンパイラのSSA(Static Single Assignment)バックエンドの実験的な実装を含むパッケージです。SSAは、コンパイラの最適化フェーズで広く使用される中間表現(IR)の一種です。各変数が一度だけ代入されるという特性を持ち、データフロー解析や最適化を容易にします。

    • ssa.Function: SSA形式で表現された関数を表す構造体です。
    • Function.FullName(): 関数の完全な名前(パッケージ名、レシーバ型などを含む)を返すメソッドです。
    • Function.DumpTo(): SSA形式の関数の内容を人間が読める形式で出力するメソッドです。これはデバッグや分析に役立ちます。
  4. gc コンパイラ: gc は、Go言語の公式なコンパイラです。Goのソースコードを機械語に変換する役割を担います。go/types パッケージは、gc コンパイラが内部的に使用する型システムと密接に関連しており、両者の出力の一貫性はGo開発者にとって重要です。

技術的詳細

このコミットは、主に go/types パッケージにおける型名の文字列表現の改善と、それに伴う exp/ssa パッケージの調整に焦点を当てています。

go/types における NamedType.String() の変更

以前の NamedType.String() の実装では、NamedType の文字列表現を生成する際に、その型が属するパッケージのインポートパスを含めていませんでした。これは、同じ名前の型が異なるパッケージに存在する場合に、その型がどのパッケージに属するのかを明確に識別できないという問題を引き起こしていました。

このコミットでは、src/pkg/go/types/errors.go 内の writeType 関数(NamedType の文字列表現を生成する部分)が変更され、NamedType が持つ Obj (オブジェクト) の Pkg (パッケージ) が存在し、かつその Pkg.Path (インポートパス) が空でない場合に、obj.Pkg.Path + "." を型名の前に付加するように修正されました。

変更前: MyType (曖昧な場合がある)

変更後: package/path.MyType (常に完全修飾される)

この変更により、NamedType.String() の出力は常にパッケージのインポートパスで修飾されるようになります。これにより、型名の曖昧さが解消され、特にエラーメッセージやデバッグ出力において、どのパッケージの型が参照されているのかが明確になります。コミットメッセージにあるように、これは gc コンパイラの診断出力に近づけるための変更です。

ただし、この変更にはトレードオフがあります。パッケージ内の参照(例: main パッケージ内で定義された MyTypemain パッケージ内で参照する場合)も main.MyType のように修飾されるため、以前よりも冗長な出力になる可能性があります。コミットメッセージでは、Type.String()from *Package のような追加のコンテキスト(どのパッケージから参照されているか)がない場合、この冗長性を避けることが難しいと説明されています。つまり、go/types パッケージは汎用的な型システムであり、特定の参照コンテキストを持たないため、常に完全修飾することが最も安全で一貫性のあるアプローチと判断されたわけです。

exp/ssa における変更

exp/ssa パッケージに対する変更は、go/types の変更と密接に関連しており、主にSSAコードのデバッグ出力の改善と、型情報の取り扱いの一貫性向上を目的としています。

  1. src/pkg/exp/ssa/builder.go の変更: createPackageImpl 関数内で、types.Package.Path フィールドが常に正しいインポートパスに設定されるように修正されました。以前は、型チェッカーが GcImported パッケージに対してのみ types.Package.Path を設定していましたが、SSAビルダーの段階では常に正しいインポートパスが分かっているため、これを保証するように変更されました。これにより、ssa.Package.ImportPath フィールドの冗長性が将来的に解消される可能性が示唆されています。

  2. src/pkg/exp/ssa/func.goFunction.FullName の変更: Function.FullName メソッドは、関数の完全な名前を生成します。以前は、クロスパッケージ参照の場合にのみパッケージ名を修飾するロジックが含まれていましたが、go/typesNamedType.String() が常にパッケージパスを修飾するようになったため、このロジックが簡素化されました。

    • 変更前は、pkgQual という変数を使ってクロスパッケージ参照の場合にのみパッケージ名を付加していました。
    • 変更後は、レシーバを持つメソッドの場合、recv.TypeString() メソッドが既にパッケージ修飾された名前を返すため、fmt.Sprintf("(%s).%s", recv.Type, f.Name_) のように直接利用できるようになりました。
    • パッケージレベルの関数の場合も、from != f.Pkg (クロスパッケージ参照) の場合にのみ f.Pkg.ImportPath を付加するようにロジックが変更され、より簡潔になりました。これにより、Function.FullName が生成する名前も、go/types の出力と一貫性を持つようになります。
  3. src/pkg/exp/ssa/func.goFunction.DumpTo の変更: Function.DumpTo メソッドは、SSA形式の関数の詳細な情報を出力します。このコミットでは、デバッグ出力の視認性と情報量を向上させるためのコスメティックな変更が行われました。

    • 冗長な # Type: 行が削除されました。関数のシグネチャ情報は、後続のコードでより詳細に表示されるため、この行は不要と判断されました。
    • 関数のシグネチャ表示において、可変長引数(...)と結果の型情報が追加されました。これにより、DumpTo の出力から関数のシグネチャがより正確に読み取れるようになります。特に、結果の型が匿名の場合や複数の戻り値がある場合にも対応できるよう、types.Result 型を使用して表現されています。

これらの変更は、Goコンパイラの内部ツールが生成する型情報の表現をより一貫性のあるものにし、開発者にとってのデバッグや分析を容易にすることを目的としています。

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

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

  1. src/pkg/exp/ssa/builder.go:

    • func (b *Builder) createPackageImpl(...) *Package 関数内。
    • typkg.Path == "" の場合に typkg.Path = importPath を設定するロジックが追加されました。
    --- a/src/pkg/exp/ssa/builder.go
    +++ b/src/pkg/exp/ssa/builder.go
    @@ -2570,11 +2570,13 @@ func (b *Builder) CreatePackage(importPath string, files []*ast.File) (*Package,\
     // from the gc compiler's object files; no code will be available.\
     //\
     func (b *Builder) createPackageImpl(typkg *types.Package, importPath string, files []*ast.File) *Package {\
    -	// TODO(gri): make this an invariant and eliminate importPath
    -	// param and Package field.\
    -	// if importPath != p.Types.Path {\
    -	// 	panic(importPath + " != " + p.Types.Path)\
    -	// }\
    +	// The typechecker sets types.Package.Path only for GcImported
    +	// packages, since it doesn't know import path until after typechecking is done.\
    +	// Here we ensure it is always set, since we know the correct path.\
    +	// TODO(adonovan): eliminate redundant ssa.Package.ImportPath field.\
    +	if typkg.Path == "" {
    +		typkg.Path = importPath
    +	}
     
      	p := &Package{
      		Prog:       b.Prog,
    
  2. src/pkg/exp/ssa/func.go:

    • func (f *Function) fullName(from *Package) string 関数内。
    • パッケージ修飾のロジックが簡素化され、pkgQual 変数とその関連ロジックが削除されました。
    • レシーバを持つメソッドとパッケージレベルの関数の名前生成ロジックが変更されました。
    --- a/src/pkg/exp/ssa/func.go
    +++ b/src/pkg/exp/ssa/func.go
    @@ -379,27 +383,20 @@ func (f *Function) fullName(from *Package) string {\
     		} else {\
     			recvType = f.Params[0].Type() // interface method thunk
     		}\
    -		// TODO(adonovan): print type package-qualified, if NamedType.\
     		return fmt.Sprintf("(%s).%s", recvType, f.Name_)\
     	}\
     
    -	// "pkg." prefix for cross-package references only.\
    -	var pkgQual string\
    -	if from != f.Pkg {\
    -		pkgQual = f.Pkg.ImportPath + ".\"\
    -	}\
    -
     	// Declared method?\
     	if recv != nil {\
    -		star := ""\
    -		if isPointer(recv.Type) {\
    -			star = "*"\
    -		}\
    -		return fmt.Sprintf("(%s%s%s).%s", star, pkgQual, deref(recv.Type), f.Name_)\
    +		return fmt.Sprintf("(%s).%s", recv.Type, f.Name_)\
     	}\
     
     	// Package-level function.\
    -	return pkgQual + f.Name_\
    +	// Prefix with package name for cross-package references only.\
    +	if from != f.Pkg {\
    +		return fmt.Sprintf("%s.%s", f.Pkg.ImportPath, f.Name_)\
    +	}\
    +	return f.Name_\
     }\
     
     // DumpTo prints to w a human readable "disassembly" of the SSA code of
    @@ -408,7 +405,6 @@ func (f *Function) fullName(from *Package) string {\
     func (f *Function) DumpTo(w io.Writer) {\
     	fmt.Fprintf(w, "# Name: %s\\n", f.FullName())\
     	fmt.Fprintf(w, "# Declared at %s\\n", f.Prog.Files.Position(f.Pos))\
    -	fmt.Fprintf(w, "# Type: %s\\n", f.Signature)\
     
      	if f.Enclosing != nil {\
      		fmt.Fprintf(w, "# Parent: %s\\n", f.Enclosing.Name())\
    @@ -421,6 +417,7 @@ func (f *Function) DumpTo(w io.Writer) {\
      		}\
      	}\
      
    +	// Function Signature in declaration syntax; derived from types.Signature.String().\
      	io.WriteString(w, "func ")\
      	params := f.Params\
      	if f.Signature.Recv != nil {\
    @@ -435,9 +432,23 @@ func (f *Function) DumpTo(w io.Writer) {\
      	\t\tio.WriteString(w, v.Name())\
      	\t\tio.WriteString(w, " ")\
    +\t\tif f.Signature.IsVariadic && i == len(params)-1 {\
    +\t\t\tio.WriteString(w, "...")\
    +\t\t}\
      	\t\tio.WriteString(w, v.Type().String())\
      	}\
    -\tio.WriteString(w, "):\\n")\
    +\tio.WriteString(w, ")")\
    +\tif res := f.Signature.Results; res != nil {\
    +\t\tio.WriteString(w, " ")\
    +\t\tvar t types.Type\
    +\t\tif len(res) == 1 && res[0].Name == "" {\
    +\t\t\tt = res[0].Type\
    +\t\t} else {\
    +\t\t\tt = &types.Result{Values: res}\
    +\t\t}\
    +\t\tio.WriteString(w, t.String())\
    +\t}\
    +\tio.WriteString(w, ":\\n")\
      
      	for _, b := range f.Blocks {\
      	\tif b == nil {\
    
  3. src/pkg/go/types/errors.go:

    • func writeType(buf *bytes.Buffer, typ Type) 関数内。
    • case *NamedType: ブロックで、obj.Pkg.Path を利用してパッケージ名を型名の前に付加するロジックが追加されました。
    --- a/src/pkg/go/types/errors.go
    +++ b/src/pkg/go/types/errors.go
    @@ -307,7 +307,11 @@ func writeType(buf *bytes.Buffer, typ Type) {\
     
      	case *NamedType:\
      	\ts := "<NamedType w/o object>"\
    -\t\tif t.Obj != nil {\
    +\t\tif obj := t.Obj; obj != nil {\
    +\t\t\tif obj.Pkg != nil && obj.Pkg.Path != "" {\
    +\t\t\t\tbuf.WriteString(obj.Pkg.Path)\
    +\t\t\t\tbuf.WriteString(".")\
    +\t\t\t}\
      	\t\ts = t.Obj.GetName()\
      	\t}\
      	\tbuf.WriteString(s)\
    

コアとなるコードの解説

src/pkg/go/types/errors.go の変更

このファイルは、go/types パッケージが型関連のエラーメッセージや診断情報を生成する際に使用するユーティリティ関数を含んでいます。writeType 関数は、types.Type インターフェースを実装する任意の型の文字列表現を bytes.Buffer に書き込む役割を担っています。

変更の核心は、case *NamedType: ブロックにあります。NamedTypetype MyType int のようにユーザーが定義した名前付きの型を表します。

  • 変更前: t.Obj != nil の場合に t.Obj.GetName() を取得し、その名前だけをバッファに書き込んでいました。これにより、同じ名前の型が異なるパッケージに存在する場合に曖昧さが生じていました。
  • 変更後: obj := t.Obj; obj != nil のチェックに加え、obj.Pkg != nil && obj.Pkg.Path != "" の条件が追加されました。これは、NamedType が属するオブジェクト(obj)がパッケージ(obj.Pkg)を持ち、そのパッケージにインポートパス(obj.Pkg.Path)が設定されている場合に、そのインポートパスとピリオド(.)を型名の前に書き込むようにしています。
    • buf.WriteString(obj.Pkg.Path): パッケージのインポートパスを書き込みます。
    • buf.WriteString("."): パッケージ名と型名を区切るピリオドを書き込みます。
    • s = t.Obj.GetName(): 型名自体を取得します。
    • buf.WriteString(s): 最終的に型名を書き込みます。

この変更により、NamedType の文字列表現は package/path.TypeName の形式となり、常に完全修飾されることで、型名の曖昧さが解消されます。

src/pkg/exp/ssa/builder.go の変更

このファイルは、GoのAST(抽象構文木)からSSA形式のコードを構築するビルダーのロジックを含んでいます。createPackageImpl 関数は、SSAパッケージを作成する際に呼び出されます。

  • 変更前: typkg.Path が設定されていない場合のコメントアウトされた panicTODO コメントがありました。これは、型チェッカーが types.Package.Path を設定するタイミングに依存しており、SSAビルダーの段階で常に正しいパスが利用可能であるという保証がなかったことを示唆しています。
  • 変更後: if typkg.Path == "" という条件が追加され、typkg.Path が空の場合に引数として渡された importPath を設定するように修正されました。
    • typkggo/types.Package 型のインスタンスであり、SSAパッケージが構築される基となる型情報パッケージです。
    • importPath は、SSAパッケージが作成される際の正しいインポートパスです。
    • この変更により、types.Package.Path がSSAビルダーの段階で常に正しい値を持つことが保証され、SSAコード生成の過程での型情報の整合性が向上します。また、コミットメッセージにあるように、これにより ssa.Package.ImportPath フィールドの冗長性が将来的に解消される可能性が生まれます。

src/pkg/exp/ssa/func.go の変更

このファイルは、SSA形式の関数(ssa.Function)の定義と、その操作に関するメソッドを含んでいます。

Function.FullName() の変更

このメソッドは、関数の完全な名前(例: math.IsNaN, (*sync.WaitGroup).Add)を生成します。

  • 変更前:
    • レシーバを持つメソッドの場合、レシーバの型名に * を付加するかどうかのロジックと、pkgQual 変数を使ってクロスパッケージ参照の場合にのみパッケージ名を付加するロジックが含まれていました。
    • パッケージレベルの関数の場合も、pkgQual を使ってパッケージ名を付加していました。
    • この pkgQual を使ったパッケージ修飾のロジックは、go/typesNamedType.String() がパッケージパスを修飾しないことを前提としていました。
  • 変更後:
    • pkgQual 変数とその関連ロジックが完全に削除されました。
    • レシーバを持つメソッドの場合、fmt.Sprintf("(%s).%s", recv.Type, f.Name_) のように、レシーバの型を直接 recv.Type として渡すようになりました。これは、recv.Type.String()go/types の変更によって既にパッケージ修飾された名前を返すようになったため、Function.FullName 側で追加の修飾が不要になったためです。
    • パッケージレベルの関数の場合も、if from != f.Pkg (クロスパッケージ参照) の場合にのみ fmt.Sprintf("%s.%s", f.Pkg.ImportPath, f.Name_) のようにパッケージ名を付加するように変更されました。これにより、パッケージ内の参照は修飾されず、クロスパッケージ参照のみが修飾されるという、より自然な形式になります。

この変更は、go/typesNamedType.String() の変更と連携し、SSA関数の名前表現の一貫性と正確性を向上させています。

Function.DumpTo() の変更

このメソッドは、SSA形式の関数の詳細なデバッグ情報を出力します。

  • 冗長な # Type: 行の削除:

    • 以前は # Type: %s という行で関数のシグネチャを出力していましたが、これは後続のコードでより詳細なシグネチャ情報が出力されるため、冗長と判断され削除されました。
  • 可変長引数(...)と結果の型情報の追加:

    • 関数のパラメータリストの出力において、if f.Signature.IsVariadic && i == len(params)-1 の条件が追加され、可変長引数を持つ関数の最後のパラメータに対して ... が表示されるようになりました。
    • 関数の戻り値の型情報の出力が大幅に強化されました。
      • if res := f.Signature.Results; res != nil で戻り値が存在するかをチェックします。
      • 戻り値が1つで名前がない場合(例: func() int)、その型を直接 t = res[0].Type で取得します。
      • それ以外の場合(複数の戻り値や名前付き戻り値)、t = &types.Result{Values: res} を使用して types.Result 型として表現し、その String() メソッドを利用して適切な文字列表現を得ます。
      • これにより、DumpTo の出力が関数のシグネチャをより正確かつ詳細に反映するようになり、SSAコードの分析が容易になります。

これらの変更は、exp/ssa パッケージが生成するデバッグ出力の品質を向上させ、Goコンパイラの開発者がSSA中間表現をより効果的に理解・デバッグできるようにすることを目的としています。

関連リンク

参考にした情報源リンク