[インデックス 15538] ファイルの概要
このコミットは、Go言語の実験的なSSA (Static Single Assignment) パッケージ exp/ssa
におけるバグ修正とリファクタリングに関するものです。具体的には、埋め込みインターフェースのためのブリッジメソッドが、レシーバーと最初の引数としてインターフェースを二重に渡してしまうバグを修正しています。また、パラメータ名の自動生成と、ブリッジメソッド生成における共通コードの抽出によるリファクタリングも含まれています。
コミット
commit 139160eb30b34ebb289c36fcbc97df5952b56dc9
Author: Alan Donovan <adonovan@google.com>
Date: Fri Mar 1 12:51:19 2013 -0500
exp/ssa: fix bug in bridge method
Bridge methods for embedded interfaces were
passing the interface twice: once as receiver,
once as first param.
Covered by $GOROOT/test/ddd.go.
Also:
- invent names ("arg%d") for parameters if missing.
- refactoring: move common code for bridge methods into
createParams and emitTailCall.
R=gri
CC=golang-dev
https://golang.org/cl/7437047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/139160eb30b34ebb289c36fcbc97df5952b56dc9
元コミット内容
exp/ssa: fix bug in bridge method
Bridge methods for embedded interfaces were
passing the interface twice: once as receiver,
once as first param.
Covered by $GOROOT/test/ddd.go.
Also:
- invent names ("arg%d") for parameters if missing.
- refactoring: move common code for bridge methods into
createParams and emitTailCall.
R=gri
CC=golang-dev
https://golang.org/cl/7437047
変更の背景
このコミットの主な背景は、Go言語のコンパイラが内部的に使用するSSA (Static Single Assignment) 形式のコード生成におけるバグの修正です。特に、インターフェースの埋め込み(embedding)機能に関連するブリッジメソッドの生成に問題がありました。
Go言語では、構造体にインターフェースを埋め込むことで、そのインターフェースが持つメソッドを構造体が直接実装しているかのように振る舞わせることができます。この際、コンパイラは内部的に「ブリッジメソッド」と呼ばれる特殊なメソッドを生成することがあります。これは、埋め込まれたインターフェースのメソッド呼び出しを、実際の型が持つメソッド呼び出しに橋渡し(ブリッジ)するためのものです。
このコミット以前の exp/ssa
パッケージでは、埋め込まれたインターフェースのブリッジメソッドを生成する際に、インターフェースの値をレシーバーとして一度、そして最初の引数としてもう一度、合計二重に渡してしまうというバグが存在していました。これは、関数呼び出しの引数リストが正しく構築されていないことを意味し、結果として不正なSSAコードが生成され、プログラムの誤動作やコンパイルエラーを引き起こす可能性がありました。
また、コードの可読性と保守性を向上させるためのリファクタリングも同時に行われています。具体的には、パラメータに名前がない場合に自動的に「arg%d」のような名前を付与する機能と、ブリッジメソッドの生成ロジックで共通する部分を createParams
および emitTailCall
というヘルパー関数に抽出することで、コードの重複を排除し、よりクリーンな設計を目指しています。
このバグは $GOROOT/test/ddd.go
というテストケースによってカバーされており、このテストが修正の検証に用いられたことが示唆されています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの概念に関する知識が必要です。
-
SSA (Static Single Assignment) 形式: SSAは、コンパイラの最適化フェーズで広く用いられる中間表現(IR)の一種です。SSA形式では、各変数が一度だけ代入されるという特性を持ちます。これにより、データフロー解析や最適化が容易になります。Goコンパイラも内部的にSSA形式を利用して、より効率的な機械語コードを生成しています。
exp/ssa
パッケージは、GoプログラムをSSA形式に変換するための実験的なライブラリでした。 -
インターフェースの埋め込み (Interface Embedding): Go言語のインターフェースは、メソッドのシグネチャの集合を定義します。構造体や他のインターフェースにインターフェースを埋め込むことができます。
type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error } type ReadCloser interface { Reader // Readerインターフェースを埋め込み Closer // Closerインターフェースを埋め込み } type MyFile struct { // ... } func (mf *MyFile) Read(p []byte) (n int, err error) { /* ... */ } func (mf *MyFile) Close() error { /* ... */ } // MyFileはReadCloserインターフェースを実装しているとみなされる
この機能により、コードの再利用性と柔軟性が向上します。
-
ブリッジメソッド (Bridge Methods): Go言語のコンパイラが、インターフェースの埋め込みや型アサーション、型変換などの特定の状況で、内部的に生成する特殊なメソッドです。これらのメソッドは、実行時に適切なメソッド呼び出しに「橋渡し」する役割を担います。例えば、ある型がインターフェースを介してメソッドを呼び出された場合、コンパイラはブリッジメソッドを生成して、そのインターフェースのメソッド呼び出しを、基となる型の実際のメソッド実装にディスパッチします。 今回のバグは、このブリッジメソッドが引数を誤って処理していたことに起因します。
-
レシーバー (Receiver): Go言語のメソッドは、特定の型に関連付けられた関数です。メソッドが呼び出される際、そのメソッドが操作するインスタンスは「レシーバー」として渡されます。例えば
func (r MyType) MyMethod()
のr
がレシーバーです。インターフェースメソッドの場合、インターフェースの値自体がレシーバーとして扱われます。 -
テールコール (Tail Call): 関数呼び出しが、その関数の最後の操作である場合、それをテールコールと呼びます。最適化されたコンパイラは、テールコールを通常の関数呼び出しではなく、ジャンプ命令に変換することで、スタックフレームのオーバーヘッドを削減し、スタックオーバーフローを防ぐことができます。このコミットでは
emitTailCall
という関数が導入されており、ブリッジメソッドが最終的に別のメソッドをテールコールする形で実装されていることが示唆されます。
技術的詳細
このコミットは、主に src/pkg/exp/ssa/promote.go
と src/pkg/exp/ssa/emit.go
の2つのファイルに大きな変更を加えています。
バグ修正の核心:
promote.go
内の makeBridgeMethod
関数は、埋め込みインターフェースのためのブリッジメソッドを生成する役割を担っています。元のコードでは、この関数が生成する Call
オブジェクト(関数呼び出しを表すSSAノード)の引数リスト c.Args
に、レシーバーと最初のパラメータが重複して追加されていました。
具体的には、以下の部分が問題でした(変更前の promote.go
の一部を推測):
// 変更前 (推測)
// makeBridgeMethod 内
if c.IsStatic() { // 静的呼び出しの場合
fn.Pos = c.Func.(*Function).Pos
c.Pos = fn.Pos
c.Args = append(c.Args, v) // レシーバーを追加
for _, arg := range fn.Params[1:] { // 最初のパラメータ以降の引数を追加
c.Args = append(c.Args, arg)
}
} else { // 動的呼び出し (インターフェースメソッド) の場合
c.Recv = v // レシーバーを設定
c.Method = 0
for _, arg := range fn.Params { // 全てのパラメータを引数として追加
c.Args = append(c.Args, arg)
}
}
このコードでは、fn.Params
にはレシーバーを含む全てのパラメータが含まれています。fn.Params[1:]
はレシーバーを除いた引数リストを意味しますが、c.Args = append(c.Args, v)
でレシーバーが既に追加されているにも関わらず、動的呼び出しのケースでは for _, arg := range fn.Params
でレシーバーが再度 c.Args
に追加されていました。これが「インターフェースを二重に渡す」バグの原因でした。
修正とリファクタリング:
このバグは、emitTailCall
関数と createParams
関数への共通ロジックの抽出によって修正されています。
-
emitTailCall
の変更 (src/pkg/exp/ssa/emit.go
):emitTailCall
は、関数f
の末尾でcall
を発行するためのヘルパー関数です。変更前は単にcall
を発行するだけでしたが、変更後はブリッジメソッドの引数処理ロジックを内包するようになりました。// 変更後 emitTailCall func emitTailCall(f *Function, call *Call) { // f の最初の仮パラメータを除く全ての仮パラメータを、call の実パラメータとして追加 for _, arg := range f.Params[1:] { call.Args = append(call.Args, arg) } // call の戻り値の型を f のシグネチャの戻り値の型に設定 call.Type_ = &types.Result{Values: f.Signature.Results} tuple := f.emit(call) // ... (後続の戻り値処理) }
この変更により、
makeBridgeMethod
やmakeImethodThunk
の中で直接引数をc.Args
に追加するロジックが不要になり、emitTailCall
に一元化されました。特にf.Params[1:]
を使用することで、レシーバーが重複して追加されることを防いでいます。 -
createParams
の導入 (src/pkg/exp/ssa/promote.go
):createParams
は、Function
オブジェクトfn
のシグネチャに基づいて、そのパラメータを生成する共通のロジックをカプセル化するために導入されました。// 新規追加 createParams func createParams(fn *Function) { var last *Parameter for i, p := range fn.Signature.Params { name := p.Name if name == "" { name = fmt.Sprintf("arg%d", i) // 名前がない場合に "arg%d" を生成 } last = fn.addParam(name, p.Type) } if fn.Signature.IsVariadic { last.Type_ = &types.Slice{Elt: last.Type_} } }
この関数は、パラメータに名前がない場合に
arg%d
という形式で名前を自動生成する機能も提供します。これにより、SSA形式のデバッグや可読性が向上します。
これらの変更により、makeBridgeMethod
と makeImethodThunk
の両方で、パラメータの生成とテールコールによる引数処理が共通化され、コードの重複が排除されるとともに、バグが修正されました。
コアとなるコードの変更箇所
src/pkg/exp/ssa/builder.go
--- a/src/pkg/exp/ssa/builder.go
+++ b/src/pkg/exp/ssa/builder.go
@@ -23,7 +23,6 @@ package ssa
// TODO(adonovan):
// - fix: support f(g()) where g has multiple result parameters.
-// - fix: multiple labels on same statement.
import (
"fmt"
// - fix: multiple labels on same statement.
のコメントが削除されました。これは直接的な機能変更ではなく、TODOリストの整理です。
src/pkg/exp/ssa/emit.go
--- a/src/pkg/exp/ssa/emit.go
+++ b/src/pkg/exp/ssa/emit.go
@@ -221,11 +221,17 @@ func emitExtract(f *Function, tuple Value, index int, typ types.Type) Value {
return f.emit(e)
}
-// emitTailCall emits to f a function call in tail position.\n+// emitTailCall emits to f a function call in tail position,\n+// passing on all but the first formal parameter to f as actual\n+// values in the call. Intended for delegating bridge methods.\n // Precondition: f does/will not use deferred procedure calls.\n // Postcondition: f.currentBlock is nil.\n //\n func emitTailCall(f *Function, call *Call) {
+\tfor _, arg := range f.Params[1:] {\n+\t\tcall.Args = append(call.Args, arg)\n+\t}\n+\tcall.Type_ = &types.Result{Values: f.Signature.Results}\n \ttuple := f.emit(call)\n \tvar ret Ret\n \tswitch {
emitTailCall
関数のシグネチャは変わっていませんが、内部ロジックが大幅に変更されました。f.Params[1:]
をループして、関数f
の最初の仮パラメータ(レシーバー)を除く全ての仮パラメータをcall.Args
に追加するようになりました。call.Type_
がf.Signature.Results
に基づいて設定されるようになりました。- コメントが更新され、この関数が「ブリッジメソッドの委譲を意図している」こと、および「最初の仮パラメータを除く全ての仮パラメータを実パラメータとして渡す」ことが明記されました。
src/pkg/exp/ssa/promote.go
--- a/src/pkg/exp/ssa/promote.go
+++ b/src/pkg/exp/ssa/promote.go
@@ -272,13 +272,7 @@ func makeBridgeMethod(prog *Program, typ types.Type, cand *candidate) *Function
}\n \tfn.start(nil)\n \tfn.addSpilledParam(sig.Recv)\n-\tvar last *Parameter\n-\tfor _, p := range fn.Signature.Params {\n-\t\tlast = fn.addParam(p.Name, p.Type)\n-\t}\n-\tif fn.Signature.IsVariadic {\n-\t\tlast.Type_ = &types.Slice{Elt: last.Type_}\n-\t}\n+\tcreateParams(fn)\n \n \t// Each bridge method performs a sequence of selections,\n \t// then tailcalls the promoted method.\n@@ -315,22 +309,30 @@ func makeBridgeMethod(prog *Program, typ types.Type, cand *candidate) *Function
\t\tfn.Pos = c.Func.(*Function).Pos // TODO(adonovan): fix: wrong.\n \t\tc.Pos = fn.Pos // TODO(adonovan): fix: wrong.\n \t\tc.Args = append(c.Args, v)\n-\t\tfor _, arg := range fn.Params[1:] {\n-\t\t\tc.Args = append(c.Args, arg)\n-\t\t}\n \t} else {\n \t\tc.Recv = v\n \t\tc.Method = 0\n-\t\tfor _, arg := range fn.Params {\n-\t\t\tc.Args = append(c.Args, arg)\n-\t\t}\n \t}\n-\tc.Type_ = &types.Result{Values: sig.Results}\n \temitTailCall(fn, &c)\n \tfn.finish()\n \treturn fn\n }\n \n+// createParams creates parameters for bridge method fn based on its Signature.\n+func createParams(fn *Function) {\n+\tvar last *Parameter\n+\tfor i, p := range fn.Signature.Params {\n+\t\tname := p.Name\n+\t\tif name == \"\" {\n+\t\t\tname = fmt.Sprintf(\"arg%d\", i)\n+\t\t}\n+\t\tlast = fn.addParam(name, p.Type)\n+\t}\n+\tif fn.Signature.IsVariadic {\n+\t\tlast.Type_ = &types.Slice{Elt: last.Type_}\n+\t}\n+}\n+\n // Thunks for standalone interface methods ----------------------------------------\n \n // makeImethodThunk returns a synthetic thunk function permitting an\n@@ -373,21 +375,10 @@ func makeImethodThunk(prog *Program, typ types.Type, id Id) *Function {\n \t// TODO(adonovan): set fn.Pos to location of interface method ast.Field.\n \tfn.start(nil)\n \tfn.addParam(\"recv\", typ)\n-\tvar last *Parameter\n-\tfor _, p := range fn.Signature.Params {\n-\t\tlast = fn.addParam(p.Name, p.Type)\n-\t}\n-\tif fn.Signature.IsVariadic {\n-\t\tlast.Type_ = &types.Slice{Elt: last.Type_}\n-\t}\n-\n+\tcreateParams(fn)\n \tvar c Call\n \tc.Method = index\n \tc.Recv = fn.Params[0]\n-\tfor _, arg := range fn.Params[1:] {\n-\t\tc.Args = append(c.Args, arg)\n-\t}\n-\tc.Type_ = &types.Result{Values: sig.Results}\n \temitTailCall(fn, &c)\n \tfn.finish()\n \treturn fn\n```
- `makeBridgeMethod` 関数と `makeImethodThunk` 関数から、パラメータ生成と `Call` オブジェクトの引数設定に関する重複したロジックが削除され、それぞれ新しく導入された `createParams` 関数と、変更された `emitTailCall` 関数に置き換えられました。
- 新しいヘルパー関数 `createParams` が追加されました。この関数は、`Function` のシグネチャに基づいてパラメータを生成し、名前がない場合には `arg%d` という形式で名前を割り当てます。
## コアとなるコードの解説
このコミットのコアとなる変更は、ブリッジメソッドの生成ロジックの修正と共通化です。
1. **`emitTailCall` の役割の拡張**:
以前の `emitTailCall` は単にテールコールを発行するだけでしたが、このコミットにより、ブリッジメソッドが呼び出す実際の関数への引数渡しロジックも担当するようになりました。
`for _, arg := range f.Params[1:] { call.Args = append(call.Args, arg) }` の行が重要です。これは、ブリッジメソッド `f` の仮パラメータリストから、最初のパラメータ(通常はレシーバー)を除いた残りの全てのパラメータを、実際に呼び出す `call` の実引数として追加しています。これにより、レシーバーが二重に渡されるバグが修正されました。また、`call.Type_ = &types.Result{Values: f.Signature.Results}` は、呼び出しの戻り値の型を正しく設定しています。
2. **`createParams` の導入によるパラメータ生成の共通化**:
`createParams` 関数は、`makeBridgeMethod` と `makeImethodThunk` の両方で必要だった、関数パラメータの生成ロジックを共通化するために導入されました。
`if name == "" { name = fmt.Sprintf("arg%d", i) }` の部分は、パラメータに明示的な名前が与えられていない場合に、`arg0`, `arg1` のように自動的に名前を生成します。これは、SSA形式のデバッグや解析において、パラメータの識別を容易にするための改善です。
これらの変更により、ブリッジメソッドの生成がより堅牢になり、バグが修正されるとともに、コードベースの保守性が向上しました。特に、引数処理のロジックが `emitTailCall` に集約されたことで、将来的な変更やデバッグが容易になります。
## 関連リンク
- Go言語のSSAパッケージに関する公式ドキュメントやブログ記事 (当時の `exp/ssa` は実験的なものであり、現在はGoコンパイラに統合されています)
- Go言語のインターフェースの埋め込みに関する公式ドキュメント
- Go言語のコンパイラ内部構造に関する資料
## 参考にした情報源リンク
- [https://golang.org/cl/7437047](https://golang.org/cl/7437047) (元のGerritチェンジリスト)
- Go言語の公式ドキュメント (インターフェース、メソッド、SSAに関する記述)
- コンパイラ設計に関する一般的な情報源 (SSA形式、中間表現など)
- Go言語のソースコード (特に `src/cmd/compile` や `src/go/types` パッケージ)
- Go言語のテストスイート (`$GOROOT/test/ddd.go` など)
# [インデックス 15538] ファイルの概要
このコミットは、Go言語の実験的なSSA (Static Single Assignment) パッケージ `exp/ssa` におけるバグ修正とリファクタリングに関するものです。具体的には、埋め込みインターフェースのためのブリッジメソッドが、レシーバーと最初の引数としてインターフェースを二重に渡してしまうバグを修正しています。また、パラメータ名の自動生成と、ブリッジメソッド生成における共通コードの抽出によるリファクタリングも含まれています。
## コミット
commit 139160eb30b34ebb289c36fcbc97df5952b56dc9 Author: Alan Donovan adonovan@google.com Date: Fri Mar 1 12:51:19 2013 -0500
exp/ssa: fix bug in bridge method
Bridge methods for embedded interfaces were
passing the interface twice: once as receiver,
once as first param.
Covered by $GOROOT/test/ddd.go.
Also:
- invent names ("arg%d") for parameters if missing.
- refactoring: move common code for bridge methods into
createParams and emitTailCall.
R=gri
CC=golang-dev
https://golang.org/cl/7437047
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/139160eb30b34ebb289c36fcbc97df5952b56dc9](https://github.com/golang/go/commit/139160eb30b34ebb289c36fcbc97df5952b56dc9)
## 元コミット内容
exp/ssa: fix bug in bridge method
Bridge methods for embedded interfaces were passing the interface twice: once as receiver, once as first param. Covered by $GOROOT/test/ddd.go.
Also:
- invent names ("arg%d") for parameters if missing.
- refactoring: move common code for bridge methods into createParams and emitTailCall.
R=gri CC=golang-dev https://golang.org/cl/7437047
## 変更の背景
このコミットの主な背景は、Go言語のコンパイラが内部的に使用するSSA (Static Single Assignment) 形式のコード生成におけるバグの修正です。特に、インターフェースの埋め込み(embedding)機能に関連するブリッジメソッドの生成に問題がありました。
Go言語では、構造体にインターフェースを埋め込むことで、そのインターフェースが持つメソッドを構造体が直接実装しているかのように振る舞わせることができます。この際、コンパイラは内部的に「ブリッジメソッド」と呼ばれる特殊なメソッドを生成することがあります。これは、埋め込まれたインターフェースのメソッド呼び出しを、実際の型が持つメソッド呼び出しに橋渡し(ブリッジ)するためのものです。
このコミット以前の `exp/ssa` パッケージでは、埋め込まれたインターフェースのブリッジメソッドを生成する際に、インターフェースの値をレシーバーとして一度、そして最初の引数としてもう一度、合計二重に渡してしまうというバグが存在していました。これは、関数呼び出しの引数リストが正しく構築されていないことを意味し、結果として不正なSSAコードが生成され、プログラムの誤動作やコンパイルエラーを引き起こす可能性がありました。
また、コードの可読性と保守性を向上させるためのリファクタリングも同時に行われています。具体的には、パラメータに名前がない場合に自動的に「arg%d」のような名前を付与する機能と、ブリッジメソッドの生成ロジックで共通する部分を `createParams` および `emitTailCall` というヘルパー関数に抽出することで、コードの重複を排除し、よりクリーンな設計を目指しています。
このバグは `$GOROOT/test/ddd.go` というテストケースによってカバーされており、このテストが修正の検証に用いられたことが示唆されています。
## 前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの概念に関する知識が必要です。
1. **SSA (Static Single Assignment) 形式**:
SSAは、コンパイラの最適化フェーズで広く用いられる中間表現(IR)の一種です。SSA形式では、各変数が一度だけ代入されるという特性を持ちます。これにより、データフロー解析や最適化が容易になります。Goコンパイラも内部的にSSA形式を利用して、より効率的な機械語コードを生成しています。`exp/ssa` パッケージは、GoプログラムをSSA形式に変換するための実験的なライブラリでした。
2. **インターフェースの埋め込み (Interface Embedding)**:
Go言語のインターフェースは、メソッドのシグネチャの集合を定義します。構造体や他のインターフェースにインターフェースを埋め込むことができます。
```go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader // Readerインターフェースを埋め込み
Closer // Closerインターフェースを埋め込み
}
type MyFile struct {
// ...
}
func (mf *MyFile) Read(p []byte) (n int, err error) { /* ... */ }
func (mf *MyFile) Close() error { /* ... */ }
// MyFileはReadCloserインターフェースを実装しているとみなされる
```
この機能により、コードの再利用性と柔軟性が向上します。
3. **ブリッジメソッド (Bridge Methods)**:
Go言語のコンパイラが、インターフェースの埋め込みや型アサーション、型変換などの特定の状況で、内部的に生成する特殊なメソッドです。これらのメソッドは、実行時に適切なメソッド呼び出しに「橋渡し」する役割を担います。例えば、ある型がインターフェースを介してメソッドを呼び出された場合、コンパイラはブリッジメソッドを生成して、そのインターフェースのメソッド呼び出しを、基となる型の実際のメソッド実装にディスパッチします。
今回のバグは、このブリッジメソッドが引数を誤って処理していたことに起因します。
4. **レシーバー (Receiver)**:
Go言語のメソッドは、特定の型に関連付けられた関数です。メソッドが呼び出される際、そのメソッドが操作するインスタンスは「レシーバー」として渡されます。例えば `func (r MyType) MyMethod()` の `r` がレシーバーです。インターフェースメソッドの場合、インターフェースの値自体がレシーバーとして扱われます。
5. **テールコール (Tail Call)**:
関数呼び出しが、その関数の最後の操作である場合、それをテールコールと呼びます。最適化されたコンパイラは、テールコールを通常の関数呼び出しではなく、ジャンプ命令に変換することで、スタックフレームのオーバーヘッドを削減し、スタックオーバーフローを防ぐことができます。このコミットでは `emitTailCall` という関数が導入されており、ブリッジメソッドが最終的に別のメソッドをテールコールする形で実装されていることが示唆されます。
## 技術的詳細
このコミットは、主に `src/pkg/exp/ssa/promote.go` と `src/pkg/exp/ssa/emit.go` の2つのファイルに大きな変更を加えています。
**バグ修正の核心**:
`promote.go` 内の `makeBridgeMethod` 関数は、埋め込みインターフェースのためのブリッジメソッドを生成する役割を担っています。元のコードでは、この関数が生成する `Call` オブジェクト(関数呼び出しを表すSSAノード)の引数リスト `c.Args` に、レシーバーと最初のパラメータが重複して追加されていました。
具体的には、以下の部分が問題でした(変更前の `promote.go` の一部を推測):
```go
// 変更前 (推測)
// makeBridgeMethod 内
if c.IsStatic() { // 静的呼び出しの場合
fn.Pos = c.Func.(*Function).Pos
c.Pos = fn.Pos
c.Args = append(c.Args, v) // レシーバーを追加
for _, arg := range fn.Params[1:] { // 最初のパラメータ以降の引数を追加
c.Args = append(c.Args, arg)
}
} else { // 動的呼び出し (インターフェースメソッド) の場合
c.Recv = v // レシーバーを設定
c.Method = 0
for _, arg := range fn.Params { // 全てのパラメータを引数として追加
c.Args = append(c.Args, arg)
}
}
このコードでは、fn.Params
にはレシーバーを含む全てのパラメータが含まれています。fn.Params[1:]
はレシーバーを除いた引数リストを意味しますが、c.Args = append(c.Args, v)
でレシーバーが既に追加されているにも関わらず、動的呼び出しのケースでは for _, arg := range fn.Params
でレシーバーが再度 c.Args
に追加されていました。これが「インターフェースを二重に渡す」バグの原因でした。
修正とリファクタリング:
このバグは、emitTailCall
関数と createParams
関数への共通ロジックの抽出によって修正されています。
-
emitTailCall
の変更 (src/pkg/exp/ssa/emit.go
):emitTailCall
は、関数f
の末尾でcall
を発行するためのヘルパー関数です。変更前は単にcall
を発行するだけでしたが、変更後はブリッジメソッドの引数処理ロジックを内包するようになりました。// 変更後 emitTailCall func emitTailCall(f *Function, call *Call) { // f の最初の仮パラメータを除く全ての仮パラメータを、call の実パラメータとして追加 for _, arg := range f.Params[1:] { call.Args = append(call.Args, arg) } // call の戻り値の型を f のシグネチャの戻り値の型に設定 call.Type_ = &types.Result{Values: f.Signature.Results} tuple := f.emit(call) // ... (後続の戻り値処理) }
この変更により、
makeBridgeMethod
やmakeImethodThunk
の中で直接引数をc.Args
に追加するロジックが不要になり、emitTailCall
に一元化されました。特にf.Params[1:]
を使用することで、レシーバーが重複して追加されることを防いでいます。 -
createParams
の導入 (src/pkg/exp/ssa/promote.go
):createParams
は、Function
オブジェクトfn
のシグネチャに基づいて、そのパラメータを生成する共通のロジックをカプセル化するために導入されました。// 新規追加 createParams func createParams(fn *Function) { var last *Parameter for i, p := range fn.Signature.Params { name := p.Name if name == "" { name = fmt.Sprintf("arg%d", i) // 名前がない場合に "arg%d" を生成 } last = fn.addParam(name, p.Type) } if fn.Signature.IsVariadic { last.Type_ = &types.Slice{Elt: last.Type_} } }
この関数は、パラメータに名前がない場合に
arg%d
という形式で名前を自動生成する機能も提供します。これにより、SSA形式のデバッグや可読性が向上します。
これらの変更により、makeBridgeMethod
と makeImethodThunk
の両方で、パラメータの生成とテールコールによる引数処理が共通化され、コードの重複が排除されるとともに、バグが修正されました。
コアとなるコードの変更箇所
src/pkg/exp/ssa/builder.go
--- a/src/pkg/exp/ssa/builder.go
+++ b/src/pkg/exp/ssa/builder.go
@@ -23,7 +23,6 @@ package ssa
// TODO(adonovan):
// - fix: support f(g()) where g has multiple result parameters.
-// - fix: multiple labels on same statement.
import (
"fmt"
// - fix: multiple labels on same statement.
のコメントが削除されました。これは直接的な機能変更ではなく、TODOリストの整理です。
src/pkg/exp/ssa/emit.go
--- a/src/pkg/exp/ssa/emit.go
+++ b/src/pkg/exp/ssa/emit.go
@@ -221,11 +221,17 @@ func emitExtract(f *Function, tuple Value, index int, typ types.Type) Value {
return f.emit(e)
}
-// emitTailCall emits to f a function call in tail position.\n+// emitTailCall emits to f a function call in tail position,\n+// passing on all but the first formal parameter to f as actual\n+// values in the call. Intended for delegating bridge methods.\n // Precondition: f does/will not use deferred procedure calls.\n // Postcondition: f.currentBlock is nil.\n //\n func emitTailCall(f *Function, call *Call) {
+\tfor _, arg := range f.Params[1:] {\n+\t\tcall.Args = append(call.Args, arg)\n+\t}\n+\tcall.Type_ = &types.Result{Values: f.Signature.Results}\n \ttuple := f.emit(call)\n \tvar ret Ret\n \tswitch {
emitTailCall
関数のシグネチャは変わっていませんが、内部ロジックが大幅に変更されました。f.Params[1:]
をループして、関数f
の最初の仮パラメータ(レシーバー)を除く全ての仮パラメータをcall.Args
に追加するようになりました。call.Type_
がf.Signature.Results
に基づいて設定されるようになりました。- コメントが更新され、この関数が「ブリッジメソッドの委譲を意図している」こと、および「最初の仮パラメータを除く全ての仮パラメータを実パラメータとして渡す」ことが明記されました。
src/pkg/exp/ssa/promote.go
--- a/src/pkg/exp/ssa/promote.go
+++ b/src/pkg/exp/ssa/promote.go
@@ -272,13 +272,7 @@ func makeBridgeMethod(prog *Program, typ types.Type, cand *candidate) *Function
}\n \tfn.start(nil)\n \tfn.addSpilledParam(sig.Recv)\n-\tvar last *Parameter\n-\tfor _, p := range fn.Signature.Params {\n-\t\tlast = fn.addParam(p.Name, p.Type)\n-\t}\n-\tif fn.Signature.IsVariadic {\n-\t\tlast.Type_ = &types.Slice{Elt: last.Type_}\n-\t}\n+\tcreateParams(fn)\n \n \t// Each bridge method performs a sequence of selections,\n \t// then tailcalls the promoted method.\n@@ -315,22 +309,30 @@ func makeBridgeMethod(prog *Program, typ types.Type, cand *candidate) *Function
\t\tfn.Pos = c.Func.(*Function).Pos // TODO(adonovan): fix: wrong.\n \t\tc.Pos = fn.Pos // TODO(adonovan): fix: wrong.\n \t\tc.Args = append(c.Args, v)\n-\t\tfor _, arg := range fn.Params[1:] {\n-\t\t\tc.Args = append(c.Args, arg)\n-\t\t}\n \t} else {\n \t\tc.Recv = v\n \t\tc.Method = 0\n-\t\tfor _, arg := range fn.Params {\n-\t\t\tc.Args = append(c.Args, arg)\n-\t\t}\n \t}\n-\tc.Type_ = &types.Result{Values: sig.Results}\n \temitTailCall(fn, &c)\n \tfn.finish()\n \treturn fn\n }\n \n+// createParams creates parameters for bridge method fn based on its Signature.\n+func createParams(fn *Function) {\n+\tvar last *Parameter\n+\tfor i, p := range fn.Signature.Params {\n+\t\tname := p.Name\n+\t\tif name == \"\" {\n+\t\t\tname = fmt.Sprintf(\"arg%d\", i)\n+\t\t}\n+\t\tlast = fn.addParam(name, p.Type)\n+\t}\n+\tif fn.Signature.IsVariadic {\n+\t\tlast.Type_ = &types.Slice{Elt: last.Type_}\n+\t}\n+}\n+\n // Thunks for standalone interface methods ----------------------------------------\n \n // makeImethodThunk returns a synthetic thunk function permitting an\n@@ -373,21 +375,10 @@ func makeImethodThunk(prog *Program, typ types.Type, id Id) *Function {\n \t// TODO(adonovan): set fn.Pos to location of interface method ast.Field.\n \tfn.start(nil)\n \tfn.addParam(\"recv\", typ)\n-\tvar last *Parameter\n-\tfor _, p := range fn.Signature.Params {\n-\t\tlast = fn.addParam(p.Name, p.Type)\n-\t}\n-\tif fn.Signature.IsVariadic {\n-\t\tlast.Type_ = &types.Slice{Elt: last.Type_}\n-\t}\n-\n+\tcreateParams(fn)\n \tvar c Call\n \tc.Method = index\n \tc.Recv = fn.Params[0]\n-\tfor _, arg := range fn.Params[1:] {\n-\t\tc.Args = append(c.Args, arg)\n-\t}\n-\tc.Type_ = &types.Result{Values: sig.Results}\n \temitTailCall(fn, &c)\n \tfn.finish()\n \treturn fn\n```
- `makeBridgeMethod` 関数と `makeImethodThunk` 関数から、パラメータ生成と `Call` オブジェクトの引数設定に関する重複したロジックが削除され、それぞれ新しく導入された `createParams` 関数と、変更された `emitTailCall` 関数に置き換えられました。
- 新しいヘルパー関数 `createParams` が追加されました。この関数は、`Function` のシグネチャに基づいてパラメータを生成し、名前がない場合には `arg%d` という形式で名前を割り当てます。
## コアとなるコードの解説
このコミットのコアとなる変更は、ブリッジメソッドの生成ロジックの修正と共通化です。
1. **`emitTailCall` の役割の拡張**:
以前の `emitTailCall` は単にテールコールを発行するだけでしたが、このコミットにより、ブリッジメソッドが呼び出す実際の関数への引数渡しロジックも担当するようになりました。
`for _, arg := range f.Params[1:] { call.Args = append(call.Args, arg) }` の行が重要です。これは、ブリッジメソッド `f` の仮パラメータリストから、最初のパラメータ(通常はレシーバー)を除いた残りの全てのパラメータを、実際に呼び出す `call` の実引数として追加しています。これにより、レシーバーが二重に渡されるバグが修正されました。また、`call.Type_ = &types.Result{Values: f.Signature.Results}` は、呼び出しの戻り値の型を正しく設定しています。
2. **`createParams` の導入によるパラメータ生成の共通化**:
`createParams` 関数は、`makeBridgeMethod` と `makeImethodThunk` の両方で必要だった、関数パラメータの生成ロジックを共通化するために導入されました。
`if name == "" { name = fmt.Sprintf("arg%d", i) }` の部分は、パラメータに明示的な名前が与えられていない場合に、`arg0`, `arg1` のように自動的に名前を生成します。これは、SSA形式のデバッグや解析において、パラメータの識別を容易にするための改善です。
これらの変更により、ブリッジメソッドの生成がより堅牢になり、バグが修正されるとともに、コードベースの保守性が向上しました。特に、引数処理のロジックが `emitTailCall` に集約されたことで、将来的な変更やデバッグが容易になります。
## 関連リンク
- Go言語のSSAパッケージに関する公式ドキュメントやブログ記事 (当時の `exp/ssa` は実験的なものであり、現在はGoコンパイラに統合されています)
- Go言語のインターフェースの埋め込みに関する公式ドキュメント
- Go言語のコンパイラ内部構造に関する資料
## 参考にした情報源リンク
- [https://golang.org/cl/7437047](https://golang.org/cl/7437047) (元のGerritチェンジリスト)
- Go言語の公式ドキュメント (インターフェース、メソッド、SSAに関する記述)
- コンパイラ設計に関する一般的な情報源 (SSA形式、中間表現など)
- Go言語のソースコード (特に `src/cmd/compile` や `src/go/types` パッケージ)
- Go言語のテストスイート (`$GOROOT/test/ddd.go` など)
- Web search results for "Go exp/ssa bridge method embedded interface" (https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGlumTjXSZHk5BK1veNivpoR9B-QNeQEqCqfLulxXLoP3mIScEv7Ee2300KJrQztOOLfA5EkaQD3xGb7vTucWGShDuw4bwAeH_uobZn66Hrghs_-F-MlqyT2J0DK600nUAgUAChCtz7a3XW5ZuJP1spDrvq12ZdygIz)
- go.dev (https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFwcaOHqNrATvoPhkHHb42kv0SVJZKxrz-OYgYgs52vWnNxBNbhemyuYRi0ddxgdz5oRYbx12eBBgrTP2e06yIwKfgk4LwLqR6mKU2dJPE8ZQj-C3LddvvpFlvPyn7wSQxt6mNezhB3)