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

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

このコミットは、Goコンパイラ(gc)の内部処理におけるシンボル名の生成ロジックに関する変更です。具体的には、パッケージパスをシンボル名に変換する際のエスケープルールが修正されています。

コミット

commit 33f1d47b38f9c48b326642e2005e24ec06176172
Author: Luuk van Dijk <lvd@golang.org>
Date:   Wed Nov 2 22:33:15 2011 +0100

    gc: package paths in symbol names: don't escape periods before last slash, always escape >=0x7f.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5323071

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

https://github.com/golang/go/commit/33f1d47b38f9c48b326642e2005e24ec06176172

元コミット内容

Goコンパイラ(gc)において、シンボル名に含まれるパッケージパスのエスケープ処理を改善しました。具体的には、最後のスラッシュ(/)より前のピリオド(.)はエスケープしないように変更し、また、ASCII範囲外の文字(0x7f以上)は常にエスケープするようにしました。

変更の背景

この変更の背景には、Goコンパイラが生成するシンボル名の可読性と正確性の向上が挙げられます。

  1. ピリオドのエスケープの最適化: 以前のバージョンでは、パッケージパス内のすべてのピリオドがエスケープされていました。しかし、Goのパッケージパスは通常、github.com/user/repo のようにピリオドを含みます。これらのピリオドがすべてエスケープされると、生成されるシンボル名が冗長になり、デバッグ時などに読みにくくなるという問題がありました。このコミットでは、「最後のスラッシュより前のピリオドはエスケープしない」というルールを導入することで、一般的なパッケージパスのシンボル名をより簡潔で読みやすいものにすることを目指しています。これは、ユーザーエクスペリエンスの向上("happier users")にも繋がるとコメントで示唆されています。

  2. 非ASCII文字の適切な処理: 以前のコードでは、非ASCII文字(0x7f以上の文字)に対する明示的なエスケープルールが不足していた可能性があります。シンボル名のような内部識別子においては、非ASCII文字が予期せぬ問題を引き起こす可能性があるため、これらを常にエスケープすることで、コンパイラの堅牢性と移植性を高めることが目的です。これにより、国際化されたパス名や特殊な文字を含むパス名が正しく処理されるようになります。

これらの変更は、Go言語がまだ初期段階にあった2011年当時、コンパイラの内部的なシンボル管理を洗練させ、将来的な拡張性や安定性を確保するための重要なステップでした。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  • Goコンパイラ (gc): Go言語の公式コンパイラの一つで、Goの初期から主要なコンパイラとして開発されてきました。このコミットがなされた2011年当時は、Goの主要なコンパイラでした。gcはGoのソースコードを機械語に変換する役割を担い、その過程でプログラム内の様々な要素(関数、変数、型など)を識別するための「シンボル」を生成します。

  • シンボルテーブルとシンボル名: コンパイラは、プログラム内のすべての識別子(変数名、関数名、型名など)を管理するために「シンボルテーブル」というデータ構造を使用します。シンボルテーブルには、各識別子に対応する「シンボル名」が格納されます。このシンボル名は、コンパイラやリンカがプログラムの異なる部分を結合したり、デバッグ情報を生成したりする際に使用される内部的な識別子です。シンボル名は、プログラムの実行ファイルやライブラリファイルに埋め込まれることもあります。

  • パッケージパス: Go言語では、コードは「パッケージ」という単位で整理されます。各パッケージは一意の「パッケージパス」によって識別されます。例えば、標準ライブラリのfmtパッケージはfmtというパスを持ち、外部ライブラリはgithub.com/user/repo/packageのようなパスを持つことがあります。コンパイラは、これらのパッケージパスをシンボル名の一部として組み込むことで、異なるパッケージに属する同じ名前のシンボルを区別します。

  • エスケープ処理: エスケープ処理とは、特定の文字が特別な意味を持つ場合に、その文字を別の表現に変換してその特別な意味を無効化する技術です。例えば、URLにおいてスペースが%20に変換されるように、シンボル名においても、特定の文字(制御文字、区切り文字、非ASCII文字など)がリンカやデバッガにとって問題を引き起こす可能性がある場合、それらを%xxのような形式でエンコード(エスケープ)します。これにより、シンボル名の構文的な整合性を保ちつつ、任意の文字を表現できるようになります。

  • ASCIIと非ASCII文字: ASCII(American Standard Code for Information Interchange)は、コンピュータでテキストを表現するための文字コードの一つで、0から127までの数値で文字を表現します。これに対し、128以上の数値で表現される文字は「非ASCII文字」と呼ばれ、日本語、中国語、アラビア語など、世界中の多様な言語の文字が含まれます。シンボル名のようなシステム内部の識別子では、非ASCII文字の扱いがシステム間で異なる場合があるため、互換性や堅牢性のためにエスケープされることが一般的です。

技術的詳細

このコミットは、src/cmd/gc/subr.cファイル内のpathtoprefix関数を変更しています。この関数は、生の文字列(主にパッケージパス)を、Goコンパイラのシンボルテーブルで使用されるプレフィックス形式に変換する役割を担っています。この変換プロセスには、特定の文字のエスケープが含まれます。

変更の核心は、エスケープされる文字の条件と、特にピリオド(.)のエスケープルールにあります。

変更前のエスケープルール

変更前は、以下の文字がエスケープされていました。

  • 制御文字(' '以下の文字)
  • ピリオド(.
  • パーセント(%
  • ダブルクォーテーション("

これらの文字が見つかると、%xx形式(xxは文字の16進数表現)に変換されていました。

変更後のエスケープルール

変更後、エスケープされる文字の条件はより洗練されました。

  1. ピリオドのエスケープ条件の変更: これが最も重要な変更点です。ピリオドは、文字列の「最後のスラッシュ(/)より前の部分」ではエスケープされなくなりました。最後のスラッシュより後の部分(つまり、パスの最後のセグメント)にピリオドがある場合にのみエスケープされます。 例:

    • github.com/user/repo.v1 の場合、github.com のピリオドはエスケープされず、repo.v1 のピリオドはエスケープされます。
    • my.package のようにスラッシュがない場合、すべてのピリオドがエスケープされます(この場合、lは文字列の先頭を指すため、r >= lが常に真となる)。
  2. 非ASCII文字(0x7f以上)の追加エスケープ: 新たに、ASCII範囲外の文字(0x7f以上の文字)が常にエスケープされるようになりました。これにより、UTF-8などのマルチバイト文字セットで表現される文字がシンボル名に含まれる場合でも、一貫した処理が保証されます。

  3. その他の文字: 制御文字(' '以下の文字)、スペース、パーセント(%)、ダブルクォーテーション(")は、引き続きエスケープされます。

l変数の導入とその役割

この新しいピリオドのエスケープルールを実装するために、pathtoprefix関数内にlという新しいポインタ変数が導入されました。

  • lは、入力文字列sの中で、最後のスラッシュの直後の文字を指すように初期化されます。
  • もし文字列中にスラッシュが存在しない場合、lは文字列の先頭(s)を指します。
  • このlポインタを使用することで、ピリオドのエスケープ条件(*r == '.' && r >= l)が評価されます。これは、「現在の文字*rがピリオドであり、かつその位置rが最後のスラッシュ以降のセグメント内にある(またはスラッシュがない場合は文字列全体)」という条件を意味します。

この変更により、生成されるシンボル名は、Goのパッケージパスの慣習により適合し、より人間が読みやすい形式になります。

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

変更は、src/cmd/gc/subr.cファイルのpathtoprefix関数内で行われています。

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2911,21 +2911,29 @@ ngotype(Node *n)
 }
 
 /*
- * Convert raw string to the prefix that will be used in the symbol table.
- * Invalid bytes turn into %xx.  Right now the only bytes that need
- * escaping are %, ., and \", but we escape all control characters too.
+ * Convert raw string to the prefix that will be used in the symbol
+ * table.  All control characters, space, '%' and '"', as well as
+ * non-7-bit clean bytes turn into %xx.  The period needs escaping
+ * only in the last segment of the path, and it makes for happier
+ * users if we escape that as little as possible.
  */
 static char*
 pathtoprefix(char *s)
 {
  	static char hex[] = "0123456789abcdef";
- 	char *p, *r, *w;\n+\tchar *p, *r, *w, *l;
  	int n;
  
+\t// find first character past the last slash, if any.
+\tl = s;
+\tfor(r=s; *r; r++)
+\t\tif(*r == '/')
+\t\t\tl = r+1;
+\n
  	// check for chars that need escaping
  	n = 0;
  	for(r=s; *r; r++)
-\t\tif(*r <= ' ' || *r == '.' || *r == '%' || *r == '"')
+\t\tif(*r <= ' ' || (*r == '.' && r >= l) || *r == '%' || *r == '"' || *r >= 0x7f)
  	\t\tn++;
  
  	// quick exit
@@ -2935,7 +2943,7 @@ pathtoprefix(char *s)
  	// escape
  	p = mal((r-s)+1+2*n);
  	for(r=s, w=p; *r; r++) {
-\t\tif(*r <= ' ' || *r == '.' || *r == '%' || *r == '"') {\n+\t\tif(*r <= ' ' || (*r == '.' && r >= l) || *r == '%' || *r == '"' || *r >= 0x7f) {
  	\t\t*w++ = '%';
  	\t\t*w++ = hex[(*r>>4)&0xF];
  	\t\t*w++ = hex[*r&0xF];

コアとなるコードの解説

このコミットにおける主要なコード変更は、pathtoprefix関数内の以下の部分です。

  1. l変数の導入と初期化:

    char *p, *r, *w, *l; // 新しく 'l' ポインタが追加された
    // ...
    // find first character past the last slash, if any.
    l = s;
    for(r=s; *r; r++)
        if(*r == '/')
            l = r+1;
    

    ここで、lポインタが導入され、入力文字列sを走査して最後のスラッシュ(/)の直後の文字を指すように設定されます。もしスラッシュが見つからない場合、lは文字列の先頭sを指したままになります。このlは、ピリオドのエスケープ条件を決定するための基準点となります。

  2. エスケープが必要な文字のカウントロジックの変更:

    // 変更前:
    // if(*r <= ' ' || *r == '.' || *r == '%' || *r == '"')
    // 変更後:
    if(*r <= ' ' || (*r == '.' && r >= l) || *r == '%' || *r == '"' || *r >= 0x7f)
    

    このif文は、エスケープが必要な文字の数を事前に数えるループ内で使用されています。

    • (*r == '.' && r >= l): この新しい条件が、ピリオドのエスケープルールを実装しています。現在の文字*rがピリオドであり、かつそのピリオドが最後のスラッシュ以降のセグメント(r >= l)にある場合にのみ、エスケープ対象としてカウントされます。
    • *r >= 0x7f: この条件が追加され、ASCII範囲外の文字(0x7f以上)もエスケープ対象としてカウントされるようになりました。
  3. 実際のエスケープ処理ロジックの変更:

    // 変更前:
    // if(*r <= ' ' || *r == '.' || *r == '%' || *r == '"') {
    // 変更後:
    if(*r <= ' ' || (*r == '.' && r >= l) || *r == '%' || *r == '"' || *r >= 0x7f) {
    

    このif文は、実際に文字を%xx形式に変換して出力文字列pに書き込むループ内で使用されています。上記のカウントロジックと同様に、ピリオドと非ASCII文字のエスケープ条件が更新されています。

これらの変更により、pathtoprefix関数は、より洗練されたルールに基づいてパッケージパスをシンボル名に変換するようになり、結果として生成されるシンボル名の可読性と正確性が向上しました。

関連リンク

参考にした情報源リンク

  • コミット内容とソースコードの分析に基づく。
  • Go言語の初期のコンパイラ設計に関する一般的な知識。
  • シンボルテーブルとエスケープ処理に関する一般的なコンピュータサイエンスの知識。
  • 特になし(特定の外部記事やドキュメントを参照したわけではありません)。