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

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

このコミットは、Goコンパイラ(cmd/gc)における誤った「not enough arguments to return」エラーの発生を修正するものです。具体的には、より根本的なエラー(例:未定義の関数呼び出し)が既に検出されている場合に、コンパイラが不必要に引数不足のエラーを報告してしまう問題を解決します。

コミット

commit 1483747f3c62fb6149cce8027e98adeda77cc343
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Fri Mar 14 16:42:42 2014 +0100

    cmd/gc: fix spurious 'not enough arguments to return' error
    
    Fixes #6405
    
    LGTM=rsc
    R=rsc, iant
    CC=golang-codereviews
    https://golang.org/cl/72920046

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

https://github.com/golang/go/commit/1483747f3c62fb6149cce8027e98adeda77cc343

元コミット内容

cmd/gc: fix spurious 'not enough arguments to return' error

Fixes #6405

LGTM=rsc
R=rsc, iant
CC=golang-codereviews
https://golang.org/cl/72920046

変更の背景

Goコンパイラ(cmd/gc)は、プログラムの型チェックを行う際に、式の引数の数や戻り値の数が期待通りであるかを検証します。しかし、特定のシナリオにおいて、コンパイラが誤って「not enough arguments to return」(戻り値の数が足りません)というエラーを報告してしまうバグが存在しました。

この「spurious」(偽りの、見せかけの)エラーは、実際には別の、より根本的なエラー(例えば、存在しない関数を呼び出している場合など)が既に検出されているにもかかわらず発生していました。コンパイラが一度エラーを検出したノード(抽象構文木の一部)に対して、その後の型チェックフェーズでさらに別の、関連性の低いエラーを報告してしまうことが問題でした。これにより、開発者は本来のエラーを見落とし、誤解を招く可能性がありました。

この問題はGoのIssue 6405として報告されており、このコミットはその問題を解決するために作成されました。

前提知識の解説

このコミットの変更内容を理解するためには、以下の概念についての基本的な知識が必要です。

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成などのフェーズが含まれます。
  • 型チェック (Type Checking): コンパイルフェーズの一つで、プログラム内の変数や式の型が言語の規則に従って正しく使用されているかを検証します。例えば、整数型の変数に文字列を代入しようとしたり、関数が期待する引数の型や数と異なる呼び出しが行われたりした場合にエラーを検出します。
  • 抽象構文木 (Abstract Syntax Tree, AST): ソースコードの構文構造を木構造で表現したものです。コンパイラはソースコードをASTに変換し、このASTを走査しながら型チェックやコード生成などの処理を行います。ASTの各要素は「ノード (Node)」として表現されます。
  • 診断フラグ (diag): コンパイラ内部で、ASTノードにエラーや警告が既に報告されたことを示すために使用されるフラグです。このフラグがセットされている場合、そのノードまたはその子孫ノードに対して既に診断メッセージが出力されていることを意味し、重複したエラー報告を防ぐために利用されます。
  • yyerror: 多くのコンパイラやパーサーで使われるエラー報告関数です。通常、yaccbisonのようなパーサージェネレータによって生成されたコードで使用され、コンパイルエラーメッセージを標準エラー出力に出力します。
  • Node構造体: src/cmd/gc/typecheck.cのようなコンパイラのソースコードでは、ASTの各ノードを表すC言語の構造体(例: Node)が定義されています。この構造体には、ノードの種類(演算子、変数、関数呼び出しなど)、型情報、子ノードへのポインタ、そして診断フラグなどのメタデータが含まれます。

技術的詳細

このコミットは、Goコンパイラの型チェックロジック、特にsrc/cmd/gc/typecheck.cファイル内のtypecheckaste関数とエラー報告メカニズムに焦点を当てています。

問題の根源は、コンパイラが式を型チェックする際に、子ノードで既にエラーが検出されているにもかかわらず、そのエラー状態が親ノードに適切に伝播されず、結果として親ノードで別の(そして誤解を招く)エラーが報告されてしまう点にありました。

具体的には、func Open() (int, error) { return OpenFile() } のようなコードにおいて、OpenFileが未定義であるというエラーが最初に検出されるべきです。しかし、古いコンパイラでは、OpenFile()の呼び出しが戻り値を期待するコンテキスト(returnステートメント)で使用されているため、未定義エラーに加えて「not enough arguments to return」という誤ったエラーも報告されていました。これは、OpenFile()が未定義であるため、その戻り値の型や数が不明であり、結果としてreturnステートメントが必要とする戻り値の数と合致しないと判断されてしまうためです。

このコミットによる修正は、以下の2つの主要な変更によってこの問題を解決します。

  1. 診断フラグの伝播: typecheckaste関数内で、n->diag |= n->left->diag;という行が追加されました。これは、左の子ノード(n->left)に既に診断フラグ(エラーが報告されたことを示す)が設定されている場合、そのフラグを現在のノード(n)にも伝播させることを意味します。これにより、子ノードでエラーが検出されたという情報が親ノードに正しく伝わり、親ノードがその後のチェックで重複または誤解を招くエラーを報告するのを防ぎます。

  2. 条件付きエラー報告: notenoughというラベル(yyerror関数を呼び出して「not enough arguments」エラーを報告する箇所)のロジックが変更されました。以前は無条件にエラーが報告されていましたが、修正後はif(n == N || !n->diag)という条件が追加されました。

    • n == N: ノードnがNULLである場合。これは、特定のノードに関連しない一般的な引数不足のエラー(例えば、関数呼び出し自体が不完全な場合など)を意味します。この場合はエラーを報告します。
    • !n->diag: ノードnにまだ診断フラグが設定されていない場合。つまり、このノードまたはその子孫ノードに対してまだエラーが報告されていない場合にのみ、この「not enough arguments」エラーを報告します。 この条件により、既に別のエラー(例:未定義の関数呼び出し)がnまたはその子孫ノードで検出され、n->diagが設定されている場合には、「not enough arguments」エラーは報告されなくなります。これにより、最も関連性の高いエラーメッセージのみがユーザーに表示されるようになります。

また、n = N;という初期化がtypecheckaste関数の冒頭に追加されています。これは、nが常に既知の状態から始まることを保証し、以前の処理からの残存値による予期せぬ動作を防ぐための防御的なプログラミングです。

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

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -1065,6 +1065,7 @@ reswitch:
 		        goto reswitch;
 		}
 		typecheck(&n->left, Erv | Etype | Ecall |(top&Eproc));
+		n->diag |= n->left->diag;
 		l = n->left;
 		if(l->op == ONAME && l->etype != 0) {
 			if(n->isddd && l->etype != OAPPEND)
@@ -2165,6 +2166,7 @@ typecheckaste(int op, Node *call, int isddd, Type *tstruct, NodeList *nl, char *
 	if(tstruct->broke)
 		goto out;
 
+	n = N;
 	if(nl != nil && nl->next == nil && (n = nl->n)->type != T)
 	if(n->type->etype == TSTRUCT && n->type->funarg) {
 		tn = n->type->type;
@@ -2239,10 +2241,14 @@ out:
 	return;
 
 notenough:
-	if(call != N)
-		yyerror("not enough arguments in call to %N", call);
-	else
-		yyerror("not enough arguments to %O", op);
+	if(n == N || !n->diag) {
+		if(call != N)
+			yyerror("not enough arguments in call to %N", call);
+		else
+			yyerror("not enough arguments to %O", op);
+		if(n != N)
+			n->diag = 1;
+	}
 	goto out;
 
 toomany:

test/fixedbugs/issue6405.go

--- /dev/null
+++ b/test/fixedbugs/issue6405.go
@@ -0,0 +1,13 @@
+// errorcheck
+
+// Copyright 2014 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.
+
+// Issue 6405: spurious 'not enough arguments to return' error
+
+package p
+
+func Open() (int, error) {
+	return OpenFile() // ERROR "undefined: OpenFile"
+}

コアとなるコードの解説

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

  1. n->diag |= n->left->diag; の追加: この行は、typecheckaste関数内のreswitchラベルの直後に挿入されています。typecheckasteは、ASTノードの型をチェックする主要な関数の一つです。n->leftは現在のノードnの左の子ノード(多くの場合、式の左辺や関数呼び出しの対象など)を指します。 |=演算子はビットOR代入です。これは、n->diag = n->diag | n->left->diag; と同等です。つまり、n->leftに既に診断フラグが立っている(エラーが報告されている)場合、そのフラグをnにも伝播させます。これにより、子ノードで発生したエラーの状態が親ノードに引き継がれ、親ノードがその後の処理で重複したエラーを報告するのを防ぎます。

  2. n = N; の追加: typecheckaste関数の冒頭近くにn = N;が追加されました。Nは通常、NULLポインタや無効なノードを示すマクロです。この初期化により、nが常に既知の無効な状態から始まることが保証され、以前の処理からの残存値が予期せぬ動作を引き起こす可能性が排除されます。これは、特にnl(NodeList)が空の場合など、nが後で適切に設定されない可能性があるシナリオでの安全性を高めます。

  3. notenough ラベル内の条件付きエラー報告: notenoughラベルは、関数呼び出しや戻り値の引数が不足している場合にジャンプされるエラー処理のセクションです。

    • 変更前:
      if(call != N)
          yyerror("not enough arguments in call to %N", call);
      else
          yyerror("not enough arguments to %O", op);
      
      これは、callノードが存在するかどうかに基づいて、引数不足のエラーを無条件に報告していました。
    • 変更後:
      if(n == N || !n->diag) {
          if(call != N)
              yyerror("not enough arguments in call to %N", call);
          else
              yyerror("not enough arguments to %O", op);
          if(n != N)
              n->diag = 1;
      }
      
      この変更により、エラー報告がif(n == N || !n->diag)という条件でガードされるようになりました。
      • n == N: nがNULLの場合、これは特定のノードに起因しない一般的な引数不足のエラーと見なされ、報告されます。
      • !n->diag: nにまだ診断フラグが設定されていない場合。つまり、このノードまたはその子孫ノードでまだエラーが報告されていない場合にのみ、この「not enough arguments」エラーが報告されます。 この条件により、既に別のエラー(例:未定義の関数呼び出し)が検出され、n->diagが設定されている場合には、この誤解を招くエラーは報告されなくなります。 また、エラーが報告された場合にif(n != N) n->diag = 1;が追加され、nが有効なノードであればdiagフラグを設定することで、将来の重複エラー報告を防ぎます。

test/fixedbugs/issue6405.go の追加

このファイルは、GoコンパイラのバグトラッキングシステムにおけるIssue 6405を再現し、修正を検証するための新しいテストケースです。

// errorcheck

// Copyright 2014 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.

// Issue 6405: spurious 'not enough arguments to return' error

package p

func Open() (int, error) {
	return OpenFile() // ERROR "undefined: OpenFile"
}
  • // errorcheck: このコメントは、Goのテストフレームワークに対して、このファイルがコンパイルエラーをチェックするためのものであることを示します。
  • func Open() (int, error): 2つの戻り値(interror)を持つ関数Openを定義しています。
  • return OpenFile(): ここで、OpenFile()という未定義の関数を呼び出しています。
  • // ERROR "undefined: OpenFile": このコメントは、コンパイラがこの行で「undefined: OpenFile」というエラーを報告することを期待していることを示します。このテストの目的は、この「undefined」エラーは報告されるべきだが、以前発生していた「not enough arguments to return」という誤ったエラーは報告されないことを確認することです。

このテストケースは、修正が正しく機能し、コンパイラが最も適切で正確なエラーメッセージのみを出力するようになったことを検証します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード(src/cmd/gc/typecheck.c
  • GoのIssueトラッカー
  • コンパイラ設計に関する一般的な知識(AST、型チェック、エラー報告など)