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

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

このコミットは、Goコンパイラ(cmd/gc)における複合リテラル(composite literals)のエクスポートに関するバグを修正するものです。特に、複合リテラルが正しくエクスポートされないために発生していた回帰バグ(regression)を解消し、コンパイラがこれらの構造を正確に処理できるようにします。

コミット

commit b0bb6f8ceec158c21dd5ea782c10c02d1cf5b2ef
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Mar 4 16:42:03 2013 +0100

    cmd/gc: unbreak exporting of composite literals.
    
    Fixes #4932.
    
    R=golang-dev, dave, rsc
    CC=golang-dev
    https://golang.org/cl/7437043

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

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

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)において、複合リテラルのエクスポートが正しく行われない問題を修正します。具体的には、Go言語のIssue #4932で報告されたバグを修正するものです。

変更の背景

Go言語では、構造体、配列、スライス、マップなどの複合型を簡潔に初期化するために「複合リテラル」という構文が提供されています。例えば、Point{X: 1, Y: 2}[]int{1, 2, 3}のような記述です。

Goコンパイラ(cmd/gc)は、ソースコードを解析して抽象構文木(AST: Abstract Syntax Tree)を構築し、それを中間表現に変換し、最終的に実行可能なバイナリを生成します。この過程で、異なるパッケージ間で型情報やシンボルを共有するために「エクスポート」という処理が行われます。これは、コンパイルされたパッケージのメタデータとして、そのパッケージが外部に公開する型や関数などの情報を含めることを指します。

Issue #4932は、「複合リテラルのエクスポートにおける回帰(regression)」として報告されました。これは、以前は正しく機能していた複合リテラルのエクスポート処理が、何らかの変更によって壊れてしまったことを意味します。このバグにより、複合リテラルを含む型や、複合リテラル自体が他のパッケージから参照される際に、コンパイラが正しくその構造を認識できず、コンパイルエラーや不正なコード生成を引き起こす可能性がありました。

特に問題となっていたのは、ASTノードのorigフィールドの扱いです。origフィールドは、コンパイラの最適化や変換によってASTノードが変更された場合でも、そのノードの元の(変換前の)状態を指し示すために使用されます。複合リテラルの処理において、このorigフィールドが正しく設定・維持されていなかったため、コンパイラの他の部分(特に型チェック後やコード生成時)が、複合リテラルの本来の構造を誤って解釈してしまうことが原因でした。

前提知識の解説

このコミットの理解には、以下のGoコンパイラの内部構造に関する知識が役立ちます。

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラの一つで、Goソースコードを機械語に変換します。
  • 抽象構文木 (AST): ソースコードの構造を木構造で表現したものです。コンパイラはソースコードを字句解析・構文解析してASTを生成します。
  • Node構造体: Goコンパイラ内部でASTの各ノードを表す基本的なデータ構造です。Nodeには、そのノードの種類(op)、左の子ノード(left)、右の子ノード(right)、型情報(type)、シンボル情報(sym)など、様々な情報が含まれます。
  • Node.origフィールド: Node構造体に含まれる重要なフィールドで、そのノードがコンパイラの変換プロセスによって変更された場合でも、元のASTノードへの参照を保持します。これにより、コンパイラの異なるフェーズで、ノードの元の意味論的な情報を参照することが可能になります。例えば、型チェック中にノードが変換されても、origを通じて元のソースコード上の表現にアクセスできます。
  • 複合リテラル (Composite Literals): Go言語で構造体、配列、スライス、マップなどの複合型の値を生成するための構文です。例: MyStruct{Field: value}[]int{1, 2, 3}
  • 型チェック (Type Checking): コンパイラのフェーズの一つで、ASTがGo言語の型システム規則に準拠しているかを確認します。型の不一致や不正な操作を検出します。
  • エクスポート (Exporting): コンパイラが、コンパイル対象のパッケージが外部に公開する型、関数、変数などの情報を、他のパッケージが利用できるようにメタデータとして生成するプロセスです。このメタデータは、通常、コンパイル済みパッケージの.aファイル(アーカイブファイル)などに含まれます。

技術的詳細

このコミットの技術的な核心は、GoコンパイラがASTノードのorigフィールドをどのように管理し、特に複合リテラルと型変換の際にその整合性をどのように保つかという点にあります。

Goコンパイラは、ソースコードをASTに変換した後、様々な最適化や変換を行います。例えば、型チェックの過程で、特定のASTノードがより具体的な内部表現に変換されることがあります。このとき、元のソースコード上の意味を失わないように、変換後のノードが元のノードを指し示すorigフィールドが重要になります。

このバグは、複合リテラル、特にポインタ型を含む複合リテラルや、ネストされた複合リテラルが型チェックされる際に、origフィールドが正しく設定されない、または途中で失われることが原因でした。これにより、コンパイラのバックエンドや、エクスポート処理が、複合リテラルの本来の構造を誤解し、不正なコードを生成したり、エクスポート情報が欠落したりしていました。

修正の主なポイントは以下の通りです。

  1. saveorignode関数の導入と利用:

    • src/cmd/gc/go.hvoid saveorignode(Node *n);という新しい関数プロトタイプが追加されました。
    • src/cmd/gc/subr.cにこのsaveorignode関数の実装が追加されました。この関数は、引数として渡されたNode *norigフィールドがまだ設定されていない場合、nの現在の状態をコピーして新しいノードを作成し、その新しいノードをn->origに設定します。これにより、ノードが変換される前にその元の状態を確実に保存できます。
    • src/cmd/gc/typecheck.cOCONV(型変換)のケースで、以前は手動で行っていたノードのコピーとorigの設定処理が、新しく導入されたsaveorignode(n);の呼び出しに置き換えられました。これにより、型変換が行われるノードの元の状態が確実に保存されるようになりました。
  2. 複合リテラル型チェック時のorigフィールドの厳密な管理:

    • src/cmd/gc/typecheck.ctypecheckcomplit関数(複合リテラルの型チェックを行う関数)において、複合リテラルノード(n)の型チェックを開始する前に、そのノードの元の状態をnorigという一時的なノードにコピーする処理が追加されました(*norig = *n;)。
    • 型チェックの様々な変換処理の後、関数の最後にn->orig = norig;という行が追加されました。これにより、複合リテラルノードのorigフィールドが、型チェックによる変換が行われる前の元の状態を指すように保証されます。これは、特に複合リテラルがポインタ型に変換される場合や、内部的に配列リテラルに変換される場合など、ノードの構造が大きく変わる可能性がある場合に重要です。
  3. assignconvにおけるorigの伝播の修正:

    • src/cmd/gc/subr.cassignconv関数(代入変換を行う関数)において、変換後のノードrorigフィールドが、変換前のノードnorigフィールドを指すように変更されました(r->orig = n->orig;)。以前はr->orig = n;となっており、これはn自体を指していましたが、nもまた変換されたノードである可能性があるため、nの元のノード(n->orig)を指すように修正することで、より正確な元のASTへの参照を維持できるようになりました。
  4. fmt.cにおける複合リテラルの表示の調整:

    • src/cmd/gc/fmt.cは、コンパイラ内部でASTノードを文字列としてフォーマットする役割を担っています。exprfmt関数内のOCOMPLIT(複合リテラル)およびOPTRLIT(ポインタリテラル)のケースで、origフィールドが正しく設定されるようになったことに合わせて、表示ロジックが調整されました。特に、エラーメッセージの表示や、&T{...}のようなポインタ複合リテラルの表示が改善されました。

これらの変更により、Goコンパイラは複合リテラルのASTノードの元の構造をより正確に追跡できるようになり、エクスポート処理やその他のコンパイラフェーズが、これらのリテラルを正しく解釈・処理できるようになりました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/cmd/gc/fmt.c:

    • exprfmt関数内で、OCOMPLITおよびOPTRLITの処理ロジックが修正され、特にポインタ複合リテラルの表示が改善されました。ptrlit変数が導入され、ポインタリテラルの検出と表示がより正確になりました。
    • OKEYの処理も、フィールド名を含むキーの表示に対応するために修正されました。
  • src/cmd/gc/go.h:

    • saveorignode(Node *n);関数のプロトタイプが追加されました。
  • src/cmd/gc/subr.c:

    • 新しくsaveorignode関数が実装されました。この関数は、ノードのorigフィールドが未設定の場合に、ノードのコピーを作成してorigに設定します。
    • assignconv関数内で、変換後のノードのorigフィールドが、変換前のノードのorigフィールドを指すように修正されました(r->orig = n->orig;)。
  • src/cmd/gc/typecheck.c:

    • OCONVのケースで、手動のノードコピー処理がsaveorignode(n);の呼び出しに置き換えられました。
    • typecheckcomplit関数内で、複合リテラルの型チェックの前後でorigノードを保存・復元するロジックが追加されました。具体的には、型チェック前にnorigにノードをコピーし、型チェック後にn->orig = norig;を設定することで、元のノード構造を保持します。
    • 不要になったOARRAYLITおよびOCOMPLITに関するimplicitフラグの復元ロジックが削除されました。
  • test/fixedbugs/issue4932.dir/ および test/fixedbugs/issue4932.go:

    • このバグを再現し、修正を検証するための新しいテストケースが追加されました。特にstate.go内のrun([]foo.Op{{}})のようなネストされた複合リテラルが、この問題の具体的なトリガーとなっていました。

コアとなるコードの解説

src/cmd/gc/subr.csaveorignode 関数

+void
+saveorignode(Node *n)
+{
+	Node *norig;
+
+	if(n->orig != N)
+		return;
+	norig = nod(n->op, N, N);
+	*norig = *n;
+	n->orig = norig;
+}

この関数は、ASTノードnorigフィールドがまだ設定されていない場合に、nの現在の状態を完全にコピーした新しいノードnorigを作成し、そのnorignorigフィールドに設定します。これにより、nが後続のコンパイラフェーズで変換されたとしても、その元の状態をn->origを通じて参照できるようになります。

src/cmd/gc/typecheck.ctypecheckcomplit 関数における orig の管理

 // typecheckcomplit 関数内
 // ...
 	setlineno(n->right);
 	l = typecheck(&n->right /* sic */, Etype|Ecomplit);
 	if((t = l->type) == T)
 		goto error;
 	nerr = nerrors;
 	n->type = t;
+
+	// Save original node (including n->right)
+	norig = nod(n->op, N, N);
+	*norig = *n;
+
 // ... (中略:複合リテラルの要素の型チェックと変換)
 // ...
+	n->orig = norig; // 型チェック後のノードに元のノードを設定
 	if(isptr[n->type->etype]) {
 		n = nod(OPTRLIT, n, N);
 		n->typecheck = 1;
 		n->left->typecheck = 1;
 	}
 
+	n->orig = norig; // ポインタリテラルに変換された場合も元のノードを設定
 	*np = n;
 	lineno = lno;
 	return;

typecheckcomplit関数は、複合リテラルの型チェックと変換を行います。この修正では、型チェックの開始直後にnorig = nod(n->op, N, N); *norig = *n;という行が追加され、複合リテラルノードnの元の状態がnorigに保存されます。そして、関数の最後でn->orig = norig;が設定されます。これにより、複合リテラルが型チェックの過程でどのように変換されたとしても、その最終的なASTノードのorigフィールドは、型チェック前の元の複合リテラルノードを指すことが保証されます。これは、特に複合リテラルがポインタリテラル(&T{...})に変換される場合など、ノードの構造が大きく変わる場合に重要です。

src/cmd/gc/subr.cassignconv 関数における orig の伝播

 // assignconv 関数内
 // ...
 	r->type = t;
 	r->typecheck = 1;
 	r->implicit = 1;
-	r->orig = n; // 変更前
+	r->orig = n->orig; // 変更後
 	return r;
 }

assignconv関数は、代入時の型変換を処理します。この修正では、変換後のノードrorigフィールドが、変換前のノードnorigフィールドを指すように変更されました。これにより、変換されたノードがさらに別の変換を受ける場合でも、ASTの元の意味論的な情報への参照が正しく伝播されるようになります。

関連リンク

参考にした情報源リンク