[インデックス 1076] ファイルの概要
このコミットは、Go言語の初期のコンパイラである6g(64ビットシステム向けGoコンパイラ)における初期化処理(ninit)に関するバグ修正を目的としています。具体的には、特定のテストケースで発生していた初期化順序の問題を解決し、抽象構文木(AST)のノードに付随する初期化文が正しく処理されるように改善しています。ただし、Rob Pikeが報告した「interface-smashing bug」は本コミットでは修正されていません。
コミット
commit 9906bfc7bb6758cb505db60452c015a90a516d8f
Author: Russ Cox <rsc@golang.org>
Date: Thu Nov 6 13:31:13 2008 -0800
6g ninit fixes - fixes the two test cases
i isolated last night. does not fix rob's
interface-smashing bug.
R=ken
OCL=18698
CL=18698
---
src/cmd/gc/go.h | 2 ++\
src/cmd/gc/go.y | 6 ++++++\
src/cmd/gc/subr.c | 11 +++++++----\
src/cmd/gc/walk.c | 6 +++++-\
4 files changed, 20 insertions(+), 5 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9906bfc7bb6758cb505db60452c015a90a516d8f
元コミット内容
6g ninit fixes - fixes the two test cases
i isolated last night. does not fix rob's
interface-smashing bug.
変更の背景
Go言語のコンパイラは、ソースコードを抽象構文木(AST)に変換し、そのASTを様々な最適化やコード生成のために「ウォーク」(走査)します。この過程で、コンパイラは一時変数の導入や、特定の操作(例えば、ドット演算子によるフィールドアクセスやメソッド呼び出し)の評価に必要な初期化文を生成することがあります。これらの初期化文は、ASTノードのninitフィールドにリストとして付随させられます。
本コミットの背景には、ninitリストの管理におけるバグが存在したことが挙げられます。具体的には、特定のコードパターン(コミットメッセージにある「two test cases」)において、初期化文が正しくノードにアタッチされなかったり、実行順序が期待通りにならなかったりする問題があったと考えられます。これは、コンパイラが生成するコードの正当性に直接影響するため、重要な修正でした。特に、adddot関数やwalkdot関数のような、複雑な式を処理する部分で問題が発生していたようです。
また、コミットメッセージには「does not fix rob's interface-smashing bug」と明記されており、このコミットが特定の既知のバグ(インターフェース関連のバグ)とは無関係であることを示しています。これは、コンパイラの開発において、複数の独立したバグが同時に存在し、それぞれが異なる修正を必要とする状況を反映しています。
前提知識の解説
本コミットの理解には、以下の知識が役立ちます。
- Goコンパイラ(初期の
gc): Go言語の初期のコンパイラは、主にC言語で書かれていました。6gは、x86-64アーキテクチャ向けのコンパイラを指します。コンパイラは、字句解析、構文解析、抽象構文木(AST)の構築、型チェック、最適化、コード生成といった段階を経て動作します。 - 抽象構文木(AST): ソースコードの構造を木構造で表現したものです。各ノードは、変数宣言、式、文などの言語構造に対応します。コンパイラはASTを走査(ウォーク)しながら、様々な処理を行います。
Node構造体とninitフィールド: Goコンパイラの内部では、ASTの各要素がNode構造体として表現されます。このNode構造体には、ninitというフィールドが存在します。ninitは、現在のノードが評価される前に実行されるべき初期化文のリスト(Nodeのリンクリスト)を保持します。例えば、コンパイラが一時変数を導入したり、複雑な式の評価に必要な副作用を伴う処理を生成したりする場合に、これらの処理がninitリストに追加されます。addtop変数: 本コミットで導入された、グローバルなNode*型の変数です。これは、ASTの走査中に一時的に初期化文を蓄積するためのバッファとして機能します。特定のノードの処理中に生成された初期化文は、まずaddtopに追加され、その後、適切なタイミングでそのノードのninitリストに結合されます。fatal関数: コンパイラ内部で、回復不能なエラーが発生した場合にプログラムを終了させるためのユーティリティ関数です。デバッグや予期せぬ状態の検出に用いられます。list関数: おそらく、Goコンパイラ内部でリンクリスト操作を行うためのユーティリティ関数で、新しいノードを既存のリストの先頭に追加する(prepend)機能を持つと考えられます。ODOT演算子: ASTにおいて、ドット演算子(.)を表す内部的なオペレータです。構造体のフィールドアクセスや、インターフェースのメソッド呼び出しなどに使用されます。OAS演算子: ASTにおいて、代入演算子を表す内部的なオペレータです。LCOLAS:go.y(Yacc文法ファイル)において、:=(短い変数宣言)演算子を表すトークン名と考えられます。- Yacc/Bison: 構文解析器ジェネレータ。
go.yファイルは、Go言語の文法規則を定義し、これを用いて構文解析器が生成されます。
技術的詳細
本コミットの技術的詳細は、ninitリストの管理とaddtop変数の導入に集約されます。
-
addtopの導入とグローバル化:src/cmd/gc/go.hにEXTERN Node* addtop;が追加されました。これにより、addtopはコンパイラの複数のCソースファイルからアクセス可能なグローバル変数となりました。以前はsrc/cmd/gc/walk.c内でstatic変数として宣言されており、そのスコープがファイル内に限定されていました。グローバル化することで、異なるコンパイルユニット間で初期化文の情報を共有し、より柔軟なninit管理が可能になります。
-
go.yにおけるaddtopのチェック:- 構文解析の段階で、
Bvardcl(変数宣言)とsimple_stmt(短い変数宣言:=を含む単純な文)のルールに、if(addtop != N) fatal(...)というチェックが追加されました。これは防御的なプログラミングであり、これらの構文要素を処理する際にaddtopが空(NはNULLまたは空のノードを意味する)であることを保証します。もしaddtopが空でなければ、それはコンパイラの内部状態に矛盾があることを示し、即座に致命的なエラーとして報告されます。これは、addtopが一時的なバッファであり、特定の処理ブロックの開始時には常にクリアされているべきであるという設計思想を反映しています。
- 構文解析の段階で、
-
subr.cのadddot関数におけるninitの結合ロジック:adddot関数は、ドット演算子(.)の処理を担当します。この関数内で、複数のreturn n;がgoto ret;に変更されました。これにより、関数の終了処理がret:ラベルに集約されます。ret:ラベルの直前に以下のコードが追加されました。
この変更が本コミットの核心部分の一つです。ret: n->ninit = list(addtop, n->ninit); addtop = N; return n;adddot関数内で生成された、またはaddtopに一時的に蓄積されていた初期化文(addtopリスト)が、現在のノードnの既存のninitリストの先頭に結合されます。これにより、adddotの処理中に発生した初期化が、そのノードの他の初期化よりも先に実行されることが保証されます。結合後、addtopはNにリセットされ、次の処理のためにクリアされます。
-
walk.cのwalkdot関数におけるninitの移動ロジック:walkdot関数は、ASTの走査中にドット演算子ノードを処理します。この関数に以下のコードが追加されました。
これは、addtop = list(addtop, n->ninit); n->ninit = N;walkdotがノードnの処理を開始する前に、既存のn->ninitリストを一時的にaddtopに移動させることを意味します。そして、n->ninitは空に設定されます。この操作により、walkdotの処理中に新たに生成される初期化文が、addtopを通じて適切に管理され、最終的にadddot関数などで元のn->ninitと結合される際に、正しい順序で配置されるようになります。これは、ASTの走査の異なる段階で生成される初期化文の順序を調整するための重要なメカニズムです。- また、
walk関数の終了時にもif(addtop != N) fatal("addtop in walk");というチェックが追加されました。これは、walk関数が終了する際にはaddtopが常にクリアされているべきであるという不変条件を強制します。
これらの変更は、コンパイラが複雑な式(特にドット演算子を含むもの)を処理する際に、初期化文の生成とASTノードへのアタッチをより正確かつ予測可能に行うためのものです。addtopというグローバルな一時バッファを導入し、それを厳密に管理することで、初期化文の順序に関するバグを修正しています。
コアとなるコードの変更箇所
src/cmd/gc/go.h
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -478,6 +478,8 @@ EXTERN ushort block; // current block number
EXTERN Node* retnil;
EXTERN Node* fskel;
+EXTERN Node* addtop;
+
EXTERN char* context;
EXTERN int thechar;
EXTERN char* thestring;
src/cmd/gc/go.y
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -303,6 +303,9 @@ Bvardcl:
}
| new_name_list_r type '=' expr_list
{
+\t\tif(addtop != N)
+\t\t\tfatal("new_name_list_r type '=' expr_list");
+\
\t$$ = rev($1);
\tdodclvar($$, $2);
@@ -423,6 +426,9 @@ simple_stmt:
}
|\texprsym3_list_r LCOLAS expr_list
{
+\t\tif(addtop != N)
+\t\t\tfatal("exprsym3_list_r LCOLAS expr_list");
+\
\t$$ = rev($1);
\t$$ = colas($$, $3);
\t$$ = nod(OAS, $$, $3);
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2417,20 +2417,20 @@ adddot(Node *n)
walktype(n->left, Erv);
t = n->left->type;
if(t == T)
-\t\treturn n;\n+\t\tgoto ret;\n
+\t\tgoto ret;\n
if(n->right->op != ONAME)
-\t\treturn n;\n+\t\tgoto ret;\n
+\t\tgoto ret;\n
\ts = n->right->sym;\
\tif(s == S)\
-\t\treturn n;\n+\t\tgoto ret;\n
+\t\tgoto ret;\n
for(d=0; d<nelem(dotlist); d++) {
c = adddot1(s, t, d);
if(c > 0)
goto out;
}
-\treturn n;\n+\t\tgoto ret;\n
+\t\tgoto ret;\n
out:
if(c > 1)
@@ -2441,6 +2441,9 @@ out:
\tn = nod(ODOT, n, n->right);
\tn->left->right = newname(dotlist[c].field->sym);\
}\
+ret:\n+\tn->ninit = list(addtop, n->ninit);\n+\taddtop = N;\n
+\tret:\n+\tn->ninit = list(addtop, n->ninit);\n+\taddtop = N;\n
return n;
}\
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -8,7 +8,6 @@ static Type* sw1(Node*, Type*);
static Type* sw2(Node*, Type*);
static Type* sw3(Node*, Type*);
static Node* curfn;
-static Node* addtop;
enum
{
@@ -65,6 +64,8 @@ walk(Node *fn)
if(curfn->type->outtuple)
\tif(walkret(curfn->nbody))
\t\tyyerror("function ends without a return statement");
+\tif(addtop != N)\n+\tfatal("addtop in walk");\n
+\tif(addtop != N)\n+\tfatal("addtop in walk");\n
walkstate(curfn->nbody);
if(debug['W']) {
\tsnprint(s, sizeof(s), "after %S", curfn->nname->sym);
@@ -1544,6 +1545,9 @@ walkdot(Node *n)
{
Type *t;
+\taddtop = list(addtop, n->ninit);\n+\tn->ninit = N;\n
+\taddtop = list(addtop, n->ninit);\n+\tn->ninit = N;\n
+\
if(n->left == N || n->right == N)
\treturn;
switch(n->op) {
コアとなるコードの解説
このコミットの核心は、GoコンパイラがASTを処理する際に、初期化文(ninit)をどのように生成し、管理するかという点にあります。
-
addtopの役割:addtopは、一時的な初期化文のバッファとして機能します。コンパイラが複雑な式(例えば、ドット演算子を含む式)を評価する際に、その評価に必要な一時変数や副作用を伴う処理が生成されることがあります。これらの処理は、すぐにASTノードのninitリストに直接追加されるのではなく、まずaddtopリストに一時的に蓄積されます。src/cmd/gc/walk.cのwalkdot関数では、ドット演算子ノードを処理する前に、そのノードに既に付随しているninitリストをaddtopに移動させ、ノードのninitをクリアします。これは、walkdotの処理中に生成される新たな初期化文が、既存の初期化文と衝突しないようにするため、または特定の順序で処理されるようにするためと考えられます。src/cmd/gc/subr.cのadddot関数では、ドット演算子の処理が完了する直前に、addtopに蓄積された初期化文を、現在のノードのninitリストの先頭に結合します。これにより、adddotの処理中に生成された初期化文が、そのノードの他の初期化文よりも先に実行されることが保証されます。
-
fatalによる防御的プログラミング:src/cmd/gc/go.yの構文解析ルール(変数宣言や短い変数宣言)や、src/cmd/gc/walk.cのwalk関数の終了時に、addtopが空であることを確認するfatal呼び出しが追加されています。これは、addtopが一時的なバッファであり、特定の処理の開始時や終了時には常にクリアされているべきであるという設計上の不変条件を強制するものです。もしaddtopが空でなければ、それはコンパイラのロジックにバグがあることを示し、即座にエラーとして報告されます。これにより、コンパイラの堅牢性が向上します。
-
goto retによる終了処理の集約:src/cmd/gc/subr.cのadddot関数では、複数のreturn n;文がgoto ret;に変更され、関数の終了処理がret:ラベルに集約されました。これにより、addtopのninitへの結合とaddtopのリセットという重要なクリーンアップ処理が、関数のどの終了パスを通っても必ず実行されるようになります。これは、コードの保守性を高め、潜在的なバグを防ぐための良いプラクティスです。
これらの変更は、Goコンパイラが複雑な式を処理する際の初期化順序の正確性を確保し、コンパイラが生成するコードの正しさを保証するために不可欠でした。特に、コンパイラの内部状態を管理するためのaddtopのような一時的なグローバル変数の導入と、その厳密なライフサイクル管理が、この修正の鍵となっています。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のコンパイラに関するドキュメント(初期の設計思想など): Go言語の初期のコンパイラ設計に関する公式ドキュメントやブログ記事は、当時のGo開発ブログやGoの設計ドキュメント(Go Design Documents)に散見される可能性があります。
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/gcディレクトリ) - Go言語のコンパイラに関する一般的な知識(AST、中間表現、コード生成など)
- Yacc/Bisonの文法定義に関する知識
- C言語のポインタとリンクリスト操作に関する知識