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

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

このコミットは、Goコンパイラの型チェックフェーズにおける構造体、インターフェース、および関数引数の処理に関する改善とバグ修正を目的としています。特に、dostruct および stotype といった型構築に関連する関数のクリーンアップ、破損したフィールドの検出、そしてそれらのエラーが構造体や関数に適切に伝播されることで、誤ったエラー報告を抑制する変更が含まれています。

コミット

commit 087bec3dcd38a5dedc16fc060a6921b8f18c34ad
Author: Luuk van Dijk <lvd@golang.org>
Date:   Mon Nov 7 21:35:13 2011 +0100

    gc: Clean up dostruct/stotype, detect broken fields and propagate up to structs and functions to supress spurious errors.
    
    Fixes #1556.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5351042

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

https://github.com/golang/go/commit/087bec3dcd38a5dedc16fc060a6921b8f18c34ad

元コミット内容

gc: Clean up dostruct/stotype, detect broken fields and propagate up to structs and functions to supress spurious errors.

このコミットは、Goコンパイラのガーベージコレクタ(gc)部分において、dostructstotype という関数のクリーンアップを行い、破損したフィールドを検出し、その破損情報を構造体や関数に伝播させることで、誤ったエラー(spurious errors)の発生を抑制することを目的としています。

変更の背景

Goコンパイラは、ソースコードを解析し、型情報を構築する過程で、構造体、インターフェース、および関数シグネチャの型を定義します。このプロセスにおいて、型定義に問題がある場合(例えば、未定義の型を参照している場合など)、コンパイラはエラーを報告します。しかし、既存の実装では、あるフィールドの型が破損している場合、その破損情報が上位の構造体や関数に適切に伝播されず、結果として関連性のない場所で追加の、あるいは誤解を招くエラーが報告される可能性がありました。

特に、埋め込み型(embedded type)やインターフェースのメソッド定義において、型チェックの順序やエラー伝播のメカニズムが不十分であったため、本来のエラーとは異なる、あるいは多数の連鎖的なエラーが発生し、デバッグを困難にする問題がありました。このコミットは、これらの問題を解決し、より正確で分かりやすいエラー報告を実現するために行われました。コミットメッセージにある "Fixes #1556" は、この変更が特定のバグ(Issue 1556)を修正するものであることを示しています。

前提知識の解説

このコミットを理解するためには、Goコンパイラの基本的な動作と、型システムに関する知識が必要です。

  • Goコンパイラ (gc): Go言語の公式コンパイラの一つで、主にsrc/cmd/gcディレクトリに実装されています。ソースコードを解析し、中間表現を生成し、最終的に実行可能なバイナリを生成します。
  • 型チェック (Type Checking): コンパイラの重要なフェーズの一つで、プログラム中の各式の型が言語の規則に従っているかを確認します。これにより、型に関するエラー(例: 整数と文字列の加算)をコンパイル時に検出します。
  • 構造体 (Structs): 異なる型のフィールドをまとめた複合データ型です。Goでは、構造体の中に別の構造体を「埋め込む」ことができます(埋め込みフィールド)。
  • インターフェース (Interfaces): メソッドのシグネチャの集合を定義する型です。特定のインターフェースを実装する型は、そのインターフェースのすべてのメソッドを定義する必要があります。
  • 関数シグネチャ (Function Signatures): 関数の名前、引数の型と順序、戻り値の型と順序を定義するものです。
  • NodeType: Goコンパイラの内部表現における主要なデータ構造です。
    • Node: 抽象構文木(AST)のノードを表します。変数、式、文など、プログラムの様々な要素に対応します。
    • Type: Go言語の型システムにおける型を表します。int, string, 構造体、インターフェース、関数型などがこれに該当します。
  • ODCLFIELD: ASTノードの一種で、構造体やインターフェースのフィールド(またはメソッド)宣言を表します。
  • TSTRUCT, TINTER, TFUNC: Type構造体における型カテゴリを表す定数です。それぞれ構造体、インターフェース、関数型を示します。
  • TFORW: 前方宣言された型、つまりまだ完全に定義されていない型を表します。循環参照などで一時的に使用されます。
  • isptr[t->etype]: 型tがポインタ型であるかどうかを判定するマクロまたは関数です。
  • yyerror: コンパイラがエラーメッセージを出力するために使用する関数です。
  • fatal: 致命的なエラーが発生した場合にコンパイラを終了させる関数です。
  • checkwidth(t): 型tのメモリ上のサイズ(幅)を計算する関数です。型が完全に解決されていないと、正確な幅を計算できません。
  • broke フラグ: Type構造体に追加された新しいフラグで、その型が何らかの理由で「破損している」(例えば、未解決の型を参照している、無効な定義があるなど)ことを示します。このフラグがセットされている場合、その型に関連するさらなるエラー報告を抑制したり、特定の処理をスキップしたりすることができます。

技術的詳細

このコミットの主要な変更点は、型構築ロジックの再編成とエラー伝播メカニズムの改善です。

  1. dostructstotype の廃止と新しい関数の導入:

    • 以前は、構造体、インターフェース、関数引数の型構築に dostructstotype という汎用的な関数が使用されていました。これらの関数は、et(要素タイプ)という引数で、構築する型が構造体、インターフェース、関数引数のいずれであるかを区別していました。
    • このコミットでは、これらの汎用関数を廃止し、より特化した関数を導入しています。
      • structfield(Node *n): 構造体の個々のフィールドの型を構築します。
      • tofunargs(NodeList *l): 関数引数リストの型を構築します。
      • interfacefield(Node *n): インターフェースの個々のメソッドの型を構築します。
      • tostruct(NodeList *l): 構造体全体の型を構築します。
      • tointerface(NodeList *l): インターフェース全体の型を構築します。
    • これにより、各型カテゴリに特化したロジックをより明確に分離し、コードの可読性と保守性を向上させています。
  2. broke フラグの導入とエラー伝播:

    • Type構造体にbrokeという新しいフィールドが追加されました。このフラグは、その型が何らかの理由で不完全または無効であることを示します。
    • structfield, interfacefield, tofunargs, tostruct, tointerface などの新しい型構築関数は、フィールドの型が解決できない場合や、埋め込み型に問題がある場合などに、brokeフラグをセットします。
    • 特に、構造体やインターフェースの型を構築する際に、その構成要素(フィールドやメソッド)のいずれかがbrokeフラグを持っている場合、上位の構造体やインターフェースのbrokeフラグもセットされるようになりました。
    • このbrokeフラグは、subr.cassignop関数(型アサーションやインターフェースの代入チェックを行う部分)でも利用され、破損した型に関連する誤ったエラーメッセージを抑制するために使用されます。これにより、コンパイラは一度エラーを検出したら、そのエラーが原因で発生するであろう後続の無関係なエラーを報告しないようになります。
  3. 埋め込み型のチェックの改善 (checkembeddedtype):

    • 埋め込み型に関するチェックがcheckembeddedtypeという独立した関数に切り出されました。
    • この関数は、埋め込み型がポインタ型であってはならないというGoの規則を強制します(例: *interface*struct を埋め込むことはできない)。
    • 前方宣言された型 (TFORW) が埋め込み型として使用される場合、その行番号を記録し、後で解決される際に循環参照などの問題を検出できるようにしています。
  4. 重複フィールド/メソッドのチェック (checkdupfields):

    • 構造体フィールドやインターフェースメソッドの重複をチェックするロジックがcheckdupfieldsという独立した関数に切り出されました。
    • これにより、重複定義によるエラー報告がより体系的に行われるようになりました。
  5. テストケースの追加と修正:

    • test/fixedbugs/bug251.go が修正され、インターフェースの代入に関する誤ったエラー報告が抑制されるようになりました。
    • test/fixedbugs/bug374.go という新しいテストケースが追加されました。このテストは、未定義の型を含む関数シグネチャがインターフェースのメソッドとして定義された場合に、コンパイラが適切なエラーを報告し、かつ余計なエラーを発生させないことを確認するためのものです。特に、xxxxという未定義の型が使用されているにもかかわらず、GC_ERROR "xxxx"というエラーが一度だけ報告され、それ以上の連鎖的なエラーが抑制されることを意図しています。

これらの変更により、Goコンパイラの型チェックの堅牢性が向上し、開発者にとってより分かりやすいエラーメッセージが提供されるようになりました。

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

主要な変更は src/cmd/gc/dcl.c に集中しています。

  • dostruct および stotype 関数の削除: これらの関数はもはや存在しません。
  • checkembeddedtype 関数の追加: 埋め込み型のポインタ禁止などのチェックを行います。
  • structfield 関数の追加: 構造体フィールドの型構築ロジックをカプセル化します。
  • checkdupfields 関数の追加: 重複フィールド/メソッドのチェックを行います。
  • tostruct 関数の追加: 構造体全体の型構築を行います。structfield を利用し、broke フラグの伝播と重複チェックを行います。
  • tofunargs 関数の追加: 関数引数リストの型構築を行います。structfield を利用し、_ 引数の名前をクリアする処理を含みます。
  • interfacefield 関数の追加: インターフェースメソッドの型構築ロジックをカプセル化します。埋め込みインターフェースの処理もここで行われます。
  • tointerface 関数の追加: インターフェース全体の型構築を行います。interfacefield を利用し、broke フラグの伝播と重複チェックを行います。
  • functype 関数の変更: 関数型の構築において、dostruct の代わりに tofunargs を使用するように変更されました。また、引数や戻り値の型にbrokeフラグがセットされている場合、関数型全体にもbrokeフラグを伝播させるロジックが追加されました。
  • addmethod 関数の変更: メソッドの追加において、stotype の代わりに structfield を使用するように変更されました。

その他のファイルでの変更点:

  • src/cmd/gc/go.h: 新しい関数 tointerfacetostruct のプロトタイプ宣言が追加され、削除された関数の宣言が削除されました。
  • src/cmd/gc/go.y: 構文解析器の定義ファイルで、構造体とインターフェースの型構築に dostruct の代わりに tostructtointerface を使用するように変更されました。
  • src/cmd/gc/subr.c: assignop 関数において、インターフェースの代入チェック時に、メソッドの型がbrokeフラグを持っている場合に、誤ったエラーメッセージを抑制するロジックが追加されました。
  • src/cmd/gc/typecheck.c: OTSTRUCTOTINTER の型チェックにおいて、dostruct の代わりに tostructtointerface を使用するように変更されました。
  • test/fixedbugs/bug251.go: インターフェースの代入に関するテストケースが修正されました。
  • test/fixedbugs/bug374.go: 未定義の型を含む関数シグネチャに関する新しいテストケースが追加されました。

コアとなるコードの解説

src/cmd/gc/dcl.c の変更を中心に解説します。

static void checkembeddedtype(Type *t)

static void
checkembeddedtype(Type *t)
{
	if (t == T)
		return;

	if(t->sym == S && isptr[t->etype]) {
		t = t->type;
		if(t->etype == TINTER)
			yyerror("embedded type cannot be a pointer to interface");
	}
	if(isptr[t->etype])
		yyerror("embedded type cannot be a pointer");
	else if(t->etype == TFORW && t->embedlineno == 0)
		t->embedlineno = lineno;
}

この関数は、埋め込み型tが有効であるかをチェックします。

  • t == T の場合(型が未定義またはエラー状態の場合)は何もせずに関数を終了します。
  • t->sym == S && isptr[t->etype] は、シンボルを持たないポインタ型の場合をチェックします。これは、*interface のような埋め込み型を検出するためです。もしそれがインターフェースへのポインタであれば、yyerror でエラーを報告します。
  • isptr[t->etype] は、型tがポインタ型であるかをチェックします。Goでは、構造体にポインタを埋め込むことはできません(匿名フィールドとしてポインタ型を宣言することはできますが、それは埋め込み型とは異なります)。したがって、ポインタ型であればエラーを報告します。
  • t->etype == TFORW && t->embedlineno == 0 は、前方宣言された型 (TFORW) が埋め込み型として使用され、かつまだその埋め込みが行われた行番号が記録されていない場合に、現在の行番号 (lineno) をt->embedlinenoに記録します。これは、後で循環参照などの問題を検出する際に役立ちます。

static Type* structfield(Node *n)

static Type*
structfield(Node *n)
{
	Type *f;
	int lno;

	lno = lineno;
	lineno = n->lineno;

	if(n->op != ODCLFIELD)
		fatal("structfield: oops %N\n", n);

	f = typ(TFIELD);
	f->isddd = n->isddd;

	if(n->right != N) {
		typecheck(&n->right, Etype);
		n->type = n->right->type;
		if(n->left != N)
			n->left->type = n->type;
		if(n->embedded)
			checkembeddedtype(n->type);
	}
	n->right = N;
		
	f->type = n->type;
	if(f->type == T)
		f->broke = 1;

	// ... (tag processing) ...

	if(n->left && n->left->op == ONAME) {
		f->nname = n->left;
		f->embedded = n->embedded;
		f->sym = f->nname->sym;
		if(importpkg && !exportname(f->sym->name))
			f->sym = pkglookup(f->sym->name, structpkg);
	}

	lineno = lno;
	return f;
}

この関数は、ODCLFIELDノード(構造体フィールド宣言)からTFIELD型のフィールド情報を構築します。

  • n->right はフィールドの型を表すノードです。typecheck で型チェックを行い、その結果をn->typeに格納します。
  • n->embedded が真の場合(埋め込みフィールドの場合)、checkembeddedtype を呼び出して埋め込み型の制約をチェックします。
  • f->type = n->type; でフィールドの型を設定します。
  • if(f->type == T) f->broke = 1; は、フィールドの型が解決できなかった場合(Tはエラー状態の型を表す)、そのフィールドが「破損している」ことを示すbrokeフラグをセットします。
  • f->nname, f->embedded, f->sym など、フィールドの名前、埋め込みフラグ、シンボル情報を設定します。

static void checkdupfields(Type *t, char* what)

static void
checkdupfields(Type *t, char* what)
{
	Type* t1;
	int lno;

	lno = lineno;

	for( ; t; t=t->down)
		if(t->sym && t->nname && !isblank(t->nname))
			for(t1=t->down; t1; t1=t1->down)
				if(t1->sym == t->sym) {
					lineno = t->nname->lineno;
					yyerror("duplicate %s %s", what, t->sym->name);
					break;
				}

	lineno = lno;
}

この関数は、与えられた型リストt(構造体フィールドまたはインターフェースメソッドのリスト)内で、重複するシンボル名を持つフィールドまたはメソッドがないかをチェックします。重複が見つかった場合、yyerror でエラーを報告します。

Type* tostruct(NodeList *l)

Type*
tostruct(NodeList *l)
{
	Type *t, *f, **tp;
	t = typ(TSTRUCT);

	for(tp = &t->type; l; l=l->next,tp = &(*tp)->down)
		*tp = structfield(l->n);

	for(f=t->type; f && !t->broke; f=f->down)
		if(f->broke)
			t->broke = 1;

	checkdupfields(t->type, "field");

	if (!t->broke)
		checkwidth(t);

	return t;
}

この関数は、NodeList l から構造体全体の型を構築します。

  • t = typ(TSTRUCT); で新しい構造体型を作成します。
  • ループでlの各ノード(フィールド宣言)をstructfieldで処理し、結果を構造体のフィールドリストに連結していきます。
  • for(f=t->type; f && !t->broke; f=f->down) if(f->broke) t->broke = 1; の部分が重要です。これは、構造体のいずれかのフィールドがbrokeフラグを持っている場合、構造体全体のbrokeフラグをセットすることで、エラー情報を上位に伝播させます。
  • checkdupfields(t->type, "field"); で重複フィールドをチェックします。
  • if (!t->broke) checkwidth(t); は、構造体が破損していない場合にのみ、そのメモリ上のサイズを計算します。破損している場合は、サイズ計算をスキップすることで、さらなるエラーを防ぎます。

Type* tointerface(NodeList *l)

tointerface 関数は tostruct と同様のロジックでインターフェース型を構築しますが、インターフェース特有の処理が含まれます。

  • 埋め込みインターフェース (l->n->left == N && f->type->etype == TINTER) の場合、そのインターフェースのメソッドをインライン展開します。
  • メソッドの型がTFORW(前方宣言)で循環参照を引き起こす場合や、埋め込み型がインターフェースではない場合にエラーを報告し、f->broke = 1 をセットします。
  • インターフェースのいずれかのメソッドがbrokeフラグを持っている場合、インターフェース全体のbrokeフラグをセットします。
  • checkdupfields(t->type, "method"); で重複メソッドをチェックします。

これらの変更により、Goコンパイラは型定義の不整合をより正確に検出し、エラーの連鎖を抑制することで、開発者にとってより有用なエラーメッセージを提供するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/gc ディレクトリ): https://github.com/golang/go/tree/master/src/cmd/gc
  • Go言語のコンパイラに関するドキュメントやブログ記事 (一般的な情報源):
  • Go言語のIssue 1556 (このコミットが修正したとされるIssue): 残念ながら、Web検索ではこのコミットが修正したとされる golang/go リポジトリのIssue 1556の直接的な情報を見つけることができませんでした。コミットメッセージに記載されている https://golang.org/cl/5351042 は、GoのGerritコードレビューシステムへのリンクであり、この変更の詳細な議論や背景がそこに存在する可能性があります。