Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10209] ファイルの概要

このコミットは、Goコンパイラ(gc)において、ポインタのポインタに対してメソッド呼び出しが行われた際に、内部エラーではなく、より分かりやすいエラーメッセージを出力するように改善するものです。具体的には、**Tのような型に対してTのメソッドを呼び出そうとした場合に、コンパイラが自動的な間接参照(implicit dereference)を適切に処理できず、内部エラーになる問題を修正し、明示的な間接参照が必要であることをユーザーに伝えるメッセージを表示するように変更しています。

コミット

gc: helpful message instead of internal error on method call on pointer to pointer.

Fixes #2343.

R=rsc
CC=golang-dev
https://golang.org/cl/5332048

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/7df9ff55948aa20ca17a4448252dc826a0ded9fb

元コミット内容

gc: helpful message instead of internal error on method call on pointer to pointer.

Fixes #2343.

R=rsc
CC=golang-dev
https://golang.org/cl/5332048

変更の背景

この変更は、Go言語のコンパイラが特定の不正なコードパターンに遭遇した際に、ユーザーにとって理解しにくい「内部エラー(fatal error)」を発生させる問題を解決するために行われました。具体的には、Go言語ではメソッドレシーバがポインタ型(*T)である場合、その型の値(T)に対してもメソッドを呼び出すことができます。この際、コンパイラは自動的に値からポインタへの変換(アドレス取得)を行います。同様に、レシーバが値型(T)である場合、ポインタ型(*T)に対してもメソッドを呼び出すことができ、この場合は自動的にポインタの指す値への間接参照(dereference)が行われます。

しかし、ポインタのポインタ(**T)に対してメソッドを呼び出す場合、Goコンパイラは自動的な間接参照を一度しか行いません。例えば、**T型の変数qがあり、T型に定義されたメソッドm()q.m()のように呼び出そうとすると、コンパイラは*qまでは自動的に間接参照しますが、その先にあるT型への間接参照は行いません。この状況で、以前のコンパイラは「メソッドが間違った型にアタッチされている」という誤った判断を下し、最終的にfatalエラー(内部エラー)として処理していました。

ユーザーがこのようなコードを記述した場合、内部エラーはコンパイラのバグを示唆するものであり、ユーザーは自身のコードの何が問題なのかを理解できませんでした。このコミットは、このような場合に内部エラーを発生させるのではなく、「ポインタのポインタに対するメソッド呼び出しには明示的な間接参照が必要である」という、より具体的で分かりやすいエラーメッセージを出すことで、ユーザーが問題を特定し、修正できるようにすることを目的としています。これは、Go言語のコンパイラがユーザーフレンドリーであるべきという設計思想に基づいています。

この問題は、GoのIssueトラッカーでIssue 2343として報告されていました。

前提知識の解説

Go言語の型システムとポインタ

Go言語は静的型付け言語であり、変数は特定の型を持ちます。ポインタは、変数のメモリアドレスを格納する特殊な型です。Goでは*記号を使ってポインタ型を宣言します。例えば、*intint型へのポインタを表します。

  • アドレス演算子 (&): 変数のメモリアドレスを取得します。例: p := &x は変数xのアドレスをpに代入します。
  • 間接参照演算子 (*): ポインタが指すメモリアドレスに格納されている値を取得します。例: v := *p はポインタpが指す値をvに代入します。

Go言語のメソッドとレシーバ

Go言語では、関数を型に関連付けることで「メソッド」を定義できます。メソッドは、そのメソッドが関連付けられている型の「レシーバ」と呼ばれる引数を持ちます。レシーバは値型またはポインタ型にすることができます。

  • 値レシーバ: func (t T) MethodName() {} のように定義されます。このメソッドはT型の値のコピーに対して動作します。
  • ポインタレシーバ: func (t *T) MethodName() {} のように定義されます。このメソッドは*T型のポインタが指す元の値に対して動作します。メソッド内でレシーバのフィールドを変更すると、元の値も変更されます。

メソッド呼び出しにおける自動的な間接参照とアドレス取得

Go言語のコンパイラは、メソッド呼び出しの際に、レシーバの型とメソッドのレシーバの型が一致しない場合でも、特定の条件下で自動的に型変換を行います。

  1. 値からポインタへの変換(アドレス取得):

    • メソッドがポインタレシーバ(*T)を持つ場合。
    • 呼び出し元が値型(T)の変数である場合。
    • コンパイラは自動的に値のアドレスを取得し、ポインタレシーバに渡します。
    • 例: type S struct{}; func (s *S) M() {}; var s S; s.M()(&s).M() と解釈されます。
  2. ポインタから値への変換(間接参照):

    • メソッドが値レシーバ(T)を持つ場合。
    • 呼び出し元がポインタ型(*T)の変数である場合。
    • コンパイラは自動的にポインタが指す値に間接参照し、値レシーバに渡します。
    • 例: type S struct{}; func (s S) M() {}; var p *S; p.M()(*p).M() と解釈されます。

この自動変換は、コードの記述を簡潔にするためのGo言語の便利な機能ですが、ポインタのポインタ(**T)のような多重ポインタの場合には、コンパイラが自動的に間接参照を行うのは一度だけです。そのため、**T型の変数に対してT型のメソッドを呼び出そうとすると、コンパイラは*T型までは自動的に間接参照しますが、その先のT型までは自動的に間接参照しません。これが、このコミットで修正される問題の根源です。

技術的詳細

このコミットは、Goコンパイラの型チェックフェーズを担当するsrc/cmd/gc/typecheck.cファイルに変更を加えています。

src/cmd/gc/typecheck.cの役割

src/cmd/gc/typecheck.cは、Goコンパイラのフロントエンドの一部であり、ソースコードの抽象構文木(AST)を走査し、Go言語の型規則に従って各ノードの型をチェックする役割を担っています。これには、変数宣言、関数呼び出し、演算子、メソッド呼び出しなどの型チェックが含まれます。型チェックの過程で、型エラーが検出された場合は適切なエラーメッセージを生成します。

lookdot関数

lookdot関数は、ドット演算子(.)によるフィールドアクセスやメソッド呼び出しの型チェックを行う主要な関数です。例えば、obj.fieldobj.method()のような式を処理します。この関数は、レシーバの型(objの型)と、アクセスしようとしているフィールドやメソッドの名前(fieldmethod)を受け取り、その操作が有効かどうかを判断します。

メソッド呼び出しの場合、lookdotはレシーバの型とメソッドのレシーバの型を比較し、必要に応じて前述の自動的な間接参照やアドレス取得を行います。

OIND操作

OINDは、Goコンパイラの内部表現における「間接参照(indirection)」操作を表すノードタイプです。AST上で*pのような間接参照が行われると、コンパイラはこれをOINDノードとして表現します。

変更点の概要

このコミットの変更は、lookdot関数内でメソッド呼び出しのレシーバの型をチェックするロジックに新しい条件分岐を追加しています。

  1. derefall関数の追加:

    • 新しいヘルパー関数derefallが追加されました。この関数は、与えられた型がポインタ型である限り、そのポインタが指す型を再帰的に辿り、最終的にポインタではない基底の型を返します。例えば、**int型に対してderefallを呼び出すと、int型が返されます。
  2. lookdot内の新しい条件分岐:

    • lookdot関数内で、メソッドのレシーバの型(rcvr)と、呼び出し元の式の型(tt)を比較する部分に新しいelse ifブロックが追加されました。
    • この新しいブロックは、以下の条件をチェックします。
      • ttがポインタ型である (tt->etype == tptr)。
      • ttが指す型もポインタ型である (tt->type->etype == tptr)。
      • derefall(tt)ttを完全に間接参照した基底の型)が、メソッドのレシーバの型(rcvr)と等しい (eqtype(derefall(tt), rcvr))。
    • これらの条件がすべて真である場合、それは「ポインタのポインタに対して、その基底の型に定義されたメソッドを呼び出そうとしている」状況を意味します。
    • この場合、コンパイラは以前のようにfatalエラーを出すのではなく、yyerror関数を使って「calling method %N with receiver %lN requires explicit dereference」(レシーバ %lN を持つメソッド %N の呼び出しには明示的な間接参照が必要です)というエラーメッセージを出力します。
    • さらに、エラーメッセージを出力した後も、コンパイラはn->left = nod(OIND, n->left, N);typecheck(&n->left, Etype|Erv);をループで実行し、ttがポインタ型でなくなるまで間接参照ノード(OIND)を挿入し、型チェックを続行します。これにより、コンパイラはエラーを報告しつつも、可能な限り後続の型チェックを続行しようとします。

この変更により、コンパイラは不正な多重ポインタによるメソッド呼び出しをより適切に検出し、ユーザーに具体的な修正方法を提示できるようになりました。

コアとなるコードの変更箇所

diff --git a/src/cmd/gc/typecheck.c b/src/cmd/gc/typecheck.c
index d2268e6641..6ae4384e0b 100644
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -1595,6 +1595,14 @@ looktypedot(Node *n, Type *t, int dostrcmp)
 	return 1;
 }
 
+static Type*
+derefall(Type* t)
+{
+	while(t && t->etype == tptr)
+		t = t->type;
+	return t;
+}
+
 static int
 lookdot(Node *n, Type *t, int dostrcmp)
 {
@@ -1652,8 +1660,15 @@ lookdot(Node *n, Type *t, int dostrcmp)
 			n->left = nod(OIND, n->left, N);
 			n->left->implicit = 1;
 			typecheck(&n->left, Etype|Erv);
+		} else if(tt->etype == tptr && tt->type->etype == tptr && eqtype(derefall(tt), rcvr)) {
+			yyerror("calling method %N with receiver %lN requires explicit dereference", n->right, n->left);
+			while(tt->etype == tptr) {
+				n->left = nod(OIND, n->left, N);
+				n->left->implicit = 1;
+				typecheck(&n->left, Etype|Erv);
+				tt = tt->type;
+			}
 		} else {
-			// method is attached to wrong type?
 			fatal("method mismatch: %T for %T", rcvr, tt);
 		}
 	}
diff --git a/test/fixedbugs/bug371.go b/test/fixedbugs/bug371.go
new file mode 100644
index 0000000000..bf993df068
--- /dev/null
+++ b/test/fixedbugs/bug371.go
@@ -0,0 +1,24 @@
+// errchk $G $D/$F.go
+
+// Copyright 2011 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// issue 2343
+
+package main
+
+type T struct {}
+
+func (t *T) pm() {}
+func (t T) m() {}
+
+func main() {
+	p := &T{}
+	p.pm()
+	p.m()
+
+	q := &p
+	q.m()  // ERROR "requires explicit dereference"
+	q.pm()
+}

コアとなるコードの解説

derefall関数の追加

static Type*
derefall(Type* t)
{
	while(t && t->etype == tptr)
		t = t->type;
	return t;
}

この新しいヘルパー関数derefallは、与えられた型tがポインタ型(tptr)である限り、そのポインタが指す型(t->type)を繰り返し辿ります。これにより、**Tのような多重ポインタ型から、最終的な基底の型Tを取得することができます。例えば、**int型が渡された場合、ループは2回実行され、最終的にint型が返されます。これは、メソッドのレシーバの型と比較するために、呼び出し元の式の「真の」基底型を知る必要があるためです。

lookdot内の新しい条件分岐

		} else if(tt->etype == tptr && tt->type->etype == tptr && eqtype(derefall(tt), rcvr)) {
			yyerror("calling method %N with receiver %lN requires explicit dereference", n->right, n->left);
			while(tt->etype == tptr) {
				n->left = nod(OIND, n->left, N);
				n->left->implicit = 1;
				typecheck(&n->left, Etype|Erv);
				tt = tt->type;
			}

このelse ifブロックは、既存の自動間接参照ロジックの後に挿入されています。

  • tt->etype == tptr && tt->type->etype == tptr: これは、呼び出し元の式の型ttが「ポインタのポインタ」であることをチェックします。つまり、**SomeTypeのような形式の型であるかを判断します。
  • eqtype(derefall(tt), rcvr): derefall(tt)は、ttが指す最終的な基底の型を取得します。この条件は、その基底の型が、メソッドのレシーバの型rcvrと等しいかどうかをチェックします。例えば、**T型の変数に対してT型に定義されたメソッドを呼び出そうとしている場合にこの条件が真になります。

これらの条件がすべて満たされた場合、コンパイラは以下の処理を行います。

  1. yyerror(...): ユーザーフレンドリーなエラーメッセージを出力します。%Nはメソッド名(n->right)、%lNはレシーバの式(n->left)に置き換えられます。このメッセージは、「レシーバ %lN を持つメソッド %N の呼び出しには明示的な間接参照が必要です」とユーザーに伝えます。
  2. while(tt->etype == tptr) { ... }: エラーを報告した後も、コンパイラはttがポインタ型でなくなるまでループを続けます。このループ内で、n->left = nod(OIND, n->left, N);によって、現在のレシーバの式n->leftに対して間接参照ノード(OIND)を挿入します。これにより、**p*(*p)のように、明示的な間接参照が複数回行われるようなASTに変換されます。n->left->implicit = 1;は、この間接参照がコンパイラによって暗黙的に挿入されたものであることを示します。typecheck(&n->left, Etype|Erv);は、新しく挿入された間接参照ノードの型チェックを行います。tt = tt->type;は、ttを一つ内側の型に進め、次の間接参照の準備をします。

このループにより、コンパイラはエラーを報告しつつも、ASTを修正して型チェックを続行しようとします。これにより、一つのエラーが原因で後続の無関係なエラーが多数報告されることを防ぎ、ユーザーが問題をより効率的に修正できるようにします。

テストケース test/fixedbugs/bug371.go

このコミットには、新しいテストファイルtest/fixedbugs/bug371.goも追加されています。このテストは、修正された問題が正しく解決されたことを検証するためのものです。

package main

type T struct {}

func (t *T) pm() {} // ポインタレシーバのメソッド
func (t T) m() {}   // 値レシーバのメソッド

func main() {
	p := &T{} // pは *T 型
	p.pm()    // *T に対して *T のメソッド呼び出し -> OK
	p.m()     // *T に対して T のメソッド呼び出し -> 自動間接参照 (*p).m() -> OK

	q := &p   // qは **T 型
	q.m()     // ERROR "requires explicit dereference"
	q.pm()    // ERROR "requires explicit dereference"
}

このテストケースでは、q := &pによって**T型の変数qが作成されます。そして、q.m()q.pm()というメソッド呼び出しが行われます。このコミットの修正が適用されていれば、これらの行で「requires explicit dereference」というエラーメッセージが期待されます。errchk $G $D/$F.goというコメントは、このファイルがコンパイル時に特定のエラーメッセージを生成することを期待するテストであることを示しています。

関連リンク

参考にした情報源リンク