[インデックス 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言語のポインタとリンクリスト操作に関する知識