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

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

このコミットでは、Goコンパイラの定数評価ロジック、特にビット反転演算子 ^ (ビットごとのNOT) の挙動が変更されています。影響を受けるファイルは以下の通りです。

  • src/cmd/gc/const.c: 定数評価のコアロジックが実装されているファイル。ビット反転演算の処理が修正されました。
  • test/const1.go: 定数に関するテストケース。新しい定数評価の挙動を検証するために更新されました。
  • test/{bugs => fixedbugs}/bug115.go: バグ修正のテストケース。ファイルが移動されましたが、内容は変更されていません。

コミット

  • コミットハッシュ: b8be809c10e86dcee31317d78d84710ef8b67c82
  • Author: Ken Thompson ken@golang.org
  • Date: Tue Mar 24 16:40:38 2009 -0700

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

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

元コミット内容

^ type(const) now inverts "enough" bits
^ signed(const) becomes illegal
^ unsigned(const) becomes legal

R=r
OCL=26697
CL=26697

変更の背景

このコミットは、Go言語における定数(特に数値定数)の型推論とビット反転演算子 ^ の挙動に関する重要な修正を導入しています。

Go言語の初期段階では、定数の扱いにはいくつかの曖昧さや不整合がありました。特に、型付けされていない定数(untyped constants)が特定の型に変換される際の挙動や、ビット反転演算子 ^ が適用された場合のビット幅の解釈が問題となっていました。

元の実装では、^ 演算子が適用された際に、定数のビット反転が常に64ビット幅で行われるなど、意図しない結果を招く可能性がありました。例えば、uint8 型の定数に対して ^ を適用した場合、結果が uint8 の範囲を超えてしまい、オーバーフローエラーとなるべきケースで適切なエラーが検出されない、あるいは予期せぬ値になることが考えられます。

このコミットの目的は、以下の点を明確にし、Go言語の定数システムをより堅牢で予測可能なものにすることです。

  1. ^ 演算子のビット幅の明確化: ^ 演算子が適用される定数に対して、その定数が最終的に割り当てられる型(または推論される型)に基づいて適切なビット幅で反転が行われるようにする。これにより、「十分な」ビット数だけが反転されるようになります。
  2. signed(const) の禁止: 符号付き定数に対する signed 変換が意味をなさない、あるいは誤解を招く可能性があるため、これを不正な操作とする。
  3. unsigned(const) の許可: 符号なし定数に対する unsigned 変換を合法化する。これは、定数の型推論や型変換の柔軟性を高めるためと考えられます。

これらの変更により、Go言語の定数に関するセマンティクスがより厳密になり、開発者が定数を使った際に期待通りの挙動が得られるようになります。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。

  1. Go言語の定数 (Constants):

    • Go言語の定数は、コンパイル時に値が決定される不変のエンティティです。
    • 定数には「型付けされた定数 (typed constants)」と「型付けされていない定数 (untyped constants)」があります。
    • 数値リテラル(例: 100, 3.14)はデフォルトで型付けされていない定数です。これらは、変数に代入されるか、演算で使用される際に、文脈に応じて適切な型に推論されます。
    • 型付けされていない定数は、その値が表現できる限り、任意の数値型として振る舞うことができます。これにより、異なる数値型間の演算が柔軟に行えます。
    • const キーワードで宣言された定数は、明示的に型を指定しない限り、型付けされていない定数となります(例: const x = 10)。
  2. ビット反転演算子 ^ (Bitwise NOT):

    • Go言語における ^ 演算子は、整数型に対してビットごとのNOT演算(1の補数)を実行します。
    • 例えば、^0 は、すべてのビットが1になった値を返します。この値は、その型における最大値(符号なしの場合)または最小値(符号付きの場合)に依存します。
    • この演算の挙動は、オペランドの型(ビット幅)に依存します。uint8^0uint32^0 は異なる値になります。
  3. Goコンパイラ gc:

    • gc はGo言語の公式コンパイラです。
    • コンパイラのフロントエンドは、ソースコードの字句解析、構文解析、意味解析を行い、抽象構文木 (AST) を構築します。
    • 定数評価は、意味解析の段階で行われる重要な処理の一つです。コンパイル時に定数式を評価し、その結果をコードに埋め込みます。
    • src/cmd/gc/const.c は、この定数評価ロジックの一部を担うファイルです。
  4. Node 構造体:

    • コンパイラ内部でASTのノードを表す構造体です。n->left, n->right などで子ノードにアクセスし、演算子の種類 (n->op) や型情報 (n->type) を保持します。
  5. Val 構造体:

    • 定数の値を表現するための構造体です。v.u.xval は、多倍長整数を扱うための Mpint 型の値を保持します。Goコンパイラは、コンパイル時の定数計算に多倍長整数ライブラリを使用します。
  6. Mpint (Multi-precision integer):

    • Goコンパイラが内部で定数を扱うために使用する多倍長整数型です。これにより、Goの整数型が表現できる範囲を超える大きな定数も正確に扱うことができます。mpmovecfix, mpnegfix, mpxorfixfix などは、この Mpint 型に対する演算を行う関数です。
  7. etype (Element Type):

    • Goコンパイラ内部で型を表す列挙型です。TINT8, TUINT8, TINT32, TUINT32 など、Goの組み込み型に対応します。

技術的詳細

このコミットの主要な変更は、src/cmd/gc/const.c 内の evconst 関数における OCOM (ビットごとのNOT) 演算の処理にあります。

evconst 関数は、抽象構文木 (AST) のノード n を受け取り、そのノードが表す定数式を評価して結果を n->val に格納します。

変更前の OCOM 処理は mpcomfix(v.u.xval); となっていました。mpcomfixMpint 型の値をビット反転する関数ですが、この実装では、オペランドの型(例えば uint8uint16)に関わらず、Mpint が内部的に持つ最大ビット幅(通常は64ビット)でビット反転を行っていました。これは、Goの型システムにおけるビット幅のセマンティクスと一致しない場合があり、特に小さい整数型でオーバーフローを引き起こす可能性がありました。

新しい実装では、この問題を解決するために、ビット反転の際に「マスク」を導入しています。

case TUP(OCOM, CTINT):
    et = Txxx;
    if(nl->type != T)
        et = nl->type->etype;

    // calculate the mask in b
    // result will be (a ^ mask)
    switch(et) {
    default:
        mpmovecfix(&b, -1);
        break;

    case TINT8:
    case TINT16:
    case TINT32:
    case TINT64:
    case TINT:
        et++;        // convert to unsigned
                    // fallthrough
    case TUINT8:
    case TUINT16:
    case TUINT32:
    case TUINT64:
    case TUINT:
    case TUINTPTR:
        mpmovefixfix(&b, maxintval[et]);
        break;
    }
    mpxorfixfix(v.u.xval, &b);
    break;

このコードブロックの動作は以下の通りです。

  1. オペランドの型 (et) の取得:

    • nl^ 演算子の左オペランド(つまり、ビット反転される定数)のノードです。
    • nl->type->etype から、その定数の型(TINT8, TUINT32 など)を取得します。もし型が不明な場合は Txxx となります。
  2. マスク b の計算:

    • switch(et) ブロックで、オペランドの型に基づいて適切なマスク b を計算します。
    • デフォルトケース: et が不明な場合(型付けされていない定数など)、mpmovecfix(&b, -1) を実行します。これは b をすべてのビットが1になった値(Mpint の最大ビット幅における -1)に設定します。これは、従来の mpcomfix と同様の挙動ですが、後続の mpxorfixfix と組み合わせることで、型付けされていない定数に対する ^ 演算が正しく機能するようにします。
    • 符号付き整数型 (TINT8 など): et++ することで、対応する符号なし整数型に変換されます。例えば、TINT8TUINT8 になります。これは、ビット反転のマスクを計算する際に、符号ビットを考慮せず、純粋なビット幅でマスクを生成するためです。
    • 符号なし整数型 (TUINT8 など): mpmovefixfix(&b, maxintval[et]) を実行します。maxintval[et] は、指定された型 et が表現できる最大値(すべてのビットが1になった値)を Mpint 形式で保持する配列です。これにより、b はオペランドの型に応じた適切なビット幅のマスク(例: uint8 なら 0xFFuint16 なら 0xFFFF)に設定されます。
  3. ビット反転の実行:

    • mpxorfixfix(v.u.xval, &b); を実行します。これは、元の定数 v.u.xval と計算されたマスク b のビットごとのXOR演算を行います。
    • ビットごとのNOT演算 ~A は、A XOR (すべてのビットが1のマスク) と等価です。
    • このアプローチにより、オペランドの型に応じた正確なビット幅でビット反転が行われるようになります。例えば、uint8(0)^ は、00xFF のXORとなり、結果は 0xFF となります。従来の mpcomfix では、00xFFFFFFFFFFFFFFFF のNOTとなり、結果は 0xFFFFFFFFFFFFFFFF となり、uint8 に代入する際にオーバーフローが発生していました。

test/const1.go の変更は、この新しい挙動を反映しています。

  • b6 = ^uint8(0); // ERROR "overflow"b6 = ^uint8(0); // OK に変更されています。
    • これは、^uint8(0) の結果が uint8 の範囲内に収まるようになったことを示しています。具体的には、uint8(0) のビット反転は uint8 の最大値である 255 (0xFF) となり、これは uint8 の有効な値です。

この変更により、Go言語の定数システムはより厳密になり、特にビット演算において、開発者が期待する型セマンティクスが保証されるようになりました。

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

src/cmd/gc/const.cevconst 関数内の case TUP(OCOM, CTINT): ブロックが変更されました。

--- a/src/cmd/gc/const.c
+++ b/src/cmd/gc/const.c
@@ -541,7 +542,34 @@ unary:
 	\t\tmpnegfix(v.u.xval);\
 	\t\tbreak;\
 	case TUP(OCOM, CTINT):\
-\t\t\tmpcomfix(v.u.xval);\
+\t\t\tet = Txxx;\
+\t\t\tif(nl->type != T)\
+\t\t\t\tet = nl->type->etype;\
+\
+\t\t// calculate the mask in b
+\t\t// result will be (a ^ mask)
+\t\tswitch(et) {\n+\t\tdefault:\n+\t\t\t\tmpmovecfix(&b, -1);\n+\t\t\tbreak;\n+\n+\t\tcase TINT8:\n+\t\tcase TINT16:\n+\t\tcase TINT32:\n+\t\tcase TINT64:\n+\t\tcase TINT:\n+\t\t\tet++;\t\t// convert to unsigned\n+\t\t\t\t\t// fallthrough\n+\t\tcase TUINT8:\n+\t\tcase TUINT16:\n+\t\tcase TUINT32:\n+\t\tcase TUINT64:\n+\t\tcase TUINT:\n+\t\tcase TUINTPTR:\n+\t\t\t\tmpmovefixfix(&b, maxintval[et]);\n+\t\t\tbreak;\n+\t\t}\n+\t\tmpxorfixfix(v.u.xval, &b);\
 \t\tbreak;\

コアとなるコードの解説

変更されたコードは、定数に対するビット反転演算子 ^ のセマンティクスを修正しています。

  • et = Txxx; if(nl->type != T) et = nl->type->etype;:

    • これは、ビット反転される定数 nl の型 (etype) を取得しようとしています。T は型が不明であることを示す特別な値です。もし型が明示的に指定されている場合(例: uint8(0))、その型が et に設定されます。型付けされていない定数の場合は Txxx のままになります。
  • switch(et) ブロック:

    • このスイッチ文は、定数の型に基づいて、ビット反転に使用する「マスク」を決定します。
    • default::
      • 型が不明な場合(Txxx のままの場合)、mpmovecfix(&b, -1); が実行されます。これは、マスク b をすべてのビットが1になった値(多倍長整数で表現可能な最大ビット幅における -1)に設定します。これは、型付けされていない定数に対する ^ 演算が、その定数が最終的に割り当てられる型に依存して適切に振る舞うための準備です。
    • case TINT8: ... case TINT::
      • 符号付き整数型の場合、et++ することで対応する符号なし整数型に変換されます。例えば、TINT8TUINT8 になります。これは、ビット反転のマスクを計算する際に、符号ビットを考慮せず、純粋なビット幅でマスクを生成するためです。Goのビット反転は、通常、符号なしのセマンティクスで理解されるため、この変換は理にかなっています。
    • case TUINT8: ... case TUINTPTR::
      • 符号なし整数型の場合、または符号付きから変換された場合、mpmovefixfix(&b, maxintval[et]); が実行されます。maxintval は、各Goの組み込み型が表現できる最大値(すべてのビットが1になった値)を Mpint 形式で保持する配列です。これにより、マスク b は、オペランドの型に応じた正確なビット幅(例: uint8 なら 0xFFuint32 なら 0xFFFFFFFF)に設定されます。
  • mpxorfixfix(v.u.xval, &b);:

    • 最後に、元の定数 v.u.xval と、計算された型に応じたマスク b のビットごとのXOR演算を行います。
    • ビットごとのNOT演算 ~A は、A XOR (すべてのビットが1のマスク) と数学的に等価です。この方法を用いることで、Goの型システムが持つビット幅のセマンティクスに厳密に従ったビット反転が実現されます。

この変更により、^ 演算子が適用された定数は、その型が持つビット幅の範囲内で正しくビット反転されるようになり、予期せぬオーバーフローや誤った値の生成が防止されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に src/cmd/gc/const.c および src/cmd/gc/mpint.c の関連関数)
  • Go言語のテストコード (test/const1.go)
  • ビット演算に関する一般的な知識
  • コンパイラの定数評価に関する一般的な知識
  • 多倍長整数演算に関する一般的な知識