[インデックス 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
) - ビット演算に関する一般的な知識
- コンパイラの定数評価に関する一般的な知識
- 多倍長整数演算に関する一般的な知識