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

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

このコミットは、Goコンパイラ(cmd/gc)における「invalid recursive type」(無効な再帰型)という誤ったエラー報告を抑制するための修正です。具体的には、型チェックの過程で既にエラーが報告されているにもかかわらず、dowidth関数が再度同じ再帰型エラーを報告してしまう問題を解決します。これにより、コンパイラの出力がよりクリーンになり、開発者が真に問題のあるエラーに集中できるようになります。

コミット

commit 933d7129c07e32ffa403c94634fa0c7045f6b3d8
Author: Russ Cox <rsc@golang.org>
Date:   Mon Sep 9 13:03:59 2013 -0400

    cmd/gc: squelch spurious "invalid recursive type" error
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/13512047

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

https://github.com/golang/go/commit/933d7129c07e32ffa403c94634fa0c7045f6b3d8

元コミット内容

cmd/gc: squelch spurious "invalid recursive type" error

このコミットは、Goコンパイラ(cmd/gc)が不必要な「invalid recursive type」エラーを抑制するためのものです。

変更の背景

Goコンパイラは、プログラムの型チェックを行う際に、型の定義が再帰的になっているかどうかを検出します。例えば、構造体が自分自身を直接的または間接的に参照している場合などです。通常、このような再帰型は有効ですが、特定の条件下(例えば、再帰の終端条件が欠けている場合や、不完全な型定義の場合)では無効な再帰型としてエラーが報告されることがあります。

このコミットが修正しようとしている問題は、コンパイラの型チェックの異なるフェーズで、同じ根本的な問題に対して複数のエラーが報告されてしまう「spurious」(偽りの、不必要な)エラーでした。具体的には、typecheckdef関数で型チェックが行われ、そこで既にエラーが検出・報告されているにもかかわらず、その後のdowidth関数(型のメモリレイアウトやサイズを計算する関数)が、不完全な型情報に基づいて再度「invalid recursive type」エラーを報告してしまうという状況です。

このような重複したエラーは、開発者にとって混乱を招き、真の問題を特定するのを困難にします。コンパイラのエラーメッセージは、できるだけ正確で、かつ冗長でないことが望ましいとされています。このコミットは、この冗長性を排除し、コンパイラのユーザーエクスペリエンスを向上させることを目的としています。

この問題は、Go issue 5581として報告されていました。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの内部構造と概念に関する知識が必要です。

  1. Goコンパイラ (cmd/gc): Go言語の公式コンパイラの一つで、Goソースコードを機械語に変換します。コンパイルプロセスは、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成など、複数のフェーズに分かれています。
  2. 型システムと型チェック: Goは静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。これにより、多くのプログラミングエラーが実行時ではなくコンパイル時に検出されます。型チェックフェーズでは、変数の型が正しく使用されているか、関数の引数と戻り値の型が一致しているか、構造体のフィールドアクセスが正しいかなどが検証されます。
  3. 再帰型 (Recursive Types): 型が自分自身を直接的または間接的に参照する構造を持つ場合、それを再帰型と呼びます。Goでは、ポインタやインターフェースを介して再帰型を定義することが一般的です。例えば、リンクリストのノードやツリー構造のノードなどがこれに該当します。
    type Node struct {
        Value int
        Next  *Node // Nodeが自分自身へのポインタを持つ
    }
    
  4. Type構造体: Goコンパイラの内部では、Go言語の型はType構造体で表現されます。この構造体には、型の種類(etype)、サイズ(width)、シンボル(sym)、関連する行番号(lineno)などの情報が含まれます。
    • t->width: 型のメモリ上のサイズを表します。-2という特殊な値は、現在その型の幅が計算中であり、再帰的な依存関係があることを示唆します。
    • t->broke: このコミットで導入された新しいフィールド(または既存のフィールドの新しい利用法)で、その型が既にエラー状態にあることを示すフラグです。
  5. TFORW (Forward Declaration Type): コンパイラが型を処理する際に、まだ完全に定義されていない型(前方宣言された型)を表すために使用される内部的な型です。再帰型を解決する過程で一時的に使用されることがあります。
  6. dowidth関数: Goコンパイラのsrc/cmd/gc/align.cに存在する関数で、Goの各型のメモリ上のサイズ(アラインメントと幅)を計算する役割を担います。この関数は、型の定義を再帰的に辿り、各フィールドや要素のサイズを合計して全体のサイズを決定します。再帰的な型定義の場合、無限ループに陥らないように、既に処理中の型を検出するメカニズムが必要です。
  7. typecheckdef関数: Goコンパイラのsrc/cmd/gc/typecheck.cに存在する関数で、定義(変数、関数、型など)の型チェックを行います。この関数は、プログラム内のシンボルとそれに関連付けられた型がGo言語の規則に従っていることを確認します。
  8. yyerror: コンパイラがエラーメッセージを出力するために使用する内部関数です。

技術的詳細

このコミットの技術的な核心は、Goコンパイラの型チェックとメモリレイアウト計算のフェーズ間で、エラー報告の重複を避けるための協調メカニズムを導入した点にあります。

問題のシナリオは以下の通りです。

  1. typecheckdef関数が型定義を処理します。この際、例えば未定義の型を参照するような不正な再帰型が検出されると、yyerrorを呼び出してエラーを報告します。
  2. しかし、typecheckdefがエラーを報告した後も、その型は内部的にTFORW(前方宣言型)としてマークされたまま、dowidth関数による幅の計算フェーズに進むことがあります。
  3. dowidth関数は、型の幅を計算する際に、t->width == -2という状態(再帰的な処理中)やTFORW型の未解決な状態に遭遇すると、「invalid recursive type」エラーを報告するように設計されています。
  4. このため、typecheckdefで既にエラーが報告されているにもかかわらず、dowidthが再度同じ問題に対してエラーを報告してしまうという冗長な出力が発生していました。

このコミットは、この問題を解決するために以下の変更を導入しました。

  • Type.brokeフラグの導入: Type構造体にbrokeという新しいフィールド(または既存のフィールドの新しい利用法)が追加されました。このフラグは、その型が既に型チェックの過程でエラーを引き起こし、そのエラーが報告済みであることを示します。
  • typecheckdefでのbrokeフラグの設定: src/cmd/gc/typecheck.ctypecheckdef関数内で、typecheckdeftype(n)の呼び出し後に、エラーが発生したかどうかをチェックします。具体的には、nerrors(エラーカウンタ)がtypecheckdeftype呼び出し前よりも増加しており、かつ型がまだTFORWの状態である場合(つまり、型チェックが完了せず、前方宣言のまま残っている場合)、n->type->broke = 1を設定します。これは、「型チェック中に何か問題が発生したが、それは既に報告済みである。将来の(この型に関する)エラーは抑制する」という意図です。
  • dowidthでのエラー抑制: src/cmd/gc/align.cdowidth関数内で、「invalid recursive type」エラーを報告する条件に!t->brokeというチェックが追加されました。これにより、もしt->brokeフラグが設定されている(つまり、既にエラーが報告済みである)場合、dowidthは重複したエラーメッセージを出力しなくなります。

これらの変更により、コンパイラは同じ根本的な問題に対して一度だけエラーを報告するようになり、出力の冗長性が解消されました。

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

src/cmd/gc/align.c

--- a/src/cmd/gc/align.c
+++ b/src/cmd/gc/align.c
@@ -119,7 +119,8 @@ dowidth(Type *t)
 	if(t->width == -2) {
 		lno = lineno;
 		lineno = t->lineno;
-		yyerror("invalid recursive type %T", t);
+		if(!t->broke)
+			yyerror("invalid recursive type %T", t);
 		t->width = 0;
 		lineno = lno;
 		return;
@@ -219,7 +220,8 @@ dowidth(Type *t)
 		checkwidth(t->down);
 		break;
 	case TFORW:		// should have been filled in
-		yyerror("invalid recursive type %T", t);
+		if(!t->broke)
+			yyerror("invalid recursive type %T", t);
 		w = 1;	// anything will do
 		break;
 	case TANY:

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -3046,7 +3046,7 @@ queuemethod(Node *n)
 Node*
 typecheckdef(Node *n)
 {
-	int lno;
+	int lno, nerrors0;
 	Node *e;
 	Type *t;
 	NodeList *l;
@@ -3174,7 +3174,13 @@ typecheckdef(Node *n)
 		n->walkdef = 1;
 		n->type = typ(TFORW);
 		n->type->sym = n->sym;
+		nerrors0 = nerrors;
 		typecheckdeftype(n);
+		if(n->type->etype == TFORW && nerrors > nerrors0) {
+			// Something went wrong during type-checking,
+			// but it was reported. Silence future errors.
+			n->type->broke = 1;
+		}
 		if(curfn)
 			resumecheckwidth();
 		break;

test/fixedbugs/issue5581.go

--- /dev/null
+++ b/test/fixedbugs/issue5581.go
@@ -0,0 +1,34 @@
+// errorcheck
+
+// Used to emit a spurious "invalid recursive type" error.
+// See golang.org/issue/5581.
+
+// Copyright 2013 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.
+
+package main
+
+import "fmt"
+
+func NewBar() *Bar { return nil }
+
+func (x *Foo) Method() (int, error) {
+	for y := range x.m {
+		_ = y.A
+	}
+	return 0, nil
+}
+
+type Foo struct {
+	m map[*Bar]int
+}
+
+type Bar struct {
+	A *Foo
+	B chan Blah // ERROR "undefined: Blah"
+}
+
+func main() {
+	fmt.Println("Hello, playground")
+}

コアとなるコードの解説

src/cmd/gc/align.c の変更

dowidth関数は型のメモリ幅を計算する際に、再帰的な型定義や未解決の前方宣言型(TFORW)に遭遇した場合にエラーを報告します。 変更前は無条件にyyerrorを呼び出していましたが、変更後はif(!t->broke)という条件が追加されました。 これは、もしt->brokeフラグがtrue(つまり、この型に関するエラーが既に報告済み)であれば、yyerrorを呼び出さないようにすることで、重複したエラーメッセージの出力を抑制します。

src/cmd/gc/typecheck.c の変更

typecheckdef関数は、型定義の型チェックを行います。 変更点としては、nerrors0 = nerrors;で型チェック開始前のエラー数を保存し、typecheckdeftype(n);で実際の型チェックを行った後、if(n->type->etype == TFORW && nerrors > nerrors0)という条件でエラーの発生をチェックしています。

  • n->type->etype == TFORW: 型がまだ前方宣言のままで、完全に解決されていないことを示します。
  • nerrors > nerrors0: typecheckdeftypeの呼び出しによって、新しいエラーが報告されたことを示します。

この両方の条件が満たされた場合、n->type->broke = 1;を設定します。これは、この型が「壊れている」(エラー状態にある)ことをマークし、将来的にdowidthのような他のフェーズでこの型が処理される際に、重複したエラー報告を避けるためのシグナルとなります。コメントにある「Silence future errors.」がその意図を明確に示しています。

test/fixedbugs/issue5581.go の追加

このテストファイルは、このコミットが修正する特定のバグを再現するために追加されました。 Bar構造体内のB chan Blahという行がポイントです。Blahはどこにも定義されていないため、これは「undefined: Blah」というエラーを引き起こします。 このエラーはtypecheckdefフェーズで報告されるべきです。 このテストの目的は、この「undefined: Blah」エラーが報告された後、dowidth関数が「invalid recursive type」という重複したエラーを報告しないことを確認することです。 // errorcheckコメントは、Goのテストフレームワークに対して、このファイルがコンパイルエラーを期待していることを示します。

関連リンク

参考にした情報源リンク

  • Go issue 5581のコメントと議論
  • Goコンパイラのソースコード(src/cmd/gc/align.c, src/cmd/gc/typecheck.c
  • Go言語の型システムに関する一般的なドキュメント
  • コンパイラの設計と実装に関する一般的な知識