[インデックス 1066] ファイルの概要
このコミットは、Go言語の初期のコンパイラ(gc
)における、エクスポートされた名前の表示に関するバグ修正です。具体的には、パッケージがリネーム(エイリアス)された場合に、シンボル名が常に元のパッケージ名で表示されるようにする変更が含まれています。
コミット
commit 553c98dca4b4ef9b745700f3d5d03ba4d8fa9fe6
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 5 15:59:34 2008 -0800
fix renamed-package bug in exported names:
always show original name
R=ken
OCL=18603
CL=18603
---
src/cmd/gc/subr.c | 21 +++++++--------------
1 file changed, 7 insertions(+), 14 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/553c98dca4b4ef9b745700f3d5d03ba4d8fa9fe6
元コミット内容
fix renamed-package bug in exported names:
always show original name
変更の背景
Go言語では、他のパッケージをインポートする際に、そのパッケージにローカルなエイリアス(別名)を付けることができます。例えば、import foo "path/to/bar"
のように記述すると、path/to/bar
パッケージを foo
という名前で参照できるようになります。
このコミットが行われた当時のGoコンパイラ(gc
)では、エクスポートされたシンボル(関数、変数、型など)の名前を表示する際に、このパッケージのリネームが原因で問題が発生していました。具体的には、シンボルが属するパッケージがリネームされた場合、コンパイラの内部処理やデバッグ出力において、シンボル名がエイリアスされたパッケージ名で表示されたり、あるいは一貫性のない表示になったりするバグが存在したと考えられます。
このような表示の不整合は、特にコンパイラのデバッグ情報、エラーメッセージ、またはシンボルテーブルのダンプなどにおいて、開発者を混乱させる原因となります。シンボルの「真の」識別子は、それが定義された元のパッケージ名とシンボル名によって構成されるべきであり、インポート時のエイリアスに影響されるべきではありません。
このコミットは、この問題を解決し、エクスポートされたシンボル名が常にそのシンボルが元々定義されたパッケージ名(オリジナル名)と共に表示されるようにすることを目的としています。これにより、コンパイラの出力の一貫性と正確性が向上します。
前提知識の解説
Go言語のパッケージとインポート
Go言語は、コードをパッケージという単位で整理します。パッケージは、関連する機能やデータ型をまとめたもので、他のパッケージからインポートして利用できます。
インポートの構文は import "path/to/package"
ですが、import alias "path/to/package"
のようにエイリアスを付けてインポートすることも可能です。このエイリアスは、インポート元のファイル内でのみ有効なローカルな名前です。
コンパイラのシンボルテーブル
コンパイラは、ソースコード内で定義されたすべての識別子(変数名、関数名、型名、パッケージ名など)に関する情報を「シンボルテーブル」と呼ばれるデータ構造に格納します。シンボルテーブルには、識別子の名前、型、スコープ、メモリ上のアドレスなどの情報が含まれます。
特に、Goのような言語では、シンボルがどのパッケージに属しているかという情報も重要です。エクスポートされたシンボルは、そのシンボルが定義されたパッケージ名とシンボル名によって一意に識別されます(例: fmt.Println
)。
gc
(Go Compiler)
gc
は、Go言語の公式コンパイラの実装の一つです。Go言語の初期から主要なコンパイラとして機能しており、C言語で書かれていました(後にGo言語自身で書き直されました)。コンパイラは、ソースコードを解析し、中間表現を生成し、最終的に実行可能なバイナリコードを生成する一連のプロセスを実行します。
このコミットのコードは、gc
の内部、特にシンボル情報のフォーマットを担当する部分に属しています。
C言語の文字列フォーマットと Fmt
構造体
コミットのコードはC言語で書かれています。C言語では、printf
や sprintf
といった関数を使って文字列をフォーマットします。
このコミットで登場する Fmt *fp
は、Goコンパイラ内部で使われるカスタムのフォーマット構造体(Fmt
)へのポインタであると推測されます。これは、標準の printf
よりも効率的であったり、コンパイラの特定のニーズに合わせた出力制御を提供するために実装されたものです。fmtstrcpy
や fmtprint
は、この Fmt
構造体を使って文字列をコピーしたり、フォーマットされた文字列を出力したりするユーティリティ関数と考えられます。
技術的詳細
このコミットの変更は、src/cmd/gc/subr.c
ファイル内の Sconv
関数に集中しています。Sconv
関数は、Goコンパイラの内部でシンボル(Sym
構造体)を文字列として表現する際に呼び出される変換関数です。これは、デバッグ出力、エラーメッセージ、またはシンボルテーブルのダンプなど、シンボル名を人間が読める形式で表示する必要がある場面で使用されます。
変更の核心は以下の点にあります。
-
出力バッファの廃止と直接フォーマットへの移行:
- 変更前は、
char buf[500];
という固定サイズのバッファを宣言し、snprint
関数を使ってそこに文字列を書き込み、最後にfmtstrcpy(fp, buf);
でFmt
オブジェクトにコピーしていました。 - 変更後は、
buf
変数が削除され、snprint
の代わりにfmtstrcpy
やfmtprint
を直接Fmt *fp
オラジェクトに対して呼び出すようになりました。これにより、中間バッファリングが不要になり、メモリコピーの回数が減り、パフォーマンスが向上する可能性があります。また、バッファオーバーフローのリスクも低減されます。
- 変更前は、
-
パッケージ名の表示ロジックの簡素化と修正:
- 変更前は、シンボルのオリジナルパッケージ名 (
opk
) と現在のパッケージ名 (pkg
)、そして現在のコンパイル対象パッケージ (package
) を比較し、複雑な条件分岐によってシンボル名の表示形式を決定していました。特に、strcmp(pkg, package)
やstrcmp(opk, pkg)
といった比較が含まれていました。これにより、パッケージがリネームされた場合に、意図しない表示になるバグが発生していました。 - 変更後は、条件式が
if(strcmp(opk, package) || (fp->flags & FmtLong))
と大幅に簡素化されました。この新しいロジックは、シンボルが現在のパッケージに属していない場合(opk
がpackage
と異なる場合)や、詳細なフォーマットが要求された場合(FmtLong
フラグがセットされている場合)に、常に「オリジナルパッケージ名.シンボル名
」の形式 (%s.%s
,opk
,nam
) で表示するように変更されました。 - これにより、「常にオリジナル名を表示する」というコミットメッセージの意図がコードに直接反映され、パッケージのリネームによる表示の不整合が解消されました。
- 変更前は、シンボルのオリジナルパッケージ名 (
-
制御フローの簡素化:
- 変更前は、複数の
goto out;
ステートメントを使用して、関数の最後にジャンプし、共通のreturn fmtstrcpy(fp, buf);
で終了していました。 - 変更後は、各条件分岐の終端で
return 0;
を直接呼び出すようになりました。これにより、goto
ステートメントが不要になり、コードの可読性と保守性が向上します。
- 変更前は、複数の
これらの変更は、Goコンパイラの初期段階における、シンボル表示の正確性と内部処理の効率化に向けた重要なステップであったと言えます。
コアとなるコードの変更箇所
src/cmd/gc/subr.c
ファイル内の Sconv
関数が変更されています。
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -912,14 +912,13 @@ out:
int
Sconv(Fmt *fp)
{
- char buf[500]; // 削除: 中間バッファ
Sym *s;
char *opk, *pkg, *nam;
s = va_arg(fp->args, Sym*);
if(s == S) {
- snprint(buf, sizeof(buf), "<S>"); // 削除: snprintによるバッファへの書き込み
- goto out; // 削除: goto文
+ fmtstrcpy(fp, "<S>"); // 追加: 直接fmtstrcpyでフォーマット
+ return 0; // 追加: 直接リターン
}
pkg = "<nil>";
@@ -934,18 +933,12 @@ Sconv(Fmt *fp)
nam = s->name;
if(!(fp->flags & FmtShort))
- if(strcmp(pkg, package) || strcmp(opk, package) || (fp->flags & FmtLong)) { // 変更前: 複雑な条件分岐
- if(strcmp(opk, pkg) == 0) {
- snprint(buf, sizeof(buf), "%s.%s", pkg, nam); // 削除
- goto out; // 削除
- }
- snprint(buf, sizeof(buf), "(%s)%s.%s", opk, pkg, nam); // 削除
- goto out; // 削除
+ if(strcmp(opk, package) || (fp->flags & FmtLong)) { // 変更後: 簡素化された条件分岐
+ fmtprint(fp, "%s.%s", opk, nam); // 追加: オリジナルパッケージ名で直接フォーマット
+ return 0; // 追加: 直接リターン
}
- snprint(buf, sizeof(buf), "%s", nam); // 削除
-
-out: // 削除: gotoのターゲットラベル
- return fmtstrcpy(fp, buf); // 削除: 共通のリターン処理
+ fmtstrcpy(fp, nam); // 追加: シンボル名のみで直接フォーマット
+ return 0; // 追加: 直接リターン
}
static char*
コアとなるコードの解説
Sconv
関数は、Sym
型のシンボルオブジェクトを受け取り、それを文字列としてフォーマットして Fmt
オブジェクト (fp
) に書き込む役割を担っています。
-
シンボルがNULLの場合の処理:
if(s == S)
の条件は、シンボルs
が無効な(NULLに相当する)シンボルであるかどうかをチェックしています。- 変更前は、
"<S>"
をバッファに書き込み、goto out;
で関数を終了していました。 - 変更後は、
fmtstrcpy(fp, "<S>");
で直接Fmt
オブジェクトに書き込み、return 0;
で即座に関数を終了するように変更されました。これにより、コードのパスが短縮され、効率が向上しています。
-
パッケージ名とシンボル名の取得:
pkg
,opk
,nam
はそれぞれ、現在のパッケージ名、オリジナルパッケージ名、シンボル名を表すポインタです。これらの値は、シンボルs
から取得されます。opk
はs->pkg->name
から、nam
はs->name
から取得されることがコードから読み取れます。
-
シンボル名のフォーマットロジックの修正:
if(!(fp->flags & FmtShort))
は、短い形式での表示が要求されていない場合(つまり、完全な形式での表示が要求されている場合)に実行されるブロックです。- 変更前:
strcmp(pkg, package) || strcmp(opk, package) || (fp->flags & FmtLong)
という複雑な条件がありました。これは、シンボルが現在のパッケージに属していない場合、または詳細な表示が要求されている場合に、パッケージ名を付加して表示しようとするものでした。- さらに、
if(strcmp(opk, pkg) == 0)
というネストされた条件があり、オリジナルパッケージ名と現在のパッケージ名が同じ場合にpkg.nam
の形式で、異なる場合に(opk)pkg.nam
の形式で表示しようとしていました。このロジックが、パッケージのリネーム時にバグを引き起こしていました。
- 変更後:
- 条件が
if(strcmp(opk, package) || (fp->flags & FmtLong))
と簡素化されました。これは、「シンボルが現在のパッケージに属していない場合」または「詳細な表示が要求されている場合」に、パッケージ名を付加して表示するという意図を明確にしています。 - この条件が真の場合、常に
fmtprint(fp, "%s.%s", opk, nam);
が実行されます。これは、シンボルがどのパッケージからインポートされたかに関わらず、常にそのシンボルが元々定義された「オリジナルパッケージ名 (opk
)」と「シンボル名 (nam
)」を結合した形式で表示することを保証します。これにより、リネームされたパッケージのバグが修正されます。 - この条件が偽の場合(つまり、短い形式での表示が要求されており、かつシンボルが現在のパッケージに属している場合)、
fmtstrcpy(fp, nam);
が実行され、シンボル名のみが表示されます。
- 条件が
この修正により、Goコンパイラは、シンボル名をより正確かつ一貫性のある方法で表示するようになり、特にパッケージのリネームが絡むシナリオでのデバッグや理解が容易になりました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のパッケージに関するドキュメント: https://go.dev/doc/code#packages
- Go言語のコンパイラに関する情報 (Go言語で書かれた現在のコンパイラ): https://go.dev/doc/install/source#go1.4 (Go 1.4でセルフホスト化された経緯など)
参考にした情報源リンク
- Go言語のソースコードリポジトリ: https://github.com/golang/go
- Go言語の初期のコンパイラ設計に関する議論やドキュメント (当時の情報を見つけるのは困難な場合がありますが、Goの歴史に関する記事などが参考になる可能性があります)
- C言語の
printf
ファミリ関数に関する一般的な情報 - コンパイラのシンボルテーブルに関する一般的な情報 (例: コンパイラ設計の教科書など)