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

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

このコミットは、Goコンパイラ(cmd/gc)におけるバグ修正に関するものです。具体的には、make組み込み関数によって作成されるマップ、スライス、チャネルの型引数が、エクスポートデータ(特にインライン化のために必要な情報)に正しく含まれない場合がある問題を解決しています。

変更されたファイルは以下の通りです。

  • src/cmd/gc/export.c: Goコンパイラのコード生成およびエクスポートデータ処理に関するC言語のソースファイル。このファイルでバグ修正の主要なロジックが変更されています。
  • test/fixedbugs/issue5470.dir/a.go: バグを再現するためのGoのテストファイル。エクスポートされた関数が、makeを使って未エクスポートの型(マップ、スライス、チャネル)を返すケースを定義しています。
  • test/fixedbugs/issue5470.dir/b.go: a.goで定義された関数をインポートし、呼び出すGoのテストファイル。クロスパッケージでの利用をシミュレートします。
  • test/fixedbugs/issue5470.go: 上記のテストファイルをコンパイルしてバグが修正されたことを確認するためのテストスクリプト。

コミット

commit 78f5b616fc308b969578cba5964fc8ae8c695c70
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu May 16 09:01:43 2013 +0200

    cmd/gc: repair make(T) in export data for inlining.
    
    When T was an unexported type it could be forgotten.
    
    Fixes #5470.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/9303050

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

https://github.com/golang/go/commit/78f5b616fc308b969578cba5964fc8ae8c695c70

元コミット内容

cmd/gc: repair make(T) in export data for inlining.

When T was an unexported type it could be forgotten.

Fixes #5470.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/9303050

変更の背景

このコミットは、Goコンパイラ(cmd/gc)が、make組み込み関数によって作成されるマップ、スライス、チャネルの型引数に関する情報を、パッケージのエクスポートデータに適切に含めないというバグを修正するために行われました。

具体的には、あるパッケージが未エクスポートの型(例えば、type myMap map[string]intのような、小文字で始まる型名)を定義し、その型をmake関数(例: make(myMap))の型引数として使用し、その結果をエクスポートされた関数(つまり、他のパッケージから呼び出し可能な関数)の戻り値として返す場合、コンパイラはその未エクスポートの型の定義をエクスポートデータに含めるのを忘れてしまうことがありました。

この問題は、特にコンパイラのインライン化最適化に影響を与えました。インライン化とは、関数呼び出しのオーバーヘッドを削減するために、呼び出される関数の本体を呼び出し元のコードに直接埋め込む最適化手法です。パッケージをまたいでインライン化を行うためには、呼び出される関数の型情報(特にmakeによって生成されるオブジェクトの基底型)が、呼び出し元のパッケージから参照可能である必要があります。未エクスポートの型がエクスポートデータに含まれない場合、他のパッケージがその型に関する情報を持たないため、コンパイルエラーや不正な動作を引き起こす可能性がありました。

このバグはGoのIssue #5470として報告されており、このコミットはその問題を解決することを目的としています。

前提知識の解説

このコミットの理解には、以下のGo言語およびGoコンパイラに関する知識が役立ちます。

  1. Goコンパイラ (cmd/gc): Go言語の公式コンパイラは、通常gc(Go Compiler)と呼ばれます。これはGoソースコードを機械語に変換する役割を担います。コンパイルプロセスには、構文解析、型チェック、最適化、コード生成などが含まれます。

  2. エクスポートデータ (Export Data): Go言語では、パッケージは独立したコンパイル単位です。あるパッケージが別のパッケージをインポートする場合、インポートされるパッケージは、そのエクスポートされた(公開された)シンボル(関数、型、変数、定数など)に関する情報を提供する必要があります。この情報が「エクスポートデータ」としてコンパイル時に生成され、他のパッケージがそのパッケージを利用する際に参照されます。エクスポートデータは、コンパイラがパッケージ間の依存関係を解決し、型チェックや最適化(インライン化など)を正しく行うために不可欠です。

  3. インライン化 (Inlining): インライン化は、コンパイラ最適化の一種です。関数呼び出しの際に発生するスタックフレームの作成、引数の渡し、戻り値の処理といったオーバーヘッドを削減するために、呼び出される関数のコードを呼び出し元の場所に直接展開します。これにより、プログラムの実行速度が向上する可能性があります。Goコンパイラは、特定の条件を満たす小さな関数に対して自動的にインライン化を行います。パッケージをまたいだインライン化も可能ですが、そのためには呼び出される関数の完全な型情報がエクスポートデータに含まれている必要があります。

  4. エクスポートされた識別子と未エクスポートの識別子 (Exported vs. Unexported Identifiers): Go言語では、識別子(変数名、関数名、型名など)の最初の文字が大文字であるか小文字であるかによって、その可視性(スコープ)が決定されます。

    • エクスポートされた識別子: 最初の文字が大文字の場合、その識別子はパッケージ外から参照可能です。
    • 未エクスポートの識別子: 最初の文字が小文字の場合、その識別子は定義されたパッケージ内でのみ参照可能です。他のパッケージからは直接アクセスできません。 このコミットの文脈では、未エクスポートの型が、エクスポートされた関数を通じて間接的に外部に公開される場合に問題が発生していました。
  5. make 組み込み関数: makeはGoの組み込み関数で、スライス、マップ、チャネルといった参照型を初期化するために使用されます。

    • make([]T, length, capacity): スライスを作成
    • make(map[K]V): マップを作成
    • make(chan T): チャネルを作成 makeは、これらの型の基底となるデータ構造をメモリ上に割り当て、初期化します。
  6. コンパイラ内部のASTノード (AST Nodes in Compiler): コンパイラはソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を構築します。ASTはプログラムの構造を木構造で表現したものです。src/cmd/gc/export.cのようなコンパイラ内部のコードでは、ASTの各ノードが特定の操作やデータ構造を表します。

    • Node *n: ASTのノードを指すポインタ。
    • n->type: ノードに関連付けられた型情報。
    • n->sym: ノードに関連付けられたシンボル情報。
    • ODOTTYPE2, OSTRUCTLIT, OPTRLIT, OMAKEMAP, OMAKESLICE, OMAKECHAN: これらはASTノードの種類を表す定数です。
      • ODOTTYPE2: 型アサーションや型変換に関連するノード。
      • OSTRUCTLIT: 構造体リテラル。
      • OPTRLIT: ポインタリテラル。
      • OMAKEMAP: make関数によるマップ作成。
      • OMAKESLICE: make関数によるスライス作成。
      • OMAKECHAN: make関数によるチャネル作成。

技術的詳細

このバグは、Goコンパイラのexport.cファイル内のreexportdep関数に起因していました。この関数は、エクスポートデータに含めるべき依存関係(特に型情報)を再エクスポートする役割を担っています。

問題の核心は、reexportdep関数が、make関数によって作成されるマップ、スライス、チャネルの型引数(例えばmake(myMap)におけるmyMap)を、それが未エクスポートの型であった場合に、エクスポートリストに追加するのを忘れていた点にあります。

reexportdep関数は、ASTノードを走査し、そのノードが参照する型がエクスポートされるべきかどうかを判断します。既存のコードでは、ODOTTYPE2(型アサーション/変換)、OSTRUCTLIT(構造体リテラル)、OPTRLIT(ポインタリテラル)といったノードタイプについては、そのノードが参照する型が未エクスポートであり、かつその型がエクスポートデータに必要であると判断された場合に、exportlistに追加するロジックがありました。

しかし、OMAKEMAPOMAKESLICEOMAKECHANといったmake関数に関連するノードタイプがこのチェックの対象外でした。そのため、例えば以下のようなコードがあった場合:

// package a
package a

type myMap map[string]bool // 未エクスポートの型

func GetMap() interface{} { // エクスポートされた関数
    return make(myMap)
}

// package b
package b

import "a"

func main() {
    _ = a.GetMap() // ここでコンパイルエラーが発生する可能性があった
}

package aがコンパイルされる際、GetMap関数はエクスポートされますが、その戻り値の型interface{}の裏にあるmyMap型がmake(myMap)によって作成されているにもかかわらず、myMapの定義がエクスポートデータに含まれませんでした。package bpackage aをインポートし、a.GetMap()を呼び出す際に、コンパイラはmyMapの型情報を見つけられず、コンパイルエラーを引き起こしていました。特に、GetMapがインライン化されるような状況では、この問題が顕著になりました。

このコミットは、reexportdep関数内のswitch文にOMAKEMAPOMAKESLICEOMAKECHANのケースを追加することで、この問題を解決しています。これにより、これらのmake関連ノードが参照する型も、必要に応じてエクスポートリストに追加されるようになります。

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

主要な変更は src/cmd/gc/export.c ファイルにあります。

--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -165,12 +165,15 @@ reexportdep(Node *n)
 	case ODOTTYPE2:
 	case OSTRUCTLIT:
 	case OPTRLIT:
+	case OMAKEMAP:
+	case OMAKESLICE:
+	case OMAKECHAN:
 	\tt = n->type;\
 	\tif(!t->sym && t->type)\
 	\t\tt = t->type;\
 	\tif(t && t->sym && t->sym->def && !exportedsym(t->sym)) {\
 	\t\tif(debug[\'E\'])\
-\t\t\t\tprint(\"reexport type for convnop %S\\n\", t->sym);\
+\t\t\t\tprint(\"reexport type for expression %S\\n\", t->sym);\
 	\t\texportlist = list(exportlist, t->sym->def);\
 	\t}\
 	\tbreak;

コアとなるコードの解説

変更されたコードは、reexportdep関数内のswitch文の一部です。

  1. case OMAKEMAP: case OMAKESLICE: case OMAKECHAN: の追加: 以前は、ODOTTYPE2(型アサーション/変換)、OSTRUCTLIT(構造体リテラル)、OPTRLIT(ポインタリテラル)といったASTノードタイプのみが、そのノードが参照する型をエクスポートリストに追加する対象となっていました。このコミットでは、make関数によって生成されるマップ、スライス、チャネルを表すASTノードタイプ(OMAKEMAP, OMAKESLICE, OMAKECHAN)がこのswitch文のケースに追加されました。これにより、これらのノードも以下の型エクスポートロジックの対象となります。

  2. 型情報の取得とチェック:

    t = n->type;
    if(!t->sym && t->type)
        t = t->type;
    
    • t = n->type;: 現在のASTノードnに関連付けられた型情報をtに代入します。
    • if(!t->sym && t->type) t = t->type;: これは、型tがシンボルを持たないが、その基底型(t->type)が存在する場合に、基底型をtとして使用するためのロジックです。例えば、匿名型や、型エイリアスが解決された後の基底型などを適切に扱うためと考えられます。
  3. エクスポートの必要性の判断と追加:

    if(t && t->sym && t->sym->def && !exportedsym(t->sym)) {
        if(debug['E'])
            print("reexport type for expression %S\n", t->sym);
        exportlist = list(exportlist, t->sym->def);
    }
    

    このif文が、型をエクスポートリストに追加するかどうかの主要な判断ロジックです。

    • t && t->sym && t->sym->def: 型tが存在し、その型がシンボルを持ち(t->sym)、さらにそのシンボルが定義を持っている(t->sym->def)ことを確認します。これは、有効な型定義が存在することを示します。
    • !exportedsym(t->sym): ここが最も重要です。exportedsym(t->sym)は、シンボルt->symがエクスポートされている(つまり、公開されている)かどうかをチェックする関数です。この条件が!(否定)されているため、未エクスポートのシンボルである場合にこのブロックが実行されます。
    • print("reexport type for expression %S\n", t->sym);: デバッグモード(debug['E']が有効な場合)での出力です。以前はconvnop(型変換操作)に関するメッセージでしたが、より一般的なexpression(式)に関するメッセージに変更され、この修正がより広範なケースに対応することを示唆しています。
    • exportlist = list(exportlist, t->sym->def);: 上記の条件(有効な未エクスポートの型定義)を満たす場合、その型の定義(t->sym->def)がexportlistに追加されます。exportlistは、最終的にパッケージのエクスポートデータに含められるべきシンボルのリストです。

この変更により、make関数が未エクスポートの型を引数として使用している場合でも、その未エクスポートの型がエクスポートされたコンテキスト(例: エクスポートされた関数の戻り値)で必要とされる場合に、コンパイラがその型定義をエクスポートデータに含めるようになります。これにより、他のパッケージがそのエクスポートされた関数を正しく利用できるようになり、特にインライン化の際に型情報が見つからないという問題が解消されます。

関連リンク

  • Go Issue #5470: https://go.dev/issue/5470
  • Go Change List (CL) 9303050: https://golang.org/cl/9303050 (これはコミットメッセージに記載されている古いURL形式ですが、現在のGoのコードレビューシステムではgo.dev/cl/9303050のような形式にリダイレクトされます。)

参考にした情報源リンク

  • Go言語の公式ドキュメント(パッケージ、エクスポート/未エクスポートの概念)
  • Goコンパイラのソースコード(src/cmd/gc/ディレクトリ内のファイル構造と関数名から推測される役割)
  • Go言語のmake組み込み関数に関するドキュメント
  • Go言語のインライン化に関する一般的な情報(コンパイラ最適化の文脈で)
  • Go Issue Tracker (go.dev/issue) の関連する議論
  • Go Code Review (go.dev/cl) の関連する変更履歴