[インデックス 1447] ファイルの概要
このコミットは、Go言語のコンパイラ(gc)におけるインターフェースの取り扱いと、それに関連するメソッド解決ロジックの修正およびテストの追加に焦点を当てています。具体的には、メソッドのレシーバ型(値レシーバとポインタレシーバ)の処理を改善し、インターフェースの型チェックを遅延実行する新しいメカニズムを導入しています。これにより、より堅牢で正確な型チェックが可能になっています。
変更されたファイルは以下の通りです。
src/cmd/gc/dcl.c: 宣言処理、特にメソッドの追加とレシーバ型の検証ロジック。src/cmd/gc/go.h: コンパイラのヘッダファイルで、関数プロトタイプと型定義。src/cmd/gc/go.y: Go言語の文法定義(Yaccファイル)。src/cmd/gc/lex.c: 字句解析器。src/cmd/gc/subr.c: コンパイラのサブルーチン群。メソッド解決、型変換、インターフェースチェックのコアロジック。src/cmd/gc/walk.c: 抽象構文木(AST)の走査処理。test/bugs/bug046.go: 既存のバグテストケースの更新。test/interface2.go: 既存のインターフェーステストケースの更新。test/interface4.go: 新規追加されたインターフェーステストケース。様々なレシーバ型でのメソッド動作を検証。test/interface5.go: 新規追加されたインターフェーステストケース。インターフェースの欠落メソッドに関するエラーチェックを検証。test/method3.go: 新規追加されたメソッドテストケース。スライス型に対するメソッドを検証。
コミット
commit e512481b17d240d20f2800189ca5f22ea012906b
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 8 18:06:06 2009 -0800
second pass on interface fixes and tests.
R=ken
OCL=22370
CL=22372
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e512481b17d240d20f2800189ca5f22ea012906b
元コミット内容
second pass on interface fixes and tests.
R=ken
OCL=22370
CL=22372
変更の背景
このコミットは、Go言語の初期開発段階におけるインターフェースとメソッドのセマンティクスを安定させるための「2回目のパス」として位置づけられています。Go言語のインターフェースは、型が特定のメソッドセットを実装していれば自動的にそのインターフェースを満たすという「暗黙的な実装」が特徴です。しかし、この暗黙的な実装は、特に値レシーバとポインタレシーバを持つメソッドが混在する場合や、型がまだ完全に定義されていない段階でのインターフェース適合性チェックにおいて、コンパイラにとって複雑な課題を提示します。
以前のコンパイラ実装では、インターフェースの適合性チェックが不完全であったり、特定のケースで誤った振る舞いをしたりする可能性がありました。例えば、型がインターフェースのすべてのメソッドを実装しているかどうかのチェックが、コンパイルプロセスの早い段階で行われすぎると、まだ定義されていない型やメソッドに関する情報が不足しているために、正確な判断ができないことが考えられます。
このコミットの主な目的は、以下の問題を解決することです。
- メソッドレシーバの厳密な処理: 値レシーバとポインタレシーバを持つメソッドがインターフェースを満たす際の規則を明確にし、コンパイラがこれを正しく処理できるようにする。特に、
T型と*T型の両方にメソッドが定義されている場合の競合や、インターフェース型自体にメソッドを定義しようとした場合の誤りを検出する。 - インターフェース適合性チェックの改善: 型がインターフェースを実装しているかどうかのチェックを、コンパイルプロセスのより適切な段階(すべての型情報が利用可能になった後)に遅延させることで、より正確なチェックを保証する。これにより、前方参照や相互参照を含む複雑な型定義でも、インターフェースの適合性を正しく評価できるようになります。
- 堅牢なテストカバレッジ: 新しいテストケースを追加することで、様々なシナリオ(異なるレシーバ型、スライス型、欠落メソッドなど)におけるインターフェースとメソッドの動作が期待通りであることを確認し、将来的な回帰を防ぐ。
これらの変更は、Go言語の型システムとインターフェースのセマンティクスをより堅牢にし、開発者が直感的で信頼性の高いコードを書けるようにするための重要なステップでした。
前提知識の解説
このコミットの技術的詳細を理解するためには、以下のGo言語およびコンパイラの基本的な概念を把握しておく必要があります。
1. Go言語のメソッドとレシーバ
Go言語では、関数を型に関連付けることで「メソッド」を定義できます。メソッドは、そのメソッドが操作する値(レシーバ)を持つ点で通常の関数と異なります。レシーバには2つの種類があります。
- 値レシーバ (Value Receiver):
func (t MyType) MyMethod() {}のように定義されます。この場合、MyMethodはMyTypeの値のコピーに対して動作します。元の値は変更されません。 - ポインタレシーバ (Pointer Receiver):
func (t *MyType) MyMethod() {}のように定義されます。この場合、MyMethodはMyTypeのポインタに対して動作し、元の値を変更できます。
重要なのは、Goのインターフェース適合性において、値レシーバを持つメソッドは値とポインタの両方の型で呼び出せますが、ポインタレシーバを持つメソッドはポインタ型の値でしか呼び出せないという点です。このコミットは、この微妙な違いをコンパイラがどのように処理するかを改善しています。
2. Go言語のインターフェースと暗黙的な実装
Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースの最大の特徴は、型がインターフェースを「実装する」と明示的に宣言する必要がないことです。ある型がインターフェースで定義されたすべてのメソッドを実装していれば、その型は自動的にそのインターフェースを満たします(暗黙的な実装)。
例:
type Reader interface {
Read(p []byte) (n int, err error)
}
type MyFile struct { /* ... */ }
func (f MyFile) Read(p []byte) (n int, err error) { /* ... */ }
// MyFile は Reader インターフェースを自動的に満たす
var r Reader = MyFile{}
このコミットは、コンパイラがこの暗黙的な適合性をどのように効率的かつ正確にチェックするかを改善しています。
3. Goコンパイラ (gc) の基本的な構造
Goの公式コンパイラ(gc)は、伝統的なコンパイラのパイプラインに従っています。
- 字句解析 (Lexing): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
src/cmd/gc/lex.cが関連します。 - 構文解析 (Parsing): トークンのストリームを解析し、プログラムの構造を表す抽象構文木(AST)を構築します。
src/cmd/gc/go.y(Yacc/Bisonの入力ファイル)が文法を定義し、yyparse()が解析を実行します。 - 宣言処理 (Declaration): 変数、関数、型などの宣言を処理し、シンボルテーブルに登録します。
src/cmd/gc/dcl.cが関連します。 - 型チェック (Type Checking): ASTを走査し、型の整合性を検証します。このコミットの主要な変更点です。
- ASTウォーク (AST Walking): ASTを走査しながら、様々な最適化やコード生成のための変換を行います。
src/cmd/gc/walk.cが関連します。 - サブルーチン (Subroutines): コンパイラの様々な段階で共通して使用されるユーティリティ関数群です。
src/cmd/gc/subr.cが関連し、このコミットで最も多くの変更が行われています。
4. src/cmd/gc ディレクトリの役割
src/cmd/gc は、Go言語のコンパイラ本体のソースコードが格納されているディレクトリです。このディレクトリ内のファイルは、Goプログラムを機械語に変換するプロセスの中核を担っています。
技術的詳細
このコミットの技術的詳細は、主にGoコンパイラの型システムとインターフェース処理の内部ロジックの変更に集約されます。
1. メソッド解決ロジックの変更 (ismethod から dclmethod へ)
-
ismethodの廃止とdclmethodの導入: 以前のismethod関数は、与えられた型tがメソッドレシーバとして有効かどうかを判断し、その際にt->methptrフィールドを更新していました。このコミットでは、この関数がdclmethodにリネームされ、そのロジックが大幅に改善されました。dclmethodは、レシーバ型tが値レシーバ (T) なのかポインタレシーバ (*T) なのかを区別し、t->methptrフィールドを使って、その型が値レシーバとしてのみ、ポインタレシーバとしてのみ、あるいはその両方でメソッドを持つことを追跡します。methptrはビットフラグとして機能し、1は値レシーバ、2はポインタレシーバを示し、3は両方を示します。もし型がTと*Tの両方でメソッドを持つことが検出された場合、yyerror("methods on both %T and *%T", t, t)というエラーが報告されます。これは、Go言語の仕様上、同じ型に対して値レシーバとポインタレシーバの両方でメソッドを定義することはできないためです(ただし、異なるメソッド名であれば可能ですが、この文脈ではインターフェース適合性における曖昧さを避けるためのチェックです)。 また、インターフェース型 (TINTER) はメソッドレシーバとして使用できないように明示的にチェックが追加されました。 -
methtypeの変更:methtype関数は、dclmethodと同様に、副作用なしでメソッドレシーバの基本型を決定するために使用されます。この関数も、ポインタレシーバの場合にポインタを剥がして基本型を返すように修正され、インターフェース型やシンボルを持たない型を適切に除外するようになりました。 -
needaddrからmethconvへの変更:needaddr関数はmethconvにリネームされ、その役割が拡張されました。この関数は、メソッド呼び出しの際にレシーバ型をどのように変換すべきかを示す操作コード(OADDRまたはOIND)を返します。OADDR(Address): メソッドがポインタレシーバ (*T) を期待しているのに、呼び出し元が値 (T) を提供した場合、値のアドレスを取る必要があることを示します。OIND(Indirect): メソッドが値レシーバ (T) を期待しているのに、呼び出し元がポインタ (*T) を提供した場合、ポインタを間接参照して値を取得する必要があることを示します。 この変更により、コンパイラはメソッド呼び出し時にレシーバの型変換をより正確に処理できるようになりました。
2. 遅延インターフェースチェック機構の導入
このコミットの最も重要な変更点の一つは、インターフェースの適合性チェックをコンパイルプロセスの終盤まで遅延させるメカニズムの導入です。
-
Icheck構造体とicheckリスト:src/cmd/gc/subr.cにIcheckという新しい構造体が定義されました。これは、インターフェース変換が行われる場所(lineno)、変換先のインターフェース型 (dst)、および変換元の具象型 (src) を記録するためのものです。 これらのIcheckエントリは、グローバルなリンクリストicheckに追加されます。 -
ifacecheck関数の導入:ifacecheck(Type *dst, Type *src, int lineno)関数は、インターフェース変換(代入など)が発生した際に呼び出され、その変換情報をicheckリストに記録します。この時点では、実際の適合性チェックは行われません。 -
runifacechecks関数の導入:src/cmd/gc/lex.cのmainlex関数(字句解析と構文解析のメインループ)の最後に、runifacechecks()の呼び出しが追加されました。これは、ソースファイル全体の構文解析が完了し、すべての型とメソッドの情報が利用可能になった後で、icheckリストに記録されたすべてのインターフェース変換をまとめてチェックすることを意味します。runifacechecksはicheckリストを走査し、各エントリに対してhasiface関数を呼び出して、src型がdstインターフェースを実際に満たしているかどうかを検証します。もし満たしていない場合、yyerrorを使ってエラーを報告します。 -
hasiface関数の導入:hasiface(Type *t, Type *iface, Type **m)関数は、具象型tがインターフェースifaceを実装しているかどうかをチェックします。この関数は、インターフェースifaceの各メソッドについて、具象型tが対応するメソッドを持っているかをifacelookdotを使って検索し、メソッドのシグネチャ(型ハッシュ)が一致するかを検証します。もし欠落しているメソッドがあれば、そのメソッド情報を*mに格納して0を返します。
この遅延チェックの導入により、コンパイラは前方参照や複雑な型依存関係がある場合でも、インターフェースの適合性を正確に評価できるようになりました。
3. インターフェース代入のロジック変更 (isandss から ifaceas へ)
isandssの廃止とifaceasの導入:src/cmd/gc/walk.cおよびsrc/cmd/gc/subr.cで使用されていたisandss関数はifaceasにリネームされ、インターフェース間の代入 (I2I) および具象型からインターフェースへの代入 (T2I) のロジックが整理されました。ifaceasは、代入元 (src) と代入先 (dst) の型がインターフェースであるかどうかに基づいて、適切な変換操作(I2IまたはT2I)を返します。特に、T2Iのケースでは、新しいifacecheck関数を呼び出して、後で適合性チェックが行われるように情報を記録します。
4. その他の変更
src/cmd/gc/dcl.cのaddmethod関数では、メソッドの再宣言エラーメッセージがより詳細になり、レシーバ型がローカルで定義されていない場合のチェックが強化されました。src/cmd/gc/go.yのfndclルール(関数宣言)からismethodの直接呼び出しが削除され、より抽象化されたメソッド解決ロジックに依存するようになりました。src/cmd/gc/walk.cのlookdot関数は、methconvを使用してレシーバの型変換を処理するようになり、メソッド解決の堅牢性が向上しました。test/bugs/bug046.goのエラーメッセージが更新され、ポインタレシーバに関するより正確なエラーが期待されるようになりました。test/interface2.goでは、interface{}型を介したインターフェース代入のテストが追加されました。- 新しいテストファイル
test/interface4.go、test/interface5.go、test/method3.goは、これらの変更が様々なシナリオで正しく機能することを検証するために不可欠です。特にinterface4.goは、値レシーバとポインタレシーバ、大きな構造体と小さな構造体、組み込み型など、多様なレシーバ型でのインターフェースメソッドの動作を網羅的にテストしています。interface5.goは、インターフェースのメソッドが不足している場合にコンパイラが正しくエラーを報告するかを検証します。method3.goは、スライス型に対するメソッドの動作をテストします。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下の部分です。
1. src/cmd/gc/subr.c における dclmethod と methconv の定義
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1525,60 +1525,50 @@ isddd(Type *t)
* given receiver of type t (t == r or t == *r)
* return type to hang methods off (r).
*/
Type*
-ismethod(Type *t)
+dclmethod(Type *t)
{
- int a;
- Sym *s;
+ int ptr;
if(t == T)
return T;
- // no interfaces
- if(t->etype == TINTER || (t->etype == tptr && t->type->etype == TINTER))
- return T;
-
- a = algtype(t);
-
- // direct receiver
- s = t->sym;
- if(s != S) {
- if(t->methptr == 2)
- goto both;
- t->methptr |= 1;
- goto out;
+ // strip away pointer if it's there
+ ptr = 0;
+ if(isptr[t->etype]) {
+ if(t->sym != S)
+ return T;
+ ptr = 1;
+ t = t->type;
+ if(t == T)
+ return T;
}
- // pointer receiver
- if(!isptr[t->etype])
+ // need a type name
+ if(t->sym == S)
return T;
- t = t->type;
- if(t == T)
- return T;
-
- s = t->sym;
- if(s != S) {
- if(t->methptr == 1)
- goto both;
- t->methptr |= 2;
- goto out;
+ // check that all method receivers are consistent
+ if(t->methptr != 0 && t->methptr != (1<<ptr)) {
+ if(t->methptr != 3) {
+ t->methptr = 3;
+ yyerror("methods on both %T and *%T", t, t);
+ }
}
+ t->methptr |= 1<<ptr;
- return T;
-
-both:
- yyerror("type %T used as both direct and indirect method", t);
- t->methptr = 3;
-
-out:
- switch(a) {
+ // check types
+ // TODO(rsc): map, chan etc are not quite right
+ if(!issimple[t->etype])
+ switch(t->etype) {
default:
- yyerror("type %T cannot be used as a method", t);
- case ASIMP:
- case APTR:
- case ASTRING:
- case ASLICE:
+ return T;
+ case TSTRUCT:
+ case TARRAY:
break;
}
@@ -1586,47 +1576,46 @@ out:
}
/*
- * this is ismethod() without side effects
+ * this is dclmethod() without side effects.
*/
Type*
methtype(Type *t)
{
- Sym *s;
-
if(t == T)
return T;
- if(t->etype == TINTER || (t->etype == tptr && t->type->etype == TINTER))
- return T;
- s = t->sym;
- if(s != S)
- return t;
- if(!isptr[t->etype])
+ if(isptr[t->etype]) {
+ if(t->sym != S)
+ return T;
+ t = t->type;
+ }
+ if(t == T || t->etype == TINTER || t->sym == S)
return T;
- t = t->type;
- if(t == T)
- return T;
- s = t->sym;
- if(s != S)
- return t;
- return T;
+ return t;
}
/*
- * this is another ismethod()
- * returns 1 if t=T and method wants *T
+ * given type t in a method call, returns op
+ * to convert t into appropriate receiver.
+ * returns OADDR if t==x and method takes *x
+ * returns OIND if t==*x and method takes x
*/
int
-needaddr(Type *t)
+methconv(Type *t)
{
- Sym *s;
+ Type *m;
- if(t == T)
+ m = methtype(t);
+ if(m == T)
return 0;
- if(t->etype == TINTER || (t->etype == tptr && t->type->etype == TINTER))
+ if(m->methptr&2) {
+ // want pointer
+ if(t == m)
+ return OADDR;
return 0;
- s = t->sym;
- if(s != S && t->methptr == 2)
- return 1;
+ }
+ // want non-pointer
+ if(t != m)
+ return OIND;
return 0;
}
2. src/cmd/gc/subr.c における遅延インターフェースチェック関連の新規追加
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2735,3 +2724,111 @@ genptrtramp(Sym *method, Sym *oldname, Type *oldthis, Type *oldtype, Sym *newnam
funcbody(fn);
}
+/*
+ * delayed interface type check.
+ * remember that there is an interface conversion
+ * on the given line. once the file is completely read
+ * and all methods are known, we can check that
+ * the conversions are valid.
+ */
+
+typedef struct Icheck Icheck;
+struct Icheck
+{
+ Icheck *next;
+ Type *dst;
+ Type *src;
+ int lineno;
+};
+Icheck *icheck;
+Icheck *ichecktail;
+
+void
+ifacecheck(Type *dst, Type *src, int lineno)
+{
+ Icheck *p;
+
+ p = mal(sizeof *p);
+ if(ichecktail)
+ ichecktail->next = p;
+ else
+ icheck = p;
+ p->dst = dst;
+ p->src = src;
+ p->lineno = lineno;
+ ichecktail = p;
+}
+
+Type*
+ifacelookdot(Sym *s, Type *t)
+{
+ int c, d;
+ Type *m;
+
+ for(d=0; d<nelem(dotlist); d++) {
+ c = adddot1(s, t, d, &m);
+ if(c > 1) {
+ yyerror("%T.%S is ambiguous", t, s);
+ return T;
+ }
+ if(c == 1)
+ return m;
+ }
+ return T;
+}
+
+int
+hasiface(Type *t, Type *iface, Type **m)
+{
+ Type *im, *tm;
+ int imhash;
+
+ t = methtype(t);
+ if(t == T)
+ return 0;
+
+ // if this is too slow,
+ // could sort these first
+ // and then do one loop.
+
+ // could also do full type compare
+ // instead of using hash, but have to
+ // avoid checking receivers, and
+ // typehash already does that for us.
+ // also, it's what the runtime will do,
+ // so we can both be wrong together.
+
+ for(im=iface->type; im; im=im->down) {
+ imhash = typehash(im, 0);
+ tm = ifacelookdot(im->sym, t);
+ if(tm == T || typehash(tm, 0) != imhash) {
+ *m = im;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+void
+runifacechecks(void)
+{
+ Icheck *p;
+ int lno;
+ Type *m, *l, *r;
+
+ lno = lineno;
+ for(p=icheck; p; p=p->next) {
+ lineno = p->lineno;
+ if(isinter(p->dst)) {
+ l = p->src;
+ r = p->dst;
+ } else {
+ l = p->dst;
+ r = p->src;
+ }
+ if(!hasiface(l, r, &m))
+ yyerror("%T is not %T - missing %S%hT",
+ l, r, m->sym, m->type);
+ }
+ lineno = lno;
+}
3. src/cmd/gc/lex.c における runifacechecks の呼び出し
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -90,6 +90,7 @@ mainlex(int argc, char *argv[])
nerrors = 0;
yyparse();
+ runifacechecks();
linehist(nil, 0);
if(curio.bin != nil)
4. src/cmd/gc/walk.c における ifaceas の使用
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -604,8 +604,8 @@ loop:
}
}
- // interface and structure
- et = isandss(n->type, l);
+ // interface assignment
+ et = ifaceas(n->type, l->type);
if(et != Inone) {
indir(n, ifaceop(n->type, l, et));
goto ret;
@@ -2817,33 +2817,33 @@ arrayop(Node *n, int top)
return r;
}
+/*
+ * assigning src to dst involving interfaces?
+ * return op to use.
+ */
int
-isandss(Type *lt, Node *r)
+ifaceas(Type *dst, Type *src)
{
-\tType *rt;\
+\tif(src == T || dst == T)
+\t\treturn Inone;
-\trt = r->type;\
-\tif(isinter(lt)) {\
-\t\tif(isinter(rt)) {\
-\t\t\tif(isnilinter(lt) && isnilinter(rt))\
+\tif(isinter(dst)) {
+\t\tif(isinter(src)) {
+\t\t\tif(eqtype(dst, src, 0))\
\t\t\t\treturn Inone;\
-\t\t\tif(!eqtype(rt, lt, 0))\
-\t\t\t\treturn I2I;\
-\t\t\treturn Inone;\
+\t\t\treturn I2I;\
\t\t}\
-\t\tif(isnilinter(lt))\
-\t\t\treturn T2I;\
-\t\tif(ismethod(rt) != T)\
+\t\tif(isnilinter(dst))\
\t\t\treturn T2I;\
-\t\treturn Inone;\
+\t\tifacecheck(dst, src, lineno);\
+\t\treturn T2I;\
\t}\
-\n-\tif(isinter(rt)) {\
-\t\tif(isnilinter(rt) || ismethod(lt) != T)\
+\tif(isinter(src)) {
+\t\tif(isnilinter(src))\
\t\t\treturn I2T;\
-\t\treturn Inone;\
+\t\tifacecheck(dst, src, lineno);\
+\t\treturn I2T;\
\t}\
-\n \treturn Inone;\
}
@@ -2988,7 +2988,7 @@ convas(Node *n)
if(eqtype(lt, rt, 0))
goto out;
-\tet = isandss(lt, r);\
+\tet = ifaceas(lt, rt);\
if(et != Inone) {
n->right = ifaceop(lt, r, et);\
goto out;
コアとなるコードの解説
1. dclmethod と methconv
dclmethod 関数は、Go言語の型がメソッドレシーバとしてどのように振る舞うかをコンパイラが理解するための中心的なロジックを含んでいます。
- レシーバ型の正規化: まず、与えられた型
tがポインタ型 (*T) であれば、そのポインタを剥がして基本型Tを取得します。ptr変数で元の型がポインタであったかどうかを記録します。 - 型名のチェック: メソッドは名前付きの型にのみ定義できるため、
t->sym == S(シンボルがない) の場合はエラーとしてTを返します。 methptrの整合性チェック:t->methptrは、その型が値レシーバ (1)、ポインタレシーバ (2)、またはその両方 (3) でメソッドを持つことを示すビットフラグです。このコミットでは、t->methptr != 0 && t->methptr != (1<<ptr)という条件で、同じ型に対して値レシーバとポインタレシーバの両方でメソッドが定義されている場合にエラー ("methods on both %T and *%T") を報告します。これは、Goの仕様で許容されない曖昧さを排除するためです。methptrの更新: 現在のレシーバ型(値またはポインタ)に応じてt->methptrを更新します。- 有効なレシーバ型のチェック:
TSTRUCT(構造体) やTARRAY(配列) など、メソッドレシーバとして有効な型であるかをswitch文で確認します。
methconv 関数は、メソッド呼び出し時にレシーバの型変換が必要かどうか、そしてどのような変換が必要かを判断します。
methtype(t)を呼び出して、レシーバの基本型mを取得します。m->methptr&2は、メソッドがポインタレシーバ (*T) を期待しているかどうかをチェックします。- もし期待していて、現在の型
tが値型 (t == m) であれば、アドレスを取る (OADDR) 必要があります。
- もし期待していて、現在の型
- それ以外の場合(メソッドが値レシーバ (
T) を期待している場合)で、現在の型tがポインタ型 (t != m) であれば、間接参照 (OIND) する必要があります。
これらの関数は、Goのメソッド呼び出しにおけるレシーバの自動的な値/ポインタ変換(例: var x T; x.Method() で Method が *T レシーバを持つ場合、&x.Method() と自動的に解釈される)をコンパイラが正しく処理するための基盤となります。
2. 遅延インターフェースチェック機構
Icheck 構造体と icheck リスト、そして ifacecheck、hasiface、runifacechecks の組み合わせは、Goコンパイラのインターフェース適合性チェックのアーキテクチャを根本的に変更しました。
-
ifacecheck(Type *dst, Type *src, int lineno): この関数は、ソースコード内でインターフェースへの代入(例:var i MyInterface = myStructInstance)が見つかるたびに呼び出されます。しかし、この時点では実際の適合性チェックは行わず、単に代入元 (src) と代入先 (dst) の型情報、および行番号 (lineno) をIcheck構造体に格納し、グローバルなリンクリストicheckに追加するだけです。 -
runifacechecks(void): この関数は、src/cmd/gc/lex.cのmainlex関数内で、ソースファイル全体の字句解析と構文解析が完了した後に一度だけ呼び出されます。runifacechecksはicheckリストを最初から最後まで走査します。リスト内の各Icheckエントリについて、hasiface関数を呼び出して、記録されたsrc型がdstインターフェースを実際に満たしているかどうかを検証します。 もしhasifaceが0を返した場合(つまり、インターフェースのメソッドが不足している場合)、yyerrorを使って詳細なエラーメッセージ(例:"%T is not %T - missing %S%hT")を報告します。 -
hasiface(Type *t, Type *iface, Type **m): この関数は、具象型tがインターフェースifaceを実装しているかどうかを判断する核心部分です。 インターフェースifaceが持つ各メソッド (im) についてループを回します。ifacelookdot(im->sym, t)を呼び出して、具象型tがimと同じ名前のメソッドを持っているかを検索します。 もしメソッドが見つからない (tm == T) か、見つかったメソッドのシグネチャ(型ハッシュtypehash(tm, 0))がインターフェースのメソッドのシグネチャと一致しない場合、そのインターフェースは満たされていないと判断し、欠落しているメソッド情報を*mに格納して0を返します。 すべてのメソッドが一致すれば1を返します。
この遅延チェックの利点は、コンパイラがソースファイル全体を完全に解析し、すべての型とメソッドの定義を把握した上でインターフェース適合性を検証できる点にあります。これにより、前方参照や相互参照を含む複雑な型定義でも、正確なインターフェース適合性チェックが可能になります。
3. ifaceas 関数
ifaceas(Type *dst, Type *src) 関数は、代入操作におけるインターフェース関連の型変換の種類を決定します。
-
インターフェースからインターフェースへの代入 (
I2I):dstとsrcの両方がインターフェース型である場合、eqtype(dst, src, 0)で型が完全に一致すれば変換は不要 (Inone) ですが、そうでなければI2I(Interface to Interface) 変換が必要であると判断します。 -
具象型からインターフェースへの代入 (
T2I):dstがインターフェース型でsrcが具象型である場合、isnilinter(dst)(nilインターフェース) でなければ、ifacecheck(dst, src, lineno)を呼び出して遅延チェックリストに情報を追加し、T2I(Type to Interface) 変換が必要であると判断します。 -
インターフェースから具象型への代入 (
I2T):srcがインターフェース型でdstが具象型である場合、isnilinter(src)(nilインターフェース) でなければ、ifacecheck(dst, src, lineno)を呼び出して遅延チェックリストに情報を追加し、I2T(Interface to Type) 変換が必要であると判断します。
この関数は、src/cmd/gc/walk.c のASTウォーク中に、代入ノード (OAS) や型変換ノード (OCONV) の処理で呼び出され、適切なインターフェース操作を生成するための情報を提供します。
これらの変更は、Go言語のインターフェースが持つ強力な機能(特に暗黙的な実装)を、コンパイラが正確かつ効率的に処理するための基盤を築きました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語の仕様: https://go.dev/ref/spec
- Goコンパイラのソースコード(GitHub): https://github.com/golang/go
- Go言語のメソッドに関する公式ブログ記事 (初期のもの): https://go.dev/blog/methods
参考にした情報源リンク
- この解説は、提供されたコミットの差分情報と、Go言語の基本的な概念に関する一般的な知識に基づいて作成されました。特定の外部ウェブサイトや文献を直接参照したものではありません。Go言語のコンパイラ内部に関する詳細な情報は、主にGoのソースコード自体から読み解かれました。