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

[インデックス 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言語では、printfsprintf といった関数を使って文字列をフォーマットします。 このコミットで登場する Fmt *fp は、Goコンパイラ内部で使われるカスタムのフォーマット構造体(Fmt)へのポインタであると推測されます。これは、標準の printf よりも効率的であったり、コンパイラの特定のニーズに合わせた出力制御を提供するために実装されたものです。fmtstrcpyfmtprint は、この Fmt 構造体を使って文字列をコピーしたり、フォーマットされた文字列を出力したりするユーティリティ関数と考えられます。

技術的詳細

このコミットの変更は、src/cmd/gc/subr.c ファイル内の Sconv 関数に集中しています。Sconv 関数は、Goコンパイラの内部でシンボル(Sym 構造体)を文字列として表現する際に呼び出される変換関数です。これは、デバッグ出力、エラーメッセージ、またはシンボルテーブルのダンプなど、シンボル名を人間が読める形式で表示する必要がある場面で使用されます。

変更の核心は以下の点にあります。

  1. 出力バッファの廃止と直接フォーマットへの移行:

    • 変更前は、char buf[500]; という固定サイズのバッファを宣言し、snprint 関数を使ってそこに文字列を書き込み、最後に fmtstrcpy(fp, buf);Fmt オブジェクトにコピーしていました。
    • 変更後は、buf 変数が削除され、snprint の代わりに fmtstrcpyfmtprint を直接 Fmt *fp オラジェクトに対して呼び出すようになりました。これにより、中間バッファリングが不要になり、メモリコピーの回数が減り、パフォーマンスが向上する可能性があります。また、バッファオーバーフローのリスクも低減されます。
  2. パッケージ名の表示ロジックの簡素化と修正:

    • 変更前は、シンボルのオリジナルパッケージ名 (opk) と現在のパッケージ名 (pkg)、そして現在のコンパイル対象パッケージ (package) を比較し、複雑な条件分岐によってシンボル名の表示形式を決定していました。特に、strcmp(pkg, package)strcmp(opk, pkg) といった比較が含まれていました。これにより、パッケージがリネームされた場合に、意図しない表示になるバグが発生していました。
    • 変更後は、条件式が if(strcmp(opk, package) || (fp->flags & FmtLong)) と大幅に簡素化されました。この新しいロジックは、シンボルが現在のパッケージに属していない場合(opkpackage と異なる場合)や、詳細なフォーマットが要求された場合(FmtLong フラグがセットされている場合)に、常に「オリジナルパッケージ名.シンボル名」の形式 (%s.%s, opk, nam) で表示するように変更されました。
    • これにより、「常にオリジナル名を表示する」というコミットメッセージの意図がコードに直接反映され、パッケージのリネームによる表示の不整合が解消されました。
  3. 制御フローの簡素化:

    • 変更前は、複数の 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) に書き込む役割を担っています。

  1. シンボルがNULLの場合の処理:

    • if(s == S) の条件は、シンボル s が無効な(NULLに相当する)シンボルであるかどうかをチェックしています。
    • 変更前は、"<S>" をバッファに書き込み、goto out; で関数を終了していました。
    • 変更後は、fmtstrcpy(fp, "<S>"); で直接 Fmt オブジェクトに書き込み、return 0; で即座に関数を終了するように変更されました。これにより、コードのパスが短縮され、効率が向上しています。
  2. パッケージ名とシンボル名の取得:

    • pkg, opk, nam はそれぞれ、現在のパッケージ名、オリジナルパッケージ名、シンボル名を表すポインタです。これらの値は、シンボル s から取得されます。opks->pkg->name から、nams->name から取得されることがコードから読み取れます。
  3. シンボル名のフォーマットロジックの修正:

    • 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://github.com/golang/go
  • Go言語の初期のコンパイラ設計に関する議論やドキュメント (当時の情報を見つけるのは困難な場合がありますが、Goの歴史に関する記事などが参考になる可能性があります)
  • C言語の printf ファミリ関数に関する一般的な情報
  • コンパイラのシンボルテーブルに関する一般的な情報 (例: コンパイラ設計の教科書など)