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

[インデックス 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から直接インポートされるわけではないため、gcimporterQを「名前を持つパッケージ」として扱う必要はありませんでした。

従来のgcimporterの実装では、このような「パスのみが重要なパッケージID」に対しても、p.importsというマップに*Packageオブジェクトを作成し、追加していました。しかし、これらのパッケージは名前を持たないため、マップにはPathはあってもNameが空の*Packageオブジェクトが格納されることになります。

この「名前のないパッケージ」がp.importsマップに存在すると、その後に別のパッケージが同じパスを持つパッケージをインポートしようとした際に、gcimporterがマップ内の既存の「名前のないパッケージ」を再利用してしまうというバグがありました。再利用された「名前のないパッケージ」は、インポート元のスコープに導入されますが、名前がないため、そのパッケージ内の要素への参照(例: pkg.Symbol)を解決できなくなり、コンパイルエラーや型チェックエラーを引き起こしていました。

このコミットは、この根本的な問題を解決し、gcimporterがエクスポートデータをより正確に解釈し、不必要な「名前のないパッケージ」の作成と再利用を防ぐことを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とGoコンパイラの内部動作に関する基本的な知識が必要です。

  1. Go言語のパッケージとインポート:

    • Goのコードはパッケージに分割され、他のパッケージの公開された要素(型、関数、変数など)を利用するにはimport文を使用します。
    • import "path/to/package"のように、パッケージは通常、そのソースコードのパスによって識別されます。
    • インポートされたパッケージには、通常、そのパッケージ名(例: fmt.Printlnfmt)が割り当てられます。
  2. Goの型システム (go/types):

    • go/typesパッケージは、Go言語の型システムをプログラム的に表現するための標準ライブラリです。
    • コンパイラや静的解析ツールは、このパッケージを使用してGoプログラムの型情報を構築し、型チェックを行います。
    • *Package構造体は、Goのパッケージを表し、その名前、パス、スコープ(パッケージ内で宣言されたエンティティの集合)などの情報を含みます。
    • Scopeは、識別子とその宣言されたエンティティ(変数、関数、型など)のマッピングを管理します。
  3. Goコンパイラのエクスポートデータ:

    • Goコンパイラ(gc)は、パッケージをコンパイルする際に、そのパッケージの公開されたAPI(型、関数、変数など)に関する情報をエクスポートデータとして生成します。
    • このエクスポートデータは、他のパッケージがそのパッケージをインポートする際に、型チェックやリンクのために利用されます。
    • エクスポートデータは、Goの内部的な形式で記述されており、gcimporterのようなツールによって解析されます。
    • 非公開のフィールドやメソッドであっても、公開された型の一部としてエクスポートデータに含まれる場合があり、その際には完全なパッケージパスで修飾されます。
  4. gcimporter:

    • go/typesパッケージの一部であるgcimporterは、gcコンパイラが生成したエクスポートデータを読み込み、go/typesの内部表現(*Package*Typeなど)に変換する役割を担います。
    • gcimporterは、インポートされたパッケージのキャッシュ(importsマップ)を保持し、同じパッケージが複数回インポートされるのを防ぎます。
  5. QualifiedName:

    • go/typesパッケージにおけるQualifiedName構造体は、パッケージによって修飾された名前(例: fmt.Printlnfmt.Println)を表します。
    • Pkgフィールドは、その名前を宣言したパッケージへのポインタを保持し、Nameフィールドは修飾されていない名前(例: Println)を保持します。

技術的詳細

このコミットの技術的詳細は、主にsrc/pkg/go/types/gcimporter.goにおけるPackageオブジェクトの生成と管理ロジックの変更に集約されます。

変更の核心: 従来のgcimporterは、エクスポートデータからパッケージID(パス)を読み取ると、無条件にp.importsマップに*Packageオブジェクトを作成しようとしていました。しかし、エクスポートデータには、名前を持たないがパスで修飾された非公開の要素(例: 公開された構造体の非公開フィールドの型)が含まれることがあります。これらの要素のパッケージIDは、単に型を完全に識別するために必要な情報であり、実際にimportされるような「名前を持つパッケージ」ではありません。

このコミットは、この問題を解決するために、*Packageオブジェクトの「実体化(materialization)」をより厳密に制御します。

  1. parsePackageId() の役割の分離:

    • 以前のparsePkgId()関数は、パッケージIDを解析し、同時にp.importsマップから*Packageオブジェクトを取得するか、新しい*Packageを作成してマップに追加していました。
    • 新しいparsePackageId()関数は、単にパッケージID(文字列)を返すだけにその役割を限定します。これにより、IDの解析と*Packageオブジェクトの生成が分離されます。
  2. getPkg(id, name string) *Package の導入:

    • この新しい関数が、*Packageオブジェクトの実体化を管理する中心的な役割を担います。
    • id(パッケージパス)とname(パッケージ名)を受け取ります。
    • unsafeパッケージは特別扱いされます。
    • p.importsマップにidに対応するパッケージが既に存在すれば、それを返します。
    • 重要な変更点: p.importsにパッケージが存在せず、かつnameが空でない場合(つまり、パッケージ名が利用可能である場合)にのみ、新しい*Packageオブジェクトを作成し、p.importsマップに追加します。
    • nameが空の場合(つまり、パッケージ名が不明な場合)は、nilを返します。これにより、名前のないパッケージがp.importsマップに誤って追加されるのを防ぎます。
  3. parseName(materializePkg bool) (pkg *Package, name string) の変更:

    • parseName関数は、識別子、?、または@で修飾された名前を解析します。
    • 新しいmaterializePkg引数が追加されました。これは、解析された名前が「実際の」*Packageオブジェクトを必要とするかどうかを示します。
    • @で修飾された名前(QualifiedName)の場合:
      • parseQualifiedName()を呼び出してidnameを取得します。
      • materializePkgtrueの場合:
        • getPkg(id, "")を呼び出してパッケージを取得しようとします。ここでnameが空文字列で渡されるのは、QualifiedNamenameは要素名であり、パッケージ名ではないためです。
        • もしgetPkgnilを返した場合(つまり、名前を持つパッケージとして存在しない場合)、&Package{Path: id}という「フェイク(fake)」の*Packageオブジェクトを作成します。このフェイクパッケージはNameScopeも持たず、p.importsマップにも追加されません。これにより、パス情報のみが必要な場合に、不必要な「実際の」パッケージの作成とマップへの追加を防ぎます。
      • materializePkgfalseの場合:
        • pkgnilのままとなり、*Packageオブジェクトは作成されません。これは、パラメータ名のようにパッケージ情報が不要な場合に適用されます。
  4. QualifiedName.IsSame() の比較ロジックの変更:

    • src/pkg/go/types/types.goQualifiedName.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ファイルに集中しています。

  1. src/pkg/go/types/gcimporter.go:

    • parsePkgId関数がparsePackageIdにリネームされ、*Packageを返す代わりにstring(パッケージID)を返すように変更。パッケージの実体化ロジックが削除された。
    • 新しい関数parsePackageName()が追加され、パッケージ名を解析する。
    • 新しい関数parseQualifiedName()が追加され、@PackageId.dotIdentifier形式の修飾名を解析し、idname(文字列)を返す。
    • 新しいヘルパー関数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()を使用するように変更され、パッケージのインポートとエクスポートの処理がより堅牢になった。
  2. src/pkg/go/types/types.go:

    • QualifiedName構造体のコメントが更新され、Pkgフィールドが「フェイクパッケージ」である可能性が明記された。
    • QualifiedName.IsSame()メソッドの比較ロジックが、非公開の名前の場合にp.Pkg == q.Pkgからp.Pkg.Path == q.Pkg.Pathに変更された。
  3. src/pkg/exp/gotype/gotype_test.go:

    • crypto/rsaのテストがコメントアウトから解除された。これは、このコミットで修正されたバグがcrypto/rsaパッケージの型情報処理に影響を与えていた可能性を示唆している。
  4. src/pkg/go/types/scope.go:

    • デバッグ目的でScope型にString()メソッドが追加された。これは直接的なバグ修正とは関係ないが、型システムのデバッグを容易にするための改善。

コアとなるコードの解説

このコミットの核心は、gcimporterがGoコンパイラのエクスポートデータを解析する際に、*Packageオブジェクトをいつ、どのように「実体化」するかを厳密に制御する点にあります。

以前は、エクスポートデータに現れるすべてのパッケージIDに対して、gcimporterp.importsマップに*Packageオブジェクトを作成しようとしていました。しかし、Goの型システムでは、非公開のフィールドやメソッドの型が、その定義元のパッケージのパスで修飾されてエクスポートされることがあります。例えば、type S struct { f P.T }のような場合、P.TPはパッケージパスであり、gcimporterPを「名前を持つパッケージ」としてインポートする必要はありませんでした。

この問題に対処するため、以下の主要な変更が行われました。

  1. 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マップに登録します。これにより、名前のないパッケージがマップに誤って追加されることを防ぎます。

  2. 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
    }
    

    materializePkgtrueの場合、getPkgを呼び出して「実際の」パッケージを取得しようとします。しかし、getPkgnilを返した場合(つまり、そのパッケージIDに対応する名前付きパッケージが存在しない場合)、parseName&Package{Path: id}という非常に軽量な「フェイクパッケージ」を作成します。このフェイクパッケージは、p.importsマップには追加されず、名前解決の際に問題を引き起こすことがありません。その唯一の目的は、QualifiedNamePkgフィールドにパッケージパス情報を提供することです。

  3. 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/gcgo/types/gcimporter)を読むことで理解を深めることができます。

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にgo/typesパッケージ)
  • コミットメッセージと差分(diff)
  • Go言語のAST(抽象構文木)に関する知識
  • Go言語のコンパイラ設計に関する一般的な知識