[インデックス 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.go
でfmt
パッケージがインポートされ、f2.go
でfmt
という名前の関数が宣言されています。Goのパッケージシステムでは、同じパッケージ内の複数のファイルは単一の論理的なパッケージとして扱われます。そのため、fmt
という名前がインポートされたパッケージ名と関数名の両方で使われることになり、再宣言エラーが発生します。
しかし、この状況でコンパイラが「fmt
が再宣言されました。以前の宣言はf2.go
のfmt
関数です」のようなメッセージを出力すると、ユーザーは「なぜインポートが関数によって再宣言されたのか?」と混乱する可能性がありました。本来、インポートはパッケージスコープにシンボルを導入するものであり、関数宣言よりも「先に」シンボルを導入していると考えるのが自然です。このコミットは、このような場合に、インポートが再宣言されたものとしてエラーメッセージを生成することで、ユーザーの理解を助けることを目指しています。
前提知識の解説
- 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
はインポートの行番号を指し、現在の行番号は関数宣言の行番号を指します。このとき、コンパイラは関数宣言を「現在の宣言」、インポートを「以前の宣言」として扱っていました。
このコミットでは、このロジックを改善し、特にインポートと宣言が異なるファイルで衝突した場合に、より直感的なエラーメッセージを生成するようにしています。
新しいロジックのポイントは以下の通りです。
s->def == N
のチェック:s->def
はシンボルの定義ノードを指します。s->def == N
(N
はnilまたはnullに相当)の場合、そのシンボルはまだ具体的な定義を持たず、インポートによって導入されたものである可能性が高いと判断されます。- 行番号の入れ替え:
s->def == N
の場合、つまりインポートされたシンボルと現在の宣言が衝突している場合、エラーメッセージの表示順序を調整します。line1
(現在の宣言の行番号)をparserline()
から取得します。line2
(以前の宣言の行番号)をs->lastlineno
から取得します。- 重要な変更:
s->def == N
の場合、line2
(以前の宣言)をline1
(現在の宣言)の値に設定し、line1
(現在の宣言)をs->lastlineno
(インポートの行番号)に設定します。これにより、エラーメッセージではインポートが「再宣言された」ものとして扱われ、関数宣言が「現在の宣言」として表示されます。
yyerrorl
の使用:yyerrorl
関数を使用することで、エラーメッセージの最初の行番号を明示的に指定できるようになります。これにより、インポートの行番号をエラーの「主たる場所」として表示し、関数宣言の行番号を「以前の宣言」として表示することが可能になります。
この変更により、コンパイラは「fmt
が再宣言されました (f2.go
のfmt
関数)。以前の宣言はインポートされた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->def
がN
であれば、そのシンボルはまだ具体的なコード定義を持たず、インポートによって導入されたものである可能性が高いと判断されます。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.go
のfmt
関数にあることを示唆しています。
関連リンク
- Go Issue 4510: https://github.com/golang/go/issues/4510
- Go CL 7001054: https://golang.org/cl/7001054
参考にした情報源リンク
- Go Issue 4510のGitHubページ
- Go CL 7001054のGo Gerritページ
- Go言語のコンパイラソースコード (
src/cmd/gc/dcl.c
) - Go言語のパッケージとスコープに関する公式ドキュメント(一般的な知識として)
- Go言語のエラーハンドリングに関する一般的な情報(一般的な知識として)