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

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

このコミットは、Goコンパイラ(cmd/gc)における型チェックのバグ修正に関するものです。具体的には、(**T).Methodのような不正なメソッド式が誤って受け入れられてしまう問題を修正しています。この修正により、コンパイラはより厳密な型チェックを行うようになり、Go言語の仕様に準拠した動作が保証されます。

コミット

commit ced8004a00c62b7aff4e6f6a702f0824b2312fd5
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sat Dec 22 19:13:45 2012 +0100

    cmd/gc: do not accept (**T).Method expressions.
    
    The typechecking code was doing an extra, unnecessary
    indirection.
    
    Fixes #4458.
    
    R=golang-dev, daniel.morsing, rsc
    CC=golang-dev
    https://golang.org/cl/6998051

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

https://github.com/golang/go/commit/ced8004a00c62b7aff4e6f6a702f0824b2312fd5

元コミット内容

cmd/gc: do not accept (**T).Method expressions.

The typechecking code was doing an extra, unnecessary
indirection.

Fixes #4458.

変更の背景

この変更は、Goコンパイラ(gc)が、(**T).Method のような無効なメソッド式を誤って受け入れてしまうバグ(Issue 4458)を修正するために行われました。Go言語の仕様では、メソッドは型 T またはポインタ型 *T に対して定義されますが、**T のようにポインタのポインタに対して直接メソッドを呼び出すことはできません。しかし、コンパイラの型チェックコードに不必要な間接参照処理が含まれていたため、この不正な式が許容されてしまっていました。この修正は、コンパイラの正確性を向上させ、言語仕様への準拠を強化することを目的としています。

前提知識の解説

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

Go言語では、関数を特定の型に関連付けることで「メソッド」を定義します。メソッドは「レシーバ」と呼ばれる特別な引数を持ち、これによりそのメソッドがどの型の値に対して呼び出されるかが決まります。レシーバには、値レシーバとポインタレシーバの2種類があります。

  • 値レシーバ (T): func (t T) MethodName() {} のように定義されます。このメソッドは、型 T の値に対して呼び出されます。
  • ポインタレシーバ (*T): func (p *T) MethodName() {} のように定義されます。このメソッドは、型 *T のポインタに対して呼び出されます。

Goのコンパイラは、メソッド呼び出しの際に、レシーバの型とメソッドの定義を適切に解決します。例えば、T 型の変数 v があり、*T 型のレシーバを持つメソッド MethodName が定義されている場合、v.MethodName() と呼び出すと、コンパイラは自動的に &v.MethodName() に変換して呼び出します(アドレス可能であれば)。同様に、*T 型の変数 p があり、T 型のレシーバを持つメソッド MethodName が定義されている場合、p.MethodName() と呼び出すと、コンパイラは自動的に (*p).MethodName() に変換して呼び出します(デリファレンス可能であれば)。

しかし、この自動的な変換は1段階のポインタに限定されます。**T のような多重ポインタに対しては、直接メソッドを呼び出すことはできません。

Goコンパイラ(gc)の型チェック

Goコンパイラ gc は、ソースコードを解析し、Go言語の仕様に準拠しているかを確認する「型チェック」のフェーズを持っています。このフェーズでは、変数や式の型が正しく、操作がその型に対して有効であるかどうかが検証されます。メソッド呼び出しもこの型チェックの対象であり、レシーバの型とメソッドのシグネチャが一致しているか、あるいは自動的なポインタ変換によって解決可能であるかが確認されます。

src/cmd/gc/typecheck.c は、Goコンパイラの型チェックロジックの一部を実装しているC言語のファイルです。このファイルには、ドットセレクタ(.)によるフィールドアクセスやメソッド呼び出しを処理する lookdot1looktypedot といった関数が含まれています。

技術的詳細

このコミットが修正する問題は、src/cmd/gc/typecheck.c 内の looktypedot 関数が、メソッド解決の際に不必要な間接参照を行っていたことに起因します。

looktypedot 関数は、ある型 t に対してシンボル s(メソッド名)を探す役割を担っています。元のコードでは、methtype 関数を呼び出す前に、t がポインタ型である場合にその基底型(t->type)を取得して tt に代入し、その ttmethtype に渡していました。

// 修正前のコードの一部
tt = t;
if(t->sym == S && isptr[t->etype])
    tt = t->type;

f2 = methtype(tt, 0); // ここで tt を使っていた

ここで isptr[t->etype]t がポインタ型であるかを確認し、もしポインタ型であれば tt をそのポインタが指す型(t->type)に設定していました。これは、*T のレシーバを持つメソッドを T 型の値から呼び出す場合や、T のレシーバを持つメソッドを *T 型の値から呼び出す場合に、コンパイラが自動的にポインタのデリファレンスやアドレス取得を行うためのロジックの一部です。

しかし、(**T).Method のような式の場合、t**T の型を表します。このとき、if(t->sym == S && isptr[t->etype]) の条件が真となり、tt*T に設定されます。そして methtype(*T, 0) が呼び出されます。methtype 関数は、T または *T の形式の型に対してメソッドを解決するように設計されています。*T は有効なレシーバ型であるため、methtype*T に定義されたメソッドを正常に解決してしまいます。

結果として、(**T).Method という不正な式が、あたかも (*T).Method のように扱われ、コンパイルが通ってしまうというバグが発生していました。これは、Go言語の型システムにおけるメソッド解決の厳密性を損なうものでした。

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

src/cmd/gc/typecheck.clooktypedot 関数における変更点です。

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -1781,7 +1781,7 @@ lookdot1(Node *errnode, Sym *s, Type *t, Type *f, int dostrcmp)\n static int\n looktypedot(Node *n, Type *t, int dostrcmp)\n {\n-\tType *f1, *f2, *tt;\n+\tType *f1, *f2;\n \tSym *s;\n \t\n \ts = n->right->sym;\n@@ -1798,11 +1798,9 @@ looktypedot(Node *n, Type *t, int dostrcmp)\n \t\treturn 1;\n \t}\n \n-\ttt = t;\n-\tif(t->sym == S && isptr[t->etype])\n-\t\ttt = t->type;\n-\n-\tf2 = methtype(tt, 0);\n+\t// Find the base type: methtype will fail if t\n+\t// is not of the form T or *T.\n+\tf2 = methtype(t, 0);\n \tif(f2 == T)\n \t\treturn 0;\n \ndiff --git a/test/fixedbugs/issue4458.go b/test/fixedbugs/issue4458.go
new file mode 100644
index 0000000000..8ee3e879ea
--- /dev/null
+++ b/test/fixedbugs/issue4458.go
@@ -0,0 +1,20 @@
+// errorcheck
+
+// Copyright 2012 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 4458: gc accepts invalid method expressions
+// like (**T).Method.
+
+package main
+
+type T struct{}
+
+func (T) foo() {}
+
+func main() {
+	av := T{}
+	pav := &av
+	(**T).foo(&pav) // ERROR "no method foo"
+}

コアとなるコードの解説

変更の核心は、looktypedot 関数内で tt 変数とその関連ロジックが削除されたことです。

修正前:

tt = t;
if(t->sym == S && isptr[t->etype])
    tt = t->type;

f2 = methtype(tt, 0);

修正後:

// Find the base type: methtype will fail if t
// is not of the form T or *T.
f2 = methtype(t, 0);

この変更により、methtype 関数には常にオリジナルの型 t が直接渡されるようになりました。methtype 関数は、Go言語のメソッド解決ルールに従い、T または *T の形式の型に対してのみメソッドを解決するように設計されています。

  • もし tT または *T であれば、methtype(t, 0) は正しくメソッドを解決します。
  • もし t**T のような多重ポインタ型であれば、methtype はその型に対して直接メソッドを解決しようとしますが、**T にはメソッドが定義されていないため、解決に失敗します。これにより、(**T).Method のような不正な式はコンパイルエラーとなるようになります。

この修正は、コンパイラが不必要なポインタの間接参照を自動的に行い、不正なメソッド式を許容してしまうというバグを根本的に解決しています。同時に、test/fixedbugs/issue4458.go という新しいテストファイルが追加され、このバグが修正されたことを検証しています。このテストファイルは、(**T).foo(&pav) という行が ERROR "no method foo" を発生させることを期待しており、コンパイラが正しく不正な式を検出することを確認します。

関連リンク

参考にした情報源リンク