[インデックス 15223] ファイルの概要
このコミットは、Go言語の型システムにおけるgcimporter
(Goコンパイラの出力するエクスポートデータを読み込む部分)のバグ修正と改善に関するものです。具体的には、エクスポートデータに含まれる非公開フィールドやメソッド名が、パッケージID(パス)で完全に修飾されている場合に発生する、パッケージの不適切な再利用とそれに伴う名前解決の失敗というバグに対処しています。
コミット
commit f6fe3271f738355f73ee79a9c5bc2a881eebd783
Author: Robert Griesemer <gri@golang.org>
Date: Wed Feb 13 10:21:24 2013 -0800
go/types: adjust gcimporter to actual gc export data
Unexported field and method names that appear in the
export data (as part of some exported type) are fully
qualified with a package id (path). In some cases, a
package with that id was never exported for any other
use (i.e. only the path is of interest).
We must not create a "real" package in those cases
because we don't have a package name. Entering an
unnamed package into the map of imported packages
makes that package accessible for other imports.
Such a subsequent import may find the unnamed
package in the map, and reuse it. That reused and
imported package is then entered into the importing
file scope, still w/o a name. References to that
package cannot resolved after that. Was bug.
R=adonovan
CC=golang-dev
https://golang.org/cl/7307112
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f6fe3271f738355f73ee79a9c5bc2a881eebd783
元コミット内容
Goの型システムにおいて、gcimporter
がGoコンパイラ(gc
)のエクスポートデータを処理する方法を調整する。
エクスポートデータに現れる非公開フィールドやメソッド名(エクスポートされた型の一部として)は、パッケージID(パス)で完全に修飾されている。しかし、場合によっては、そのIDを持つパッケージが他の目的でエクスポートされたことがない(つまり、パスのみが重要である)ことがある。
このような場合、パッケージ名がないため「実際の」パッケージを作成してはならない。名前のないパッケージをインポートされたパッケージのマップに入れると、そのパッケージが他のインポートからアクセス可能になる。その後のインポートがマップ内で名前のないパッケージを見つけ、それを再利用する可能性がある。再利用されインポートされたパッケージは、インポート元のファイルスコープに入り、依然として名前がない状態になる。その後、そのパッケージへの参照を解決できなくなる。これはバグであった。
変更の背景
Go言語のコンパイラ(gc
)は、コンパイル済みのパッケージ情報をエクスポートデータとして出力します。このエクスポートデータには、公開された型だけでなく、公開された型の一部として現れる非公開のフィールドやメソッドの情報も含まれることがあります。これらの非公開の要素は、それらが属するパッケージの完全なパス(パッケージID)で修飾されてエクスポートされます。
問題は、このパッケージIDが、実際にGoのソースコードでimport
文によってインポートされるような「名前を持つパッケージ」に対応しない場合があることでした。例えば、あるパッケージP
が、別のパッケージQ
の非公開型Q.T
を公開型P.S
のフィールドとして使用している場合、P
のエクスポートデータにはQ.T
の情報が含まれ、Q
のパッケージIDが修飾子として現れます。しかし、Q
自体がP
から直接インポートされるわけではないため、gcimporter
がQ
を「名前を持つパッケージ」として扱う必要はありませんでした。
従来のgcimporter
の実装では、このような「パスのみが重要なパッケージID」に対しても、p.imports
というマップに*Package
オブジェクトを作成し、追加していました。しかし、これらのパッケージは名前を持たないため、マップにはPath
はあってもName
が空の*Package
オブジェクトが格納されることになります。
この「名前のないパッケージ」がp.imports
マップに存在すると、その後に別のパッケージが同じパスを持つパッケージをインポートしようとした際に、gcimporter
がマップ内の既存の「名前のないパッケージ」を再利用してしまうというバグがありました。再利用された「名前のないパッケージ」は、インポート元のスコープに導入されますが、名前がないため、そのパッケージ内の要素への参照(例: pkg.Symbol
)を解決できなくなり、コンパイルエラーや型チェックエラーを引き起こしていました。
このコミットは、この根本的な問題を解決し、gcimporter
がエクスポートデータをより正確に解釈し、不必要な「名前のないパッケージ」の作成と再利用を防ぐことを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とGoコンパイラの内部動作に関する基本的な知識が必要です。
-
Go言語のパッケージとインポート:
- Goのコードはパッケージに分割され、他のパッケージの公開された要素(型、関数、変数など)を利用するには
import
文を使用します。 import "path/to/package"
のように、パッケージは通常、そのソースコードのパスによって識別されます。- インポートされたパッケージには、通常、そのパッケージ名(例:
fmt.Println
のfmt
)が割り当てられます。
- Goのコードはパッケージに分割され、他のパッケージの公開された要素(型、関数、変数など)を利用するには
-
Goの型システム (
go/types
):go/types
パッケージは、Go言語の型システムをプログラム的に表現するための標準ライブラリです。- コンパイラや静的解析ツールは、このパッケージを使用してGoプログラムの型情報を構築し、型チェックを行います。
*Package
構造体は、Goのパッケージを表し、その名前、パス、スコープ(パッケージ内で宣言されたエンティティの集合)などの情報を含みます。Scope
は、識別子とその宣言されたエンティティ(変数、関数、型など)のマッピングを管理します。
-
Goコンパイラのエクスポートデータ:
- Goコンパイラ(
gc
)は、パッケージをコンパイルする際に、そのパッケージの公開されたAPI(型、関数、変数など)に関する情報をエクスポートデータとして生成します。 - このエクスポートデータは、他のパッケージがそのパッケージをインポートする際に、型チェックやリンクのために利用されます。
- エクスポートデータは、Goの内部的な形式で記述されており、
gcimporter
のようなツールによって解析されます。 - 非公開のフィールドやメソッドであっても、公開された型の一部としてエクスポートデータに含まれる場合があり、その際には完全なパッケージパスで修飾されます。
- Goコンパイラ(
-
gcimporter
:go/types
パッケージの一部であるgcimporter
は、gc
コンパイラが生成したエクスポートデータを読み込み、go/types
の内部表現(*Package
、*Type
など)に変換する役割を担います。gcimporter
は、インポートされたパッケージのキャッシュ(imports
マップ)を保持し、同じパッケージが複数回インポートされるのを防ぎます。
-
QualifiedName
:go/types
パッケージにおけるQualifiedName
構造体は、パッケージによって修飾された名前(例:fmt.Println
のfmt.Println
)を表します。Pkg
フィールドは、その名前を宣言したパッケージへのポインタを保持し、Name
フィールドは修飾されていない名前(例:Println
)を保持します。
技術的詳細
このコミットの技術的詳細は、主にsrc/pkg/go/types/gcimporter.go
におけるPackage
オブジェクトの生成と管理ロジックの変更に集約されます。
変更の核心:
従来のgcimporter
は、エクスポートデータからパッケージID(パス)を読み取ると、無条件にp.imports
マップに*Package
オブジェクトを作成しようとしていました。しかし、エクスポートデータには、名前を持たないがパスで修飾された非公開の要素(例: 公開された構造体の非公開フィールドの型)が含まれることがあります。これらの要素のパッケージIDは、単に型を完全に識別するために必要な情報であり、実際にimport
されるような「名前を持つパッケージ」ではありません。
このコミットは、この問題を解決するために、*Package
オブジェクトの「実体化(materialization)」をより厳密に制御します。
-
parsePackageId()
の役割の分離:- 以前の
parsePkgId()
関数は、パッケージIDを解析し、同時にp.imports
マップから*Package
オブジェクトを取得するか、新しい*Package
を作成してマップに追加していました。 - 新しい
parsePackageId()
関数は、単にパッケージID(文字列)を返すだけにその役割を限定します。これにより、IDの解析と*Package
オブジェクトの生成が分離されます。
- 以前の
-
getPkg(id, name string) *Package
の導入:- この新しい関数が、
*Package
オブジェクトの実体化を管理する中心的な役割を担います。 id
(パッケージパス)とname
(パッケージ名)を受け取ります。unsafe
パッケージは特別扱いされます。p.imports
マップにid
に対応するパッケージが既に存在すれば、それを返します。- 重要な変更点:
p.imports
にパッケージが存在せず、かつname
が空でない場合(つまり、パッケージ名が利用可能である場合)にのみ、新しい*Package
オブジェクトを作成し、p.imports
マップに追加します。 name
が空の場合(つまり、パッケージ名が不明な場合)は、nil
を返します。これにより、名前のないパッケージがp.imports
マップに誤って追加されるのを防ぎます。
- この新しい関数が、
-
parseName(materializePkg bool) (pkg *Package, name string)
の変更:parseName
関数は、識別子、?
、または@
で修飾された名前を解析します。- 新しい
materializePkg
引数が追加されました。これは、解析された名前が「実際の」*Package
オブジェクトを必要とするかどうかを示します。 @
で修飾された名前(QualifiedName
)の場合:parseQualifiedName()
を呼び出してid
とname
を取得します。materializePkg
がtrue
の場合:getPkg(id, "")
を呼び出してパッケージを取得しようとします。ここでname
が空文字列で渡されるのは、QualifiedName
のname
は要素名であり、パッケージ名ではないためです。- もし
getPkg
がnil
を返した場合(つまり、名前を持つパッケージとして存在しない場合)、&Package{Path: id}
という「フェイク(fake)」の*Package
オブジェクトを作成します。このフェイクパッケージはName
もScope
も持たず、p.imports
マップにも追加されません。これにより、パス情報のみが必要な場合に、不必要な「実際の」パッケージの作成とマップへの追加を防ぎます。
materializePkg
がfalse
の場合:pkg
はnil
のままとなり、*Package
オブジェクトは作成されません。これは、パラメータ名のようにパッケージ情報が不要な場合に適用されます。
-
QualifiedName.IsSame()
の比較ロジックの変更:src/pkg/go/types/types.go
のQualifiedName.IsSame()
メソッドは、2つのQualifiedName
が同じであるかを比較します。- 非公開の名前の場合の比較ロジックが
p.Pkg == q.Pkg
からp.Pkg.Path == q.Pkg.Path
に変更されました。 - これは非常に重要です。なぜなら、上記の変更により、同じパッケージパスを持つ「フェイクパッケージ」が複数作成される可能性があるためです。これらのフェイクパッケージは異なるメモリ上のアドレスを持つ(
p.Pkg != q.Pkg
)かもしれませんが、論理的には同じパッケージパスを指しています。したがって、ポインタの比較ではなく、パッケージパス(Path
フィールド)の文字列比較を行うことで、正しい同等性チェックが可能になります。
これらの変更により、gcimporter
はエクスポートデータをより正確に処理し、名前のないパッケージがp.imports
マップに誤って追加され、その後のインポートで再利用されるというバグを防ぎます。
コアとなるコードの変更箇所
このコミットにおける主要な変更は、src/pkg/go/types/gcimporter.go
ファイルに集中しています。
-
src/pkg/go/types/gcimporter.go
:parsePkgId
関数がparsePackageId
にリネームされ、*Package
を返す代わりにstring
(パッケージID)を返すように変更。パッケージの実体化ロジックが削除された。- 新しい関数
parsePackageName()
が追加され、パッケージ名を解析する。 - 新しい関数
parseQualifiedName()
が追加され、@PackageId.dotIdentifier
形式の修飾名を解析し、id
とname
(文字列)を返す。 - 新しいヘルパー関数
getPkg(id, name string) *Package
が追加。この関数が、パッケージIDとパッケージ名に基づいて*Package
オブジェクトを取得または作成する中心的なロジックを担う。名前が提供されない場合は、*Package
を作成しない。 parseExportedName()
がparseQualifiedName()
とgetPkg()
を使用するように変更。parseName(materializePkg bool)
関数にmaterializePkg
引数が追加。この引数に基づいて、@
修飾名の場合に「フェイクパッケージ」を作成するかどうかが決定される。parseField()
,parseParameter()
,parseInterfaceType()
,parseMethodDecl()
などの関数が、parseName()
を呼び出す際にmaterializePkg
引数を適切に渡すように変更された。parseImportDecl()
とparseExport()
がparsePackageName()
とgetPkg()
を使用するように変更され、パッケージのインポートとエクスポートの処理がより堅牢になった。
-
src/pkg/go/types/types.go
:QualifiedName
構造体のコメントが更新され、Pkg
フィールドが「フェイクパッケージ」である可能性が明記された。QualifiedName.IsSame()
メソッドの比較ロジックが、非公開の名前の場合にp.Pkg == q.Pkg
からp.Pkg.Path == q.Pkg.Path
に変更された。
-
src/pkg/exp/gotype/gotype_test.go
:crypto/rsa
のテストがコメントアウトから解除された。これは、このコミットで修正されたバグがcrypto/rsa
パッケージの型情報処理に影響を与えていた可能性を示唆している。
-
src/pkg/go/types/scope.go
:- デバッグ目的で
Scope
型にString()
メソッドが追加された。これは直接的なバグ修正とは関係ないが、型システムのデバッグを容易にするための改善。
- デバッグ目的で
コアとなるコードの解説
このコミットの核心は、gcimporter
がGoコンパイラのエクスポートデータを解析する際に、*Package
オブジェクトをいつ、どのように「実体化」するかを厳密に制御する点にあります。
以前は、エクスポートデータに現れるすべてのパッケージIDに対して、gcimporter
はp.imports
マップに*Package
オブジェクトを作成しようとしていました。しかし、Goの型システムでは、非公開のフィールドやメソッドの型が、その定義元のパッケージのパスで修飾されてエクスポートされることがあります。例えば、type S struct { f P.T }
のような場合、P.T
のP
はパッケージパスであり、gcimporter
がP
を「名前を持つパッケージ」としてインポートする必要はありませんでした。
この問題に対処するため、以下の主要な変更が行われました。
-
getPkg(id, name string) *Package
関数: この関数は、パッケージの実体化を集中管理します。func (p *gcParser) getPkg(id, name string) *Package { // package unsafe is not in the imports map - handle explicitly if id == "unsafe" { return Unsafe } pkg := p.imports[id] // まず既存のパッケージを探す if pkg == nil && name != "" { // 既存でなく、かつ名前がある場合のみ pkg = &Package{Name: name, Path: id, Scope: new(Scope)} // 新しい「実際の」パッケージを作成 p.imports[id] = pkg // importsマップに追加 } return pkg // 名前がない場合はnilを返す }
この関数は、パッケージ名(
name
引数)が提供されている場合にのみ、*Package
オブジェクトをp.imports
マップに登録します。これにより、名前のないパッケージがマップに誤って追加されることを防ぎます。 -
parseName(materializePkg bool) (pkg *Package, name string)
関数: この関数は、様々な種類の名前(識別子、?
、@
で修飾された名前)を解析します。特に@
で修飾された名前(QualifiedName
)の処理が重要です。func (p *gcParser) parseName(materializePkg bool) (pkg *Package, name string) { switch p.tok { // ... (scanner.Ident, '?' のケース) case '@': // exported name prefixed with package path var id string id, name = p.parseQualifiedName() // パッケージIDと要素名を解析 if materializePkg { // 「実際の」パッケージが必要な場合 // we don't have a package name - if the package // doesn't exist yet, create a fake package instead pkg = p.getPkg(id, "") // getPkgを呼び出すが、パッケージ名は空文字列を渡す if pkg == nil { // getPkgがnilを返した場合(名前がないため) pkg = &Package{Path: id} // フェイクパッケージを作成(名前なし、スコープなし、importsマップに追加しない) } } // ... (default ケース) } p.next() return }
materializePkg
がtrue
の場合、getPkg
を呼び出して「実際の」パッケージを取得しようとします。しかし、getPkg
がnil
を返した場合(つまり、そのパッケージIDに対応する名前付きパッケージが存在しない場合)、parseName
は&Package{Path: id}
という非常に軽量な「フェイクパッケージ」を作成します。このフェイクパッケージは、p.imports
マップには追加されず、名前解決の際に問題を引き起こすことがありません。その唯一の目的は、QualifiedName
のPkg
フィールドにパッケージパス情報を提供することです。 -
QualifiedName.IsSame()
の変更:func (p QualifiedName) IsSame(q QualifiedName) bool { if p.Name != q.Name { return false } // p.Name == q.Name return ast.IsExported(p.Name) || p.Pkg.Path == q.Pkg.Path // ここが変更点 }
この変更は、上記の「フェイクパッケージ」の導入と密接に関連しています。同じパッケージパスを持つフェイクパッケージが複数作成された場合、それらはメモリ上では異なるオブジェクト(異なるポインタ)になります。しかし、論理的には同じパッケージを指しています。したがって、
p.Pkg == q.Pkg
というポインタ比較ではなく、p.Pkg.Path == q.Pkg.Path
というパスの文字列比較を行うことで、QualifiedName
の正しい同等性を判断できるようになりました。
これらの変更により、gcimporter
はエクスポートデータから読み取ったパッケージ情報を、その用途に応じて適切に処理できるようになり、名前のないパッケージが誤ってインポートマップに登録され、その後の名前解決を妨げるというバグが解消されました。
関連リンク
- Go言語の型システムに関するドキュメント: https://pkg.go.dev/go/types
- Go言語のパッケージとインポートに関する公式ドキュメント: https://go.dev/doc/code
- Goコンパイラのエクスポートデータ形式に関する詳細な技術文書は公開されていませんが、Goのソースコード(特に
cmd/compile/internal/gc
やgo/types/gcimporter
)を読むことで理解を深めることができます。
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
go/types
パッケージ) - コミットメッセージと差分(diff)
- Go言語のAST(抽象構文木)に関する知識
- Go言語のコンパイラ設計に関する一般的な知識