[インデックス 10530] ファイルの概要
このコミットは、Goコンパイラの型チェックフェーズにおける、関数引数(特にブランク識別子 _
を使用した引数)の特殊な処理を削除し、より汎用的で堅牢なメカニズムに置き換えるものです。これにより、インターフェースのメソッドシグネチャでブランク識別子が使用された場合でも、その引数が正しく処理されるようになります。
コミット
commit 8e515485e277b982ce4265d72b0d92f76242b651
Author: Russ Cox <rsc@golang.org>
Date: Mon Nov 28 16:40:39 2011 -0500
gc: remove funarg special case in structfield
This should make CL 5431046 a little simpler.
R=ken2
CC=golang-dev
https://golang.org/cl/5444048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8e515485e277b982ce4265d72b0d92f76242b651
元コミット内容
gc: remove funarg special case in structfield
This should make CL 5431046 a little simpler.
R=ken2
CC=golang-dev
https://golang.org/cl/5444048
変更の背景
Go言語では、ブランク識別子 _
を使用して、変数を宣言しつつその値を破棄することができます。これは、例えば関数の戻り値のうち一部だけが必要な場合や、インターフェースのメソッドシグネチャで特定の引数の名前が重要でないことを示す場合などに利用されます。
このコミット以前のGoコンパイラ(gc
)では、関数引数としてブランク識別子 _
が使われた場合に、その引数に対する特別な処理(シンボルを解除するなど)が行われていました。しかし、この特殊な処理が、特にインターフェースのメソッドと具象型のメソッドが一致するかどうかを型チェックする際に、予期せぬ挙動や複雑さを引き起こす可能性がありました。
具体的には、type I interface { M(_ int) }
のようにインターフェースメソッドの引数に _
が使われている場合、コンパイラは「この引数の値は不要である」と解釈し、呼び出し時にその引数への値の割り当てを抑制してしまう可能性がありました。しかし、インターフェースを実装する具象型(例: func (T) M(x int)
)は、その引数 x
の値を必要とする場合があります。この不一致が問題となるため、ブランク識別子であっても、その引数に値が渡されるべき状況では正しく処理されるようにする必要がありました。
このコミットは、既存の特殊ケースを削除し、より一貫性のある方法でブランク識別子を扱うことで、コンパイラのロジックを簡素化し、同時にインターフェースのセマンティクスをより正確に反映させることを目的としています。コミットメッセージにある CL 5431046
は、この変更によって簡素化される別の変更リストを指していると考えられますが、公開されている情報からは詳細を特定できませんでした。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラの内部構造とGo言語の概念に関する知識が必要です。
-
Goコンパイラ (
gc
) の構造:src/cmd/gc/
: Goコンパイラの主要なソースコードが置かれているディレクトリです。dcl.c
(Declaration): 変数や型の宣言に関する処理を扱う部分です。関数引数の処理もここに含まれます。subr.c
(Subroutines): コンパイラの様々なユーティリティ関数や補助的な処理が含まれます。typecheck.c
(Type Checking): 型チェックのロジックを実装している部分です。Goの型システムにおける互換性や正当性を検証します。Node
: コンパイラがソースコードを解析して生成する抽象構文木 (AST) のノードを表す構造体です。Type
: Goの型システムにおける型を表す構造体です。Sym
(Symbol): 変数名、関数名、型名などの識別子(シンボル)を表す構造体です。
-
ブランク識別子 (
_
):- Go言語の特殊な識別子で、値を破棄するために使用されます。例えば、
_ = someFunc()
はsomeFunc
の戻り値を破棄します。 - 関数やメソッドの引数リストで
_
を使用すると、その引数の値は受け取られますが、その引数名を使ってコード内で参照することはできません。例:func foo(_ int, bar string)
。
- Go言語の特殊な識別子で、値を破棄するために使用されます。例えば、
-
インターフェースとメソッド:
- Goのインターフェースは、メソッドのシグネチャの集合を定義します。
- 具象型がインターフェースを実装するには、インターフェースで定義されたすべてのメソッドを、そのシグネチャ(メソッド名、引数の型、戻り値の型)と完全に一致する形で実装する必要があります。
- インターフェースのメソッドシグネチャにブランク識別子が含まれる場合(例:
M(_ int)
)、その引数は「名前がない」と見なされますが、型は存在します。
-
structfield
とtofunargs
:structfield
は、構造体のフィールドや関数引数などの要素を表現するための内部的な構造体フィールド情報を生成する関数です。tofunargs
は、関数引数リストを処理し、それらをコンパイラ内部の表現に変換する関数です。
-
trampoline
(トランポリン):- Goコンパイラ内部で、特定の呼び出し規約やコンテキストの変換が必要な場合に生成される小さなコードスニペット(ラッパー関数)を指すことがあります。例えば、メソッド呼び出しの際にレシーバの型を調整したり、引数の渡し方を調整したりするために使われます。
技術的詳細
このコミットの核心は、Goコンパイラがブランク識別子 _
を含む関数引数やインターフェースメソッドの引数をどのように扱うかというセマンティクスの変更にあります。
以前のコンパイラでは、src/cmd/gc/dcl.c
の tofunargs
関数内に、ブランク識別子 _
を持つ引数に対して f->nname = nil; f->sym = nil; f->embedded = 0;
のようにシンボル情報を明示的に解除する特殊なロジックが存在しました。これは、_
引数がコード内で参照されないため、そのシンボル情報を不要と見なすための最適化、あるいは単純化の試みだったと考えられます。
しかし、このアプローチは、特にインターフェースのメソッドシグネチャと具象型のメソッド実装の間の型チェックにおいて問題を引き起こす可能性がありました。インターフェース I
が M(_ int)
というメソッドを持つ場合、_
はその引数の名前が重要でないことを示しますが、int
型の引数自体は存在し、具象型の実装は func (T) M(x int)
のようにその引数 x
を利用するかもしれません。もしコンパイラが _
引数のシンボルを完全に解除し、その引数への値の割り当てを抑制してしまうと、具象型のメソッドが期待する値を受け取れなくなるという不整合が生じます。
このコミットでは、この問題を解決するために以下の変更が行われました。
-
src/cmd/gc/dcl.c
からの特殊ケースの削除:tofunargs
関数から、ブランク識別子_
引数に対するシンボル解除のロジックが削除されました。これにより、_
引数も他の引数と同様に、少なくとも内部的にはシンボル情報を持つか、あるいは後続の処理で適切に扱われるようになります。 -
src/cmd/gc/subr.c
のstructargs
の変更:structargs
関数は、構造体のフィールドや関数引数(内部的には構造体フィールドとして扱われることがある)に対して名前を割り当てる役割を担います。変更前は、t->sym
が存在すればそれを使用し、そうでなければ(かつmustname
が真であれば)匿名名を生成していました。変更後は、mustname
が真の場合に、t->sym
がnil
であるか、またはその名前が_
である場合に、明示的に匿名名を生成するようになりました。これは、ブランク識別子であっても、トランポリンなどの内部的な参照のために名前が必要な場合に、匿名名を割り当てることを保証します。これにより、_
引数も内部的に識別可能な形で扱われるようになります。 -
src/cmd/gc/typecheck.c
のdomethod
へのロジック追加: これが最も重要な変更点です。domethod
関数は、メソッドの型チェックを行う際に呼び出されます。この関数内に、インターフェースメソッドの引数にブランク識別子_
が使われている場合の新しいロジックが追加されました。// If we have // type I interface { // M(_ int) // } // then even though I.M looks like it doesn't care about the // value of its argument, a specific implementation of I may // care. The _ would suppress the assignment to that argument // while generating a call, so remove it. for(t=getinargx(nt->type)->type; t; t=t->down) { if(t->sym != nil && strcmp(t->sym->name, "_") == 0) t->sym = nil; }
このコードは、インターフェースメソッドの入力引数を走査し、もし引数のシンボルが存在し、かつその名前が
_
であれば、そのシンボルをnil
に設定します。この操作は、dcl.c
で削除されたロジックと似ていますが、実行されるコンテキストが異なります。typecheck.c
でこの処理を行うことで、インターフェースの型チェック時に、_
引数が「名前は持たないが、値は渡されるべき通常の引数」として扱われるようになります。これにより、呼び出し生成時に値の割り当てが抑制されることを防ぎ、具象型のメソッドが期待通りに引数を受け取れるようになります。 -
src/cmd/gc/subr.c
のデバッグ出力の有効化:genwrapper
関数内のif(0 && debug['r'])
がif(debug['r'])
に変更され、デバッグ出力が有効になりました。これは直接的な機能変更ではありませんが、コンパイラのデバッグ可能性を向上させます。
これらの変更により、Goコンパイラはブランク識別子 _
を含む引数を、より一貫性のある方法で処理するようになります。特にインターフェースの文脈において、_
は「名前は不要だが、値は渡される」というセマンティクスを正確に反映するようになります。
コアとなるコードの変更箇所
src/cmd/gc/dcl.c
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -840,13 +839,6 @@ tofunargs(NodeList *l)
for(tp = &t->type; l; l=l->next) {
f = structfield(l->n);
- // Unlink the name for _ arguments.
- if(l->n->left && l->n->left->op == ONAME && isblank(l->n->left)) {
- f->nname = nil;
- f->sym = nil;
- f->embedded = 0;
- }
-
// esc.c needs to find f given a PPARAM to add the tag.
if(l->n->left && l->n->left->class == PPARAM)
l->n->left->paramfld = f;
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2226,13 +2226,12 @@ structargs(Type **tl, int mustname)
gen = 0;
for(t = structfirst(&savet, tl); t != T; t = structnext(&savet)) {
n = N;
- if(t->sym)
- n = newname(t->sym);
- else if(mustname) {
- // have to give it a name so we can refer to it in trampoline
+ if(mustname && (t->sym == nil || strcmp(t->sym->name, "_") == 0)) {
+ // invent a name so that we can refer to it in the trampoline
snprint(buf, sizeof buf, ".anon%d", gen++);
n = newname(lookup(buf));
- }
+ } else if(t->sym)
+ n = newname(t->sym);
a = nod(ODCLFIELD, n, typenod(t->type));
a->isddd = t->isddd;
if(n != N)
@@ -2274,7 +2273,7 @@ genwrapper(Type *rcvr, Type *method, Sym *newnam, int iface)
int isddd;
Val v;
- if(0 && debug['r'])
+ if(debug['r'])
print("genwrapper rcvrtype=%T method=%T newnam=%S\n",
rcvr, method, newnam);
src/cmd/gc/typecheck.c
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2465,6 +2465,7 @@ static void
domethod(Node *n)
{
Node *nt;
+ Type *t;
nt = n->type->nname;
typecheck(&nt, Etype);
@@ -2474,6 +2475,20 @@ domethod(Node *n)
n->type->nod = N;
return;
}\n+\t\n+\t// If we have
+\t//\ttype I interface {\n+\t//\t\tM(_ int)\n+\t//\t}\n+\t// then even though I.M looks like it doesn\'t care about the
+\t// value of its argument, a specific implementation of I may
+\t// care. The _ would suppress the assignment to that argument
+\t// while generating a call, so remove it.\n+\tfor(t=getinargx(nt->type)->type; t; t=t->down) {\n+\t\tif(t->sym != nil && strcmp(t->sym->name, \"_\") == 0)\n+\t\t\tt->sym = nil;\n+\t}\n+\n \t*n->type = *nt->type;\n \tn->type->nod = N;\n \tcheckwidth(n->type);\
test/blank.go
--- a/test/blank.go
+++ b/test/blank.go
@@ -101,6 +101,29 @@ func main() {
}\n \th(a, b)\n+\t\n+\tm()\n+}\n+\n+type I interface {\n+\tM(_ int, y int)\n+}\n+\n+type TI struct{}\n+\n+func (TI) M(x int, y int) {\n+\tif x != y {\n+\t\tprintln(\"invalid M call:\", x, y)\n+\t\tpanic(\"bad M\")\n+\t}\n+}\n+\n+func m() {\n+\tvar i I\n+\t\n+\ti = TI{}\n+\ti.M(1, 1)\n+\ti.M(2, 2)\n }\n \n // useless but legal\n@@ -120,3 +143,4 @@ func _() {\n func ff() {\n \tvar _ int = 1\n }\n+\n```
## コアとなるコードの解説
このコミットの主要な変更は、ブランク識別子 `_` を含む引数の処理を、コンパイラの異なるステージで、より適切な方法で行うように再配置した点です。
1. **`src/cmd/gc/dcl.c` の変更**:
`tofunargs` 関数から、ブランク識別子 `_` を持つ引数のシンボルを明示的に解除するコードが削除されました。これは、この段階でシンボルを解除することが、後続の型チェックやコード生成において問題を引き起こす可能性があったためです。この変更により、`_` 引数も一時的にシンボル情報を持つか、あるいは後続の処理でより柔軟に扱われるようになります。
2. **`src/cmd/gc/subr.c` の変更**:
`structargs` 関数では、`mustname` が真(つまり、引数に名前が必要な場合)で、かつ引数のシンボルが `nil` であるか、または `_` である場合に、匿名名を生成するロジックが追加されました。これは、`_` 引数であっても、内部的な処理(例えば、トランポリンコードの生成)のために一時的な名前が必要な場合に、それが確実に割り当てられるようにするためです。これにより、`_` 引数が完全に「名前なし」として扱われることによる問題を防ぎます。
3. **`src/cmd/gc/typecheck.c` の変更**:
`domethod` 関数に新しいロジックが追加されました。このロジックは、インターフェースメソッドの引数を走査し、もし引数のシンボルが `_` であれば、そのシンボルを `nil` に設定します。この処理は、インターフェースの型チェック時に行われるため、`_` 引数が「名前は持たないが、値は渡されるべき通常の引数」として扱われることを保証します。これにより、インターフェースのメソッドシグネチャに `_` が含まれていても、それを実装する具象型のメソッドが引数の値を受け取れるようになります。これは、`dcl.c` で削除されたロジックの目的を、より適切なコンテキスト(型チェック時)で、より正確なセマンティクス(値は渡される)で実現するものです。
4. **`test/blank.go` の追加**:
新しいテストケースが追加され、インターフェースのメソッドシグネチャにブランク識別子 `_` が含まれる場合の挙動が検証されます。このテストでは、`I` インターフェースが `M(_ int, y int)` メソッドを持ち、`TI` 型が `M(x int, y int)` を実装しています。テストコード `i.M(1, 1)` と `i.M(2, 2)` は、`_` に対応する引数 `x` にも値が正しく渡されていることを `if x != y` のチェックで確認しています。これにより、コンパイラの変更が意図した通りに機能し、`_` 引数であっても値が正しく伝播することが保証されます。
これらの変更は、Goコンパイラがブランク識別子をより正確かつ一貫性のある方法で処理するための重要な改善であり、特にインターフェースと具象型の間の互換性を向上させます。
## 関連リンク
* Go言語のブランク識別子に関する公式ドキュメント: [https://go.dev/doc/effective_go#blank](https://go.dev/doc/effective_go#blank)
* Goコンパイラのソースコードリポジトリ: [https://github.com/golang/go](https://github.com/golang/go)
## 参考にした情報源リンク
* GitHub上のコミットページ: [https://github.com/golang/go/commit/8e515485e277b982ce4265d72b0d92f76242b651](https://github.com/golang/go/commit/8e515485e277b982ce4265d72b0d92f76242b651)
* Go言語の公式ドキュメント (Effective Go): [https://go.dev/doc/effective_go](https://go.dev/doc/effective_go)
* Goコンパイラの内部構造に関する一般的な情報源 (例: Goのソースコードを解説しているブログや書籍など)