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

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

コミット

commit 892fa3ae6c2d70fa1554ac2c817adfbe3c4c0c50
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sun Oct 7 17:35:21 2012 +0200

    cmd/gc: replace "typechecking loop" by nicer errors in some cases.
    
    For issue 3757:
    BEFORE:  test/fixedbugs/bug463.go:12: typechecking loop involving a
                 test/fixedbugs/bug463.go:12 a
                 test/fixedbugs/bug463.go:12 <node DCLCONST>
    AFTER:   test/fixedbugs/bug463.go:12: constant definition loop
                 test/fixedbugs/bug463.go:12: a uses a
    
    For issue 3937:
    BEFORE: test/fixedbugs/bug464.go:12: typechecking loop involving foo
                test/fixedbugs/bug464.go:12 <T>
                test/fixedbugs/bug464.go:12 foo
                test/fixedbugs/bug464.go:12 <node DCLFUNC>
    AFTER:  test/fixedbugs/bug464.go:12: foo is not a type
    
    Fixes #3757.
    Fixes #3937.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6614058

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

https://github.com/golang/go/commit/892fa3ae6c2d70fa1554ac2c817adfbe3c4c0c50

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)における型チェック時のエラーメッセージを改善することを目的としています。特に、循環参照("typechecking loop")が検出された場合に、より分かりやすく具体的なエラーメッセージを出力するように変更されています。

具体的には、以下の2つの問題(Issue)に対応しています。

  • Issue 3757: 定数定義における循環参照に対して、より明確なエラーメッセージ(例: "constant definition loop")を提供する。
  • Issue 3937: 型として誤って使用された識別子(例: 関数名を変数型として使用)に対して、より適切なエラーメッセージ(例: "foo is not a type")を提供する。

変更前と変更後のエラーメッセージの例がコミットメッセージに示されており、改善されたメッセージがユーザーにとって診断しやすいものになっていることが分かります。

変更の背景

Go言語のコンパイラは、コードの型チェックを行う際に、変数や定数、関数の定義が互いに依存し合っている場合に「型チェックループ (typechecking loop)」を検出することがあります。しかし、以前のコンパイラでは、この「型チェックループ」が検出された際のエラーメッセージが非常に汎用的で、ユーザーが問題の根本原因を特定しにくいという課題がありました。

具体的には、以下の2つのGitHub Issueがこの変更の背景にあります。

  • Issue 3757: 定数定義における循環参照が検出された際に、単に「typechecking loop involving a」のような抽象的なメッセージが表示され、どの定数がどのように循環しているのかが分かりづらいという問題がありました。例えば、const a = a のような単純なケースでも、エラーメッセージからは具体的な原因が読み取りにくかったのです。
  • Issue 3937: 関数名などの識別子を誤って型として使用した場合に、これもまた「typechecking loop involving foo」のようなメッセージが表示され、本来「fooは型ではありません」といった、より直接的なエラーメッセージが期待される状況で、不必要なまでに複雑な情報が提示されていました。

これらの問題は、開発者がGoコードを記述する際に、コンパイラのエラーメッセージから問題を迅速に理解し、修正する妨げとなっていました。そのため、コンパイラのエラーメッセージをよりユーザーフレンドリーで、具体的な問題解決に役立つものに改善する必要がありました。このコミットは、これらの課題に対処し、開発者のデバッグ体験を向上させることを目的としています。

前提知識の解説

このコミットの理解には、以下のGoコンパイラの内部構造と概念に関する知識が役立ちます。

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラの一部であり、ソースコードの解析、型チェック、最適化、コード生成などを担当します。
  • 型チェック (Typechecking): プログラム内の各式や変数が、その文脈において正しい型を持っているかを検証するプロセスです。Goは静的型付け言語であるため、コンパイル時に厳密な型チェックが行われます。
  • AST (Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。コンパイラはソースコードをASTに変換し、このASTを操作しながら型チェックやコード生成を行います。Goコンパイラでは、ASTの各ノードがNode構造体で表現されます。
  • typecheck関数: src/cmd/gc/typecheck.c に存在するGoコンパイラの主要な型チェック関数です。ASTのノードを再帰的に処理し、そのノードの型を決定します。
  • typechecktop引数: typecheck関数に渡されるビットフラグで、型チェックのコンテキスト(期待される型、値、宣言など)を示します。例えば、Ervは「右辺値(式の結果として評価される値)」、Etypeは「型」を期待していることを示します。
  • ONAMEノード: ASTにおいて、識別子(変数名、関数名など)を表すノードです。
  • OLITERALノード: ASTにおいて、リテラル(数値、文字列、真偽値など)を表すノードです。定数定義における循環参照は、このOLITERALノードの型チェック中に発生することがあります。
  • 循環参照 (Circular Reference / Loop): 2つ以上のエンティティが互いに直接的または間接的に依存し合っている状態です。型チェックの文脈では、ある変数の型を決定するために別の変数の型が必要で、その別の変数の型を決定するために最初の変数の型が必要になる、といった状況を指します。
  • yyerror / yyerrorl: Goコンパイラがエラーメッセージを出力するために使用する内部関数です。yyerrorlは特定の行番号を指定してエラーを出力します。
  • Fmt構造体とfmtstrinit/fmtstrflush: Goコンパイラ内部で文字列フォーマットを行うためのユーティリティです。Fmtはフォーマットの状態を保持し、fmtstrinitで初期化し、fmtstrflushで最終的な文字列を取得します。
  • NodeList: ASTノードのリンクリストで、型チェックのスタック(tcstack)として使用され、循環参照の検出に役立ちます。

これらの概念を理解することで、コミットがGoコンパイラの型チェックロジックのどの部分に影響を与え、どのようにエラーメッセージを改善しているのかを深く把握することができます。

技術的詳細

このコミットの主要な変更は、src/cmd/gc/typecheck.c ファイル内の typecheck 関数と、新たに追加された sprint_depchain 関数に集中しています。

  1. sprint_depchain 関数の追加:

    • この関数は、定数定義の循環参照(OLITERALノードの場合)において、依存関係の連鎖を整形して出力するために導入されました。
    • Fmt構造体とNodeList(型チェックスタックtcstack)を受け取り、循環しているノードとその依存関係を「%L: %N uses %N」(行番号: ノードAはノードBを使用)という形式でフォーマットします。
    • これにより、以前は抽象的だった「typechecking loop」のエラーが、「constant definition loop」という具体的なメッセージと、どの定数がどの定数を使用しているかという詳細な情報を含むようになりました。
  2. typecheck 関数の変更:

    • typecheck関数内で、ノードのtypecheckフィールドが2(型チェック中、つまり循環が検出された可能性のある状態)になった場合の処理が変更されました。
    • ONAMEノードの改善:
      • ONAMEノード(識別子)が型として期待されているコンテキスト(top & (Erv|Etype)) == Etype)で、かつ型チェックループが検出された場合、以前は汎用的な「typechecking loop」エラーが出力されていました。
      • 変更後は、yyerror("%N is not a type", n); という、より直接的で分かりやすいエラーメッセージ(例: "foo is not a type")が出力されるようになりました。これは、関数名などを誤って型として使用した場合に特に有効です。
    • OLITERALノードの改善:
      • OLITERALノード(リテラル、特に定数)が型として期待されているコンテキストで、かつ型チェックループが検出された場合、sprint_depchain関数が呼び出されます。
      • sprint_depchainによって整形された依存関係の文字列が、yyerrorl(n->lineno, "constant definition loop%s", fmtstrflush(&fmt)); という形式でエラーメッセージに追加されます。これにより、定数定義の循環参照がより詳細に報告されるようになりました。
    • 汎用的な「typechecking loop」エラーの抑制:
      • 上記の特定のケース(ONAMEOLITERAL)でより具体的なエラーメッセージが出力されるようになったため、これらのケースでは以前の汎用的な「typechecking loop」エラーメッセージ(スタックトレースを含む)が出力されなくなりました。
      • if(nsavederrors+nerrors == 0) の条件は、まだエラーが報告されていない場合にのみ汎用的なエラーを出力するためのもので、今回の変更により、特定のケースではこの条件が満たされる前に具体的なエラーが報告されるようになります。

これらの変更により、Goコンパイラは型チェック時のエラーをよりインテリジェントに診断し、開発者にとってより有用なフィードバックを提供するようになりました。

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

このコミットにおける主要なコードの変更箇所は以下の通りです。

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

    • sprint_depchain 関数の新規追加 (行 105-119)
    • typecheck 関数内のエラーハンドリングロジックの変更 (行 155-176)
      • n->typecheck == 2 のブロック内に switch(n->op) が追加され、ONAMEOLITERAL ノードに対する特殊なエラー処理が実装されました。
      • ONAME の場合、yyerror("%N is not a type", n); が追加。
      • OLITERAL の場合、sprint_depchain を使用した詳細なエラーメッセージの生成と yyerrorl による出力が追加。
      • 既存の汎用的な「typechecking loop」エラー出力の前に、これらの具体的なエラーが優先されるようになりました。
  2. test/fixedbugs/bug463.go:

    • 新規追加されたテストファイル。
    • 定数定義の循環参照(const a = aA = B, B = D, D = A のようなケース)を意図的に含み、新しいエラーメッセージ「refers to itself|definition loop」が正しく出力されることを検証します。
  3. test/fixedbugs/bug464.go:

    • 新規追加されたテストファイル。
    • 関数名(foo)を誤って型として使用するケース(func foo(x foo) {})を意図的に含み、新しいエラーメッセージ「expected type|not a type」が正しく出力されることを検証します。

これらの変更により、コンパイラの型チェックロジックが改善され、同時にその改善が新しいテストケースによって検証されています。

コアとなるコードの解説

sprint_depchain 関数

static void
sprint_depchain(Fmt *fmt, NodeList *stack, Node *cur, Node *first)
{
	NodeList *l;

	for(l = stack; l; l=l->next) {
		if(l->n->op == cur->op) {
			if(l->n != first)
				sprint_depchain(fmt, l->next, l->n, first);
			fmtprint(fmt, "\n\t%L: %N uses %N", l->n->lineno, l->n, cur);
			return;
		}
	}
}

この関数は、定数定義の循環参照を診断する際に、その依存関係の連鎖を整形して出力するために使用されます。

  • fmt: フォーマット結果を書き込むFmt構造体へのポインタ。
  • stack: 現在の型チェックスタック(tcstack)のリンクリスト。このスタックは、型チェック中のノードの履歴を保持し、循環参照の検出に利用されます。
  • cur: 現在処理中のノード。
  • first: 循環の開始点となるノード。

関数はstackを逆順に辿り、curノードと同じop(操作タイプ)を持つノードを探します。もし見つかれば、それが循環の一部である可能性が高いと判断し、再帰的にsprint_depchainを呼び出して依存関係の連鎖を構築します。最終的に、fmtprintを使って「\n\t%L: %N uses %N」(改行、タブ、行番号: ノードAはノードBを使用)という形式で、循環しているノードとその依存関係を出力します。これにより、ユーザーはどの定数がどの定数に依存しているために循環が発生しているのかを具体的に把握できます。

typecheck 関数内の変更

	if(n->typecheck == 2) {
		// Typechecking loop. Trying printing a meaningful message,
		// otherwise a stack trace of typechecking.
		switch(n->op) {
		case ONAME:
			// We can already diagnose variables used as types.
			if((top & (Erv|Etype)) == Etype)
				yyerror("%N is not a type", n);
			break;
		case OLITERAL:
			if((top & (Erv|Etype)) == Etype) {
				yyerror("%N is not a type", n);
				break;
			}
			fmtstrinit(&fmt);
			sprint_depchain(&fmt, tcstack, n, n);
			yyerrorl(n->lineno, "constant definition loop%s", fmtstrflush(&fmt));
			break;
		}
		if(nsavederrors+nerrors == 0) {
			fmtstrinit(&fmt);
			for(l=tcstack; l; l=l->next)
				fmtprint(&fmt, "\n\t%L %N", l->n->lineno, l->n);
			yyerror("typechecking loop involving %N%s", n, fmtstrflush(&fmt));
		}
		return n;
	}

このコードブロックは、typecheck関数がノードnの型チェック中に、そのノードが既に型チェック中(n->typecheck == 2)であることを検出した場合に実行されます。これは循環参照の兆候です。

  • switch(n->op): ノードの操作タイプ(op)に基づいて、より具体的なエラーメッセージを生成するための分岐が追加されました。
    • case ONAME::
      • ONAMEノード(識別子)が型として期待されているコンテキスト(topフラグにEtypeが含まれる)で循環が検出された場合、yyerror("%N is not a type", n); が呼び出されます。これは、関数名などを誤って型として使用した場合に、より適切なエラーメッセージを出力します。
    • case OLITERAL::
      • OLITERALノード(リテラル、特に定数)が型として期待されているコンテキストで循環が検出された場合、まずyyerror("%N is not a type", n); が呼び出されます。
      • その後、fmtstrinit(&fmt); でフォーマッタを初期化し、sprint_depchain(&fmt, tcstack, n, n); を呼び出して依存関係の連鎖を整形します。
      • 最後に、yyerrorl(n->lineno, "constant definition loop%s", fmtstrflush(&fmt)); を使用して、行番号、"constant definition loop"というメッセージ、そしてsprint_depchainが生成した詳細な依存関係の文字列を含むエラーを出力します。
  • if(nsavederrors+nerrors == 0):
    • この条件は、まだエラーが報告されていない場合にのみ、以前の汎用的な「typechecking loop involving %N」というエラーメッセージ(スタックトレースを含む)を出力するためのものです。
    • 上記のswitch文で特定のケースに対する具体的なエラーが報告された場合、この条件は満たされなくなり、汎用的なエラーは抑制されます。これにより、冗長なエラーメッセージの出力が避けられます。

これらの変更により、Goコンパイラは型チェック時の循環参照に対して、より具体的で診断しやすいエラーメッセージを提供するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (src/cmd/gc/typecheck.c, test/fixedbugs/bug463.go, test/fixedbugs/bug464.go)
  • GitHubのGoリポジトリのIssueトラッカー
  • Go言語の公式ドキュメント (型システム、コンパイラに関する一般的な情報)
  • Go言語のコンパイラ設計に関する一般的な知識
  • Web検索: "Go issue 3757", "Go issue 3937" (これらのIssueに関する背景情報を得るため)