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

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

このコミットは、Goコンパイラ(cmd/gc)において、インライン化の際にローカル変数に一意のIDを付与する変更を導入します。これにより、ローカル変数が他の名前(特に型名)をシャドウイングすることによって発生する問題を回避し、コンパイラの堅牢性を向上させます。

コミット

  • コミットハッシュ: cb856adea965955c4d2424b2946b0db90a682b78
  • Author: Russ Cox rsc@golang.org
  • Date: Wed Nov 7 09:59:19 2012 -0500

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

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

元コミット内容

cmd/gc: annotate local variables with unique ids for inlining

Avoids problems with local declarations shadowing other names.
We write a more explicit form than the incoming program, so there
may be additional type annotations. For example:

        int := "hello"
        j := 2

would normally turn into

        var int string = "hello"
        var j int = 2

but the int variable shadows the int type in the second line.

This CL marks all local variables with a per-function sequence number,
so that this would instead be:

        var int·1 string = "hello"
        var j·2 int = 2

Fixes #4326.

R=ken2
CC=golang-dev
https://golang.org/cl/6816100

変更の背景

この変更は、Goコンパイラがコードを処理する際に発生する、ローカル変数の名前が既存の型名や他の識別子を「シャドウイング(shadowing)」する問題に対処するために導入されました。特に、Go言語の短変数宣言(:=)は、変数の型を自動的に推論するため、開発者が意図しない形で既存の識別子と同じ名前の変数を宣言してしまう可能性があります。

元のコミットメッセージに示されている例がこの問題の核心を突いています。

int := "hello" // 'int'という名前の文字列変数を宣言
j := 2         // 'int'という名前の型(組み込み型)を参照しようとするが、直前の変数宣言によってシャドウされる

Goコンパイラがこのコードを内部的に処理し、より明示的な形式に変換しようとすると、以下のような形になります。

var int string = "hello"
var j int = 2

ここで問題が発生します。2行目のvar j int = 2において、intは組み込みの整数型を指すべきですが、1行目で宣言されたintという名前のローカル変数によってシャドウされてしまいます。これにより、コンパイラはintを型ではなく変数として解釈し、型エラーや予期せぬ動作を引き起こす可能性がありました。

この問題は、特にコードのインライン化(inlining)処理において顕在化します。インライン化は、関数呼び出しのオーバーヘッドを削減するために、呼び出し元のコードに関数本体を直接埋め込む最適化手法です。この際、インライン化される関数のローカル変数が、呼び出し元のスコープ内の識別子と衝突する可能性があり、シャドウイング問題がより複雑になります。

このコミットは、Go issue #4326 (https://code.google.com/p/go/issues/detail?id=4326) で報告されたバグを修正することを目的としています。このバグは、特定の状況下でコンパイラが誤ったコードを生成したり、コンパイルに失敗したりする原因となっていました。

前提知識の解説

1. Goコンパイラ (cmd/gc)

Go言語の公式コンパイラは、主にcmd/gcというディレクトリに実装されています。これは、Goソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成など、複数のフェーズが含まれます。このコミットは、主に意味解析と中間コード生成のフェーズに関連する変更です。

2. インライン化 (Inlining)

インライン化は、コンパイラ最適化の一種です。関数呼び出しの際に発生するスタックフレームの作成、引数の渡し、戻り値の処理といったオーバーヘッドを削減するために、呼び出される関数の本体を呼び出し元のコードに直接展開します。これにより、実行時のパフォーマンスが向上する可能性があります。しかし、インライン化はコードサイズを増加させる可能性もあり、コンパイラはインライン化の対象となる関数を慎重に選択します。

3. ローカル変数 (Local Variables)

ローカル変数は、特定の関数やブロック内で宣言され、そのスコープ内でのみアクセス可能な変数です。関数が呼び出されるたびに新しく作成され、関数が終了すると破棄されます。

4. スコープ (Scope)

スコープは、プログラム内で識別子(変数、関数、型など)が参照可能である範囲を定義します。Go言語では、ブロックレベルのスコープがあり、内側のスコープで宣言された識別子は、外側のスコープで宣言された同じ名前の識別子を「シャドウイング」することができます。

5. シャドウイング (Shadowing)

シャドウイングとは、内側のスコープで宣言された識別子が、外側のスコープで宣言された同じ名前の識別子を「隠す」現象を指します。これにより、内側のスコープからは外側の識別子に直接アクセスできなくなります。Go言語では、短変数宣言(:=)がシャドウイングを引き起こす一般的な原因の一つです。

6. Node構造体とSym構造体

Goコンパイラの内部では、プログラムの抽象構文木(AST)がNode構造体で表現されます。各Nodeは、変数、式、文などのプログラム要素に対応します。Sym構造体は、シンボルテーブルのエントリを表し、識別子の名前やその属性(型、スコープなど)を管理します。

7. vargenフィールド

このコミットで導入されるvargenは、Node構造体に追加されるフィールドで、ローカル変数に一意のシーケンス番号を割り当てるために使用されます。この番号は、関数ごとにリセットされ、その関数内で宣言されるローカル変数に順次割り当てられます。

技術的詳細

このコミットの主要な技術的アプローチは、Goコンパイラがローカル変数を処理する際に、その変数名に加えて関数内で一意のシーケンス番号を付与することです。これにより、たとえユーザーがintのような組み込み型と同じ名前のローカル変数を宣言したとしても、コンパイラ内部ではint·1のように区別され、名前の衝突(シャドウイング)が回避されます。

具体的には、以下の変更が行われています。

  1. vargenカウンターの導入:

    • src/cmd/gc/dcl.cstatic int vargen;が追加されました。これは、現在の関数内で宣言されるローカル変数に割り当てる一意のシーケンス番号を管理するためのカウンターです。
    • funcargs関数(関数の引数と戻り値を宣言する部分)の開始時に、vargenがリセットされます。これは、戻り値変数に小さい番号を割り当てるためです。
    • ローカル変数(PAUTOPPARAMPPARAMOUTクラスの変数)が宣言される際に、n->left->vargen = ++vargen;のように、vargenの値をインクリメントしてNodevargenフィールドに割り当てます。これにより、各ローカル変数は関数内で一意のIDを持つことになります。
  2. 変数名のフォーマット変更:

    • src/cmd/gc/fmt.cexprfmt関数(式をフォーマットする関数)において、ONAMEノード(変数名を表すノード)の処理が変更されました。
    • PAUTOPPARAMPPARAMOUTクラスのローカル変数で、vargenが0より大きい場合、変数名とvargenを結合した形式(例: int·1)で出力するように変更されました。これは、コンパイラ内部での表現であり、Goのソースコードに直接現れるものではありません。この変更は、コンパイラが生成する中間コードやデバッグ情報において、変数を一意に識別するために使用されます。
    • fmtmode == FExpという条件は、エクスポートされたシンボルをフォーマットする際に適用されることを示唆しています。
  3. エスケープ解析の調整:

    • src/cmd/gc/esc.cのエスケープ解析ロジックが微調整されました。特に、dst->vargen < 20dst->vargen <= 20に変更され、src->esc |= 1<<(dst->vargen + EscBits);src->esc |= 1<<((dst->vargen-1) + EscBits);に変更されています。これは、vargenの新しい割り当てロジック(1から始まる)に合わせて、エスケープ解析が正しく機能するように調整されたものです。エスケープ解析は、変数がヒープに割り当てられるべきか、スタックに割り当てられるべきかを決定するコンパイラの重要なフェーズです。
  4. テストケースの追加:

    • test/fixedbugs/issue4326.dir/以下に複数のテストファイル(p1.go, p2.go, q1.go, q2.go, z.go)と、それらをコンパイルしてテストするissue4326.goが追加されました。これらのテストは、シャドウイング問題が修正されたことを検証するために設計されています。特に、q1.goDeref関数内のtyp, ok := typ.(*int)のような短変数宣言が、int型をシャドウイングしないことを確認しています。

これらの変更により、コンパイラはローカル変数を一意に識別できるようになり、名前の衝突によるバグや予期せぬ動作を防ぐことができます。これは、Goコンパイラの堅牢性と正確性を向上させる上で重要な改善です。

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

このコミットにおける主要なコード変更は、以下のファイルに集中しています。

  1. src/cmd/gc/dcl.c:

    • static int vargen; の追加: ローカル変数の一意なID生成のためのカウンター。
    • declare 関数内の変更:
      • else if(n->op == ONAME && ctxt == PAUTO && strstr(s->name, "·") == nil) の条件が追加され、PAUTO(自動変数、つまりローカル変数)の場合にのみvargenをインクリメントして割り当てるようになりました。strstr(s->name, "·") == nilは、既に·を含む名前(つまり、既に一意化された名前)には再度vargenを割り当てないためのガードです。
    • funcargs 関数内の変更:
      • vargen = count(nt->rlist); の追加: 関数の引数と戻り値の宣言時にvargenをリセットし、戻り値変数に小さい番号を割り当てるための初期化。
      • 引数と戻り値の宣言ループ内で、dclcontext == PAUTOの場合にn->left->vargen = ++vargen;が追加され、これらの変数にも一意のIDが割り当てられるようになりました。
  2. src/cmd/gc/esc.c:

    • escwalk 関数内の変更:
      • if(dst->op == ONAME && dst->class == PPARAMOUT && dst->vargen < 20)if(dst->op == ONAME && dst->class == PPARAMOUT && dst->vargen <= 20) に変更。
      • src->esc |= 1<<(dst->vargen + EscBits);src->esc |= 1<<((dst->vargen-1) + EscBits); に変更。
      • これらの変更は、vargenの割り当てロジック(1から始まる)に合わせて、エスケープ解析が正しく機能するように調整されたものです。
  3. src/cmd/gc/fmt.c:

    • typefmt 関数内の変更:
      • if(t->funarg) の条件が追加され、関数引数の名前をフォーマットする際の挙動が調整されました。
    • stmtfmt 関数内の変更:
      • case ODCL: の処理に、fmtmode == FExp(エクスポートモード)の場合にローカル変数(PPARAM, PPARAMOUT, PAUTO)の宣言をvar %N %Tの形式で出力するロジックが追加されました。これは、コンパイラが生成する中間表現において、変数の型情報をより明示的にするためと考えられます。
    • exprfmt 関数内の変更:
      • case ONAME: の処理に、ローカル変数(PAUTO, PPARAM, PPARAMOUT)でvargenが0より大きい場合に、fmtprint(f, "%S·%d", n->sym, n->vargen); の形式で変数名とvargenを結合して出力するロジックが追加されました。これが、int·1のような形式を生成する核心部分です。
  4. src/pkg/exp/types/gcimporter_test.go:

    • importedObjectTests 配列内の math.Sin のテストケースの期待される出力が func(x float64) (_ float64) から func(x·2 float64) (_ float64) に変更されました。これは、インポートされた関数の引数名にもvargenが付与されるようになったことを反映しています。
  5. test/fixedbugs/issue4326.dir/ および test/fixedbugs/issue4326.go:

    • シャドウイング問題の修正を検証するための新しいテストケースが追加されました。

コアとなるコードの解説

src/cmd/gc/dcl.c の変更

// dcl.c
static int vargen; // 新しく追加されたvargenカウンター

// declare関数の一部
else if(n->op == ONAME && ctxt == PAUTO && strstr(s->name, "·") == nil)
    gen = ++vargen; // ローカル変数に一意のIDを割り当てる

// funcargs関数の一部
// re-start the variable generation number
// we want to use small numbers for the return variables,
// so let them have the chunk starting at 1.
vargen = count(nt->rlist); // vargenをリセットし、戻り値の数で初期化

// 引数と戻り値の宣言ループ内
if(dclcontext == PAUTO)
    n->left->vargen = ++vargen; // 引数や戻り値にもvargenを割り当てる

dcl.cは、Goコンパイラの宣言処理を担当するファイルです。vargenという静的変数が導入され、関数内で宣言されるローカル変数(PAUTOコンテキスト)に対して、vargenをインクリメントした値をNode構造体のvargenフィールドに割り当てます。これにより、各ローカル変数は関数内で一意の識別子を持つことになります。funcargs関数でのvargenのリセットは、各関数スコープ内でvargenが独立して機能することを保証します。

src/cmd/gc/fmt.c の変更

// fmt.c
// exprfmt関数の一部 (ONAMEノードの処理)
case ONAME:
    // Special case: name used as local variable in export.
    switch(n->class&~PHEAP){
    case PAUTO:
    case PPARAM:
    case PPARAMOUT:
        if(fmtmode == FExp && n->sym && !isblanksym(n->sym) && n->vargen > 0)
            return fmtprint(f, "%S·%d", n->sym, n->vargen); // 変数名とvargenを結合して出力
    }

fmt.cは、Goコンパイラが内部表現を文字列としてフォーマットする際に使用されます。この変更は、ONAME(変数名)ノードを処理する際に、その変数がローカル変数(PAUTO, PPARAM, PPARAMOUT)であり、かつvargenが割り当てられている場合(n->vargen > 0)、変数名とvargen·で結合した形式(例: int·1)で出力するようにします。fmtmode == FExpは、エクスポートされたシンボルをフォーマットする際にこのルールが適用されることを示唆しており、主にコンパイラ内部での一意な識別やデバッグ情報の生成に利用されます。

src/cmd/gc/esc.c の変更

// esc.c
// escwalk関数の一部
if(dst->op == ONAME && dst->class == PPARAMOUT && dst->vargen <= 20) { // 比較演算子の変更
    // ...
    src->esc |= 1<<((dst->vargen-1) + EscBits); // vargenのオフセット調整
}

esc.cは、エスケープ解析(変数がスタックに割り当てられるかヒープに割り当てられるかを決定する)を担当します。この変更は、vargenの割り当てが1から始まるようになったことに合わせて、エスケープ解析のロジックを調整するものです。dst->vargen < 20dst->vargen <= 20に変更され、dst->vargenをビットシフトのオフセットとして使用する際にvargen-1を使用することで、正しいビット位置が参照されるように修正されています。

これらの変更により、Goコンパイラはローカル変数をより堅牢かつ一意に識別できるようになり、名前のシャドウイングによる潜在的な問題を根本的に解決しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード
  • Go issue #4326 の議論
  • Go CL 6816100 のレビューコメント
  • コンパイラ最適化に関する一般的な情報(インライン化、エスケープ解析など)