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

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

このコミットは、Go言語のコンパイラ(gc)における組み込みシンボル(built-in symbol)の解決方法に関する修正です。具体的には、src/cmd/gc/lex.c ファイル内の変更を通じて、組み込みシンボルが正しく自身のシンボルを参照するように修正されています。

コミット

gc: resolve built-ins to built-in symbol

R=lvd
CC=golang-dev
https://golang.org/cl/5480049

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

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

元コミット内容

このコミットの目的は、「組み込みシンボルを組み込みシンボル自身に解決する」ことです。これは、Goコンパイラがtruefalsenilなどの言語に組み込まれた定数や型、関数を処理する際に、それらのシンボル定義が正しく行われるようにするための修正です。以前の実装では、組み込みシンボルが誤ったシンボルを参照してしまう可能性があり、それがコンパイラの内部的な整合性に問題を引き起こす可能性がありました。

変更の背景

Go言語のコンパイラgcは、ソースコードを解析し、抽象構文木(AST)を構築し、最終的に実行可能なバイナリを生成する役割を担っています。このプロセスにおいて、言語に最初から定義されているtruefalsenilintstringなどの「組み込み(built-in)」の識別子を適切に扱う必要があります。

コンパイラは、これらの組み込み識別子を通常のユーザー定義の識別子とは異なる方法で処理し、特別な意味を持たせる必要があります。そのため、コンパイラのシンボルテーブルには、これらの組み込み識別子に対応する特別なシンボルエントリが用意されています。

このコミットが行われた2011年12月は、Go言語がまだ活発に開発され、初期の安定化フェーズにあった時期です。コンパイラの内部構造やシンボル解決のメカニズムは、言語仕様の進化やバグ修正に伴って頻繁に調整されていました。この特定の変更は、組み込みシンボルがシンボルテーブル内で正しく自己参照するようにすることで、コンパイラのシンボル解決ロジックの堅牢性を向上させることを目的としています。

具体的には、組み込みシンボルが定義される際に、そのシンボルが指し示すべき定義(defフィールド)が、そのシンボル自身を指すように修正されました。これにより、コンパイラが組み込み識別子を処理する際に、常に正しい組み込み定義にたどり着けるようになり、潜在的なバグや誤ったコード生成を防ぐことができます。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの基本的な概念とC言語のコード構造に関する知識が必要です。

  1. Goコンパイラ (gc):

    • Go言語の公式コンパイラの一つで、Goのソースコードを機械語に変換します。
    • gcは、字句解析(lexical analysis)、構文解析(parsing)、型チェック(type checking)、最適化(optimization)、コード生成(code generation)といった複数のフェーズを経てコンパイルを行います。
    • このコミットが関連するのは、主に字句解析とシンボル管理のフェーズです。
  2. 字句解析 (Lexical Analysis):

    • ソースコードをトークン(識別子、キーワード、演算子など)のストリームに変換するプロセスです。
    • src/cmd/gc/lex.cは、この字句解析に関連するコードが含まれているファイルです。
  3. シンボルテーブル (Symbol Table):

    • コンパイラがプログラム内で使用されるすべての識別子(変数名、関数名、型名など)に関する情報を格納するデータ構造です。
    • 各シンボルエントリには、識別子の名前、型、スコープ、定義場所などの属性が含まれます。
    • 組み込みシンボルもこのシンボルテーブルに登録されます。
  4. 組み込みシンボル (Built-in Symbols):

    • Go言語の仕様で最初から定義されている識別子です。例: true, false, nil, int, string, len, cap, make, newなど。
    • これらは特別な意味を持ち、コンパイラによって特別に扱われます。
  5. Node構造体とSym構造体:

    • Goコンパイラの内部では、プログラムの要素を表現するために様々なデータ構造が使われます。
    • Nodeは抽象構文木(AST)のノードを表すことが多く、プログラムの構造を表現します。
    • Sym(Symbol)はシンボルテーブルのエントリを表し、識別子に関するメタデータを保持します。
    • Nodeには、それが参照するシンボルへのポインタ(symフィールド)を持つことがあります。また、Nodeはそれが表す定義(defフィールド)を持つこともあります。
  6. ONAME:

    • Goコンパイラの内部で使われるオペレーションコード(Opcode)の一つで、名前(識別子)を表すノードタイプです。
  7. pkglookup:

    • パッケージと名前からシンボルを検索または作成する関数です。
  8. C言語のポインタと構造体:

    • GoコンパイラのコードベースはC言語で書かれているため、ポインタの概念(s1->def->symなど)と構造体(syms[i]など)の理解が不可欠です。

技術的詳細

このコミットの核心は、Goコンパイラが組み込みシンボルを初期化する際のシンボル解決の正確性に関するものです。

Goコンパイラは、起動時または特定のフェーズで、truefalsenilといった組み込みの識別子をシンボルテーブルに登録し、それらに対応する内部的な定義を設定します。このプロセスは、通常、lexinitのような初期化関数の中で行われます。

変更前のコードでは、組み込みシンボルs1の定義ノード(s1->def)のシンボル参照(s1->def->sym)が、ループ内で使用されていた一時的なシンボルsに設定されていました。このsは、syms[i].symから取得されるシンボルであり、必ずしもs1自身を指しているとは限りませんでした。

具体的には、syms配列は組み込みシンボルのリストを保持しており、syms[i].symは、その組み込みシンボルに対応するシンボルオブジェクトを指します。しかし、pkglookupによって返されるs1は、builtinpkgという特別なパッケージスコープで検索された、その組み込み識別子に対応するシンボルです。理想的には、s1の定義はs1自身を指すべきです。

変更後のコードでは、s1->def->sym = s1; と修正されました。これにより、s1という組み込みシンボルが表す定義ノードが、そのシンボル自身(s1)を指すようになります。これは、自己参照(self-referential)の構造を確立し、コンパイラが組み込み識別子を解決する際に、常にその識別子自身の正しい定義にたどり着くことを保証します。

この修正は、コンパイラのシンボル解決ロジックにおける微妙なバグを防ぎます。例えば、ある組み込み識別子を参照した際に、誤って別の(あるいは無効な)シンボル定義に解決されてしまうといった問題を回避できます。これは、コンパイラの内部的な整合性と、生成されるコードの正確性を保証するために非常に重要です。

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

変更は src/cmd/gc/lex.c ファイルの1箇所のみです。

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -1745,7 +1745,7 @@ lexinit(void)\n \t\t\ts1 = pkglookup(syms[i].name, builtinpkg);\n \t\t\ts1->lexical = LNAME;\n \t\t\ts1->def = nod(ONAME, N, N);\n-\t\t\ts1->def->sym = s;\n+\t\t\ts1->def->sym = s1;\n \t\t\ts1->def->etype = etype;\n \t\t\ts1->def->builtin = 1;\n \t\t}\

具体的には、lexinit関数内のループ内で、以下の行が変更されました。

- s1->def->sym = s; + s1->def->sym = s1;

コアとなるコードの解説

この変更は、lexinit関数内で組み込みシンボルを初期化する部分にあります。

lexinit関数は、Goコンパイラの字句解析器の初期化を行う関数であり、Go言語の組み込み型や定数(true, false, nilなど)のシンボルをシンボルテーブルに登録する役割を担っています。

コードの該当部分を詳しく見てみましょう。

// src/cmd/gc/lex.c
// ...
void
lexinit(void)
{
    // ...
    for(i=0; syms[i].name; i++) {
        // ...
        // s1は、組み込みパッケージ(builtinpkg)内でsyms[i].nameに対応するシンボルを検索または作成します。
        s1 = pkglookup(syms[i].name, builtinpkg);
        s1->lexical = LNAME; // 字句の種類をLNAME(名前)に設定
        s1->def = nod(ONAME, N, N); // シンボルの定義ノードを作成。ONAMEは名前を表すノードタイプ。
                                   // Nはnilノードを意味し、ここでは子ノードがないことを示す。

        // 変更点: ここでs1の定義ノードのシンボル参照を設定します。
        // 変更前: s1->def->sym = s;
        // 変更後: s1->def->sym = s1;

        s1->def->etype = etype; // 定義ノードの要素型を設定
        s1->def->builtin = 1;   // 定義ノードが組み込みであることを示すフラグを設定
        // ...
    }
    // ...
}
  • syms[i].name: 組み込みシンボルの名前(例: "true", "false", "nil")。
  • builtinpkg: 組み込みシンボルが属する特別なパッケージ。
  • pkglookup(syms[i].name, builtinpkg): builtinpkg内でsyms[i].nameに対応するシンボルを検索し、見つからなければ新しく作成して返します。この返り値がs1です。つまり、s1は現在処理している組み込み識別子に対応するシンボルオブジェクトです。
  • s1->lexical = LNAME;: s1の字句の種類をLNAME(名前)に設定します。
  • s1->def = nod(ONAME, N, N);: s1の「定義」を表すノードを作成し、s1->defに割り当てます。ONAMEは、このノードが名前(識別子)を表すことを示します。
  • s1->def->sym = s; (変更前): ここが問題の箇所でした。sはループの外部で定義された、あるいは別の目的で使用されていた可能性のある一時的なシンボル変数です。組み込みシンボルs1の定義ノードが、s1自身ではなく、このsを指してしまうと、シンボル解決の際に誤った参照が発生する可能性があります。
  • s1->def->sym = s1; (変更後): この修正により、s1の定義ノードが、そのシンボル自身(s1)を指すようになります。これは、trueというシンボルの定義はtrue自身である、という直感的な関係をコンパイラの内部で正確に表現します。これにより、コンパイラがtrueという識別子に遭遇した際に、常にその正しい組み込み定義に解決されることが保証されます。
  • s1->def->etype = etype;: 定義ノードの型を設定します。
  • s1->def->builtin = 1;: この定義ノードが組み込みのものであることを示すフラグを設定します。

この変更は、コンパイラのシンボルテーブルの整合性を保ち、組み込み識別子の正しいセマンティクスを保証するために不可欠な修正でした。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Goコンパイラに関する一般的な情報): https://go.dev/doc/
  • Goコンパイラのソースコード (特にsrc/cmd/gcディレクトリ): https://github.com/golang/go/tree/master/src/cmd/compile
  • Go言語の組み込み関数と型に関する仕様: https://go.dev/ref/spec#Predeclared_identifiers
  • Goコンパイラの内部構造に関する一般的な解説記事 (当時の情報を含む): (具体的なURLは特定できませんでしたが、Goコンパイラの内部に関するブログ記事やカンファレンス発表資料などを参考にしました。)
  • C言語のポインタと構造体に関する一般的な知識。
  • コンパイラ設計に関する一般的な書籍やオンラインリソース。