[インデックス 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言語の定数システムをより堅牢で予測可能なものにすることです。
^演算子のビット幅の明確化:^演算子が適用される定数に対して、その定数が最終的に割り当てられる型(または推論される型)に基づいて適切なビット幅で反転が行われるようにする。これにより、「十分な」ビット数だけが反転されるようになります。signed(const)の禁止: 符号付き定数に対するsigned変換が意味をなさない、あるいは誤解を招く可能性があるため、これを不正な操作とする。unsigned(const)の許可: 符号なし定数に対するunsigned変換を合法化する。これは、定数の型推論や型変換の柔軟性を高めるためと考えられます。
これらの変更により、Go言語の定数に関するセマンティクスがより厳密になり、開発者が定数を使った際に期待通りの挙動が得られるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。
-
Go言語の定数 (Constants):
- Go言語の定数は、コンパイル時に値が決定される不変のエンティティです。
- 定数には「型付けされた定数 (typed constants)」と「型付けされていない定数 (untyped constants)」があります。
- 数値リテラル(例:
100,3.14)はデフォルトで型付けされていない定数です。これらは、変数に代入されるか、演算で使用される際に、文脈に応じて適切な型に推論されます。 - 型付けされていない定数は、その値が表現できる限り、任意の数値型として振る舞うことができます。これにより、異なる数値型間の演算が柔軟に行えます。
constキーワードで宣言された定数は、明示的に型を指定しない限り、型付けされていない定数となります(例:const x = 10)。
-
ビット反転演算子
^(Bitwise NOT):- Go言語における
^演算子は、整数型に対してビットごとのNOT演算(1の補数)を実行します。 - 例えば、
^0は、すべてのビットが1になった値を返します。この値は、その型における最大値(符号なしの場合)または最小値(符号付きの場合)に依存します。 - この演算の挙動は、オペランドの型(ビット幅)に依存します。
uint8の^0とuint32の^0は異なる値になります。
- Go言語における
-
Goコンパイラ
gc:gcはGo言語の公式コンパイラです。- コンパイラのフロントエンドは、ソースコードの字句解析、構文解析、意味解析を行い、抽象構文木 (AST) を構築します。
- 定数評価は、意味解析の段階で行われる重要な処理の一つです。コンパイル時に定数式を評価し、その結果をコードに埋め込みます。
src/cmd/gc/const.cは、この定数評価ロジックの一部を担うファイルです。
-
Node構造体:- コンパイラ内部でASTのノードを表す構造体です。
n->left,n->rightなどで子ノードにアクセスし、演算子の種類 (n->op) や型情報 (n->type) を保持します。
- コンパイラ内部でASTのノードを表す構造体です。
-
Val構造体:- 定数の値を表現するための構造体です。
v.u.xvalは、多倍長整数を扱うためのMpint型の値を保持します。Goコンパイラは、コンパイル時の定数計算に多倍長整数ライブラリを使用します。
- 定数の値を表現するための構造体です。
-
Mpint(Multi-precision integer):- Goコンパイラが内部で定数を扱うために使用する多倍長整数型です。これにより、Goの整数型が表現できる範囲を超える大きな定数も正確に扱うことができます。
mpmovecfix,mpnegfix,mpxorfixfixなどは、このMpint型に対する演算を行う関数です。
- Goコンパイラが内部で定数を扱うために使用する多倍長整数型です。これにより、Goの整数型が表現できる範囲を超える大きな定数も正確に扱うことができます。
-
etype(Element Type):- Goコンパイラ内部で型を表す列挙型です。
TINT8,TUINT8,TINT32,TUINT32など、Goの組み込み型に対応します。
- Goコンパイラ内部で型を表す列挙型です。
技術的詳細
このコミットの主要な変更は、src/cmd/gc/const.c 内の evconst 関数における OCOM (ビットごとのNOT) 演算の処理にあります。
evconst 関数は、抽象構文木 (AST) のノード n を受け取り、そのノードが表す定数式を評価して結果を n->val に格納します。
変更前の OCOM 処理は mpcomfix(v.u.xval); となっていました。mpcomfix は Mpint 型の値をビット反転する関数ですが、この実装では、オペランドの型(例えば uint8 や uint16)に関わらず、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;
このコードブロックの動作は以下の通りです。
-
オペランドの型 (
et) の取得:nlは^演算子の左オペランド(つまり、ビット反転される定数)のノードです。nl->type->etypeから、その定数の型(TINT8,TUINT32など)を取得します。もし型が不明な場合はTxxxとなります。
-
マスク
bの計算:switch(et)ブロックで、オペランドの型に基づいて適切なマスクbを計算します。- デフォルトケース:
etが不明な場合(型付けされていない定数など)、mpmovecfix(&b, -1)を実行します。これはbをすべてのビットが1になった値(Mpintの最大ビット幅における-1)に設定します。これは、従来のmpcomfixと同様の挙動ですが、後続のmpxorfixfixと組み合わせることで、型付けされていない定数に対する^演算が正しく機能するようにします。 - 符号付き整数型 (
TINT8など):et++することで、対応する符号なし整数型に変換されます。例えば、TINT8はTUINT8になります。これは、ビット反転のマスクを計算する際に、符号ビットを考慮せず、純粋なビット幅でマスクを生成するためです。 - 符号なし整数型 (
TUINT8など):mpmovefixfix(&b, maxintval[et])を実行します。maxintval[et]は、指定された型etが表現できる最大値(すべてのビットが1になった値)をMpint形式で保持する配列です。これにより、bはオペランドの型に応じた適切なビット幅のマスク(例:uint8なら0xFF、uint16なら0xFFFF)に設定されます。
-
ビット反転の実行:
mpxorfixfix(v.u.xval, &b);を実行します。これは、元の定数v.u.xvalと計算されたマスクbのビットごとのXOR演算を行います。- ビットごとのNOT演算
~Aは、A XOR (すべてのビットが1のマスク)と等価です。 - このアプローチにより、オペランドの型に応じた正確なビット幅でビット反転が行われるようになります。例えば、
uint8(0)の^は、0と0xFFのXORとなり、結果は0xFFとなります。従来のmpcomfixでは、0と0xFFFFFFFFFFFFFFFFの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.c の evconst 関数内の 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++することで対応する符号なし整数型に変換されます。例えば、TINT8はTUINT8になります。これは、ビット反転のマスクを計算する際に、符号ビットを考慮せず、純粋なビット幅でマスクを生成するためです。Goのビット反転は、通常、符号なしのセマンティクスで理解されるため、この変換は理にかなっています。
- 符号付き整数型の場合、
case TUINT8: ... case TUINTPTR::- 符号なし整数型の場合、または符号付きから変換された場合、
mpmovefixfix(&b, maxintval[et]);が実行されます。maxintvalは、各Goの組み込み型が表現できる最大値(すべてのビットが1になった値)をMpint形式で保持する配列です。これにより、マスクbは、オペランドの型に応じた正確なビット幅(例:uint8なら0xFF、uint32なら0xFFFFFFFF)に設定されます。
- 符号なし整数型の場合、または符号付きから変換された場合、
-
mpxorfixfix(v.u.xval, &b);:- 最後に、元の定数
v.u.xvalと、計算された型に応じたマスクbのビットごとのXOR演算を行います。 - ビットごとのNOT演算
~Aは、A XOR (すべてのビットが1のマスク)と数学的に等価です。この方法を用いることで、Goの型システムが持つビット幅のセマンティクスに厳密に従ったビット反転が実現されます。
- 最後に、元の定数
この変更により、^ 演算子が適用された定数は、その型が持つビット幅の範囲内で正しくビット反転されるようになり、予期せぬオーバーフローや誤った値の生成が防止されます。
関連リンク
- Go言語仕様 (Constants): https://go.dev/ref/spec#Constants
- Go言語仕様 (Operators): https://go.dev/ref/spec#Operators
- Go言語のソースコード (src/cmd/gc/): https://github.com/golang/go/tree/master/src/cmd/gc
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
src/cmd/gc/const.cおよびsrc/cmd/gc/mpint.cの関連関数) - Go言語のテストコード (
test/const1.go) - ビット演算に関する一般的な知識
- コンパイラの定数評価に関する一般的な知識
- 多倍長整数演算に関する一般的な知識