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

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

このコミットは、Goコンパイラ(gc)におけるエクスポートデータ内の暗黙的な型に関するバグ修正を目的としています。具体的には、複合リテラル(&T{...}T{...})の処理において、コンパイラが内部的に使用する「暗黙的」フラグの扱いを修正し、その情報がエクスポートデータに誤って含まれることで発生していた問題を解決します。

コミット

commit 5c52404aca4f4c2800031ce60624c76b7058b245
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 9 00:26:08 2012 -0500

    gc: implicit type bug fix in export data
    
    TBR=lvd
    CC=golang-dev
    https://golang.org/cl/5644064

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

https://github.com/golang/go/commit/5c52404aca4f4c2800031ce60624c76b7058b245

元コミット内容

gc: implicit type bug fix in export data

TBR=lvd
CC=golang-dev
https://golang.org/cl/5644064

変更の背景

Go言語のコンパイラ(gc)は、ソースコードを解析し、抽象構文木(AST)を構築し、型チェックを行い、最終的に実行可能なバイナリを生成します。このプロセスの中で、コンパイラはパッケージ間で型情報を共有するために「エクスポートデータ」を生成します。

このコミットが行われた当時、Go言語には複合リテラル(Composite Literal)という構文がありました。これは、構造体、配列、スライス、マップなどの複合型を簡潔に初期化するためのものです。例えば、&T{...}という構文は、型Tの構造体のインスタンスを生成し、そのポインタを返すものです。

問題は、コンパイラが内部的にASTノードに付与するimplicitというフラグの扱いにありました。このフラグは、ソースコードには明示的に書かれていないが、コンパイラが内部的に生成したノード(例えば、&T{...}が内部的に(*T){...}と解釈される際のポインタ逆参照ノード)を示すために使われていました。

バグの背景には、このimplicitフラグがエクスポートデータに誤って含まれてしまうことがありました。エクスポートデータは、他のパッケージが現在のパッケージの公開された要素(型、関数など)を利用する際に参照する情報です。もし、内部的な実装詳細であるimplicitフラグがエクスポートデータに含まれてしまうと、他のパッケージがそのエクスポートデータを読み込んだ際に、予期せぬ型情報として解釈され、コンパイルエラーや不正な動作を引き起こす可能性がありました。

特に、&T{...}のような複合リテラルが関連していました。この構文は、内部的にはポインタの逆参照(OIND)と複合リテラル(OCOMPLIT)の組み合わせとして表現されます。この際に生成されるOINDノードにimplicitフラグが設定されていましたが、このフラグがエクスポートデータに漏れ出すことで、他のパッケージがこの型をインポートした際に問題が生じていたと考えられます。

このコミットは、このimplicitフラグの定義を簡素化し、その伝播をより厳密に制御することで、エクスポートデータに不要な内部情報が漏洩するのを防ぎ、コンパイラの堅牢性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラ(gc)の内部構造と概念に関する知識が必要です。

  1. Goコンパイラ (gc) の構造:

    • gcはGo言語の公式コンパイラであり、C言語で書かれています(当時は)。
    • コンパイラは、字句解析、構文解析、型チェック、最適化、コード生成といった複数のフェーズで構成されます。
    • src/cmd/gc/ディレクトリには、コンパイラの主要なソースコードが含まれています。
  2. 抽象構文木 (AST):

    • コンパイラはソースコードを解析して、プログラムの構造を木構造で表現したASTを構築します。
    • ASTの各ノードは、変数、式、文、型などを表します。
    • Node構造体(src/cmd/gc/go.hで定義)は、ASTのノードを表す基本的なデータ構造です。
  3. implicit フラグ:

    • Node構造体にはimplicitというフィールドがあります。これは、コンパイラがソースコードに明示的に書かれていないが、内部的な変換や最適化の過程で生成したASTノードをマークするために使用されます。
    • 例えば、&T{...}という構文は、ユーザーが明示的にポインタ逆参照を記述していませんが、コンパイラは内部的にポインタを生成し、そのポインタを逆参照して複合リテラルにアクセスするようなASTを構築します。この際に生成されるポインタ逆参照ノードにimplicitフラグが設定されることがあります。
    • このコミット以前は、implicitExplicit, Implicit, ImplPtrというenumで表現されていました。
      • Explicit: 明示的に書かれたノード。
      • Implicit: コンパイラが生成したノードで、出力時には表示しない。
      • ImplPtr: &T{...}のような複合リテラルで、ポインタ逆参照が暗黙的に追加されたノード。
    • このコミットでは、このenumが削除され、implicitが単なるuchar(0または1)として扱われるようになります。これは、implicitのセマンティクスを「出力時に表示しない」という単純なブール値に統一する変更です。
  4. 複合リテラル (Composite Literal):

    • Go言語の構文の一つで、構造体、配列、スライス、マップなどの複合型を初期化するために使われます。
    • 例: MyStruct{Field: value}, []int{1, 2, 3}, map[string]int{"a": 1}
    • &T{...}: 構造体Tのインスタンスを生成し、そのポインタを返す。
  5. エクスポートデータ (Export Data):

    • Goコンパイラは、コンパイルされたパッケージの公開された(エクスポートされた)型、関数、変数などの情報を、他のパッケージがインポートして利用できるように、特別な形式で出力します。これがエクスポートデータです。
    • エクスポートデータは、コンパイル済みパッケージの.aファイル(アーカイブファイル)や、Goモジュールキャッシュ内のファイルに格納されます。
    • 他のパッケージがimport "mypackage"とすると、コンパイラはこのエクスポートデータを読み込み、mypackageの公開された要素の型情報を取得します。
  6. Yacc/Bison:

    • src/cmd/gc/go.yは、Go言語の構文を定義するYacc(またはBison)の文法ファイルです。
    • Yacc/Bisonは、文法定義ファイル(.y)からC言語のパーサーコード(y.tab.cy.tab.h)を生成するツールです。
    • y.tab.cは生成されたパーサーの実装、y.tab.hはトークン定義やパーサーのインターフェースを含むヘッダーファイルです。
  7. fmt.c:

    • コンパイラのデバッグ出力や内部表現のフォーマットを担当するファイル。ASTノードを文字列として表現するロジックが含まれます。
  8. typecheck.c:

    • コンパイラの型チェックフェーズを担当するファイル。ASTノードの型を推論し、型の一貫性を検証します。

技術的詳細

このコミットの技術的詳細は、主にimplicitフラグのセマンティクスの変更と、それに伴うコンパイラ内部の処理の調整にあります。

  1. implicitフラグの簡素化 (go.hの変更):

    • 以前はenumExplicit, Implicit, ImplPtrの3つの状態を持っていたimplicitフラグが、単なるuchar(符号なし文字型、実質的にブール値)に変更されました。
    • これにより、implicitは「このノードはコンパイラによって暗黙的に生成されたものであり、通常は出力に表示すべきではない」という単純な意味を持つようになりました。ImplPtrのような特定の暗黙的生成のケースを区別する必要がなくなったことを示唆しています。
  2. fmt.cにおけるexprfmtの変更:

    • exprfmt関数は、ASTノードを文字列としてフォーマットする役割を担っています。
    • 変更前: while(n && n->implicit)
    • 変更後: while(n && n->implicit && (n->op == OIND || n->op == OADDR))
      • これは、暗黙的なノードをスキップする条件をより厳密にしたものです。以前はimplicitフラグが立っていれば無条件にスキップしていましたが、変更後はOIND(ポインタ逆参照)またはOADDR(アドレス取得)の操作である場合にのみスキップするようになりました。これにより、他の種類の暗黙的なノードが誤ってスキップされることを防ぎます。
    • OPTRLIT(ポインタリテラル)、OSTRUCTLIT(構造体リテラル)、OMAPLIT(マップリテラル)のケースでも、n->left->right->implicit == Implicitn->right->implicit == Implicitといったenum値の比較が、単にn->left->implicitn->implicitといったブール値のチェックに変わっています。これはgo.hの変更と整合しています。
  3. go.yにおけるパーサーの変更:

    • &T{...}構文の処理において、以前は$$->right->implicit = ImplPtr;と設定されていた箇所が、$$->right->implicit = 1;に変更されました。
    • これは、ImplPtrという特定のenum値が廃止されたことに伴う変更であり、implicitが単なるブール値として扱われるようになったことを反映しています。
  4. typecheck.cにおける型チェックの変更:

    • pushtype関数では、n->right->implicit = 1;に加えてn->implicit = 1;が設定されるようになりました。これは、複合リテラルの型が暗黙的に生成される場合に、そのノード自体も暗黙的であるとマークすることで、エクスポートデータに不要な情報が漏洩するのを防ぐ意図があると考えられます。
    • typecheckcomplit関数では、ポインタ型が複合リテラル型として許可されないというチェックにおいて、n->right->implicit == Explicitという条件が!n->right->implicitに変更されました。これは、「明示的でない(つまり暗黙的である)場合にのみ許可する」というロジックを、implicitがブール値になった新しいセマンティクスに合わせて調整したものです。
  5. y.tab.cy.tab.hの更新:

    • これらのファイルはYacc/Bisonによって自動生成されるため、go.ygo.hの変更に伴い、再生成されました。
    • 特にy.tab.cの差分が大きいのは、Bisonのバージョンが2.3から2.4.1に更新されたことによるボイラープレートコードの変更と、go.yの文法定義の変更が反映されたためです。
  6. テストケースの追加 (test/fixedbugs/bug392.dirbug392.go):

    • このコミットには、bug392.goという新しいテストケースが追加されています。このテストは、修正されたバグを再現し、修正が正しく機能することを確認するために書かれました。
    • bug392.goの内容は、おそらく複合リテラルと型のエクスポートに関連する特定のコードパターンを含んでおり、以前のコンパイラでは問題を引き起こしていたものです。

全体として、このコミットはGoコンパイラの内部的な型表現のクリーンアップと、エクスポートデータ生成の堅牢性向上に貢献しています。implicitフラグのセマンティクスをよりシンプルにすることで、コンパイラのコードベースの理解と保守が容易になり、将来的なバグの発生を防ぐ効果も期待できます。

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

このコミットのコアとなる変更は、src/cmd/gc/go.hにおけるimplicitフラグの定義の変更と、それに伴うsrc/cmd/gc/fmt.csrc/cmd/gc/go.ysrc/cmd/gc/typecheck.cでのimplicitフラグの利用箇所の修正です。

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -215,13 +215,6 @@ enum
 	EscNever,
 };
 
-enum
-{
-	Explicit,
-	Implicit,  // don't print in output
-	ImplPtr,   // OIND added by &T{ ... } literal
-};
-
 struct	Node
 {
 	// Tree structure.
@@ -257,7 +250,7 @@ struct	Node
 	uchar	used;
 	uchar	isddd;
 	uchar	readonly;
-	uchar	implicit;	// Explicit, Implicit, ImplPtr. 
+	uchar	implicit;
 	uchar	addrtaken;	// address taken, even if not moved to heap
 	uchar	dupok;	// duplicate definitions ok (for func)
 

src/cmd/gc/fmt.c

--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1058,7 +1058,7 @@ exprfmt(Fmt *f, Node *n, int prec)
 	NodeList *l;
 	Type *t;
 
-	while(n && n->implicit)
+	while(n && n->implicit && (n->op == OIND || n->op == OADDR))
 		n = n->left;
 
 	if(n == N)
@@ -1160,13 +1160,13 @@ exprfmt(Fmt *f, Node *n, int prec)
 		return fmtprint(f, "%N{ %,H }", n->right, n->list);
 
 	case OPTRLIT:
-		if (fmtmode == FExp && n->left->right->implicit == Implicit) 
+		if(fmtmode == FExp && n->left->implicit)
 			return fmtprint(f, "%N", n->left);
 		return fmtprint(f, "&%N", n->left);
 
 	case OSTRUCTLIT:
-		if (fmtmode == FExp) {   // requires special handling of field names
-			if(n->right->implicit == Implicit)
+		if(fmtmode == FExp) {   // requires special handling of field names
+			if(n->implicit)
 				fmtstrcpy(f, "{");
 			else 
 				fmtprint(f, "%T{", n->type);
@@ -1194,6 +1194,8 @@ exprfmt(Fmt *f, Node *n, int prec)
 	case OMAPLIT:
 		if(fmtmode == FErr)
 			return fmtprint(f, "%T literal", n->type);
+		if(fmtmode == FExp && n->implicit)
+			return fmtprint(f, "{ %,H }", n->list);
 		return fmtprint(f, "%T{ %,H }", n->type, n->list);
 
 	case OKEY:

src/cmd/gc/go.y

--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -808,7 +808,7 @@ uexpr:
 			// Special case for &T{...}: turn into (*T){...}.
 			$$ = $2;
 			$$->right = nod(OIND, $$->right, N);
-			$$->right->implicit = ImplPtr;
+			$$->right->implicit = 1;
 		} else {
 			$$ = nod(OADDR, $2, N);
 		}

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2008,7 +2008,8 @@ pushtype(Node *n, Type *t)
 	
 	if(n->right == N) {
 		n->right = typenod(t);
-		n->right->implicit = 1;
+		n->implicit = 1;  // don't print
+		n->right->implicit = 1;  // * is okay
 	}
 	else if(debug['s']) {
 		typecheck(&n->right, Etype);
@@ -2048,8 +2049,8 @@ typecheckcomplit(Node **np)
 	
 	if(isptr[t->etype]) {
 		// For better or worse, we don't allow pointers as the composite literal type,
-		// except when using the &T syntax, which sets implicit to ImplPtr.
-		if(n->right->implicit == Explicit) {
+		// except when using the &T syntax, which sets implicit on the OIND.
+		if(!n->right->implicit) {
 			yyerror("invalid pointer type %T for composite literal (use &%T instead)", t, t->type);
 			goto error;
 		}

コアとなるコードの解説

上記の変更は、GoコンパイラのASTノードにおけるimplicitフラグの扱いを根本的に変更し、それに伴うパーサー、型チェッカー、およびフォーマッターの動作を調整しています。

  1. go.hの変更:

    • enum { Explicit, Implicit, ImplPtr }が削除され、Node構造体のimplicitフィールドが単なるuchar型になりました。
    • これは、implicitフラグがもはや複数の状態を持つ必要がなく、「暗黙的に生成されたノードであるか否か」という単純なブール値として機能することを意味します。これにより、コンパイラの内部ロジックが簡素化され、エクスポートデータに不要な詳細が漏洩するリスクが低減されます。
  2. fmt.cの変更:

    • exprfmt関数は、ASTノードを人間が読める形式に変換する際に、暗黙的なノードをスキップするロジックを含んでいます。
    • while(n && n->implicit)からwhile(n && n->implicit && (n->op == OIND || n->op == OADDR))への変更は、暗黙的なノードのスキップ条件をより厳密にしました。これにより、OIND(ポインタ逆参照)やOADDR(アドレス取得)といった特定の操作に関連する暗黙的なノードのみがスキップされるようになり、他の種類の暗黙的なノードが誤って出力されることを防ぎます。
    • OPTRLIT, OSTRUCTLIT, OMAPLITのケースでのimplicitチェックも、enum値との比較から単純なブール値チェックに変わっています。これはgo.hの変更に合わせたものです。特にOMAPLITでは、FExpモード(式をフォーマットするモード)でn->implicitが真の場合に{ %,H }という形式で出力する新しいロジックが追加されました。これは、マップリテラルが暗黙的に生成された場合に、その型情報を省略してより簡潔に表示するためのものです。
  3. go.yの変更:

    • &T{...}という複合リテラル構文は、Goコンパイラ内部では(*T){...}のようにポインタ逆参照ノード(OIND)が暗黙的に追加される形で表現されます。
    • 以前は、この暗黙的に追加されたOINDノードのimplicitフラグにImplPtrという特定のenum値が設定されていました。この変更により、ImplPtrが廃止されたため、単に1(真)が設定されるようになりました。これは、implicitがブール値として機能するという新しいセマンティクスに準拠しています。
  4. typecheck.cの変更:

    • pushtype関数は、型推論や型変換の際にASTノードに型情報を付与します。この関数内で、n->right->implicit = 1;に加えてn->implicit = 1;が設定されるようになりました。これは、複合リテラルの型が暗黙的に生成される場合、その型ノード自体も暗黙的であるとマークすることで、エクスポートデータに不要な内部表現が漏洩するのを防ぐための重要な変更です。
    • typecheckcomplit関数は、複合リテラルの型チェックを行います。ポインタ型が複合リテラル型として直接使用できないという制約(&T{...}形式でのみ許可)をチェックするロジックが変更されました。以前はn->right->implicit == Explicit(明示的なポインタ型の場合にエラー)でしたが、!n->right->implicit(暗黙的でないポインタ型の場合にエラー)に変更されました。これは、implicitがブール値になった新しいセマンティクスに合わせて、&T{...}のように暗黙的にポインタ逆参照が追加されるケースを正しく区別するための調整です。

これらの変更は、Goコンパイラの内部的な型表現の整合性を高め、特に複合リテラルとエクスポートデータ生成におけるバグを修正することを目的としています。implicitフラグのセマンティクスを簡素化し、その使用箇所を厳密にすることで、コンパイラの堅牢性と保守性が向上しました。

関連リンク

参考にした情報源リンク