[インデックス 1811] ファイルの概要
このコミットは、Goコンパイラ(6g
)におけるラベル付きbreak
およびcontinue
文の挙動に関するバグ(内部的に「bug 137」と識別されていたもの)を修正します。具体的には、コード生成フェーズにおいて、ループやスイッチ文に付随するラベルのbreak
およびcontinue
ターゲットの解決ロジックが改善されています。これにより、ネストされたループ内でラベル付きbreak
やcontinue
が正しく機能するようになります。
コミット
commit 48f6b516e24d55e14d866a7e7140fb3b1d1ae218
Author: Ken Thompson <ken@golang.org>
Date: Wed Mar 11 16:25:45 2009 -0700
bug 137
R=r
OCL=26142
CL=26142
---
src/cmd/6g/gen.c | 150 +++++++++++++++++++------------------
src/cmd/6g/gg.h | 4 +-\
test/{bugs => fixedbugs}/bug137.go | 0
test/golden.out | 5 --
4 files changed, 81 insertions(+), 78 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/48f6b516e24d55e14d866a7e7140fb3b1d1ae218
元コミット内容
bug 137
R=r
OCL=26142
CL=26142
変更の背景
このコミットは、Goコンパイラ(6g
)がラベル付きbreak
およびcontinue
文を正しく処理できないという既知のバグ(内部的な「bug 137」)に対応するために行われました。Go言語では、for
ループやswitch
文、select
文にラベルを付与し、そのラベルを指定してbreak
やcontinue
を行うことができます。これにより、ネストされたループの特定の外側のループから抜け出したり、特定のループの次のイテレーションに進んだりすることが可能になります。
しかし、この機能の実装には問題があり、コンパイラがラベル付きbreak
やcontinue
のジャンプ先を誤って解決してしまうことがありました。特に、gen
関数がループのコンテキストをLabel* labloop
という引数で受け取っていましたが、この方法では複雑なネスト構造や複数のラベルが存在する場合に、正しいジャンプ先を特定できないケースが発生していました。その結果、「break label is not defined」のようなコンパイルエラーが発生したり、意図しない挙動を引き起こしたりしていました。
このコミットは、ラベルの管理と解決のメカニズムを根本的に見直し、より堅牢な方法でラベル付き制御フローをサポートすることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGoコンパイラの基本的な概念と、Go言語の制御フローに関する知識が必要です。
-
Goコンパイラ (
6g
) の構造:6g
は、Go言語のソースコードをアセンブリコードに変換するコンパイラの一つです(6
はamd64アーキテクチャを指します)。- コンパイルプロセスには、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成などのフェーズがあります。
- このコミットが関連するのは主にコード生成フェーズです。
-
src/cmd/6g/gen.c
:- Goコンパイラのコード生成を担当するC言語のソースファイルです。
- 抽象構文木(AST)をトラバースし、対応するアセンブリ命令(
Prog
構造体で表現される)を生成します。 gen
関数は、ASTのノードを受け取り、そのノードに対応するコードを生成する主要な関数です。
-
src/cmd/6g/gg.h
:6g
コンパイラで使用される共通の定義や構造体が含まれるヘッダファイルです。Node
、Sym
、Prog
などの重要な構造体が定義されています。- 特に、
Label
構造体は、goto
、break
、continue
などの制御フロー文のジャンプ先を管理するために使用されます。
-
Label
構造体:Label
構造体は、Goコンパイラがコード生成時にラベル情報を管理するために使用されます。sym
: ラベル名に対応するシンボル。op
: ラベルの種類(例:OLABEL
、OGOTO
)。label
: ラベルが指すコードのプログラムカウンタ(Prog
へのポインタ)。breakpc
: このラベルに関連付けられたbreak
文がジャンプするべきプログラムカウンタ。continpc
: このラベルに関連付けられたcontinue
文がジャンプするべきプログラムカウンタ。labellist
: 現在アクティブなラベルのリンクリスト。
-
break
とcontinue
文:- Go言語では、
break
はループやswitch
、select
文を終了させます。continue
はループの次のイテレーションに進みます。 - ラベル付き
break
/continue
:break LabelName
やcontinue LabelName
のように、特定のラベルが付与されたループやswitch
/select
文を対象に制御を移すことができます。これは、ネストされた制御構造から特定の外側の構造を操作する際に特に有用です。
- Go言語では、
-
プログラムカウンタ (
pc
) とパッチ (patch
):pc
は、現在生成中のアセンブリ命令の場所を示すポインタです。gbranch(AJMP, T)
のような関数は、条件なしジャンプ命令(AJMP
)を生成し、そのジャンプ先のポインタを返します。patch(p, target_pc)
は、以前に生成されたジャンプ命令p
のジャンプ先をtarget_pc
に修正(パッチ)するために使用されます。これは、ジャンプ先がコード生成時にまだ不明な場合に、後で解決するために使われる一般的な手法です。
このコミットは、これらの要素がどのように連携してラベル付き制御フローを実装し、その中で発生していたバグをどのように修正したかを示しています。
技術的詳細
このコミットの技術的な核心は、Goコンパイラがラベル付きbreak
およびcontinue
文のジャンプ先を解決する方法の変更にあります。
-
gen
関数のシグネチャ変更:- 変更前:
void gen(Node *n, Label *labloop)
- 変更後:
void gen(Node *n)
labloop
引数が削除されました。これは、以前はgen
関数が現在のループのラベル情報を引数として受け取っていましたが、この方法では複雑なラベル解決に対応しきれなかったことを示唆しています。代わりに、ラベル情報はグローバルなlabellist
リンクリストを通じて管理されるようになりました。これにより、gen
関数はより汎用的なコード生成ロジックに集中できるようになります。
- 変更前:
-
newlab
関数の導入:- 新しいヘルパー関数
newlab(int op, Sym *s)
が導入されました。 - この関数は、新しい
Label
構造体を動的に割り当て、labellist
の先頭にリンクし、sym
、op
、および現在のプログラムカウンタpc
をlabel
フィールドに設定します。 - これにより、ラベルの作成と
labellist
への追加が中央集権化され、コードの重複が減り、一貫性が向上します。
- 新しいヘルパー関数
-
OLIST
ノードの処理の簡素化:OLIST
は、複数の文を順次実行するリストを表すASTノードです。- 変更前は、
OLIST
内でラベルが検出された場合に、gen
関数を再帰的に呼び出す際にlabellist
を渡すなど、複雑なロジックがありました。 - 変更後、この複雑なロジックは削除され、
gen(n->left)
とn = n->right; goto loop;
というシンプルな再帰呼び出しとループ処理に置き換えられました。これは、ラベルの解決がgen
関数の引数ではなく、labellist
の動的な走査によって行われるようになったため、OLIST
の処理が簡素化されたことを意味します。
-
OLABEL
とOGOTO
の処理の改善:OLABEL
(ラベル定義)とOGOTO
(goto
文)のケースでは、newlab
関数を使用してLabel
構造体が作成されるようになりました。これにより、ラベルの登録がよりクリーンに行われます。
-
OBREAK
とOCONTINUE
の解決ロジックの変更:- 変更前:
findlab(n->left->sym)
を使用してラベルを検索し、そのラベルのbreakpc
またはcontinpc
が有効かどうかを確認していました。findlab
がどのように機能していたかは不明ですが、このアプローチでは問題があったようです。 - 変更後:
labellist
を先頭から(つまり、最も新しく定義されたラベルから)走査し、有効なbreakpc
またはcontinpc
を持つ最初のLabel
を見つけます。そして、そのbreakpc
またはcontinpc
に対してジャンプ命令をパッチします。 - この変更は非常に重要です。ラベル付き
break
/continue
の場合、n->left
はラベルのシンボルを指します。このシンボルに対応するLabel
をlabellist
から探し、そのbreakpc
/continpc
をパッチします。ラベルなしの場合(n->left == N
)、グローバルなbreakpc
/continpc
が使用されます。この動的な走査により、ネストされた構造における正しいジャンプ先が確実に解決されるようになります。
- 変更前:
-
ループ/スイッチ/セレクト文におけるラベルの関連付け:
OFOR
(for
ループ)、OSWITCH
(switch
文)、OSELECT
(select
文)のコード生成ロジックが変更されました。- これらの構造の開始時に、現在のプログラムカウンタ
pc
がp3
として保存されます。 - その後、
labellist
を走査し、p3
と一致するlabel
フィールドを持つOLABEL
(つまり、このループ/スイッチ/セレクトの直前に定義されたラベル)を探します。 - 見つかった場合、その
Label
のbreakpc
とcontinpc
(OFOR
の場合)を、現在生成中のループ/スイッチ/セレクトのbreak
およびcontinue
ターゲットに設定します。 - このメカニズムにより、ラベルがその直後の制御フロー構造に正しく「関連付け」られるようになります。これにより、ラベル付き
break
やcontinue
が、意図した特定のループやスイッチ文を対象にできるようになります。
-
テストケースの移動:
test/bugs/bug137.go
がtest/fixedbugs/bug137.go
にリネームされました。これは、このコミットによってバグが修正され、テストが「修正済みバグ」のカテゴリに移動したことを示しています。test/golden.out
からもbug137.go
に関するエラーメッセージが削除されています。
これらの変更により、Goコンパイラはラベル付きbreak
およびcontinue
文をより正確に処理できるようになり、コンパイル時のエラーや実行時の誤った挙動が解消されました。
コアとなるコードの変更箇所
このコミットの主要な変更は、src/cmd/6g/gen.c
ファイルに集中しています。
-
gen
関数のシグネチャ変更:--- a/src/cmd/6g/gen.c +++ b/src/cmd/6g/gen.c @@ -82,8 +82,8 @@ compile(Node *fn) // inarggen(); ginit(); - gen(curfn->enter, L); - gen(curfn->nbody, L); + gen(curfn->enter); + gen(curfn->nbody); gclean(); checklabels();
gen
関数の呼び出し箇所からL
(null Label)引数が削除されています。 -
newlab
関数の追加:--- a/src/cmd/6g/gen.c +++ b/src/cmd/6g/gen.c @@ -143,16 +143,29 @@ allocparams(void) } } +void +newlab(int op, Sym *s) +{ + Label *lab; + + lab = mal(sizeof(*lab)); + lab->link = labellist; + labellist = lab; + + lab->sym = s; + lab->op = op; + lab->label = pc; +} + /* * compile statements */ void -gen(Node *n, Label *labloop) +gen(Node *n) { int32 lno; Prog *scontin, *sbreak; Prog *p1, p2, p3; - Node *l; Label *lab;
newlab
関数が新しく定義され、gen
関数のシグネチャも変更されています。 -
OLIST
、OLABEL
、OGOTO
の処理変更:--- a/src/cmd/6g/gen.c +++ b/src/cmd/6g/gen.c @@ -160,8 +173,10 @@ gen(Node *n, Label *labloop) loop: if(n == N) goto ret; + + p3 = pc; // save pc for loop labels if(n->ninit) - gen(n->ninit, L); + gen(n->ninit); setlineno(n); @@ -171,24 +186,8 @@ loop: break; case OLIST: - l = n->left; - gen(l, L); - if(l != N && l->op == OLABEL) { - // call the next statement with a label - l = n->right; - if(l != N) { - if(l->op != OLIST) { - gen(l, labellist); - break; - } - gen(l->left, labellist); - n = l->right; - labloop = L; - goto loop; - } - } + gen(n->left); n = n->right; - labloop = L; goto loop; case OPANIC: @@ -203,37 +202,26 @@ loop: break; case OLABEL: - lab = mal(sizeof(*lab)); - lab->link = labellist; - labellist = lab; - lab->sym = n->left->sym; - - lab->op = OLABEL; - lab->label = pc; + newlab(OLABEL, n->left->sym); break; case OGOTO: - lab = mal(sizeof(*lab)); - lab->link = labellist; - labellist = lab; - lab->sym = n->left->sym; - - lab->op = OGOTO; - lab->label = pc; + newlab(OGOTO, n->left->sym); gbranch(AJMP, T); break;
OLIST
の処理が簡素化され、OLABEL
とOGOTO
でnewlab
が使われるようになりました。 -
OBREAK
とOCONTINUE
の解決ロジック変更:--- a/src/cmd/6g/gen.c +++ b/src/cmd/6g/gen.c @@ -215,20 +203,20 @@ loop: case OBREAK: if(n->left != N) { - lab = findlab(n->left->sym); - if(lab == L || lab->breakpc == P) { - yyerror("break label is not defined: %S", n->left->sym); - break; + for(lab=labellist; lab!=L; lab=lab->link) { + if(lab->breakpc != P) { + patch(gbranch(AJMP, T), lab->breakpc); + break; + } } - patch(gbranch(AJMP, T), lab->breakpc); + if(lab == L) + yyerror("break label not defined: %S", n->left->sym); break; } - if(breakpc == P) { yyerror("break is not in a loop"); break; @@ -243,12 +231,14 @@ loop: case OCONTINUE: if(n->left != N) { - lab = findlab(n->left->sym); - if(lab == L || lab->continpc == P) { - yyerror("continue label is not defined: %S", n->left->sym); - break; + for(lab=labellist; lab!=L; lab=lab->link) { + if(lab->continpc != P) { + patch(gbranch(AJMP, T), lab->continpc); + break; + } } - patch(gbranch(AJMP, T), lab->continpc); + if(lab == L) + yyerror("break label not defined: %S", n->left->sym); break; }
findlab
の使用が削除され、labellist
を走査してbreakpc
/continpc
を解決するロジックに変更されています。 -
ループ/スイッチ/セレクト文におけるラベルの関連付け:
--- a/src/cmd/6g/gen.c +++ b/src/cmd/6g/gen.c @@ -265,18 +255,24 @@ loop: breakpc = gbranch(AJMP, T); // break: goto done scontin = continpc; continpc = pc; - gen(n->nincr, L); // contin: incr + + // define break and cotinue labels + for(lab=labellist; lab!=L; lab=lab->link) { + if(lab->label != p3) + break; + if(lab->op == OLABEL) { + lab->breakpc = breakpc; + lab->continpc = continpc; + } + } + + gen(n->nincr); // contin: incr patch(p1, pc); // test: if(n->ntest != N) if(n->ntest->ninit != N) - gen(n->ntest->ninit, L); + gen(n->ntest->ninit); bgen(n->ntest, 0, breakpc); // if(!test) goto break - if(labloop != L) { - labloop->op = OFOR; - labloop->continpc = continpc; - labloop->breakpc = breakpc; - } - gen(n->nbody, L); // body + gen(n->nbody); // body patch(gbranch(AJMP, T), continpc); // goto contin patch(breakpc, pc); // done: continpc = scontin; @@ -289,12 +285,12 @@ loop: patch(p1, pc); // test: if(n->ntest != N) if(n->ntest->ninit != N) - gen(n->ntest->ninit, L); + gen(n->ntest->ninit); bgen(n->ntest, 0, p2); // if(!test) goto p2 - gen(n->nbody, L); // then + gen(n->nbody); // then p3 = gbranch(AJMP, T); // goto done patch(p2, pc); // else: - gen(n->nelse, L); // else + gen(n->nelse); // else patch(p3, pc); // done: break; @@ -302,12 +298,18 @@ loop: sbreak = breakpc; p1 = gbranch(AJMP, T); // goto test breakpc = gbranch(AJMP, T); // break: goto done - patch(p1, pc); // test: - if(labloop != L) { - labloop->op = OFOR; - labloop->breakpc = breakpc; + // define break label + for(lab=labellist; lab!=L; lab=lab->link) { + if(lab->label != p3) + break; + if(lab->op == OLABEL) { + lab->breakpc = breakpc; + } } - gen(n->nbody, L); // switch(test) body + + patch(p1, pc); // test: + gen(n->nbody); // switch(test) body patch(breakpc, pc); // done: breakpc = sbreak; break; @@ -316,12 +318,18 @@ loop: sbreak = breakpc; p1 = gbranch(AJMP, T); // goto test breakpc = gbranch(AJMP, T); // break: goto done - patch(p1, pc); // test: - if(labloop != L) { - labloop->op = OFOR; - labloop->breakpc = breakpc; + // define break label + for(lab=labellist; lab!=L; lab=lab->link) { + if(lab->label != p3) + break; + if(lab->op == OLABEL) { + lab->breakpc = breakpc; + } } - gen(n->nbody, L); // select() body + + patch(p1, pc); // test: + gen(n->nbody); // select() body patch(breakpc, pc); // done: breakpc = sbreak; break;
labloop
の使用が削除され、p3
とlabellist
を組み合わせた新しいラベル関連付けロジックが導入されています。 -
その他の
gen
呼び出しの変更:--- a/src/cmd/6g/gen.c +++ b/src/cmd/6g/gen.c @@ -484,7 +492,7 @@ cgen_callinter(Node *n, Node *res, int proc) i = &tmpi; } - gen(n->right, L); // args + gen(n->right); // args regalloc(&nodr, types[tptr], res); regalloc(&nodo, types[tptr], &nodr); @@ -561,7 +569,7 @@ cgen_call(Node *n, int proc) cgen(n->left, &afun); } - gen(n->right, L); // assign the args + gen(n->right); // assign the args t = n->left->type; setmaxarg(t); @@ -694,7 +702,7 @@ cgen_aret(Node *n, Node *res) void cgen_ret(Node *n) { - gen(n->left, L); // copy out args + gen(n->left); // copy out args if(hasdefer) ginscall(deferreturn, 0); gins(ARET, N, N);
cgen_callinter
、cgen_call
、cgen_ret
内のgen
呼び出しも引数L
が削除されています。 -
src/cmd/6g/gg.h
の変更:--- a/src/cmd/6g/gg.h +++ b/src/cmd/6g/gg.h @@ -74,7 +74,7 @@ struct Pool typedef struct Label Label; struct Label { - uchar op; // OFOR/OGOTO/OLABEL + uchar op; // OGOTO/OLABEL Sym* sym; Prog* label; // pointer to code Prog* breakpc; // pointer to code @@ -133,7 +133,7 @@ EXTERN int sizeof_Array; // runtime sizeof(Array) */ void compile(Node*); void proglist(void); -void gen(Node*, Label*); +void gen(Node*); Node* lookdot(Node*, Node*, int); void inarggen(void); void cgen_as(Node*, Node*);
Label
構造体のop
フィールドのコメントからOFOR
が削除され、gen
関数のプロトタイプが更新されています。 -
テストファイルの移動と
golden.out
の変更:--- a/test/bugs/bug137.go +++ b/test/fixedbugs/bug137.go similarity index 100% rename from test/bugs/bug137.go rename to test/fixedbugs/bug137.go
--- a/test/golden.out +++ b/test/golden.out @@ -140,11 +140,6 @@ BUG: compilation succeeds incorrectly =========== bugs/bug136.go BUG: should not compile -=========== bugs/bug137.go -bugs/bug137.go:9: break label is not defined: L2 -bugs/bug137.go:15: break label is not defined: L4 -BUG should compile - =========== bugs/bug138.go bugs/bug138.go:8: overflow converting constant to uint bugs/bug138.go:8: illegal combination of literals CONV 7
bug137.go
がfixedbugs
ディレクトリに移動し、golden.out
から関連するエラーメッセージが削除されています。
コアとなるコードの解説
このコミットの核心は、gen
関数におけるラベルの管理と解決のパラダイムシフトにあります。
以前のgen(Node *n, Label *labloop)
では、labloop
引数を通じて現在のループのラベル情報を明示的に渡していました。しかし、これはネストされたループや複数のラベルが存在する複雑なシナリオにおいて、正しいbreak
やcontinue
のターゲットを特定するのに不十分でした。
新しいアプローチでは、labloop
引数を削除し、代わりにグローバルなlabellist
リンクリストと、gen
関数内で動的にラベルを関連付けるロジックを使用します。
-
newlab
関数: この関数は、新しいLabel
構造体を作成し、それをlabellist
の先頭に追加します。labellist
は、現在アクティブな(定義された)すべてのラベルを追跡するスタックのような役割を果たします。新しいラベルが追加されると、それはリストの先頭に来るため、最も最近定義されたラベルが最初に検索されるようになります。 -
OLABEL
とOGOTO
の処理: これらのケースでは、newlab
関数を呼び出すことで、ラベルの定義(OLABEL
)やgoto
文のターゲット(OGOTO
)がlabellist
に登録されます。lab->label = pc;
という行は、ラベルが指すコードの開始位置(プログラムカウンタ)を記録します。 -
OBREAK
とOCONTINUE
の解決: ラベル付きbreak
やcontinue
(n->left != N
の場合)では、もはやfindlab
関数は使用されません。代わりに、labellist
を先頭から走査します。for(lab=labellist; lab!=L; lab=lab->link) { if(lab->breakpc != P) { // または lab->continpc != P patch(gbranch(AJMP, T), lab->breakpc); // または lab->continpc break; } }
このループは、
labellist
内で有効なbreakpc
(またはcontinpc
)を持つ最初のLabel
を見つけます。labellist
は新しいラベルが先頭に追加されるため、この走査は最も内側の(最も最近定義された)有効なbreak
/continue
ターゲットを優先的に見つけます。これにより、ラベルなしのbreak
/continue
が最も内側のループを対象とするのと同様に、ラベル付きの場合も正しいターゲットにジャンプできるようになります。 -
ループ/スイッチ/セレクト文におけるラベルの関連付け:
OFOR
、OSWITCH
、OSELECT
のコード生成時、p3 = pc;
で現在の文の開始位置のプログラムカウンタを保存します。その後、以下のループでlabellist
を走査します。for(lab=labellist; lab!=L; lab=lab->link) { if(lab->label != p3) // 現在の文の開始位置と異なるラベルはスキップ break; if(lab->op == OLABEL) { // ラベル定義の場合 lab->breakpc = breakpc; lab->continpc = continpc; // OFORの場合のみ } }
このループは、
p3
(現在のループ/スイッチ/セレクトの開始位置)と一致するlabel
フィールドを持つOLABEL
を探します。これは、この制御フロー構造の直前に定義されたラベルを意味します。見つかった場合、そのラベルのbreakpc
とcontinpc
を、現在生成中の制御フロー構造のbreak
およびcontinue
ターゲットに設定します。 このメカニズムにより、Label: for { ... break Label }
のようなコードで、Label
が正しくfor
ループに関連付けられ、break Label
がそのfor
ループから抜け出すようにコンパイルされるようになります。
この変更は、ラベルの解決をより動的かつコンテキスト依存的に行うことで、Go言語のラベル付き制御フローのセマンティクスを正確に反映することを可能にしました。
関連リンク
- Go言語の
break
とcontinue
文に関する公式ドキュメント(Go言語のバージョンによって内容が異なる可能性がありますが、基本的な概念は共通です):
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/6g/
ディレクトリ) - Go言語のコンパイラに関する一般的な知識
- Go言語の
break
とcontinue
のセマンティクスに関する知識 - Web検索: "Go bug 137 golang" (ただし、このコミットの「bug 137」はGoの公式Issueトラッカーの番号ではなく、当時の内部的な識別子である可能性が高いことを考慮)