[インデックス 1441] ファイルの概要
このコミットは、Go言語のコンパイラとランタイムにおけるインターフェース関連の多数のバグ修正と、言語の重要な機能拡張、特に多値戻り値の扱いに関する改善を導入しています。具体的には、関数が複数の値を返す場合に、その戻り値を直接別の関数の引数として渡したり、別の関数から直接戻り値として返したりする構文 (f(g())
や return g()
) を可能にしています。
コミット
commit 20595ac4b00ecf11576549c4a3b4c6c1115a3abc
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 8 14:30:00 2009 -0800
many interface bug fixes.
also, after
func g() (int, int)
func f(int, int)
allow
f(g())
and
func h() (int, int) { return g() }
R=ken
DELTA=356 (252 added, 26 deleted, 78 changed)
OCL=22319
CL=22325
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/20595ac4b00ecf11576549c4a3b4c6c1115a3abc
元コミット内容
many interface bug fixes.
also, after
func g() (int, int)
func f(int, int)
allow
f(g())
and
func h() (int, int) { return g() }
R=ken
DELTA=356 (252 added, 26 deleted, 78 changed)
OCL=22319
CL=22325
変更の背景
このコミットが行われた2009年1月は、Go言語がまだ一般に公開される前の初期開発段階に当たります。当時のGoコンパイラとランタイムは、インターフェースのセマンティクス、特にメソッドセットの解決、埋め込み型(embedded types)の扱い、そしてポインタレシーバと値レシーバ間の変換において、いくつかのバグや不完全な実装を抱えていました。
また、Go言語の大きな特徴の一つである「多値戻り値(multiple return values)」は、その設計当初から存在していましたが、その戻り値を別の関数の引数として直接渡す (f(g())
)、あるいは別の関数から直接戻り値として返す (return g()
) といった、より柔軟な構文がサポートされていませんでした。これは、コンパイラが多値戻り値を単一のタプル(または構造体)として扱い、それを個々の引数や戻り値に展開するメカニズムが不足していたためです。
このコミットは、これらの問題を解決し、Go言語のインターフェースの堅牢性を高めるとともに、多値戻り値の利用をより自然で効率的なものにすることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。
-
Goの型システムとインターフェース:
- インターフェース: Goのインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たします(暗黙的な実装)。
- メソッドセット: 特定の型が持つメソッドの集合です。値型
T
とポインタ型*T
ではメソッドセットが異なります。T
のメソッドセットには、レシーバがT
のメソッドが含まれます。*T
のメソッドセットには、レシーバが*T
のメソッドと、レシーバがT
のメソッド(ポインタから値への自動変換が行われるため)が含まれます。
- 埋め込み型 (Embedded Types): 構造体内に別の型をフィールド名なしで埋め込むことで、埋め込まれた型のメソッドを外側の構造体が「継承」したかのように利用できます。
- インターフェースの内部表現: Goのインターフェース値は、通常、2つのポインタで構成されます。1つは「型情報(type descriptor)」へのポインタ、もう1つは「データ(value)」へのポインタです。型情報は、そのインターフェース値が保持する具体的な型の情報(メソッドテーブルなど)を含みます。
-
多値戻り値 (Multiple Return Values):
- Goの関数は複数の値を返すことができます。例えば
func f() (int, string)
。 - コンパイラ内部では、これらの多値は通常、一時的な構造体として扱われます。
- Goの関数は複数の値を返すことができます。例えば
-
コンパイラのフェーズ:
- 字句解析・構文解析: ソースコードをトークンに分解し、抽象構文木(AST)を構築します。
- 型チェック: AST上の各ノードの型を検証し、型の一貫性を保証します。
- 中間コード生成: ASTを、より低レベルな中間表現(IR)に変換します。Goコンパイラでは、この段階で最適化やコード変換が行われます。
- コード生成: IRをターゲットアーキテクチャの機械語に変換します。
-
トラップ(Trampoline):
- コンパイラやランタイムの文脈で「トラップ」または「トランポリン」とは、特定の関数呼び出しを別の関数呼び出しに「ジャンプ」させるための小さなコードスニペットを指します。
- Goにおいては、特にメソッド呼び出しにおいて、レシーバの型(値かポインタか)がメソッドの定義と異なる場合に、適切なレシーバ形式に変換して実際のメソッドを呼び出すために使用されます。例えば、値レシーバのメソッドをポインタで呼び出す場合や、その逆の場合に、この変換処理を担うコードが生成されます。
-
src/cmd/gc
とsrc/cmd/6g
:src/cmd/gc
はGoコンパイラの共通部分(フロントエンド、型チェック、中間表現の生成など)を扱います。src/cmd/6g
はgc
のバックエンドであり、x86-64アーキテクチャ(6gは64-bit Goの意)向けのコード生成を担当します。
技術的詳細
このコミットは、主に以下の2つの主要な技術的課題に対処しています。
1. インターフェースとメソッドディスパッチの改善
Goのインターフェースは、その動的な性質から、コンパイラとランタイムが密接に連携して動作する必要があります。特に、メソッド呼び出しの際に、具体的な型のメソッドを正しく解決し、適切なレシーバ(値またはポインタ)で呼び出すことが重要です。
dumpsigt
関数の強化:src/cmd/6g/obj.c
にあるdumpsigt
関数は、型のメソッドシグネチャをランタイムが利用できる形式でダンプする役割を担っています。このコミットでは、dumpsigt
の引数が(Type *t0, Type *t, Sym *s)
から(Type *progt, Type *ifacet, Type *rcvrt, Type *methodt, Sym *s)
へと大幅に拡張されました。これは、Goの型システムが持つ複数の「視点」(プログラム中の元の型progt
、インターフェースに格納される型ifacet
、メソッドのレシーバとして使われる型rcvrt
、メソッドが定義されている型methodt
)をより正確に区別し、それぞれに応じた処理を行う必要があったことを示しています。これにより、インターフェースのメソッド解決がより堅牢になります。- トランポリンの導入 (
genptrtramp
,genembedtramp
):genptrtramp
は、ポインタレシーバと値レシーバのミスマッチを解決するためのトランポリンを生成します。例えば、struct S
にfunc (s S) M()
というメソッドがあり、*S
型の変数からM
を呼び出す場合、コンパイラは*S
をS
にデリファレンスしてからM
を呼び出す必要があります。逆に、func (s *S) M()
というメソッドをS
型の変数から呼び出す場合、S
のアドレスを取得して*S
に変換する必要があります。これらの変換を自動的に行うための小さなアダプタコードがトランポリンとして生成されます。genembedtramp
は、埋め込み型(embedded types)のメソッド呼び出しを処理するためのトランポリンを生成します。埋め込み型を通じてアクセスされるメソッドは、元の型とは異なるレシーバのコンテキストで呼び出される可能性があるため、適切な変換が必要です。- これらのトランポリンは、
dumpsigt
の中で必要に応じて生成され、ランタイムがインターフェースメソッドをディスパッチする際に利用されます。
methodsym
の改善:src/cmd/gc/dcl.c
のmethodsym
関数は、メソッドのシンボル名を生成します。このコミットでは、ポインタ型と値型のレシーバを持つメソッドのシンボル名生成ロジックが改善され、特にポインタを介してアクセスされるメソッドのシンボルが正確に解決されるようになりました。これにより、リンカやデバッガがメソッドを正しく識別できるようになります。.
(ドット) 演算子の解決の強化:src/cmd/gc/subr.c
のlookdot0
およびadddot1
関数は、構造体のフィールドやメソッドへのアクセス(ドット演算子)を解決します。これらの関数は、Type** save
という新しい引数を受け取るようになり、解決されたフィールドやメソッドの型情報を呼び出し元に返すことができるようになりました。これにより、曖昧な参照の検出とエラー報告が改善され、src/cmd/gc/walk.c
のlookdot1
でより詳細なエラーメッセージ (ambiguous DOT reference %T.%S
) が出力されるようになっています。
2. 多値戻り値の柔軟な利用 (f(g())
と return g()
)
Goの多値戻り値は強力な機能ですが、このコミット以前は、関数 g()
が (int, int)
を返す場合、その戻り値を直接 f(int, int)
の引数として渡す f(g())
のような構文は許可されていませんでした。同様に、func h() (int, int) { return g() }
のような直接的な多値の転送もできませんでした。
src/cmd/gc/walk.c
のascompatte
関数の変更: この関数は、代入式の互換性をチェックし、必要に応じて変換を行います。このコミットで追加された「1対多(1 to many)」のルールが、この機能を実現する核心です。
このコードブロックは、左辺が複数の値(関数の引数リストや戻り値リスト)を期待し、右辺が単一のタプル(多値戻り値を持つ関数の呼び出し結果)を提供する場合に、そのタプルを左辺の個々の要素に展開して代入を許可します。これにより、// 1 to many peekl = savel; peekr = saver; if(l != T && r != N && structnext(&peekl) != T && listnext(&peekr) == N) && eqtype(r->type, *nl, 0)) return convas(nod(OAS, nodarg(*nl, fp), r));
f(g())
やreturn g()
のような構文がコンパイラによって正しく解釈され、中間コードに変換されるようになります。src/cmd/6g/gsubr.c
のnodarg
関数の変更:nodarg
関数は、関数の引数ノードを生成します。このコミットでは、TSTRUCT
型でfunarg
フラグが設定されている場合(これは多値戻り値や多値引数を表す内部的な構造体)、その構造体全体を単一の引数ノードとして扱うロジックが追加されました。これにより、コンパイラは多値のグループを一つの単位として扱い、ascompatte
での展開処理と連携して、多値の直接的な受け渡しを可能にします。
その他の変更
src/runtime/iface.c
のデバッグ変数名がdebug
からiface_debug
に変更され、インターフェース関連のデバッグ出力がより明確になりました。src/runtime/runtime.h
のIface
構造体のdata
フィールドに関するコメントが更新され、コンパイラとの同期の重要性が強調されました。これは、インターフェースの内部表現がコンパイラとランタイムの間で厳密に一致している必要があることを示唆しています。
コアとなるコードの変更箇所
このコミットにおける主要な変更は以下のファイルと関数に集中しています。
src/cmd/6g/gsubr.c
:nodarg
: 多値引数/戻り値を表す構造体ノードの扱いを改善。
src/cmd/6g/obj.c
:dumpsigt
: インターフェースのメソッドシグネチャ生成ロジックを大幅に強化。genembedtramp
: 埋め込みメソッドのトランポリン生成。genptrtramp
(新規追加): ポインタ/値レシーバ変換のトランポリン生成。
src/cmd/gc/dcl.c
:methodsym
: メソッドシンボル名の生成ロジックを改善。
src/cmd/gc/go.h
:genptrtramp
の宣言追加。lookdot0
,adddot1
の関数シグネチャ変更。
src/cmd/gc/subr.c
:lookdot0
,adddot1
: ドット演算子解決ロジックの強化。structargs
(新規追加): 関数引数/戻り値構造体をノードリストに変換。
src/cmd/gc/walk.c
:ascompatte
: 多値戻り値を多値引数として直接渡す「1対多」の代入ルールを追加。lookdot1
: ドット演算子の曖昧性エラーメッセージを改善。
src/runtime/iface.c
:- デバッグ変数名の変更 (
debug
->iface_debug
)。
- デバッグ変数名の変更 (
コアとなるコードの解説
src/cmd/gc/walk.c
の ascompatte
関数
この関数の変更は、f(g())
や return g()
のような多値の直接的な受け渡しを可能にする核心部分です。
変更前は、多値戻り値は単一の式として扱われ、複数の引数や戻り値の場所へ直接マッピングできませんでした。変更後は、ascompatte
内で、左辺が複数の要素(structnext(&peekl) != T
)を期待し、右辺が単一の式(listnext(&peekr) == N
)であり、かつその単一の式が多値のタプルである場合に、そのタプルを左辺の複数の要素に展開して代入するロジックが追加されました。これにより、コンパイラは多値の「パック」と「アンパック」を自動的に処理できるようになります。
src/cmd/6g/obj.c
の dumpsigt
関数と genptrtramp
関数
dumpsigt
は、Goのインターフェースがランタイムでメソッドをディスパッチするために必要な型情報(メソッドテーブルなど)を生成します。このコミットでは、dumpsigt
が受け取る型情報がより詳細になり、progt
(プログラム中の型)、ifacet
(インターフェースに格納される型)、rcvrt
(レシーバ型)、methodt
(メソッドが定義されている型) といった複数の視点から型を扱うようになりました。
特に重要なのは、dumpsigt
の内部で genptrtramp
が呼び出されるようになった点です。
genptrtramp
は、レシーバの型がメソッドの定義と異なる場合に、そのミスマッチを解決するための小さなアダプタ関数(トランポリン)を生成します。例えば、type T struct{}
に func (t T) M()
というメソッドがあり、var p *T
のようにポインタ型で p.M()
を呼び出す場合、Goは自動的に *p
を T
にデリファレンスしてメソッドを呼び出します。このデリファレンス処理を担うのが genptrtramp
が生成するトランポリンです。これにより、Goのメソッドセットのルール(ポインタレシーバのメソッドは値でも呼び出せるが、値レシーバのメソッドはポインタでも呼び出せる)が、コンパイル時に適切なアダプタコードを生成することで実現されます。
src/cmd/gc/dcl.c
の methodsym
関数
methodsym
は、メソッドの完全修飾シンボル名(例: main.MyType.MyMethod
)を生成します。このコミットでは、特にポインタ型レシーバを持つメソッドのシンボル名生成ロジックが改善されました。例えば、type MyType int
と type PtrMyType *MyType
があり、func (p PtrMyType) Method()
のようなメソッドがある場合、そのシンボル名が main.(*MyType).Method
のように、基底の型を正確に反映するように調整されました。これは、コンパイラが内部的に型を扱う際の厳密性を高め、デバッグやプロファイリングの際に正しいメソッドを識別するために重要です。
関連リンク
- Go言語のインターフェースに関する公式ドキュメント: https://go.dev/tour/methods/10
- Go言語の多値戻り値に関する公式ドキュメント: https://go.dev/tour/basics/6
参考にした情報源リンク
- Go言語のコンパイラ設計に関する一般的な情報源 (Goのソースコード自体、特に
src/cmd/gc
とsrc/cmd/6g
ディレクトリ) - Go言語の初期のコミット履歴と設計ドキュメント (GoのGitHubリポジトリの履歴)
- Go言語のインターフェース実装に関する技術記事 (例: "The Laws of Reflection" by Rob Pike)
- コンパイラにおける「トランポリン」の概念に関する一般的な情報 (計算機科学の教科書やオンラインリソース)