[インデックス 10307] ファイルの概要
このコミットは、Goコンパイラ(gc)における、関数の戻り値パラメータの匿名変数(ブランク識別子 _ で宣言された変数)の扱いに関するバグ修正です。具体的には、匿名変数に内部的に割り当てられる名前(.anon%d)が、デバッグ情報やエラーメッセージの表示時に元のブランク識別子を上書きしてしまう問題を解決します。
変更されたファイルは以下の通りです。
src/cmd/gc/dcl.c: Goコンパイラの宣言処理(declaration)を担当する部分。匿名戻り値パラメータの内部名生成ロジックが変更されています。src/cmd/gc/fmt.c: Goコンパイラの型フォーマット(type formatting)を担当する部分。型情報の表示時に、匿名変数の元の名前を正しく参照するように修正されています。test/fixedbugs/bug377.dir/one.go: バグを再現するためのテストケースの一部。test/fixedbugs/bug377.dir/two.go: バグを再現するためのテストケースの一部。test/fixedbugs/bug377.go: バグを再現するためのテストスクリプト。
コミット
このコミットは、Goコンパイラが関数の戻り値パラメータとしてブランク識別子(_)を使用した場合に、その変数の元の名前(ブランク識別子)を保持するように修正します。これにより、デバッグ時やエラー報告時に、内部的に生成された匿名名(.anon%d)ではなく、元のブランク識別子として表示されるようになります。これは、Go Issue 1802で報告された問題を修正するものです。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d5a5855ba19fad0e3b237c5c77c5575af1690d92
元コミット内容
commit d5a5855ba19fad0e3b237c5c77c5575af1690d92
Author: Luuk van Dijk <lvd@golang.org>
Date: Wed Nov 9 11:27:27 2011 +0100
gc: Preserve original blank name for .anon substitution on out params.
Fixes #1802.
R=rsc
CC=golang-dev
https://golang.org/cl/5364043
---
src/cmd/gc/dcl.c | 9 +++++----\
src/cmd/gc/fmt.c | 28 +++++++++++++++++++++-------\
test/fixedbugs/bug377.dir/one.go | 6 ++++++\
test/fixedbugs/bug377.dir/two.go | 4 ++++\
test/fixedbugs/bug377.go | 9 +++++++++
5 files changed, 45 insertions(+), 11 deletions(-)
変更の背景
このコミットは、Go言語のIssue 1802「gc: blank out parameters show up as .anon%d」を修正するために行われました。
Go言語では、関数の戻り値に名前を付けることができます。例えば、func foo() (result int) のように書けます。また、戻り値に名前を付けたくないが、型だけを指定したい場合は、ブランク識別子 _ を使用することができます。例えば、func foo() (_ int) のように書きます。
Goコンパイラ(gc)は、このようなブランク識別子で宣言された戻り値パラメータを内部的に処理するために、一時的な匿名変数(例: .anon0, .anon1 など)を生成します。これは、コンパイラがこれらの戻り値を内部的に参照し、値を割り当てるために必要です。
しかし、この匿名変数の名前が、デバッグ情報やエラーメッセージ、あるいはコンパイラ内部の型情報の表示時に、元のブランク識別子を上書きしてしまうという問題がありました。これにより、ユーザーが意図的にブランク識別子を使用したにもかかわらず、コンパイラの出力に予期せぬ内部名が表示され、混乱を招く可能性がありました。特に、エラーメッセージやデバッグ出力において、ユーザーが書いたコードと異なる名前が表示されることは、デバッグの妨げとなります。
このコミットの目的は、コンパイラが内部的に匿名名を生成する際にも、元のブランク識別子という情報を保持し、必要に応じて元の名前(ブランク識別子)を表示できるようにすることです。
前提知識の解説
このコミットを理解するためには、Goコンパイラの基本的な構造と、Go言語の特定の機能に関する知識が必要です。
-
Goコンパイラ (
gc):- Go言語の公式コンパイラの一つで、C言語で書かれています(当時は)。
- コンパイルプロセスは、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成といった段階に分かれています。
src/cmd/gcディレクトリには、コンパイラの主要なソースコードが含まれています。dcl.c: Declaration(宣言)処理を担当するファイルです。変数、関数、型などの宣言を解析し、コンパイラ内部のデータ構造(AST: 抽象構文木)に変換します。関数の引数や戻り値の処理もここで行われます。fmt.c: Formatting(フォーマット)処理を担当するファイルです。コンパイラ内部のデータ構造(型、ノード、シンボルなど)を、人間が読める形式の文字列に変換する機能を提供します。デバッグ出力やエラーメッセージの生成に利用されます。
-
ブランク識別子 (
_):- Go言語の特殊な識別子で、値を破棄したい場合や、変数を宣言するがその値を使用しないことを明示したい場合に使用されます。
- 関数の戻り値パラメータとして使用される場合、その戻り値は名前を持たないことを意味します。例:
func f() (int, _ string)は、int型の戻り値と、名前のないstring型の戻り値を持つ関数です。
-
コンパイラ内部のデータ構造:
Node: 抽象構文木(AST)のノードを表す構造体です。Goのソースコードの各要素(変数、式、文、関数など)はNodeとして表現されます。Type: 型情報を表す構造体です。int,string,func,structなどの型定義が含まれます。Sym(Symbol): シンボルテーブルのエントリを表す構造体です。変数名、関数名、型名などの識別子と、それに関連する情報(型、スコープなど)を管理します。
-
関数の戻り値パラメータの処理:
- Goでは、関数の戻り値に名前を付けることができます。名前付き戻り値は、関数内で通常の変数として扱われ、
returnステートメントで明示的に値を返さなくても、関数の最後に自動的にその変数の値が返されます。 - ブランク識別子
_を使用して戻り値パラメータを宣言した場合でも、コンパイラは内部的にその戻り値のためのストレージを確保し、アクセス可能なように一時的な名前を割り当てます。
- Goでは、関数の戻り値に名前を付けることができます。名前付き戻り値は、関数内で通常の変数として扱われ、
技術的詳細
このコミットの技術的な核心は、Goコンパイラがブランク識別子で宣言された戻り値パラメータを処理する際に、その「元の名前がブランク識別子であった」という情報を保持し、必要に応じてその情報を利用できるようにすることです。
Goコンパイラは、関数の戻り値パラメータを処理する際に、funcargs 関数(src/cmd/gc/dcl.c 内)で各パラメータを Node として表現します。もしパラメータがブランク識別子(isblank(n->left))である場合、コンパイラは内部的に ".anon%d" のような名前を生成し、その Node のシンボル(n->left->sym)として割り当てます。これは、コンパイラがその匿名変数にアクセスし、値を割り当てるために必要です。
しかし、この処理だけでは、元のブランク識別子であるという情報が失われてしまいます。fmt.c のようなフォーマット処理を行う部分では、Node や Type に関連付けられたシンボル名を参照して文字列を生成します。このとき、シンボル名が既に ".anon%d" に置き換えられていると、元の意図(ブランク識別子)が反映されず、デバッグ出力などで不自然な表示になってしまいます。
このコミットでは、この問題を解決するために以下の変更を導入しています。
-
src/cmd/gc/dcl.cにおけるorigフィールドの導入:- ブランク識別子で宣言された戻り値パラメータに対して、内部的な匿名名(
.anon%d)を割り当てる前に、元のNodeのコピーを作成し、そのコピーを新しいNodeのorigフィールドに保存します。 - これにより、新しい
Nodeは匿名名を持ちつつも、origフィールドを通じて元のブランク識別子であったNodeを参照できるようになります。
- ブランク識別子で宣言された戻り値パラメータに対して、内部的な匿名名(
-
src/cmd/gc/fmt.cにおけるorigフィールドの利用:- 型情報をフォーマットする
typefmt関数内で、TFIELD(構造体のフィールドや関数のパラメータを表す型)を処理する際に、シンボル名を表示するロジックが変更されています。 - 特に、エラーモード(
FErr)やエクスポートモード(FExp)の場合に、t->nname(Nodeへのポインタ)が存在し、そのNodeのorigフィールドが設定されている場合、t->nname->orig->symを参照して元のシンボル名(この場合はブランク識別子)を取得するように変更されています。 - これにより、匿名名が割り当てられた変数であっても、元のブランク識別子として表示されるようになります。
- 型情報をフォーマットする
この修正により、コンパイラは内部的な処理のために匿名名を使いつつも、ユーザーへの表示やデバッグ情報においては、元のソースコードの意図(ブランク識別子)を正確に反映できるようになります。
コアとなるコードの変更箇所
src/cmd/gc/dcl.c
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -573,7 +573,7 @@ funchdr(Node *n)
static void
funcargs(Node *nt)
{
- Node *n;
+ Node *n, *nn;
NodeList *l;
int gen;
@@ -615,6 +615,10 @@ funcargs(Node *nt)
n->left->ntype = n->right;
if(isblank(n->left)) {
// Give it a name so we can assign to it during return.
+ // preserve the original in ->orig
+ nn = nod(OXXX, N, N);
+ *nn = *n->left;
+ n->left = nn;
snprint(namebuf, sizeof(namebuf), ".anon%d", gen++);
n->left->sym = lookup(namebuf);
}
src/cmd/gc/fmt.c
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -542,6 +542,7 @@ static int
typefmt(Fmt *fp, Type *t)
{
Type *t1;
+ Sym *s;
if(t == T)
return fmtstrcpy(fp, "<T>");
@@ -680,10 +681,23 @@ typefmt(Fmt *fp, Type *t)
case TFIELD:
if(!(fp->flags&FmtShort)) {
- if(t->sym != S && !t->embedded)
- fmtprint(fp, "%hS ", t->sym);
- if((!t->sym || t->embedded) && fmtmode == FExp)
- fmtstrcpy(fp, "? ");
+ s = t->sym;
+ switch(fmtmode) {
+ case FErr:
+ case FExp:
+ // Take the name from the original, lest we substituted it with .anon%d
+ if (t->nname)
+ s = t->nname->orig->sym;
+
+ if((s == S || t->embedded)) {
+ fmtstrcpy(fp, "? ");
+ break;
+ }
+ // fallthrough
+ default:
+ if(!(s == S || t->embedded))
+ fmtprint(fp, "%hS ", s);
+ }
}
if(t->isddd)
コアとなるコードの解説
src/cmd/gc/dcl.c の変更
funcargs 関数は、関数の引数と戻り値の宣言を処理します。
変更前のコードでは、ブランク識別子 _ で宣言された戻り値パラメータ(isblank(n->left) が真の場合)に対して、直接 n->left->sym に ".anon%d" のような匿名名を割り当てていました。これにより、元のブランク識別子であるという情報が失われていました。
変更後のコードでは、以下のステップが追加されています。
nn = nod(OXXX, N, N);: 新しいNodennを作成します。OXXXは汎用的なオペレーションタイプです。*nn = *n->left;: 元のブランク識別子を表すNoden->leftの内容を、新しく作成したnnにコピーします。これにより、nnは元のNodeのすべての情報(型、位置など)を保持します。n->left = nn;:n->leftが、新しく作成したnnを指すように変更します。これにより、n->leftは匿名名が割り当てられる新しいNodeとなります。// preserve the original in ->orig: コメントが示すように、この変更の意図は元の情報をorigフィールドに保存することです。ただし、このパッチではn->left->orig = ...のような直接的な代入は見られません。これは、Node構造体自体にorigフィールドが追加され、*nn = *n->left;のコピー操作によって、nnが元のn->leftの情報を保持し、そのnnがn->leftに代入されることで、間接的に元の情報が保持される、という設計になっている可能性があります。あるいは、Node構造体の定義がこのコミットの範囲外で変更され、origフィールドが追加されていることを前提としている可能性もあります。Goコンパイラのコードベースでは、Node構造体内にNode *orig;のようなフィールドが存在し、これが元のノードを指すために使用されることがあります。この変更は、n->leftが指すNodeを新しいNodeに置き換え、その新しいNodeが元のNodeの情報を保持するようにすることで、匿名名を割り当てつつも元の情報を参照できるようにしています。
この変更により、n->left は匿名名を持つ新しい Node となりますが、その Node は元のブランク識別子の Node の情報を内部的に保持しているため、後続の処理で元の情報を参照することが可能になります。
src/cmd/gc/fmt.c の変更
typefmt 関数は、Goコンパイラ内部の型情報を文字列にフォーマットする役割を担っています。特に TFIELD(関数のパラメータや構造体のフィールド)の型をフォーマットする際に、その名前を表示するロジックが変更されています。
変更前のコードでは、t->sym を直接参照してシンボル名を表示していました。もし t->sym が既に匿名名(.anon%d)に置き換えられている場合、その匿名名が表示されてしまいます。
変更後のコードでは、switch(fmtmode) ブロックが追加され、特に FErr(エラーメッセージ)と FExp(エクスポートされたシンボル)のフォーマットモードにおいて、以下のロジックが導入されています。
if (t->nname):Type構造体tにnnameフィールド(Nodeへのポインタ)が存在するかどうかを確認します。nnameは、この型が関連付けられているNodeを指します。s = t->nname->orig->sym;: もしnnameが存在し、かつそのNodeにorigフィールドが設定されている場合、origフィールドが指す元のNodeのシンボル(sym)を取得し、それをsに代入します。これにより、匿名名ではなく、元のブランク識別子のシンボルが取得されます。if((s == S || t->embedded)): 取得したシンボルsが空(S)であるか、または埋め込みフィールドである場合、"? "を表示します。これは、名前がないか、表示する必要がない場合のエラー表示です。default:: それ以外のフォーマットモードや、origフィールドが利用できない場合は、元のt->symを使用してシンボル名を表示します。
この変更により、エラーメッセージやエクスポートされたシンボルの表示時に、匿名名ではなく、元のブランク識別子(またはユーザーが指定した名前)が優先的に表示されるようになり、より正確で分かりやすい出力が生成されるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/d5a5855ba19fad0e3b237c5c77c5575af1690d92
- Go Issue 1802:
gc: blank out parameters show up as .anon%d: https://go.dev/issue/1802 - Go CL 5364043: https://golang.org/cl/5364043
参考にした情報源リンク
- Go Issue 1802の議論内容
- Goコンパイラのソースコード(
src/cmd/gcディレクトリ内のdcl.cおよびfmt.cの周辺コード) - Go言語のブランク識別子に関する公式ドキュメント
- Go言語のコンパイラ設計に関する一般的な情報
- Go言語のAST(抽象構文木)に関する情報
- Go言語のシンボルテーブルに関する情報