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

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

このコミットは、Goコンパイラ(gc)におけるインターフェースのメソッドセット展開に関するバグ修正を目的としています。具体的には、src/cmd/gc/subr.c 内の型展開ロジックから、エクスポートされていないシンボルに関する不必要なチェックを削除し、関連するテストケースを修正済みバグのディレクトリに移動しています。

コミット

Goコンパイラにおける「0個の予期されるバグ」という状態を目指すコミットであり、特にLuukの「qualified exporting code」が導入されたことで、以前は複雑だったバグの修正が容易になったことを示しています。この修正により、インターフェースの型アサーションが正しく機能するようになります。

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

https://github.com/golang/go/commit/1cb7f85d74f03668294267d465b9c20d49318ab9

元コミット内容

gc: 0 expected bugs

Now that Luuk's qualified exporting code
is in, fixing this bug is trivial.

R=ken2
CC=golang-dev
https://golang.org/cl/5479048

変更の背景

このコミットの背景には、Go言語のコンパイラ(gc)におけるインターフェースのメソッドセットの解決に関する既存のバグが存在していました。特に、異なるパッケージ間で型やメソッドがどのようにエクスポートされ、インターフェースの適合性がどのように判断されるかという点に問題があったと考えられます。

コミットメッセージにある「Luuk's qualified exporting code is in」という記述は非常に重要です。これは、Luuk van der Valk氏によって実装された、Goのパッケージシステムにおける「qualified exporting」(修飾されたエクスポート)に関する新しいコードが既にマージされていることを示唆しています。この「qualified exporting」は、パッケージの外部からアクセス可能なシンボル(関数、変数、型、メソッドなど)の可視性ルールをより厳密かつ正確に管理するための仕組みであると推測されます。

従来のコンパイラでは、インターフェースのメソッドセットを展開する際に、エクスポートされていないシンボルが誤って考慮されたり、逆に考慮されるべきシンボルが除外されたりするケースがあった可能性があります。この不正確な挙動が、bug367として知られるインターフェース変換時のパニックを引き起こしていました。

Luuk氏の新しいエクスポートコードが導入されたことで、コンパイラはシンボルのエクスポート状態とパッケージの境界をより正確に認識できるようになりました。これにより、src/cmd/gc/subr.c 内で以前は必要とされていた、エクスポートされていないシンボルを明示的にスキップするロジックが不要になった、あるいはむしろそのロジックが新しいエクスポートシステムと競合し、誤動作の原因となっていた可能性が考えられます。このコミットは、その冗長または有害なチェックを削除することで、バグを「自明に」修正できるようになったことを示しています。

前提知識の解説

Go言語のインターフェースと型アサーション

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たしているとみなされます(暗黙的なインターフェースの実装)。 型アサーション (value.(Type)) は、インターフェース型の値が特定の具象型または別のインターフェース型であるかどうかをチェックし、可能であればその型に変換するために使用されます。例えば、x.(I) は、x がインターフェース I を満たすかどうかをチェックします。

Go言語のパッケージとエクスポート/非エクスポートルール

Go言語では、識別子(変数名、関数名、型名、メソッド名など)が大文字で始まる場合、その識別子はパッケージ外にエクスポートされ、他のパッケージからアクセス可能になります。小文字で始まる識別子はエクスポートされず、そのパッケージ内でのみアクセス可能です。このルールは、Goのモジュール性とカプセル化の基本です。

Goコンパイラ (gc) の役割

gc はGo言語の公式コンパイラです。ソースコードを解析し、抽象構文木(AST)を構築し、型チェック、シンボル解決、最適化、そして最終的に実行可能なバイナリコードを生成します。このコミットが変更している src/cmd/gc/subr.c は、コンパイラのサブルーチン、特に型システムやシンボル解決に関連する部分を扱っていると考えられます。

埋め込み型 (Embedded Types)

Goの構造体は、他の構造体やインターフェースを匿名フィールドとして埋め込むことができます。これにより、埋め込まれた型のメソッドやフィールドが、外側の構造体のメソッドセットやフィールドとして「昇格」されます。インターフェースの適合性を判断する際には、この埋め込みによるメソッドの昇格も考慮されます。

exportname 関数と localpkg (Goコンパイラ内部)

Goコンパイラの内部では、exportname のような関数がシンボル名がエクスポート可能かどうかを判断するために使用されます。また、localpkg は現在コンパイル中のパッケージを表す内部的な概念です。これらの要素は、コンパイラがパッケージの境界を越えたシンボルの可視性を管理するために利用されます。

技術的詳細

このコミットの核心的な変更は、src/cmd/gc/subr.c ファイル内の expand0 関数から特定の条件分岐を削除した点にあります。

expand0 関数は、Goコンパイラにおいて、インターフェースのメソッドセットや構造体のフィールドを「展開」する役割を担っています。これは、型がどのようなメソッドを持っているか、あるいはどのようなフィールドを持っているかをコンパイラが正確に把握するために不可欠なプロセスです。特に、インターフェースの型アサーションやメソッド呼び出しの解決において、この展開処理は重要です。

削除された条件は以下の通りです。

if(!exportname(f->sym->name) && f->sym->pkg != localpkg)
    continue;

この条件は、expand0 関数がインターフェースのメソッドや構造体のフィールドを走査する際に、以下の2つの条件が同時に満たされる場合にそのシンボルをスキップ(continue)するというものでした。

  1. !exportname(f->sym->name): シンボル f->sym->name がエクスポートされていない(つまり、小文字で始まる)場合。
  2. f->sym->pkg != localpkg: シンボルが現在のパッケージ(localpkg)とは異なるパッケージに属している場合。

つまり、この条件は「他のパッケージに属する、エクスポートされていないシンボルは、インターフェースのメソッドセット展開の対象から除外する」という意図を持っていました。

しかし、この条件が削除されたのは、Luuk氏によって導入された「qualified exporting code」が、シンボルの可視性に関するより正確で包括的なメカニズムを提供したためと考えられます。新しいエクスポートシステムは、コンパイラがシンボルのエクスポート状態とパッケージの境界をより正確に追跡できるようにしたため、expand0 内でのこの明示的なフィルタリングが冗長になったか、あるいはむしろ誤った動作を引き起こす原因となっていた可能性があります。

具体的には、bug367 は、main パッケージの型 T が、埋め込み型 *p.S を介して p パッケージのインターフェース p.I を満たすべきであるにもかかわらず、コンパイラがそれを正しく認識できないという問題でした。元の bug367.dir/main.go では、panic: interface conversion: main.T is not p.I: missing method get というエラーが発生していました。これは、main.Tp.I インターフェースの get() メソッドを実装していないとコンパイラが誤って判断したことを意味します。

p.go の変更も重要です。元の p.go では get() T というシグネチャでしたが、修正版では get() となっています。これは、インターフェースのメソッドシグネチャがより単純化され、インターフェース適合性の判断が容易になったことを示唆しています。

このバグは、おそらく expand0p.Sget メソッドを main.T のメソッドセットに含めるべきかどうかを判断する際に、削除された条件によって誤って除外してしまっていたために発生したと考えられます。新しいエクスポートコードが導入されたことで、このフィルタリングが不要になり、コンパイラが埋め込み型を介したメソッドの昇格を正しく処理できるようになりました。

テストケースの変更は、この修正を検証するために行われました。

  • test/bugs/bug367.dir/main.go が削除され、test/fixedbugs/bug367.dir/main.go が新規作成されました。新しいテストでは、main.I という独自のインターフェースと p.I というパッケージ p のインターフェースの両方に対して型アサーションを行い、main.Tmain.I を満たさないこと(panic("should not satisfy main.I"))と、p.I を満たすこと(panic("should satisfy p.I"))を明示的にチェックしています。これにより、インターフェース適合性の判断が正しく行われるようになったことを確認しています。
  • test/golden.out から関連するパニックメッセージが削除されたことも、バグが修正され、予期されたエラーが発生しなくなったことを裏付けています。

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

src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2087,8 +2087,6 @@ expand0(Type *t, int followptr)\n 
  	if(u->etype == TINTER) {
  		for(f=u->type; f!=T; f=f->down) {
-			if(!exportname(f->sym->name) && f->sym->pkg != localpkg)
-				continue;
  			if(f->sym->flags & SymUniq)
  				continue;
  			f->sym->flags |= SymUniq;
@@ -2104,8 +2102,6 @@ expand0(Type *t, int followptr)\n 
  	u = methtype(t);\n  	if(u != T) {
  		for(f=u->method; f!=T; f=f->down) {
-			if(!exportname(f->sym->name) && f->sym->pkg != localpkg)
-				continue;
  			if(f->sym->flags & SymUniq)
  				continue;
  			f->sym->flags |= SymUniq;

test/bugs/bug367.dir/main.go (削除)

--- a/test/bugs/bug367.dir/main.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package main
-
-import (
-	"./p"
-)
-
-type T struct{ *p.S }
-
-func main() {
-	var t T
-	p.F(t)
-}

test/fixedbugs/bug367.dir/main.go (新規作成)

--- /dev/null
+++ b/test/fixedbugs/bug367.dir/main.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+	"./p"
+)
+
+type T struct{ *p.S }
+type I interface {
+	get()
+}
+
+func main() {
+	var t T
+	p.F(t)
+	var x interface{} = t
+	_, ok := x.(I)
+	if ok {
+		panic("should not satisfy main.I")
+	}
+	_, ok = x.(p.I)
+	if !ok {
+		panic("should satisfy p.I")
+	}
+}

test/{bugs => fixedbugs}/bug367.dir/p.go (リネームと変更)

--- a/test/bugs/bug367.dir/p.go
+++ b/test/fixedbugs/bug367.dir/p.go
@@ -3,14 +3,13 @@ package p
 type T struct{ x int }
 type S struct{}
 
-func (p *S) get() T {
-	return T{0}
+func (p *S) get() {
 }
 
 type I interface {
-	get() T
+	get()
 }
 
 func F(i I) {
-	_ = i.get()
+	i.get()
 }

コアとなるコードの解説

src/cmd/gc/subr.cexpand0 関数内の変更は、インターフェースのメソッドセットや型のフィールドをコンパイラが内部的に構築する際のロジックに影響を与えます。

削除されたコード if(!exportname(f->sym->name) && f->sym->pkg != localpkg) continue; は、以前は「他のパッケージに属し、かつエクスポートされていないシンボル(メソッドやフィールド)は、現在の型のメソッドセットやフィールドリストに含めない」というフィルタリングを行っていました。

このフィルタリングが削除された理由は、Luuk氏の「qualified exporting code」の導入により、コンパイラがシンボルの可視性ルールをより正確に、かつ自動的に処理できるようになったためです。新しいエクスポートシステムは、シンボルがどのパッケージに属し、どの範囲で可視であるかをより厳密に管理します。その結果、expand0 関数内で手動でこのようなフィルタリングを行う必要がなくなり、むしろこのフィルタリングが存在することで、埋め込み型を介したメソッドの昇格など、複雑なケースで誤った判断を引き起こす可能性があったと考えられます。

具体的には、bug367 のケースでは、main.T*p.S を埋め込んでおり、p.Sget() メソッドを持っています。p.I インターフェースは get() メソッドを要求します。Goのルールでは、埋め込み型が持つメソッドは外側の型に「昇格」されるため、main.Tp.I を満たすべきです。しかし、以前のコンパイラでは、p.Sget() メソッドが p パッケージに属し、かつエクスポートされていない(小文字で始まる)ため、上記の削除された条件によって main.T のメソッドセットから誤って除外されてしまっていた可能性があります。この除外が原因で、main.Tp.I を満たさないと誤判定され、パニックが発生していました。

このコミットにより、不要なフィルタリングが取り除かれたことで、コンパイラはGo言語の仕様に従って、埋め込み型を介したメソッドの昇格とインターフェース適合性を正しく判断できるようになりました。これにより、main.Tp.I を正しく満たすようになり、bug367 が修正されました。

test/fixedbugs/bug367.dir/p.go における get() メソッドのシグネチャ変更(get() T から get() へ)は、バグの再現と修正の検証をよりシンプルにするための調整であると考えられます。戻り値の型が特定のパッケージの型であることによる複雑さを排除し、インターフェース適合性の本質的な問題に焦点を当てやすくした可能性があります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: https://go.dev/doc/
  • Go言語の仕様書: https://go.dev/ref/spec (特にInterfaces, Packages and files, Exported identifiersのセクション)
  • Goコンパイラのソースコード (特に src/cmd/gc ディレクトリ): https://github.com/golang/go/tree/master/src/cmd/compile
  • Luuk van der Valk氏の関連するコミットや議論("qualified exporting code"に関する情報源)
    • go-review.googlesource.comLuuk van der Valkexportqualified で検索すると関連するコミットが見つかる可能性があります。
    • 例: https://go-review.googlesource.com/c/go/+/5479048 (このコミットのCL)
    • このCLの親コミットや関連コミットを辿ることで、"Luuk's qualified exporting code" の詳細が見つかる可能性があります。
    • GoのIssue 367に関する情報: https://go.dev/issue/367 (このコミットが修正したバグのIssue)