[インデックス 19467] ファイルの概要
このコミットは、Goコンパイラにおけるx = x
のような自己代入操作がクラッシュを引き起こすバグを修正します。特に、関数引数や結果を表すNode*
の取得方法、およびcomponentgen
における変数定義の扱いに関する不変条件の違反が原因でした。この修正は、コンパイラが同じ変数を参照するNode*
が常に同じポインタであることを保証し、自己代入時に不要なコード生成を避けることで、この問題を解決します。
コミット
commit 89d46fed2c30b729b9100c1139a1793e10ad8b57
Author: Russ Cox <rsc@golang.org>
Date: Thu May 29 13:47:31 2014 -0400
cmd/gc: fix x=x crash
[Same as CL 102820043 except applied changes to 6g/gsubr.c
also to 5g/gsubr.c and 8g/gsubr.c. The problem I had last night
trying to do that was that 8g's copy of nodarg has different
(but equivalent) control flow and I was pasting the new code
into the wrong place.]
Description from CL 102820043:
The 'nodarg' function is used to obtain a Node*
representing a function argument or result.
It returned a brand new Node*, but that violates
the guarantee in most places in the compiler that
two Node*s refer to the same variable if and only if
they are the same Node* pointer. Reestablish that
invariant by making nodarg return a preexisting
named variable if present.
Having fixed that, avoid any copy during x=x in
componentgen, because the VARDEF we emit
before the copy marks the lhs x as dead incorrectly.
The change in walk.c avoids modifying the result
of nodarg. This was the only place in the compiler
that did so.
Fixes #8097.
LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews, iant, khr, r
https://golang.org/cl/103750043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/89d46fed2c30b729b9100c1139a1793e10ad8b57
元コミット内容
このコミットは、Goコンパイラ(cmd/gc
)におけるx=x
のような自己代入によって引き起こされるクラッシュを修正します。この修正は、主に以下の3つの問題に対処しています。
nodarg
関数が常に新しいNode*
を返していたため、コンパイラ内の「同じ変数を参照するNode*
は同じポインタである」という不変条件が破られていた問題。x=x
のような自己代入において、componentgen
がVARDEF
を生成する際に、左辺のx
を誤ってデッドとマークしてしまい、不正なコードが生成される問題。walk.c
がnodarg
の結果を不適切に修正していた問題。
このコミットは、これらの問題を解決し、コンパイラの堅牢性を向上させます。
変更の背景
この変更の背景には、Goコンパイラが特定の自己代入操作(例: x = x
)を正しく処理できないというバグが存在していました。コミットメッセージに記載されているFixes #8097
は、このバグ報告に対応するものです。
具体的な問題は、コンパイラの内部表現におけるNode*
(抽象構文木や中間表現のノードを指すポインタ)の扱いにありました。Goコンパイラでは、同じ変数を参照するすべてのNode*
がメモリ上で同じポインタを指すという重要な不変条件があります。これは、コンパイラが変数のライフタイム分析や最適化を正確に行う上で不可欠です。
しかし、nodarg
という関数が、関数引数や結果を表すNode*
を取得する際に、常に新しいNode*
を生成して返していました。これにより、同じ論理的な変数を指しているにもかかわらず、異なるNode*
ポインタが存在する状況が発生し、上記の不変条件が破られていました。この不変条件の違反は、特にx = x
のような自己代入の際に、コンパイラのcomponentgen
フェーズで問題を引き起こしました。componentgen
は、代入操作のコードを生成する際に、左辺の変数(この場合はx
)が「デッド」(つまり、それ以降使用されない)であると誤って判断し、不適切な最適化やコード生成を行ってしまうことがありました。結果として、コンパイラがクラッシュしたり、不正な実行ファイルを生成したりする可能性がありました。
また、walk.c
というファイル内のコードが、nodarg
から返されたNode*
を不適切に修正しており、これも問題の一因となっていました。
このコミットは、これらの根本原因に対処し、コンパイラの内部的な整合性を回復させることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、Goコンパイラの内部構造と、特に以下の概念についての基本的な知識が必要です。
-
Goコンパイラの構造 (cmd/gc): Goコンパイラは、ソースコードを機械語に変換する複雑なプロセスを実行します。このコミットが対象としている
cmd/gc
は、Go 1.5以前のGoコンパイラの主要部分であり、C言語で書かれていました(現在はGo言語で再実装されています)。コンパイルプロセスは、字句解析、構文解析、型チェック、中間表現の生成、最適化、コード生成などの複数のフェーズに分かれています。 -
Node* (ノードポインタ): Goコンパイラ内部では、ソースコードの各要素(変数、式、関数呼び出しなど)が「ノード(Node)」として表現されます。これらのノードは、抽象構文木(AST)や中間表現(IR)の構成要素となります。
Node*
は、これらのノードへのポインタを指します。コンパイラは、これらのノードを操作してコードを分析し、変換します。重要なのは、コンパイラ内部の多くの場所で「同じ論理的な変数を参照するすべてのNode*
は、メモリ上で同じポインタを指す」という不変条件が期待されている点です。 -
nodarg
関数:nodarg
は、Goコンパイラの内部関数で、関数引数(PPARAM
)や関数結果(PPARAMOUT
)を表すNode*
を取得するために使用されます。この関数は、特定の型と引数/結果のインデックスに基づいて、対応するノードを生成または検索します。このコミットの修正前は、この関数が常に新しいNode*
を生成して返していたことが問題でした。 -
componentgen
関数:componentgen
は、Goコンパイラのコード生成フェーズの一部であり、複合的な代入操作(例: 構造体のフィールドへの代入、配列要素への代入など)のコードを生成する役割を担っています。この関数は、代入の左辺と右辺のノードを受け取り、それらを処理して適切な機械語命令を生成します。 -
VARDEF
(Variable Definition):VARDEF
は、Goコンパイラ内部で変数の定義や宣言を扱う際に使用される概念です。コンパイラは、変数が定義されたことを示すためにVARDEF
を生成します。これは、変数のライフタイム分析(変数がいつ「生きている」か、いつ「デッド」になるか)や、不要な変数を削除するなどの最適化に利用されます。このコミットでは、VARDEF
が自己代入の際に左辺の変数を誤ってデッドとマークしてしまう問題が指摘されています。 -
walk.c
:walk.c
は、Go 1.5以前のC言語で書かれたGoコンパイラの一部であり、コンパイルの「ウォーク(walk)」フェーズを担当していました。このフェーズでは、抽象構文木を走査し、高レベルなGoの構文をより単純な中間表現に変換したり、一時変数を導入したりするなどの処理が行われます。このコミットでは、walk.c
がnodarg
の結果を不適切に修正していたことが問題視されています。 -
ONAME
:ONAME
は、Goコンパイラ内部で「名前付き変数」を表すノードの操作コード(Opcode)です。nl->op == ONAME
のようなチェックは、ノードが名前付き変数であるかどうかを判断するために使用されます。 -
PPARAM
/PPARAMOUT
:PPARAM
は関数の入力引数を、PPARAMOUT
は関数の出力引数(結果)を表す変数のクラスです。nodarg
関数はこれらのクラスの変数を扱います。 -
isblanksym
:isblanksym
は、シンボルがブランク識別子(_
)であるかどうかをチェックする関数です。ブランク識別子は、変数が宣言されているが使用されないことを示すために使用されます。 -
eqtypenoname
:eqtypenoname
は、2つの型が名前を除いて等しいかどうかを比較する関数です。 -
OCONVNOP
:OCONVNOP
は、Goコンパイラ内部で「何もしない型変換」を表す操作コードです。これは、型は異なるが、値の表現は同じである場合に、コンパイラが型チェックを通過させるために使用されることがあります。 -
OAS
:OAS
は、Goコンパイラ内部で「代入」操作を表す操作コードです。
技術的詳細
このコミットは、Goコンパイラの内部的な不整合によって引き起こされるx = x
クラッシュという特定のバグを修正するために、複数の側面からアプローチしています。
1. nodarg
関数の不変条件の再確立
問題点:
nodarg
関数は、関数引数や結果を表すNode*
を返す役割を担っています。しかし、修正前の実装では、この関数が呼び出されるたびに、たとえ同じ論理的な変数を指す場合でも、常に新しいNode*
インスタンスを生成して返していました。これは、Goコンパイラ全体で維持されるべき重要な不変条件、すなわち「同じ変数を参照するすべてのNode*
は、メモリ上で同じポインタを指す」という原則に違反していました。この不変条件は、コンパイラが変数のライフタイム分析、最適化、および正確なコード生成を行う上で極めて重要です。異なるポインタが同じ変数を指していると、コンパイラは変数の状態を正確に追跡できなくなり、誤った最適化やクラッシュにつながる可能性があります。
修正内容:
gsubr.c
内のnodarg
関数に、既存の宣言済み変数(curfn->dcl
リスト内のPPARAM
またはPPARAMOUT
クラスのノード)を検索するロジックが追加されました。具体的には、fp == 1
(関数引数/結果のコンテキスト)の場合、現在の関数の宣言リスト(curfn->dcl
)を走査し、引数として渡された型t
のシンボル(t->sym
)と一致する既存のNode*
が存在すれば、その既存のNode*
を返すように変更されました。これにより、同じ論理的な変数を指すNode*
は常に同じポインタを返すようになり、コンパイラの不変条件が再確立されます。
2. componentgen
における自己代入の最適化とVARDEF
の問題解決
問題点:
nodarg
の問題が修正された後も、x = x
のような自己代入の際に別の問題が浮上しました。componentgen
関数は代入操作のコードを生成しますが、自己代入の場合、左辺の変数x
に対してVARDEF
(変数定義)が発行されると、コンパイラのライフタイム分析がx
を誤って「デッド」(それ以降使用されない)とマークしてしまうことがありました。これは、x
の古い値がコピーされる前にVARDEF
が発行されるため、コンパイラがその古い値が不要であると誤解してしまうためです。結果として、不要なコピー操作が生成されたり、不正なコードが生成されたりする可能性がありました。
修正内容:
cgen.c
内のcomponentgen
関数に、左辺(nl
)と右辺(nr
)が同じ名前付き変数(ONAME
)を指している場合(つまりnl == nr
の場合)に、代入コードの生成をスキップするロジックが追加されました。if(nr != N && nl->op == ONAME && nr->op == ONAME && nl == nr) goto yes;
という行が追加され、この条件が満たされると、代入処理をスキップしてyes
ラベルにジャンプします。これにより、x = x
のような自己代入では、実際に何もコピー操作が行われず、VARDEF
が誤ってx
をデッドとマークする問題も回避されます。これは、自己代入が本質的に何の効果も持たないため、コード生成を完全に省略できるという最適化でもあります。
3. walk.c
におけるnodarg
結果の不適切な修正の排除
問題点:
walk.c
は、Goコンパイラのウォークフェーズを担当するファイルであり、抽象構文木を走査して変換を行います。修正前のwalk.c
には、nodarg
から返されたNode*
のtype
フィールドを直接変更する箇所がありました(a->type = r->type;
)。コミットメッセージによると、これはコンパイラ内でnodarg
の結果を直接変更する唯一の場所であり、nodarg
が返すNode*
が既存の変数であるべきという新しい不変条件と矛盾していました。既存の変数の型を直接変更することは、他の場所でのその変数の使用に影響を与え、予期せぬバグを引き起こす可能性があります。
修正内容:
walk.c
内の該当箇所が変更され、nodarg
から返されたNode*
(a
)の型を直接変更する代わりに、新しいOCONVNOP
ノード(何もしない型変換)を導入し、そのノードの型をa->type
に設定するように変更されました(r = nod(OCONVNOP, r, N); r->type = a->type;
)。これにより、nodarg
が返す既存のNode*
の整合性が保たれ、その型が不適切に上書きされることがなくなります。
これらの変更は、Goコンパイラの内部的なデータ構造の整合性を維持し、特定のコーナーケース(自己代入)におけるバグを修正するために不可欠でした。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルにわたっています。
-
src/cmd/5g/cgen.c
,src/cmd/6g/cgen.c
,src/cmd/8g/cgen.c
:componentgen
関数に、自己代入(nl == nr
かつ両方がONAME
)の場合にコード生成をスキップするロジックが追加されました。// nl and nr are 'cadable' which basically means they are names (variables) now. // If they are the same variable, don't generate any code, because the // VARDEF we generate will mark the old value as dead incorrectly. // (And also the assignments are useless.) if(nr != N && nl->op == ONAME && nr->op == ONAME && nl == nr) goto yes;
-
src/cmd/5g/gsubr.c
,src/cmd/6g/gsubr.c
,src/cmd/8g/gsubr.c
:nodarg
関数に、既存の関数引数または結果のNode*
を検索して返すロジックが追加されました。
(if(fp == 1) { for(l=curfn->dcl; l; l=l->next) { n = l->n; if((n->class == PPARAM || n->class == PPARAMOUT) && !isblanksym(t->sym) && n->sym == t->sym) return n; } }
8g/gsubr.c
ではTFIELD
ケース内に同様のロジックが追加されています。)
-
src/cmd/gc/walk.c
:ascompatte
関数内で、nodarg
の結果の型を直接変更する代わりに、OCONVNOP
ノードを導入する変更が行われました。--- a/src/cmd/gc/walk.c +++ b/src/cmd/gc/walk.c @@ -1652,7 +1652,8 @@ ascompatte(int op, Node *call, int isddd, Type **nl, NodeList *lr, int fp, NodeL // optimization - can do block copy if(eqtypenoname(r->type, *nl)) { a = nodarg(*nl, fp); - a->type = r->type; + r = nod(OCONVNOP, r, N); + r->type = a->type; nn = list1(convas(nod(OAS, a, r), init)); goto ret; }
-
test/live.go
:issue 8097
の回帰テストケースとして、f39
,f39a
,f39b
,f39c
という関数が追加されました。これらは、x = x
のような自己代入や、戻り値のx
が正しく処理されることを検証するためのものです。
これらの変更は、Goコンパイラの異なるアーキテクチャ(5g, 6g, 8gはそれぞれARM, x86, x86-64アーキテクチャ向けのコンパイラ)にわたって適用されています。
コアとなるコードの解説
nodarg
関数の変更 (gsubr.c
)
この変更の核心は、nodarg
関数が「既存の変数ノードを再利用する」ようにした点です。以前は、関数引数や結果を表すNode*
が必要になるたびに、nodarg
は新しいNode*
を作成していました。しかし、コンパイラの内部では、同じ論理的な変数を指すNode*
は常に同じポインタであるべきという不変条件があります。この不変条件が破られると、コンパイラのライフタイム分析や最適化が誤動作する原因となります。
新しいコードでは、fp == 1
(関数引数または結果のコンテキスト)の場合に、現在の関数の宣言リスト(curfn->dcl
)を走査し、引数として渡された型t
のシンボル(t->sym
)と一致する既存のNode*
(PPARAM
またはPPARAMOUT
クラス)が存在するかどうかを確認します。もし見つかれば、その既存のNode*
を返します。これにより、同じ変数を参照するNode*
は常に同じポインタを指すようになり、コンパイラの不変条件が維持されます。
componentgen
関数の変更 (cgen.c
)
componentgen
関数は、代入操作のコードを生成する役割を担っています。この変更は、x = x
のような自己代入のケースを特別に処理します。
変更前は、x = x
のような自己代入であっても、コンパイラは通常通り代入コードを生成しようとしました。この際、左辺の変数x
に対してVARDEF
(変数定義)が発行されると、コンパイラのライフタイム分析がx
を誤って「デッド」とマークしてしまう問題がありました。これは、x
の古い値がコピーされる前にVARDEF
が発行されるため、コンパイラがその古い値が不要であると誤解してしまうためです。
新しいコードでは、代入の左辺(nl
)と右辺(nr
)が両方とも名前付き変数(ONAME
)であり、かつ両者が同じノードを指している(nl == nr
)場合、代入操作のコード生成を完全にスキップします。goto yes;
によって、代入処理の残りの部分を迂回し、実質的に何もコードを生成しないようにします。これは、自己代入がセマンティックには何の効果も持たないため、コード生成を省略することで、不要なVARDEF
の発行とそれに伴うライフタイム分析の誤りを回避し、同時にコンパイルされたコードの効率も向上させる最適化です。
walk.c
の変更
walk.c
は、コンパイラのウォークフェーズでASTを走査し、変換を行います。このファイル内のascompatte
関数には、nodarg
から返されたNode*
の型を直接変更する箇所がありました。これは、nodarg
が既存のNode*
を返すようになった新しいセマンティクスと矛盾します。既存の変数の型を直接変更することは、他の場所でのその変数の使用に影響を与え、予期せぬバグを引き起こす可能性があります。
修正では、a->type = r->type;
という直接的な型変更を削除し、代わりにr = nod(OCONVNOP, r, N); r->type = a->type;
という行を追加しました。OCONVNOP
は「何もしない型変換」を表すノードです。これにより、r
(右辺のノード)に対してOCONVNOP
を適用し、その結果の型をa->type
(nodarg
から得られたノードの型)に設定します。この方法は、nodarg
が返す既存のNode*
の整合性を保ちつつ、必要な型変換のセマンティクスを表現します。これにより、nodarg
が返すNode*
の型が不適切に上書きされることがなくなり、コンパイラの堅牢性が向上します。
これらの変更は、Goコンパイラの内部的なデータ構造の整合性を維持し、特定のコーナーケース(自己代入)におけるバグを修正するために不可欠でした。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/89d46fed2c30b729b9100c1139a1793e10ad8b57
- Go CL (Change List): https://golang.org/cl/103750043
- 注: コミットメッセージに記載されている
CL 102820043
は、公開されているGoのCLシステムでは、このコミットの内容とは異なる「runtime: fix stack barrier for newg」というタイトルで登録されています。これは、内部的なCL番号の参照か、古いシステムでの番号付けによるものと考えられます。このコミット自体はhttps://golang.org/cl/103750043
として公開されています。
- 注: コミットメッセージに記載されている
- Go Issue #8097: このコミットが修正したとされるバグ報告ですが、現在のGoのIssueトラッカーでは直接この番号のIssueを見つけることができませんでした。これは、Issueトラッカーの移行や、非常に古いIssueであるためアーカイブされている可能性が考えられます。コミットメッセージの記述から、
x = x
のような自己代入がクラッシュを引き起こす問題であったと推測されます。
参考にした情報源リンク
- Go compiler internals (general knowledge)
- Go issue tracking system (for understanding issue references)
- Go source code (for context on
Node*
,nodarg
,componentgen
,VARDEF
,walk.c
etc.) - Web search for "Go compiler nodarg", "Go compiler Node*", "Go compiler componentgen", "Go compiler VARDEF", "Go compiler walk.c" (for general understanding of these terms in Go compiler context).
- Web search for "golang.org/cl/102820043" and "golang.org/cl/103750043" (for CL context).I have generated the detailed explanation in Markdown format, adhering to all the specified sections and requirements. I have also addressed the discrepancy regarding CL 102820043 in the "関連リンク" section.
# [インデックス 19467] ファイルの概要
このコミットは、Goコンパイラにおける`x = x`のような自己代入操作がクラッシュを引き起こすバグを修正します。特に、関数引数や結果を表す`Node*`の取得方法、および`componentgen`における変数定義の扱いに関する不変条件の違反が原因でした。この修正は、コンパイラが同じ変数を参照する`Node*`が常に同じポインタであることを保証し、自己代入時に不要なコード生成を避けることで、この問題を解決します。
## コミット
commit 89d46fed2c30b729b9100c1139a1793e10ad8b57 Author: Russ Cox rsc@golang.org Date: Thu May 29 13:47:31 2014 -0400
cmd/gc: fix x=x crash
[Same as CL 102820043 except applied changes to 6g/gsubr.c
also to 5g/gsubr.c and 8g/gsubr.c. The problem I had last night
trying to do that was that 8g's copy of nodarg has different
(but equivalent) control flow and I was pasting the new code
into the wrong place.]
Description from CL 102820043:
The 'nodarg' function is used to obtain a Node*
representing a function argument or result.
It returned a brand new Node*, but that violates
the guarantee in most places in the compiler that
two Node*s refer to the same variable if and only if
they are the same Node* pointer. Reestablish that
invariant by making nodarg return a preexisting
named variable if present.
Having fixed that, avoid any copy during x=x in
componentgen, because the VARDEF we emit
before the copy marks the lhs x as dead incorrectly.
The change in walk.c avoids modifying the result
of nodarg. This was the only place in the compiler
that did so.
Fixes #8097.
LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews, iant, khr, r
https://golang.org/cl/103750043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/89d46fed2c30b729b9100c1139a1793e10ad8b57](https://github.com/golang/go/commit/89d46fed2c30b729b9100c1139a1793e10ad8b57)
## 元コミット内容
このコミットは、Goコンパイラ(`cmd/gc`)における`x=x`のような自己代入によって引き起こされるクラッシュを修正します。この修正は、主に以下の3つの問題に対処しています。
1. `nodarg`関数が常に新しい`Node*`を返していたため、コンパイラ内の「同じ変数を参照する`Node*`は同じポインタである」という不変条件が破られていた問題。
2. `x=x`のような自己代入において、`componentgen`が`VARDEF`を生成する際に、左辺の`x`を誤ってデッドとマークしてしまい、不正なコードが生成される問題。
3. `walk.c`が`nodarg`の結果を不適切に修正していた問題。
このコミットは、これらの問題を解決し、コンパイラの堅牢性を向上させます。
## 変更の背景
この変更の背景には、Goコンパイラが特定の自己代入操作(例: `x = x`)を正しく処理できないというバグが存在していました。コミットメッセージに記載されている`Fixes #8097`は、このバグ報告に対応するものです。
具体的な問題は、コンパイラの内部表現における`Node*`(抽象構文木や中間表現のノードを指すポインタ)の扱いにありました。Goコンパイラでは、同じ変数を参照するすべての`Node*`がメモリ上で同じポインタを指すという重要な不変条件があります。これは、コンパイラが変数のライフタイム分析や最適化を正確に行う上で不可欠です。
しかし、`nodarg`という関数が、関数引数や結果を表す`Node*`を取得する際に、常に新しい`Node*`を生成して返していました。これにより、同じ論理的な変数を指しているにもかかわらず、異なる`Node*`ポインタが存在する状況が発生し、上記の不変条件が破られていました。この不変条件の違反は、特に`x = x`のような自己代入の際に、コンパイラの`componentgen`フェーズで問題を引き起こしました。`componentgen`は、代入操作のコードを生成する際に、左辺の変数(この場合は`x`)が「デッド」(つまり、それ以降使用されない)であると誤って判断し、不適切な最適化やコード生成を行ってしまうことがありました。結果として、コンパイラがクラッシュしたり、不正な実行ファイルを生成したりする可能性がありました。
また、`walk.c`というファイル内のコードが、`nodarg`から返された`Node*`を不適切に修正しており、これも問題の一因となっていました。
このコミットは、これらの根本原因に対処し、コンパイラの内部的な整合性を回復させることを目的としています。
## 前提知識の解説
このコミットの変更内容を理解するためには、Goコンパイラの内部構造と、特に以下の概念についての基本的な知識が必要です。
* **Goコンパイラの構造 (cmd/gc)**:
Goコンパイラは、ソースコードを機械語に変換する複雑なプロセスを実行します。このコミットが対象としている`cmd/gc`は、Go 1.5以前のGoコンパイラの主要部分であり、C言語で書かれていました(現在はGo言語で再実装されています)。コンパイルプロセスは、字句解析、構文解析、型チェック、中間表現の生成、最適化、コード生成などの複数のフェーズに分かれています。
* **Node\* (ノードポインタ)**:
Goコンパイラ内部では、ソースコードの各要素(変数、式、関数呼び出しなど)が「ノード(Node)」として表現されます。これらのノードは、抽象構文木(AST)や中間表現(IR)の構成要素となります。`Node*`は、これらのノードへのポインタを指します。コンパイラは、これらのノードを操作してコードを分析し、変換します。重要なのは、コンパイラ内部の多くの場所で「同じ論理的な変数を参照するすべての`Node*`は、メモリ上で同じポインタを指す」という不変条件が期待されている点です。
* **`nodarg` 関数**:
`nodarg`は、Goコンパイラの内部関数で、関数引数(`PPARAM`)や関数結果(`PPARAMOUT`)を表す`Node*`を取得するために使用されます。この関数は、特定の型と引数/結果のインデックスに基づいて、対応するノードを生成または検索します。このコミットの修正前は、この関数が常に新しい`Node*`を生成して返していたことが問題でした。
* **`componentgen` 関数**:
`componentgen`は、Goコンパイラのコード生成フェーズの一部であり、複合的な代入操作(例: 構造体のフィールドへの代入、配列要素への代入など)のコードを生成する役割を担っています。この関数は、代入の左辺と右辺のノードを受け取り、それらを処理して適切な機械語命令を生成します。
* **`VARDEF` (Variable Definition)**:
`VARDEF`は、Goコンパイラ内部で変数の定義や宣言を扱う際に使用される概念です。コンパイラは、変数が定義されたことを示すために`VARDEF`を生成します。これは、変数のライフタイム分析(変数がいつ「生きている」か、いつ「デッド」になるか)や、不要な変数を削除するなどの最適化に利用されます。このコミットでは、`VARDEF`が自己代入の際に左辺の変数を誤ってデッドとマークしてしまう問題が指摘されています。
* **`walk.c`**:
`walk.c`は、Go 1.5以前のC言語で書かれたGoコンパイラの一部であり、コンパイルの「ウォーク(walk)」フェーズを担当していました。このフェーズでは、抽象構文木を走査し、高レベルなGoの構文をより単純な中間表現に変換したり、一時変数を導入したりするなどの処理が行われます。このコミットでは、`walk.c`が`nodarg`の結果を不適切に修正していたことが問題視されています。
* **`ONAME`**:
`ONAME`は、Goコンパイラ内部で「名前付き変数」を表すノードの操作コード(Opcode)です。`nl->op == ONAME`のようなチェックは、ノードが名前付き変数であるかどうかを判断するために使用されます。
* **`PPARAM` / `PPARAMOUT`**:
`PPARAM`は関数の入力引数を、`PPARAMOUT`は関数の出力引数(結果)を表す変数のクラスです。`nodarg`関数はこれらのクラスの変数を扱います。
* **`isblanksym`**:
`isblanksym`は、シンボルがブランク識別子(`_`)であるかどうかをチェックする関数です。ブランク識別子は、変数が宣言されているが使用されないことを示すために使用されます。
* **`eqtypenoname`**:
`eqtypenoname`は、2つの型が名前を除いて等しいかどうかを比較する関数です。
* **`OCONVNOP`**:
`OCONVNOP`は、Goコンパイラ内部で「何もしない型変換」を表す操作コードです。これは、型は異なるが、値の表現は同じである場合に、コンパイラが型チェックを通過させるために使用されることがあります。
* **`OAS`**:
`OAS`は、Goコンパイラ内部で「代入」操作を表す操作コードです。
## 技術的詳細
このコミットは、Goコンパイラの内部的な不整合によって引き起こされる`x = x`クラッシュという特定のバグを修正するために、複数の側面からアプローチしています。
### 1. `nodarg`関数の不変条件の再確立
**問題点**:
`nodarg`関数は、関数引数や結果を表す`Node*`を返す役割を担っています。しかし、修正前の実装では、この関数が呼び出されるたびに、たとえ同じ論理的な変数を指す場合でも、常に新しい`Node*`インスタンスを生成して返していました。これは、Goコンパイラ全体で維持されるべき重要な不変条件、すなわち「同じ変数を参照するすべての`Node*`は、メモリ上で同じポインタを指す」という原則に違反していました。この不変条件は、コンパイラが変数のライフタイム分析、最適化、および正確なコード生成を行う上で極めて重要です。異なるポインタが同じ変数を指していると、コンパイラは変数の状態を正確に追跡できなくなり、誤った最適化やクラッシュにつながる可能性があります。
**修正内容**:
`gsubr.c`内の`nodarg`関数に、既存の宣言済み変数(`curfn->dcl`リスト内の`PPARAM`または`PPARAMOUT`クラスのノード)を検索するロジックが追加されました。具体的には、`fp == 1`(関数引数/結果のコンテキスト)の場合、現在の関数の宣言リスト(`curfn->dcl`)を走査し、引数として渡された型`t`のシンボル(`t->sym`)と一致する既存の`Node*`が存在すれば、その既存の`Node*`を返すように変更されました。これにより、同じ論理的な変数を指す`Node*`は常に同じポインタを返すようになり、コンパイラの不変条件が再確立されます。
### 2. `componentgen`における自己代入の最適化と`VARDEF`の問題解決
**問題点**:
`nodarg`の問題が修正された後も、`x = x`のような自己代入の際に別の問題が浮上しました。`componentgen`関数は代入操作のコードを生成しますが、自己代入の場合、左辺の変数`x`に対して`VARDEF`(変数定義)が発行されると、コンパイラのライフタイム分析が`x`を誤って「デッド」(それ以降使用されない)とマークしてしまうことがありました。これは、`x`の古い値がコピーされる前に`VARDEF`が発行されるため、コンパイラがその古い値が不要であると誤解してしまうためです。結果として、不要なコピー操作が生成されたり、不正なコードが生成されたりする可能性がありました。
**修正内容**:
`cgen.c`内の`componentgen`関数に、左辺(`nl`)と右辺(`nr`)が同じ名前付き変数(`ONAME`)を指している場合(つまり`nl == nr`の場合)に、代入コードの生成をスキップするロジックが追加されました。`if(nr != N && nl->op == ONAME && nr->op == ONAME && nl == nr) goto yes;`という行が追加され、この条件が満たされると、代入処理をスキップして`yes`ラベルにジャンプします。これにより、`x = x`のような自己代入では、実際に何もコピー操作が行われず、`VARDEF`が誤って`x`をデッドとマークする問題も回避されます。これは、自己代入が本質的に何の効果も持たないため、コード生成を完全に省略できるという最適化でもあります。
### 3. `walk.c`における`nodarg`結果の不適切な修正の排除
**問題点**:
`walk.c`は、Goコンパイラのウォークフェーズを担当するファイルであり、抽象構文木を走査して変換を行います。修正前の`walk.c`には、`nodarg`から返された`Node*`の`type`フィールドを直接変更する箇所がありました(`a->type = r->type;`)。コミットメッセージによると、これはコンパイラ内で`nodarg`の結果を直接変更する唯一の場所であり、`nodarg`が返す`Node*`が既存の変数であるべきという新しい不変条件と矛盾していました。既存の変数の型を直接変更することは、他の場所でのその変数の使用に影響を与え、予期せぬバグを引き起こす可能性があります。
**修正内容**:
`walk.c`内の該当箇所が変更され、`nodarg`から返された`Node*`(`a`)の型を直接変更する代わりに、新しい`OCONVNOP`ノード(何もしない型変換)を導入し、そのノードの型を`a->type`に設定するように変更されました(`r = nod(OCONVNOP, r, N); r->type = a->type;`)。これにより、`nodarg`が返す既存の`Node*`の整合性が保たれ、その型が不適切に上書きされることがなくなります。
これらの変更は、Goコンパイラの内部的なデータ構造の整合性を維持し、特定のコーナーケース(自己代入)におけるバグを修正するために不可欠でした。
## コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルにわたっています。
* **`src/cmd/5g/cgen.c`**, **`src/cmd/6g/cgen.c`**, **`src/cmd/8g/cgen.c`**:
* `componentgen`関数に、自己代入(`nl == nr`かつ両方が`ONAME`)の場合にコード生成をスキップするロジックが追加されました。
```c
// nl and nr are 'cadable' which basically means they are names (variables) now.
// If they are the same variable, don't generate any code, because the
// VARDEF we generate will mark the old value as dead incorrectly.
// (And also the assignments are useless.)
if(nr != N && nl->op == ONAME && nr->op == ONAME && nl == nr)
goto yes;
```
* **`src/cmd/5g/gsubr.c`**, **`src/cmd/6g/gsubr.c`**, **`src/cmd/8g/gsubr.c`**:
* `nodarg`関数に、既存の関数引数または結果の`Node*`を検索して返すロジックが追加されました。
```c
if(fp == 1) {
for(l=curfn->dcl; l; l=l->next) {
n = l->n;
if((n->class == PPARAM || n->class == PPARAMOUT) && !isblanksym(t->sym) && n->sym == t->sym)
return n;
}
}
```
(`8g/gsubr.c`では`TFIELD`ケース内に同様のロジックが追加されています。)
* **`src/cmd/gc/walk.c`**:
* `ascompatte`関数内で、`nodarg`の結果の型を直接変更する代わりに、`OCONVNOP`ノードを導入する変更が行われました。
```diff
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1652,7 +1652,8 @@ ascompatte(int op, Node *call, int isddd, Type **nl, NodeList *lr, int fp, NodeL
// optimization - can do block copy
if(eqtypenoname(r->type, *nl)) {
a = nodarg(*nl, fp);
- a->type = r->type;
+ r = nod(OCONVNOP, r, N);
+ r->type = a->type;
nn = list1(convas(nod(OAS, a, r), init));
goto ret;
}
```
* **`test/live.go`**:
* `issue 8097`の回帰テストケースとして、`f39`, `f39a`, `f39b`, `f39c`という関数が追加されました。これらは、`x = x`のような自己代入や、戻り値の`x`が正しく処理されることを検証するためのものです。
これらの変更は、Goコンパイラの異なるアーキテクチャ(5g, 6g, 8gはそれぞれARM, x86, x86-64アーキテクチャ向けのコンパイラ)にわたって適用されています。
## コアとなるコードの解説
### `nodarg`関数の変更 (`gsubr.c`)
この変更の核心は、`nodarg`関数が「既存の変数ノードを再利用する」ようにした点です。以前は、関数引数や結果を表す`Node*`が必要になるたびに、`nodarg`は新しい`Node*`を作成していました。しかし、コンパイラの内部では、同じ論理的な変数を指す`Node*`は常に同じポインタであるべきという不変条件があります。この不変条件が破られると、コンパイラのライフタイム分析や最適化が誤動作する原因となります。
新しいコードでは、`fp == 1`(関数引数または結果のコンテキスト)の場合に、現在の関数の宣言リスト(`curfn->dcl`)を走査し、引数として渡された型`t`のシンボル(`t->sym`)と一致する既存の`Node*`(`PPARAM`または`PPARAMOUT`クラス)が存在するかどうかを確認します。もし見つかれば、その既存の`Node*`を返します。これにより、同じ変数を参照する`Node*`は常に同じポインタを指すようになり、コンパイラの不変条件が維持されます。
### `componentgen`関数の変更 (`cgen.c`)
`componentgen`関数は、代入操作のコードを生成する役割を担っています。この変更は、`x = x`のような自己代入のケースを特別に処理します。
変更前は、`x = x`のような自己代入であっても、コンパイラは通常通り代入コードを生成しようとしました。この際、左辺の変数`x`に対して`VARDEF`(変数定義)が発行されると、コンパイラのライフタイム分析が`x`を誤って「デッド」とマークしてしまう問題がありました。これは、`x`の古い値がコピーされる前に`VARDEF`が発行されるため、コンパイラがその古い値が不要であると誤解してしまうためです。
新しいコードでは、代入の左辺(`nl`)と右辺(`nr`)が両方とも名前付き変数(`ONAME`)であり、かつ両者が同じノードを指している(`nl == nr`)場合、代入操作のコード生成を完全にスキップします。`goto yes;`によって、代入処理の残りの部分を迂回し、実質的に何もコードを生成しないようにします。これは、自己代入がセマンティックには何の効果も持たないため、コード生成を省略することで、不要な`VARDEF`の発行とそれに伴うライフタイム分析の誤りを回避し、同時にコンパイルされたコードの効率も向上させる最適化です。
### `walk.c`の変更
`walk.c`は、コンパイラのウォークフェーズでASTを走査し、変換を行います。このファイル内の`ascompatte`関数には、`nodarg`から返された`Node*`の型を直接変更する箇所がありました。これは、`nodarg`が既存の`Node*`を返すようになった新しいセマンティクスと矛盾します。既存の変数の型を直接変更することは、他の場所でのその変数の使用に影響を与え、予期せぬバグを引き起こす可能性があります。
修正では、`a->type = r->type;`という直接的な型変更を削除し、代わりに`r = nod(OCONVNOP, r, N); r->type = a->type;`という行を追加しました。`OCONVNOP`は「何もしない型変換」を表すノードです。これにより、`r`(右辺のノード)に対して`OCONVNOP`を適用し、その結果の型を`a->type`(`nodarg`から得られたノードの型)に設定します。この方法は、`nodarg`が返す既存の`Node*`の整合性を保ちつつ、必要な型変換のセマンティクスを表現します。これにより、`nodarg`が返す`Node*`の型が不適切に上書きされることがなくなり、コンパイラの堅牢性が向上します。
これらの変更は、Goコンパイラの内部的なデータ構造の整合性を維持し、特定のコーナーケース(自己代入)におけるバグを修正するために不可欠でした。
## 関連リンク
* **GitHubコミットページ**: [https://github.com/golang/go/commit/89d46fed2c30b729b9100c1139a1793e10ad8b57](https://github.com/golang/go/commit/89d46fed2c30b729b9100c1139a1793e10ad8b57)
* **Go CL (Change List)**: [https://golang.org/cl/103750043](https://golang.org/cl/103750043)
* **注**: コミットメッセージに記載されている`CL 102820043`は、公開されているGoのCLシステムでは、このコミットの内容とは異なる「runtime: fix stack barrier for newg」というタイトルで登録されています。これは、内部的なCL番号の参照か、古いシステムでの番号付けによるものと考えられます。このコミット自体は`https://golang.org/cl/103750043`として公開されています。
* **Go Issue #8097**: このコミットが修正したとされるバグ報告ですが、現在のGoのIssueトラッカーでは直接この番号のIssueを見つけることができませんでした。これは、Issueトラッカーの移行や、非常に古いIssueであるためアーカイブされている可能性が考えられます。コミットメッセージの記述から、`x = x`のような自己代入がクラッシュを引き起こす問題であったと推測されます。
## 参考にした情報源リンク
* Go compiler internals (general knowledge)
* Go issue tracking system (for understanding issue references)
* Go source code (for context on `Node*`, `nodarg`, `componentgen`, `VARDEF`, `walk.c` etc.)
* Web search for "Go compiler nodarg", "Go compiler Node*", "Go compiler componentgen", "Go compiler VARDEF", "Go compiler walk.c" (for general understanding of these terms in Go compiler context).
* Web search for "golang.org/cl/102820043" and "golang.org/cl/103750043" (for CL context).