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

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

このコミットは、Goコンパイラ(cmd/gc)における、インポートされたシンボルと関数宣言の間で発生する再宣言エラーメッセージの改善を目的としています。特に、異なるファイル間で同じ名前のシンボルがインポートと関数として宣言された場合に、より分かりやすいエラーメッセージを提供するように修正されています。

コミット

commit ae2131ab3b0ade61a3b21bfea013350a825ad45a
Author: Russ Cox <rsc@golang.org>
Date:   Wed Jan 2 15:34:28 2013 -0500

    cmd/gc: make redeclaration between import and func less confusing
    
    Fixes #4510.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7001054

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

https://github.com/golang/go/commit/ae2131ab3b0ade61a3b21bfea013350a825ad45a

元コミット内容

cmd/gc: make redeclaration between import and func less confusing

このコミットは、Goコンパイラがインポートと関数宣言の間で発生する再宣言を検出した際のエラーメッセージを、より分かりやすくすることを目的としています。具体的には、Go言語のIssue #4510で報告された問題に対応しています。

変更の背景

Go言語のコンパイラは、同じスコープ内で同じ名前のシンボルが複数回宣言される「再宣言」をエラーとして扱います。しかし、Go言語ではパッケージのインポートもシンボルを導入する行為であり、特に異なるファイルでインポートと関数宣言が衝突した場合に、コンパイラが出力するエラーメッセージがユーザーにとって混乱を招く可能性がありました。

Issue #4510では、以下のようなシナリオが報告されていました。

ファイル f1.go:

package p

import "fmt"

ファイル f2.go:

package p

func fmt() {}

この場合、f1.gofmtパッケージがインポートされ、f2.gofmtという名前の関数が宣言されています。Goのパッケージシステムでは、同じパッケージ内の複数のファイルは単一の論理的なパッケージとして扱われます。そのため、fmtという名前がインポートされたパッケージ名と関数名の両方で使われることになり、再宣言エラーが発生します。

しかし、この状況でコンパイラが「fmtが再宣言されました。以前の宣言はf2.gofmt関数です」のようなメッセージを出力すると、ユーザーは「なぜインポートが関数によって再宣言されたのか?」と混乱する可能性がありました。本来、インポートはパッケージスコープにシンボルを導入するものであり、関数宣言よりも「先に」シンボルを導入していると考えるのが自然です。このコミットは、このような場合に、インポートが再宣言されたものとしてエラーメッセージを生成することで、ユーザーの理解を助けることを目指しています。

前提知識の解説

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラです。ソースコードを解析し、実行可能なバイナリに変換します。このコミットで変更されているdcl.cは、コンパイラの宣言処理(declaration)を担当する部分のC言語ソースファイルです。
  • シンボル (Symbol): プログラミング言語において、変数、関数、型、パッケージなどの名前付きエンティティを指します。コンパイラはシンボルテーブルを管理し、各シンボルの属性(型、スコープ、定義場所など)を記録します。
  • 再宣言 (Redeclaration): 同じスコープ内で同じ名前のシンボルが複数回宣言されることです。Go言語では、通常、再宣言はコンパイルエラーとなります。
  • インポート (Import): Go言語において、他のパッケージで定義された機能(関数、型、変数など)を現在のパッケージで使用するために、そのパッケージを読み込む操作です。インポートされたパッケージ名は、現在のパッケージのスコープにシンボルとして導入されます。
  • yyerror / yyerrorl: Goコンパイラ内部で使用されるエラー報告関数です。yyerrorは現在の行番号でエラーを報告し、yyerrorlは指定された行番号でエラーを報告します。
  • Sym構造体: コンパイラ内部でシンボル情報を保持するための構造体です。s->lastlinenoはシンボルが最後に宣言された行番号を、s->defはシンボルの定義ノードを指します。
  • parserline(): 現在解析中のソースコードの行番号を返す関数です。

技術的詳細

このコミットの核心は、src/cmd/gc/dcl.c内のredeclare関数におけるエラーメッセージ生成ロジックの変更です。redeclare関数は、シンボルの再宣言が検出された際に呼び出され、適切なエラーメッセージを出力します。

変更前は、再宣言エラーが発生した場合、s->lastlineno(シンボルが以前に宣言された行番号)と現在の行番号(parserline())を比較し、単純に「%S%sとして再宣言されました。以前の宣言は%Lです」という形式のエラーメッセージを出力していました。

しかし、インポートと関数宣言の衝突の場合、s->lastlinenoはインポートの行番号を指し、現在の行番号は関数宣言の行番号を指します。このとき、コンパイラは関数宣言を「現在の宣言」、インポートを「以前の宣言」として扱っていました。

このコミットでは、このロジックを改善し、特にインポートと宣言が異なるファイルで衝突した場合に、より直感的なエラーメッセージを生成するようにしています。

新しいロジックのポイントは以下の通りです。

  1. s->def == N のチェック: s->defはシンボルの定義ノードを指します。s->def == NNはnilまたはnullに相当)の場合、そのシンボルはまだ具体的な定義を持たず、インポートによって導入されたものである可能性が高いと判断されます。
  2. 行番号の入れ替え: s->def == Nの場合、つまりインポートされたシンボルと現在の宣言が衝突している場合、エラーメッセージの表示順序を調整します。
    • line1(現在の宣言の行番号)をparserline()から取得します。
    • line2(以前の宣言の行番号)をs->lastlinenoから取得します。
    • 重要な変更: s->def == Nの場合、line2(以前の宣言)をline1(現在の宣言)の値に設定し、line1(現在の宣言)をs->lastlineno(インポートの行番号)に設定します。これにより、エラーメッセージではインポートが「再宣言された」ものとして扱われ、関数宣言が「現在の宣言」として表示されます。
  3. yyerrorlの使用: yyerrorl関数を使用することで、エラーメッセージの最初の行番号を明示的に指定できるようになります。これにより、インポートの行番号をエラーの「主たる場所」として表示し、関数宣言の行番号を「以前の宣言」として表示することが可能になります。

この変更により、コンパイラは「fmtが再宣言されました (f2.gofmt関数)。以前の宣言はインポートされたfmtパッケージです (f1.goのインポート行)」のような、より分かりやすいメッセージを出力できるようになります。これは、ユーザーがエラーの原因を特定し、修正する上で非常に役立ちます。

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

src/cmd/gc/dcl.cファイルのredeclare関数が変更されています。

--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -151,16 +151,30 @@ void
 redeclare(Sym *s, char *where)
 {
 	Strlit *pkgstr;
+	int line1, line2;
 
 	if(s->lastlineno == 0) {
 		pkgstr = s->origpkg ? s->origpkg->path : s->pkg->path;
 		yyerror("%S redeclared %s\n"
 			"\tprevious declaration during import \"%Z\"",
 			s, where, pkgstr);
-	} else
-		yyerror("%S redeclared %s\n"
+	} else {
+		line1 = parserline();
+		line2 = s->lastlineno;
+		
+		// When an import and a declaration collide in separate files,
+		// present the import as the "redeclared", because the declaration
+		// is visible where the import is, but not vice versa.
+		// See issue 4510.
+		if(s->def == N) {
+			line2 = line1;
+			line1 = s->lastlineno;
+		}
+
+		yyerrorl(line1, "%S redeclared %s (%#N)\n"
 			"\tprevious declaration at %L",
-			s, where, s->lastlineno);
+			s, where, s->def, line2);
+	}
 }
 
 static int vargen;

また、この変更を検証するためのテストケースが追加されています。

  • test/fixedbugs/issue4510.dir/f1.go
  • test/fixedbugs/issue4510.dir/f2.go
  • test/fixedbugs/issue4510.go

コアとなるコードの解説

変更されたredeclare関数内のelseブロックに注目します。

	} else {
		line1 = parserline(); // 現在の宣言の行番号
		line2 = s->lastlineno; // 以前の宣言の行番号

		// When an import and a declaration collide in separate files,
		// present the import as the "redeclared", because the declaration
		// is visible where the import is, but not vice versa.
		// See issue 4510.
		if(s->def == N) { // シンボルがインポートによって導入されたもので、まだ具体的な定義がない場合
			line2 = line1; // 以前の宣言の行番号を現在の宣言の行番号に設定
			line1 = s->lastlineno; // 現在の宣言の行番号をインポートの行番号に設定
		}

		yyerrorl(line1, "%S redeclared %s (%#N)\n"
			"\tprevious declaration at %L",
			s, where, s->def, line2);
	}
  • line1 = parserline();: line1には、現在コンパイラが処理している(つまり、再宣言を引き起こしている)コードの行番号が格納されます。これは通常、関数宣言の行番号です。
  • line2 = s->lastlineno;: line2には、同じシンボルが以前に宣言された行番号が格納されます。インポートと関数宣言の衝突の場合、これはインポート文の行番号になります。
  • if(s->def == N): この条件がこのコミットの肝です。s->defはシンボルの定義ノードを指します。NはGoコンパイラ内部でnilまたはnullを表すマクロです。もしs->defNであれば、そのシンボルはまだ具体的なコード定義を持たず、インポートによって導入されたものである可能性が高いと判断されます。
  • line2 = line1; line1 = s->lastlineno;: この行番号の入れ替えが、エラーメッセージの表示順序を逆転させるマジックです。
    • 元のline1(関数宣言の行番号)が新しいline2になります。
    • 元のline2(インポートの行番号)が新しいline1になります。
    • これにより、yyerrorlに渡されるline1はインポートの行番号となり、エラーメッセージの冒頭でインポートが「再宣言された」かのように表示されます。そして、line2は関数宣言の行番号となり、「以前の宣言」として表示されます。
  • yyerrorl(line1, "%S redeclared %s (%#N)\n\tprevious declaration at %L", s, where, s->def, line2);:
    • yyerrorlは、指定されたline1を行番号としてエラーメッセージを出力します。
    • %S: シンボル名(例: fmt
    • %s: 再宣言された場所の種類(例: func
    • (%#N): シンボルの定義ノードの詳細(例: (func fmt))。これにより、関数宣言が「現在の宣言」として明確に示されます。
    • %L: line2で指定された行番号。これにより、インポートが「以前の宣言」として示されます。

結果として、fmtの例では、以下のようなエラーメッセージが出力されるようになります。

f1.go:7: fmt redeclared in this block (func fmt)
	previous declaration at f1.go:7

これは、f1.goのインポート行でfmtが再宣言されたと示し、その原因がf2.gofmt関数にあることを示唆しています。

関連リンク

参考にした情報源リンク

  • Go Issue 4510のGitHubページ
  • Go CL 7001054のGo Gerritページ
  • Go言語のコンパイラソースコード (src/cmd/gc/dcl.c)
  • Go言語のパッケージとスコープに関する公式ドキュメント(一般的な知識として)
  • Go言語のエラーハンドリングに関する一般的な情報(一般的な知識として)