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

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

このコミットは、Go言語のコンパイラ(gc)におけるインターフェースの処理に関する変更を元に戻すものです。具体的には、以前のコミットで導入された「埋め込みインターフェースにおける無限再帰の修正」が、複雑なプログラムのコンパイル時に「interface type loop」エラーを引き起こすことが判明したため、その修正を大部分取り消しています。この変更は、Go 1のリリース前に、安定性を優先し、稀なエラーケースへの対応を延期するという判断に基づいています。

コミット

  • コミットハッシュ: 290e68b9833da723cbe9138856f7d6d494e5b07b
  • 作者: Russ Cox rsc@golang.org
  • コミット日時: Fri Jan 20 17:14:09 2012 -0500

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

https://github.com/golang/go/commit/290e68b9833da723cbe9138856f7d6d494e5b07b

元コミット内容

gc: undo most of 'fix infinite recursion for embedded interfaces'

Preserve test.

changeset:   11593:f1deaf35e1d1
user:        Luuk van Dijk <lvd@golang.org>
date:        Tue Jan 17 10:00:57 2012 +0100
summary:     gc: fix infinite recursion for embedded interfaces

This is causing 'interface type loop' errors during compilation
of a complex program.  I don't understand what's happening
well enough to boil it down to a simple test case, but undoing
this change fixes the problem.

The change being undone is fixing a corner case (uses of
pointer to interface in an interface definition) that basically
only comes up in erroneous Go programs.  Let's not try to
fix this again until after Go 1.

Unfixes issue 1909.

TBR=lvd
CC=golang-dev
https://golang.org/cl/5555063

変更の背景

このコミットの背景には、Go言語のコンパイラ(gc)におけるインターフェース型の処理に関する複雑な問題があります。

  1. 先行する修正の導入: 以前、Luuk van Dijkによってコミット f1deaf35e1d1 が導入されました。このコミットの目的は、「埋め込みインターフェースにおける無限再帰」という特定のコーナーケースを修正することでした。これは、インターフェースが自身を(直接的または間接的に)埋め込むような、通常は不正なGoプログラムで発生する可能性のある問題に対処しようとしたものです。

  2. 新たな問題の発生: しかし、この修正が導入された結果、より複雑なGoプログラムのコンパイル時に「interface type loop」という新たなエラーが発生するようになりました。このエラーは、コンパイラがインターフェースの型定義を処理する際に、無限ループに陥ることを示唆しています。

  3. 問題の特定と理解の困難さ: Russ Cox(このコミットの作者)は、この新たな「interface type loop」エラーの原因を単純なテストケースに落とし込むほど十分に理解できていないと述べています。しかし、Luuk van Dijkの修正を元に戻すことで、この問題が解決することが確認されました。

  4. Go 1リリース前の安定性優先: コミットメッセージには「Let's not try to fix this again until after Go 1.」と明記されています。これは、Go 1の正式リリースが間近に迫っていた時期であり、コンパイラの安定性を最優先し、稀な(かつ不正なプログラムでしか発生しない可能性のある)コーナーケースの修正よりも、既存の複雑なプログラムが正しくコンパイルされることを重視した開発方針を示しています。Go 1は2012年3月にリリースされており、このコミットはその約2ヶ月前のものです。

  5. Issue 1909の再オープン: このコミットは「Unfixes issue 1909」と記載されており、Luuk van Dijkの修正がクローズしたはずのGo issue 1909を再び未解決の状態に戻しています。これは、元の問題が根本的に解決されたわけではなく、一時的に回避されたに過ぎないことを意味します。

要するに、このコミットは、Goコンパイラの安定性を確保するため、意図せず新たなバグを導入してしまった以前の修正を、Go 1リリース前の重要な時期に元に戻すという、実用主義的な判断の結果です。

前提知識の解説

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、JavaやC#のような明示的なimplementsキーワードを必要とせず、型がインターフェースで定義されたすべてのメソッドを実装していれば、そのインターフェースを満たすと見なされます(構造的型付け)。

埋め込みインターフェース (Embedded Interfaces)

Goの構造体と同様に、インターフェースも他のインターフェースを「埋め込む」ことができます。インターフェースAがインターフェースBを埋め込む場合、ABのすべてのメソッドシグネチャを自動的に継承します。これにより、より大きなインターフェースを小さなインターフェースの組み合わせで構築できます。

例:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader // Readerインターフェースを埋め込む
    Writer // Writerインターフェースを埋め込む
}

ReadWriterインターフェースは、ReadメソッドとWriteメソッドの両方を持つことになります。

Goコンパイラ (gc)

gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担います。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成などが含まれます。このコミットで変更されているdcl.cexport.cfmt.cは、それぞれ宣言処理、型情報のエクスポート、型のフォーマット(表示)といったコンパイラの重要な部分を構成するC言語のソースファイルです。

型システムにおける再帰的な型定義と無限ループの問題

プログラミング言語の型システムでは、型が自身を参照するような再帰的な定義が許される場合があります。しかし、このような再帰が適切に処理されないと、コンパイラが型を解決する際に無限ループに陥る可能性があります。例えば、インターフェースが自身を埋め込むような定義(type MyInterface interface { MyInterface })は、通常は不正な定義ですが、コンパイラはこれを検出してエラーを報告する必要があります。もしコンパイラがこの再帰を無限に追いかけてしまうと、スタックオーバーフローやコンパイル時間の著しい増加につながります。

このコミットで言及されている「infinite recursion for embedded interfaces」や「interface type loop errors」は、まさにこの問題に関連しています。コンパイラがインターフェースの型構造を解析する際に、再帰的な参照を適切に検出・処理できず、無限ループに陥っていた、あるいは陥る可能性があったことを示唆しています。

Go 1リリース前の開発状況

Go言語は2009年に公開され、2012年3月にGo 1がリリースされました。Go 1は、言語仕様と標準ライブラリの安定性を保証する最初のメジャーリリースであり、以降のGoのバージョンはGo 1との後方互換性を維持することが約束されました。このコミットが行われた2012年1月は、Go 1のリリースに向けて、言語の安定化とバグ修正が最優先されていた時期です。そのため、稀なケースのバグ修正よりも、既存のコードベースのコンパイルを妨げないことが重視されました。

技術的詳細

このコミットは、Goコンパイラ(gc)の内部におけるインターフェース型の表現と処理に関する変更を元に戻しています。特に、インターフェースが他のインターフェースを埋め込む際の内部的な型構造の扱いが焦点となっています。

Goコンパイラは、型をType構造体で表現します。インターフェース型の場合、そのメソッドや埋め込みインターフェースの情報は、Type構造体内のポインタを通じてリンクされたリストのような形で保持されます。

元のLuuk van Dijkのコミット(f1deaf35e1d1)は、インターフェースのorigフィールド(元の型情報を保持するフィールド)を操作することで、埋め込みインターフェースの無限再帰を検出・防止しようとしました。しかし、このアプローチが複雑なプログラムで新たな問題を引き起こしたため、Russ Coxのこのコミットでは、その変更の大部分が元に戻されています。

具体的に元に戻された変更点は以下のファイルに見られます。

  1. src/cmd/gc/dcl.c:

    • tointerface関数は、Goのソースコードからインターフェース型を構築する役割を担っています。
    • 元の修正では、インターフェースのorigフィールド(t->orig = typ(TINTER);)を初期化し、埋め込みインターフェースの情報をt->orig->typeに格納しようとしていました。また、埋め込みインターフェースのフィールドにf->embedded = 1;というフラグを設定していました。
    • このコミットでは、これらのorigフィールドへの操作やembeddedフラグの設定が削除されています。これにより、インターフェースの型構築ロジックが、元の(Luuk van Dijkの修正前の)状態に戻されています。これは、埋め込みインターフェースの処理をより単純な形に戻し、再帰的な型チェックの複雑さを軽減することを目的としています。
  2. src/cmd/gc/export.c:

    • dumpexporttype関数は、コンパイルされた型情報を他のパッケージから利用できるようにエクスポートする役割を担っています。
    • 元の修正では、インターフェース型をエクスポートする際に、そのorigフィールドに格納された埋め込みインターフェースの型情報も再帰的にエクスポートするロジックが追加されていました。
    • このコミットでは、そのorigフィールドを介したエクスポートロジックが削除されています。これは、dcl.cでの変更と一貫しており、origフィールドを使った埋め込みインターフェースの特殊な処理を廃止しています。
    • また、importtype関数における型の一貫性チェックも変更されています。以前はpt->origt->origを比較していましたが、このコミットではpt->origtを比較するように戻されています。これは、origフィールドが元の型情報を保持するというより単純な役割に戻されたことを示唆しています。
  3. src/cmd/gc/fmt.c:

    • typefmt関数は、デバッグ出力やエラーメッセージのために型を文字列としてフォーマットする役割を担っています。
    • 元の修正では、インターフェース型をフォーマットする際に、t = t->orig;としてorigフィールドを参照し、そのorigフィールドに格納された埋め込みインターフェースの情報を表示しようとしていました。また、!t1->symという条件でシンボルを持たない(つまり匿名で埋め込まれた)インターフェースの表示を特別扱いしていました。
    • このコミットでは、t = t->orig;の行が削除され、インターフェースのフォーマットが直接t->type(メソッドリスト)に基づいて行われるように戻されています。また、!t1->symの条件も削除され、埋め込みインターフェースの表示ロジックが簡素化されています。
  4. test/fixedbugs/bug395.go:

    • このテストファイルはtest/fixedbugs/bug395.goからtest/bugs/bug395.goにリネームされています。これは、このバグがまだ修正されていない(Unfixes issue 1909)ため、fixedbugsディレクトリからbugsディレクトリに移動されたことを示唆しています。
    • テストケースの内容自体も変更されています。元のテストケースは、interface{Foo}という形でインターフェースFooが自身を埋め込むような定義を持っていました。
    • 変更後のテストケースでは、interface { Foo }という形で、より明示的に匿名でFooインターフェースを埋め込む形になっています。これは、Goのインターフェース埋め込みの構文に沿ったものであり、元のバグがどのような状況で発生していたかをより正確に再現しようとしている可能性があります。
    • コメント行も変更され、// echo bug395 is broken # takes 90+ seconds to breakというコメントが追加されています。これは、このバグがコンパイルに非常に長い時間(90秒以上)を要し、最終的に失敗することを示しており、コンパイラが無限ループに陥っていた可能性を強く示唆しています。

これらの変更は、Goコンパイラがインターフェース型、特に埋め込みインターフェースを内部でどのように表現し、処理するかという、コンパイラの型システムの中核部分に影響を与えています。元の修正が複雑なプログラムで問題を引き起こしたため、より単純で安定した以前のロジックに戻すことが選択されました。

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

src/cmd/gc/dcl.c

--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -940,19 +940,13 @@ interfacefield(Node *n)
 Type*
 tointerface(NodeList *l)
 {
-	Type *t, *f, **tp, **otp, *t1;
+	Type *t, *f, **tp, *t1;
 
 	t = typ(TINTER);
-	t->orig = typ(TINTER);
 
 	tp = &t->type;
-	otp = &t->orig->type;
-
 	for(; l; l=l.next) {
 		f = interfacefield(l.n);
-		*otp = typ(TFIELD);
-		**otp = *f;
-		otp = &(*otp)->down;
 
 		if (l.n->left == N && f->type->etype == TINTER) {
 			// embedded interface, inline methods
@@ -961,7 +955,6 @@ tointerface(NodeList *l)
 			f->type = t1->type;
 			f->broke = t1->broke;
 			f->sym = t1->sym;
-			f->embedded = 1;
 			if(f->sym)
 				f->nname = newname(f->sym);
 			*tp = f;

src/cmd/gc/export.c

--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -241,13 +241,6 @@ dumpexporttype(Type *t)
 	if(t->sym != S && t->etype != TFIELD)
 		dumppkg(t->sym->pkg);
 
-	// fmt will print the ->orig of an interface, which has the original embedded interfaces.
-	// be sure to dump them here
-	if(t->etype == TINTER)
-		for(f=t->orig->type; f; f=f->down)
-			if(f->sym == S)
-				dumpexporttype(f->type);
-
 	dumpexporttype(t->type);
 	dumpexporttype(t->down);
 
@@ -477,7 +470,7 @@ importtype(Type *pt, Type *t)
 		pt->sym->lastlineno = parserline();
 		declare(n, PEXTERN);
 		checkwidth(pt);
-	} else if(!eqtype(pt->orig, t->orig))
+	} else if(!eqtype(pt->orig, t))
 		yyerror("inconsistent definition for type %S during import\n\t%lT\n\t%lT", pt->sym, pt, t);
 
 	if(debug['E'])

src/cmd/gc/fmt.c

--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -640,15 +640,9 @@ typefmt(Fmt *fp, Type *t)
 		return fmtprint(fp, "map[%T]%T", t->down, t->type);
 
 	case TINTER:
-		t = t->orig;
 		fmtstrcpy(fp, "interface {");
 		for(t1=t->type; t1!=T; t1=t1->down)
-			if(!t1->sym) {
-				if(t1->down)
-					fmtprint(fp, " %T;", t1->type);
-				else
-					fmtprint(fp, " %T ", t1->type);
-			} else if(exportname(t1->sym->name)) {
+			if(exportname(t1->sym->name)) {
 				if(t1->down)
 					fmtprint(fp, " %hS%hT;", t1->sym, t1->type);
 				else

test/fixedbugs/bug395.go (リネームと内容変更)

--- a/test/fixedbugs/bug395.go
+++ b/test/bugs/bug395.go
@@ -1,4 +1,5 @@
-// $G $D/$F.go || echo "Bug395"
+// echo bug395 is broken  # takes 90+ seconds to break
+// # $G $D/$F.go || echo bug395
 
 // Copyright 2011 The Go Authors.  All rights reserved.
 // Use of this source code is governed by a BSD-style
@@ -9,7 +10,13 @@
 package test
 
 type Foo interface {
-       Bar() interface{Foo}
-       Baz() interface{Foo}
-       Bug() interface{Foo}
+	Bar() interface {
+		Foo
+	}
+	Baz() interface {
+		Foo
+	}
+	Bug() interface {
+		Foo
+	}
 }

コアとなるコードの解説

このコミットの核心は、Goコンパイラがインターフェース型、特に埋め込みインターフェースを内部でどのように表現し、処理するかという点にあります。

  1. src/cmd/gc/dcl.c の変更:

    • Type *t, *f, **tp, **otp, *t1; から Type *t, *f, **tp, *t1; への変更は、otporigフィールドの型ポインタ)が不要になったことを示しています。
    • t->orig = typ(TINTER); の削除は、インターフェース型torigフィールドを、それ自身のインターフェース型で初期化するという特殊な処理が不要になったことを意味します。以前の修正では、このorigフィールドを使って埋め込みインターフェースの「元の」構造を追跡しようとしていたと考えられます。
    • otpに関連する行(*otp = typ(TFIELD);など)の削除は、origフィールドを介して埋め込みインターフェースのフィールドを構築するロジックが完全に廃止されたことを示します。
    • f->embedded = 1; の削除は、埋め込みインターフェースに特別なembeddedフラグを設定する処理が不要になったことを意味します。
    • これらの変更は、インターフェースの型構築において、origフィールドを用いた複雑な埋め込みインターフェースの追跡メカニズムを放棄し、より直接的なt->type(メソッドリスト)による表現に戻したことを示しています。これにより、再帰的な型チェックの複雑さが軽減され、無限ループの発生を防ぐ狙いがあります。
  2. src/cmd/gc/export.c の変更:

    • dumpexporttype関数内のif(t->etype == TINTER)ブロックの削除は、インターフェース型をエクスポートする際に、そのorigフィールドに格納された埋め込みインターフェースの型情報を特別にダンプするロジックが不要になったことを示します。これはdcl.cでの変更と整合しており、origフィールドが埋め込みインターフェースの特殊な情報を保持しなくなったためです。
    • importtype関数内の!eqtype(pt->orig, t->orig)から!eqtype(pt->orig, t)への変更は、型の一貫性チェックにおいて、インポートされる型のorigフィールドではなく、型そのもの(t)と比較するように戻されたことを意味します。これは、origフィールドが元の型情報を保持するというより単純な役割に戻されたことを示唆しています。
  3. src/cmd/gc/fmt.c の変更:

    • t = t->orig; の削除は、インターフェース型をフォーマットする際に、origフィールドに切り替えてからメソッドリストを走査するという特殊な処理が不要になったことを意味します。これにより、インターフェースの表示ロジックが簡素化され、直接t->type(メソッドリスト)に基づいて行われるようになります。
    • if(!t1->sym)ブロックの削除は、シンボルを持たない(匿名で埋め込まれた)インターフェースの表示を特別扱いするロジックが不要になったことを示します。これは、埋め込みインターフェースの内部表現が簡素化された結果と考えられます。

これらのコード変更は、Goコンパイラがインターフェース、特に再帰的に埋め込まれたインターフェースを処理する際の内部的な複雑さを軽減し、コンパイル時の無限ループエラーを回避することを目的としています。Go 1リリース前の安定性を優先し、稀なコーナーケースの修正よりも、コンパイラの堅牢性を確保するための「元に戻す」という選択がなされたことが、コードレベルからも読み取れます。

関連リンク

  • Go Issue 1909: https://github.com/golang/go/issues/1909
    • このコミットによって「Unfixes」されたイシューです。元のイシューは「gc: infinite recursion for embedded interfaces」と題されており、このコミットが元に戻した修正が対処しようとしていた問題そのものです。
  • Go CL 5555063: https://golang.org/cl/5555063
    • このコミットが参照しているGoのコードレビューリンクです。元のLuuk van Dijkによる修正(f1deaf35e1d1)に関連する議論や変更内容が確認できます。

参考にした情報源リンク

  • Go言語の公式ドキュメント(インターフェース、型システムに関する情報)
  • Go言語のコンパイラソースコード(src/cmd/gcディレクトリ内のファイル構造と関数)
  • Go言語のイシュー追跡システム(GitHub Issues)
  • Go言語のコードレビューシステム(Gerrit)
  • Go 1リリースに関する情報(Go言語の歴史的背景)
  • Go言語の埋め込みインターフェースに関する解説記事
  • コンパイラの型チェックにおける再帰的型定義の処理に関する一般的な情報

(注:具体的なURLは、Web検索ツールを使用した場合に動的に生成されるため、ここでは一般的な情報源のカテゴリを記載しています。)