[インデックス 18512] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc)における、関数の戻り値の内部表現に関する重要な改善を扱っています。具体的には、名前のない戻り値とブランク識別子(_)で命名された戻り値の内部的な区別をより明確にし、それによって発生していたライブネス解析のバグを修正しています。
コミット
commit a069cf048dcfcd4c657d59b40ff318c8ab09b65c
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 13 20:59:39 2014 -0500
cmd/gc: distinguish unnamed vs blank-named return variables better
Before, an unnamed return value turned into an ONAME node n with n->sym
named ~anon%d, and n->orig == n.
A blank-named return value turned into an ONAME node n with n->sym
named ~anon%d but n->orig == the original blank n. Code generation and
printing uses n->orig, so that this node formatted as _ .
But some code does not use n->orig. In particular the liveness code does
not know about the n->orig convention and so mishandles blank identifiers.
It is possible to fix but seemed better to avoid the confusion entirely.
Now the first kind of node is named ~r%d and the second ~b%d; both have
n->orig == n, so that it doesn\'t matter whether code uses n or n->orig.
After this change the ->orig field is only used for other kinds of expressions,
not for ONAME nodes.
This requires distinguishing ~b from ~r names in a few places that care.
It fixes a liveness analysis bug without actually changing the liveness code.
TBR=ken2
CC=golang-codereviews
https://golang.org/cl/63630043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a069cf048dcfcd4c657d59b40ff318c8ab09b65c
元コミット内容
このコミットの元の内容は、Goコンパイラ(cmd/gc)が関数の戻り値を内部的にどのように表現するかに関するものです。
以前のGoコンパイラでは、以下の2種類の戻り値が内部で似たようなシンボル名を持っていました。
- 名前のない戻り値: 例えば
func f() (int)のように、戻り値に変数が明示的に宣言されていない場合。- コンパイラ内部では、この戻り値は
ONAMEノードとして表現され、そのシンボル(n->sym)は~anon%dのような匿名名が付けられていました。 - このノードの
n->origフィールドはn自身を指していました。
- コンパイラ内部では、この戻り値は
- ブランク識別子で命名された戻り値: 例えば
func f() (_ int)のように、戻り値がブランク識別子_で命名されている場合。- この場合も、コンパイラ内部では
ONAMEノードとして表現され、シンボル(n->sym)は~anon%dのような匿名名が付けられていました。 - しかし、このノードの
n->origフィールドは、元のブランク識別子を表すノードを指していました。これは、コード生成や出力時に_としてフォーマットされるようにするためでした。
- この場合も、コンパイラ内部では
問題は、一部のコンパイラコード(特にライブネス解析コード)が n->orig の規約を認識しておらず、ブランク識別子で命名された戻り値を正しく扱えないことでした。これにより、ライブネス解析において誤った結果が生じるバグが発生していました。
変更の背景
Goコンパイラは、ソースコードを解析し、中間表現を生成し、最終的に実行可能なバイナリを生成する複雑なソフトウェアです。このプロセスの中で、変数のスコープ、型、ライフタイムなどを正確に追跡することが不可欠です。
関数の戻り値は、特に名前付き戻り値(func f() (result int))やブランク識別子で命名された戻り値(func f() (_ int))、あるいは名前のない戻り値(func f() (int))など、様々な形式で宣言できます。コンパイラはこれらを内部的に一貫した方法で表現し、後続の最適化やコード生成フェーズで利用できるようにする必要があります。
このコミットの背景には、Goコンパイラのライブネス解析における特定のバグがありました。ライブネス解析は、プログラムの特定のポイントでどの変数が「生きている」(将来使用される可能性がある)かを判断する静的解析の一種です。これは、ガベージコレクションの効率化や、不要なコードの削除(デッドコードエリミネーション)などの最適化に不可欠です。
以前の実装では、名前のない戻り値とブランク識別子で命名された戻り値が内部的に曖昧な表現になっていたため、ライブネス解析がブランク識別子を正しく扱えず、誤ったライブネス情報に基づいて最適化を行ってしまう可能性がありました。この曖昧さを解消し、コンパイラの堅牢性を高めることが、この変更の主な動機です。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラに関する前提知識が必要です。
-
Goコンパイラの構造 (
cmd/gc):cmd/gcはGo言語の公式コンパイラです。これは複数のフェーズに分かれており、字句解析、構文解析、型チェック、中間表現(IR)の生成、最適化(エスケープ解析、ライブネス解析など)、コード生成といった段階を経て実行ファイルを生成します。- AST (Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。コンパイラの初期段階で生成されます。
- ONAMEノード: コンパイラ内部で変数を表現するためのノードタイプの一つです。Goの変数、関数パラメータ、戻り値などは、内部的に
ONAMEノードとして扱われます。 Node構造体: コンパイラの中間表現における基本的な要素で、ASTノードやその他の内部表現を構成します。Node構造体には、ノードの種類を示すopフィールド、関連するシンボルを指すsymフィールド、そして元のノードへのポインタorigフィールドなどがあります。Sym構造体: シンボルテーブルのエントリを表し、変数名や関数名などの識別子に関する情報(名前、型など)を保持します。
-
Goの戻り値の宣言:
- 名前付き戻り値:
func f() (x int)のように、戻り値に変数を明示的に名前を付けて宣言します。関数内でこの変数に値を代入し、returnステートメントで値を指定せずに戻ることができます。 - 名前のない戻り値:
func f() (int)のように、戻り値の型のみを指定し、変数名を指定しない形式です。この場合、returnステートメントでは明示的に値を指定する必要があります。 - ブランク識別子 (
_) で命名された戻り値:func f() (_ int)のように、戻り値に変数を_で命名します。これは、戻り値が存在するが、その値は使用しないことを意図する場合に用いられます。名前のない戻り値と同様に、returnステートメントでは明示的に値を指定する必要があります。
- 名前付き戻り値:
-
エスケープ解析 (Escape Analysis):
- Goコンパイラの最適化フェーズの一つで、変数がヒープに割り当てられるべきか(エスケープするか)スタックに割り当てられるべきか(エスケープしないか)を決定します。変数が関数のスコープ外から参照される可能性がある場合、その変数はヒープに「エスケープ」すると判断されます。エスケープ解析は、ガベージコレクションの負荷を軽減し、プログラムのパフォーマンスを向上させるために重要です。エスケープ解析は、変数のライフタイムを正確に追跡するために、変数の内部表現に依存します。
-
ライブネス解析 (Liveness Analysis):
- コンパイラのデータフロー解析の一つで、プログラムの各ポイントにおいて、どの変数が「ライブ」(その値が将来の計算で使われる可能性がある)であるかを決定します。ライブでない変数は、そのメモリを再利用したり、デッドコードとして削除したりすることができます。ライブネス解析は、ガベージコレクタが不要なオブジェクトを効率的に回収するために、正確な情報を提供する必要があります。
-
n->origフィールドの役割:Node構造体内のorigフィールドは、「元のノード」へのポインタとして機能します。これは、コンパイラがコード変換や最適化を行う際に、元のソースコードの構造や意味を保持するために使用されることがあります。例えば、あるノードが別のノードから派生した場合、origはその派生元のノードを指すことで、追跡可能性を維持します。
これらの概念を理解することで、このコミットがGoコンパイラの内部動作にどのように影響し、特定のバグをどのように修正したかを深く把握することができます。
技術的詳細
このコミットの核心は、Goコンパイラが関数の戻り値を内部的に表現する方法の変更にあります。特に、ONAME ノードのシンボル命名規則と n->orig フィールドの利用方法が修正されました。
変更前の問題点:
- 匿名戻り値 (
func f() (int)): 内部的にはONAMEノードが生成され、そのシンボル名(n->sym->name)は~anon%dの形式でした。このノードのn->origはn自身を指していました。 - ブランク識別子戻り値 (
func f() (_ int)): 内部的にはONAMEノードが生成され、そのシンボル名も~anon%dの形式でした。しかし、このノードのn->origは、元のブランク識別子を表すノードを指していました。これは、コード生成や出力時に_として表示されるようにするための特別な規約でした。
この n->orig の規約は、一部のコンパイラパス(特にライブネス解析)で認識されていませんでした。ライブネス解析は n->orig を参照せずに n 自体を見ていたため、匿名戻り値とブランク識別子戻り値の区別がつかず、ブランク識別子戻り値が誤ってライブであると判断されるなどのバグを引き起こしていました。
変更後の解決策:
このコミットでは、この曖昧さを根本的に解消するために、以下の変更が導入されました。
-
新しいシンボル命名規則:
- 匿名戻り値: 内部シンボル名を
~r%d(r は "result" の略) に変更しました。 - ブランク識別子戻り値: 内部シンボル名を
~b%d(b は "blank" の略) に変更しました。 この変更により、シンボル名自体で両者を明確に区別できるようになりました。
- 匿名戻り値: 内部シンボル名を
-
n->origフィールドの統一:- 匿名戻り値とブランク識別子戻り値の両方において、
ONAMEノードのn->origフィールドがn自身を指すように変更されました。 - これにより、
ONAMEノードに関してはn->origを参照する必要がなくなり、n自体を参照すれば十分になりました。コミットメッセージにあるように、「->origフィールドは、ONAMEノードではなく、他の種類の式にのみ使用される」ようになりました。
- 匿名戻り値とブランク識別子戻り値の両方において、
影響と利点:
- ライブネス解析の修正:
n->origの規約に依存しないようにしたことで、ライブネス解析コードを変更することなく、ブランク識別子戻り値に関するバグが修正されました。これは、コンパイラの異なる部分間の結合度を減らし、将来のメンテナンスを容易にするという点で重要な改善です。 - コンパイラコードの簡素化:
ONAMEノードのn->origの扱いが統一されたことで、コンパイラ内の他のパスもn->origの特殊なケースを考慮する必要がなくなり、コードが簡素化されます。 - 明確な区別:
~r%dと~b%dという新しい命名規則により、コンパイラのデバッグや内部動作の理解が容易になります。例えば、func f() (_ int)とfunc g() intのように、returnステートメントの引数の有無が異なる関数をコンパイラが区別する必要がある場合、この新しい命名規則が役立ちます。
この変更は、Goコンパイラの内部的な堅牢性と正確性を向上させるための、低レベルながらも重要な修正です。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、Goコンパイラの以下のファイルに集中しています。
-
src/cmd/gc/dcl.c:funcargs関数: 関数の引数と戻り値を処理する部分。- 名前のない戻り値 (
n->left == Nのケース) の内部シンボル名を~anon%dから~r%dに変更。 - ブランク識別子 (
isblank(n->left)) で命名された戻り値の処理を修正。- 新しい
ONAMEノードnnを作成し、そのorigフィールドをnn自身に設定 (nn->orig = nn;)。 - シンボル名を
~anon%dから~b%dに変更 (snprint(namebuf, sizeof(namebuf), "~b%d", gen++); nn->sym = lookup(namebuf);)。 n->origを使用しない理由と、~rと~bの区別の必要性に関するコメントが追加されました。
- 新しい
- 名前のない戻り値 (
functype関数: 関数の型情報を処理する部分。t->outnamedの設定ロジックにおいて、匿名戻り値のシンボル名が~r%dであることを考慮するように条件を修正 (s->name[0] != '~' || s->name[1] != 'r')。
-
src/cmd/gc/fmt.c:typefmt関数: 型のフォーマット(表示)を行う部分。t->nname->origを参照する際に、シンボル名が~で始まる場合にs->name[1]をチェックして~r(匿名結果) と~b(ブランク識別子) を区別するように変更。~rの場合はシンボルを空にし、~bの場合は_シンボルをルックアップするようにしました。
exprfmt関数: 式のフォーマットを行う部分。ONAMEノードのフォーマットにおいて、内部シンボル名が~bで始まる場合は_として表示するように特別なケースを追加。
-
src/cmd/gc/walk.c:paramstoheap関数: パラメータがヒープにエスケープするかどうかを判断する部分。- 匿名戻り値 (
v->sym->name[0] == '~') のチェックに加えて、v->sym->name[1] == 'r'を追加し、~rで始まるシンボルを匿名結果として正しく識別するように変更。
- 匿名戻り値 (
-
test/escape5.go:- エスケープ解析のテストファイル。
leaktoret2,leaktoret22,leaktoret22b,leaktoret22c関数における期待されるエラーメッセージが、内部シンボル名の変更に合わせて.anonから~rに更新されました。これは、コンパイラの内部的な命名規則が外部に露出する可能性があることを示しています。
-
test/live.go:- ライブネス解析のテストファイル。
f6()という新しいテスト関数が追加されました。この関数はブランク識別子で命名された戻り値を使用しており、以前のライブネス解析のバグが修正されたことを確認するためのものです。コメントで「_結果に関する混乱が以前はf6へのエントリで_がライブであるという誤ったメッセージを引き起こしていた」と明記されています。
これらの変更は、Goコンパイラの内部的なシンボル管理とノード表現の整合性を高め、特にライブネス解析のような重要な最適化パスの正確性を保証するために不可欠でした。
コアとなるコードの解説
このコミットのコアとなる変更は、src/cmd/gc/dcl.c の funcargs 関数における戻り値の処理と、src/cmd/gc/fmt.c の typefmt および exprfmt 関数におけるシンボル名の表示ロジックに集約されます。
src/cmd/gc/dcl.c の変更
funcargs 関数は、関数の引数と戻り値を処理し、それらをコンパイラの内部表現である Node に変換する役割を担っています。
// 変更前 (簡略化)
if(n->left == N) { // 名前なし戻り値
snprint(namebuf, sizeof(namebuf), "~anon%d", gen++);
n->left = newname(lookup(namebuf));
}
// ...
if(isblank(n->left)) { // ブランク識別子戻り値
// preserve the original in ->orig
nn = nod(OXXX, N, N);
*nn = *n->left;
nn->orig = n->left; // ここで元のノードをorigに保存
snprint(namebuf, sizeof(namebuf), "~anon%d", gen++);
nn->sym = lookup(namebuf);
n->left = nn;
}
// 変更後 (簡略化)
if(n->left == N) { // 名前なし戻り値
// Name so that escape analysis can track it. ~r stands for 'result'.
snprint(namebuf, sizeof(namebuf), "~r%d", gen++); // ~r%d に変更
n->left = newname(lookup(namebuf));
}
// ...
if(isblank(n->left)) { // ブランク識別子戻り値
// Give it a name so we can assign to it during return. ~b stands for 'blank'.
// The name must be different from ~r above because if you have
// func f() (_ int)
// func g() int
// f is allowed to use a plain 'return' with no arguments, while g is not.
// So the two cases must be distinguished.
// We do not record a pointer to the original node (n->orig).
// Having multiple names causes too much confusion in later passes.
nn = nod(OXXX, N, N);
*nn = *n->left;
nn->orig = nn; // ここを nn 自身を指すように変更
snprint(namebuf, sizeof(namebuf), "~b%d", gen++); // ~b%d に変更
nn->sym = lookup(namebuf);
n->left = nn;
}
この変更のポイントは以下の通りです。
- 命名規則の明確化: 名前なし戻り値には
~r%d、ブランク識別子戻り値には~b%dという異なる内部シンボル名を割り当てることで、コンパイラ内部で両者を明確に区別できるようにしました。 n->origの統一: ブランク識別子戻り値のONAMEノードもn->orig = nnとすることで、ONAMEノードに関してはorigフィールドが常にノード自身を指すように統一しました。これにより、origフィールドの特殊な扱いを必要とするコードパスが不要になり、ライブネス解析のような他のパスでの混乱を防ぎます。
src/cmd/gc/fmt.c の変更
fmt.c は、コンパイラの内部表現を人間が読める形式(例えば、エラーメッセージやデバッグ出力)にフォーマットする役割を担っています。
// typefmt 関数内の変更 (簡略化)
// 変更前
// Take the name from the original, lest we substituted it with ~anon%d
if ((fmtmode == FErr || fmtmode == FExp) && t->nname != N) {
if(t->nname->orig != N) {
s = t->nname->orig->sym;
if(s != S && s->name[0] == '~')
s = S;
} else
s = S;
}
// 変更後
// Take the name from the original, lest we substituted it with ~r%d or ~b%d.
// ~r%d is a (formerly) unnamed result.
if ((fmtmode == FErr || fmtmode == FExp) && t->nname != N) {
if(t->nname->orig != N) {
s = t->nname->orig->sym;
if(s != S && s->name[0] == '~') {
if(s->name[1] == 'r') // originally an unnamed result
s = S; // 名前なしなので表示しない
else if(s->name[1] == 'b') // originally the blank identifier _
s = lookup("_"); // _ として表示
}
} else
s = S;
}
// exprfmt 関数内の変更 (簡略化)
// 変更前
// if(fmtmode == FExp && n->sym && !isblanksym(n->sym) && n->vargen > 0)
// return fmtprint(f, "%S·%d", n->sym, n->vargen);
// 変更後
// _ becomes ~b%d internally; print as _ for export
if(fmtmode == FExp && n->sym && n->sym->name[0] == '~' && n->sym->name[1] == 'b')
return fmtprint(f, "_"); // ~b%d は _ として表示
if(fmtmode == FExp && n->sym && !isblank(n) && n->vargen > 0)
return fmtprint(f, "%S·%d", n->sym, n->vargen);
これらの変更により、コンパイラが内部的に ~r%d や ~b%d といったシンボル名を使用していても、外部に表示される際には、名前なし戻り値は表示されず、ブランク識別子戻り値は正しく _ として表示されるようになります。これは、コンパイラの内部実装とユーザーに見える挙動との間の整合性を保つ上で重要です。
これらの変更は、Goコンパイラの内部的な堅牢性を高め、特定のバグを修正しつつ、将来的な拡張性も考慮に入れたものです。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Goコンパイラのソースコード (
cmd/gc): https://github.com/golang/go/tree/master/src/cmd/compile (現在のGoコンパイラはcmd/compileに統合されていますが、このコミット当時はcmd/gcでした) - Go言語のブランク識別子に関する公式ドキュメント: https://go.dev/doc/effective_go#blank
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な情報 (Goの公式ドキュメントやブログ記事)
- Goコンパイラのソースコード (
src/cmd/gcディレクトリ内のファイル) - Go言語のライブネス解析やエスケープ解析に関する技術記事や論文 (一般的なコンパイラ最適化の概念)
- コミットメッセージと差分 (GitHubのコミットページ)
- Goのコードレビューシステム (Gerrit) の該当CL (Change List): https://golang.org/cl/63630043 (コミットメッセージに記載)