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

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

このコミットは、Goコンパイラ(cmd/gc)における複合リテラル(&T{}形式)のエクスポート時のバグ修正に関するものです。具体的には、&T{}形式で記述された複合リテラルが誤ってエクスポートされ、その結果、インポート時に予期せぬエラーが発生するという問題に対処しています。

コミット

commit 9e66ee456210024ad05ba95f3b245cdc974aba43
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Feb 26 00:43:31 2013 +0100

    cmd/gc: fix corruption in export of &T{} literals.
    
    Composite literals using the &T{} form were incorrectly
    exported, leading to weird errors at import time.
    
    Fixes #4879.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7395054

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

https://github.com/golang/go/commit/9e66ee456210024ad05ba95f3b245cdc974aba43

元コミット内容

このコミットは、Goコンパイラのcmd/gcにおいて、&T{}形式の複合リテラルがエクスポートされる際に発生していたデータ破損(corruption)を修正します。この問題により、エクスポートされたコードを別のパッケージがインポートする際に、奇妙なエラーが発生していました。この修正は、GoのIssue #4879を解決するものです。

変更の背景

Go言語では、構造体、配列、スライス、マップなどの複合型を初期化する際に「複合リテラル(composite literal)」という構文を使用します。例えば、MyStruct{Field: value}のような形式です。また、この複合リテラルのアドレスを取得するために、&MyStruct{Field: value}のように&演算子を前置することがよくあります。これは、複合リテラルが一時的な値(unaddressable value)であるため、そのアドレスを直接取得することはできませんが、&を前置することで、コンパイラがそのリテラルをメモリ上に配置し、そのアドレスを返すようにします。

このコミットが修正する問題は、Goコンパイラのバックエンドの一つであるgc(Go Compiler)が、&T{}形式の複合リテラルをコンパイルし、その情報を他のパッケージが利用できるようにエクスポートする際に発生していました。具体的には、エクスポートされるメタデータに不整合が生じ、インポート側のコンパイラがその情報を正しく解釈できないために、コンパイルエラーや予期せぬランタイムエラーを引き起こしていました。

Issue #4879の報告によると、特にインライン化された関数内で&T{}形式の複合リテラルが使用された場合に問題が顕在化しやすかったようです。コンパイラがコードを最適化する過程で、この特定のリテラルの表現が正しく保持されず、エクスポート情報が破損していたと考えられます。

前提知識の解説

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラツールチェーンの一部であり、Goソースコードを機械語に変換する主要なコンポーネントです。gcは、フロントエンド(構文解析、型チェック)とバックエンド(コード生成、最適化)の両方の機能を含んでいます。
  • 複合リテラル (Composite Literals): Go言語で構造体、配列、スライス、マップなどの複合型の値を初期化するための構文です。例えば、Point{X: 1, Y: 2}(構造体)、[]int{1, 2, 3}(スライス)、map[string]int{"a": 1}(マップ)などがあります。
  • &演算子 (Address-of Operator): 変数や値のメモリアドレスを取得するために使用されます。&T{}のように複合リテラルの前に置かれる場合、コンパイラはそのリテラルをメモリ上に割り当て、そのアドレスを返します。これは、リテラルが通常は一時的な値であり、直接アドレスを持たないため、ポインタとして扱いたい場合に必要となります。
  • エクスポートとインポート (Export and Import): Go言語では、パッケージ間で型、関数、変数などを共有するためにエクスポート/インポートの仕組みがあります。エクスポートされた情報は、コンパイル時に生成されるオブジェクトファイル(.aファイルなど)に含まれ、他のパッケージがそのオブジェクトファイルをインポートすることで、エクスポートされた要素を利用できるようになります。このプロセスにおいて、コンパイラは内部的な表現(ASTなど)をエクスポート可能な形式に変換し、インポート側はその形式を読み込んで再構築します。
  • implicitフラグ: Goコンパイラの内部表現(ASTノード)には、そのノードがコード中で明示的に書かれたものか、コンパイラによって暗黙的に生成されたものかを示すimplicitフラグが存在します。このフラグは、コードの整形(go fmt)やデバッグ情報の生成、あるいはエクスポート時の挙動に影響を与えることがあります。

技術的詳細

このコミットの修正は、主にGoコンパイラのsrc/cmd/gc/fmt.csrc/cmd/gc/typecheck.cの2つのファイルに集中しています。

  1. src/cmd/gc/fmt.cの変更: このファイルは、コンパイラの内部表現(ASTノード)を文字列としてフォーマットする役割を担っています。特に、エクスポート時に使用されるFExpモードでのフォーマット処理が問題でした。

    • OPTRLIT (ポインタリテラル、つまり&T{}形式) の処理において、以前はFExpモードでn->left->implicitが真の場合に&を省略していましたが、この修正では&を常に明示的に出力するように変更されました。これにより、エクスポートされる情報がより正確になります。
    • OLIT (複合リテラル) の処理において、FExpモードでn->right->implicitが真の場合(つまり、&T{}形式で、T{}の部分が暗黙的に生成された場合)に、&%T{という形式で出力するように変更されました。これは、ポインタリテラルであることを明示的に示すための変更です。
    • 同様に、OLITの処理で、n->implicitn->right->implicitの両方が偽でない場合に})を閉じる条件が変更され、n->right->implicitが真の場合にも&%T{ %,H }という形式で出力されるようになりました。

    これらの変更は、&T{}形式の複合リテラルがエクスポートされる際に、そのポインタとしての性質が正しく表現されるように、内部的な文字列フォーマットのロジックを調整したものです。

  2. src/cmd/gc/typecheck.cの変更: このファイルは、Goソースコードの型チェックを行う役割を担っています。

    • typecheckcomplit関数内で、複合リテラルの型チェック後、特にポインタ型(isptr[n->type->etype]が真)の場合に、複合リテラルの右側のノード(n->right)のimplicitフラグを1(真)に設定する行が追加されました。
    • この変更は、&T{}形式の複合リテラルにおいて、T{}の部分がコンパイラによって暗黙的に生成されたものであることを明示的にマークすることで、fmt.cでのエクスポート処理が正しくその情報を利用できるようにするためのものです。implicitフラグの適切な設定は、エクスポートされるASTノードの構造が正確であることを保証するために重要です。

これらの修正により、&T{}形式の複合リテラルがコンパイラの内部で正しく表現され、エクスポートされる情報もその正確な表現を反映するようになり、インポート時のエラーが解消されました。

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

src/cmd/gc/fmt.c

--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1206,7 +1206,7 @@ exprfmt(Fmt *f, Node *n, int prec)\n 		return fmtprint(f, "(%N{ %,H })", n->right, n->list);\n 
 	case OPTRLIT:\n-		if(fmtmode == FExp && n->left->implicit)\n+		if(fmtmode == FExp)  // handle printing of '&' below.\n 			return fmtprint(f, "%N", n->left);\n 		return fmtprint(f, "&%N", n->left);\n 
@@ -1214,6 +1214,8 @@ exprfmt(Fmt *f, Node *n, int prec)\n 		if(fmtmode == FExp) {   // requires special handling of field names\n 			if(n->implicit)\n 				fmtstrcpy(f, "{");\n+			else if(n->right->implicit)\n+				fmtprint(f, "&%T{", n->type);\n 			else\n 				fmtprint(f, "(%T{", n->type);\n 			for(l=n->list; l; l=l->next) {\n@@ -1224,7 +1226,7 @@ exprfmt(Fmt *f, Node *n, int prec)\n 			else\n 				fmtstrcpy(f, " ");\n 			}\n-			if(!n->implicit)\n+			if(!n->implicit && !n->right->implicit)\n 				return fmtstrcpy(f, "})");\n 			return fmtstrcpy(f, "}");\n 		}\n@@ -1236,6 +1238,8 @@ exprfmt(Fmt *f, Node *n, int prec)\n 			return fmtprint(f, "%T literal", n->type);\n 		if(fmtmode == FExp && n->implicit)\n 			return fmtprint(f, "{ %,H }", n->list);\n+		if(fmtmode == FExp && n->right->implicit)\n+			return fmtprint(f, "&%T{ %,H }", n->type, n->list);\n 		return fmtprint(f, "(%T{ %,H })", n->type, n->list);\n 
 	case OKEY:

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2354,13 +2354,12 @@ typecheckcomplit(Node **np)\n 			yyerror("invalid pointer type %T for composite literal (use &%T instead)", t, t->type);\n 			goto error;\n 		}\n-\t\t
 \t\t// Also, the underlying type must be a struct, map, slice, or array.\n \t\tif(!iscomptype(t)) {\n \t\t\tyyerror("invalid pointer type %T for composite literal", t);\n \t\t\tgoto error;\n \t\t}\n-\t\tt = t->type;\t\t
+\t\tt = t->type;\n 	}\n 
 	switch(t->etype) {\n@@ -2414,6 +2413,9 @@ typecheckcomplit(Node **np)\n 		if(t->bound < 0)\n 			n->right = nodintconst(len);\n 		n->op = OARRAYLIT;\n+		// restore implicitness.\n+		if(isptr[n->type->etype])\n+			n->right->implicit = 1;\n 		break;\n 
 	case TMAP:

コアとなるコードの解説

src/cmd/gc/fmt.cの変更点

  • OPTRLIT (ポインタリテラル) の処理:
    • 変更前: if(fmtmode == FExp && n->left->implicit) の条件で、&を省略して%N(左側のノード)のみを出力していました。これは、コンパイラが暗黙的に生成したポインタリテラルの場合、エクスポート時に&を省略するという意図があったのかもしれません。
    • 変更後: if(fmtmode == FExp) の条件のみになり、コメントで「&の出力は後で処理する」と示されています。これにより、OPTRLITノード自体が&を伴うリテラルであることを明示的に表現するようになります。
  • OLIT (複合リテラル) の処理:
    • else if(n->right->implicit) の追加: fmtmode == FExp(エクスポートモード)かつ、複合リテラルの右側のノード(通常はリテラルの中身)がコンパイラによって暗黙的に生成されたものである場合(つまり、&T{}形式のT{}の部分)、&%T{という形式で出力するように変更されました。これは、この複合リテラルがポインタとして扱われるべきであることをエクスポート情報に含めるための重要な変更です。
    • if(!n->implicit && !n->right->implicit) の条件変更: 複合リテラルの閉じ括弧})の出力条件が変更されました。これにより、&T{}形式でn->right->implicitが真の場合でも、正しく}が閉じられるようになります。
    • if(fmtmode == FExp && n->right->implicit) の追加: fmtmode == FExpかつn->right->implicitが真の場合に、&%T{ %,H }という形式で出力するように変更されました。これは、&T{}形式の複合リテラル全体を正しくエクスポートするための最終的な調整です。

これらの変更は、&T{}形式の複合リテラルがコンパイラの内部表現からエクスポート形式に変換される際に、そのポインタとしての意味論が失われないようにするためのものです。特にimplicitフラグの状態に基づいて、&記号を適切に付加することで、インポート側が元のコードの意図を正しく解釈できるようにします。

src/cmd/gc/typecheck.cの変更点

  • typecheckcomplit関数内の変更:
    • if(isptr[n->type->etype]) n->right->implicit = 1; の追加:
      • typecheckcomplit関数は、複合リテラルの型チェックとASTノードの構築を行います。
      • この追加された行は、複合リテラルがポインタ型(&T{}形式)である場合(isptr[n->type->etype]が真)、その複合リテラルの右側のノード(n->right)のimplicitフラグを1(真)に設定します。
      • このn->rightは、&T{}におけるT{}の部分、つまり実際の複合リテラルの中身を表すASTノードです。
      • この変更の目的は、&T{}という構文において、T{}の部分がユーザーによって明示的に書かれたものではなく、&演算子によって暗黙的に参照される対象であることをコンパイラの内部でマークすることです。このimplicitフラグが正しく設定されることで、前述のfmt.cでのエクスポート処理が、このノードを&を伴うポインタリテラルとして正しくフォーマットできるようになります。

これらの変更は相互に関連しており、typecheck.cimplicitフラグを適切に設定することで、fmt.cがそのフラグを読み取り、エクスポート時に正しい形式(&を伴う)で複合リテラルを出力できるように連携しています。これにより、エクスポートされた情報が破損することなく、インポート側で正しく解釈されるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード
  • Go Issue Tracker
  • Go Code Review Comments (CL)
  • Go言語の複合リテラルに関する一般的な解説記事
  • Go言語のコンパイラ内部構造に関する技術ブログや論文