[インデックス 12136] ファイルの概要
このコミットは、Goコンパイラ(gc
)におけるブール型のルールを緩和し、より柔軟な型変換を可能にする変更を導入しています。具体的には、比較演算の結果として得られる「理想的なブール型(idealbool
)」の扱いが変更され、明示的な型変換なしに他のブール型やインターフェース型に割り当てられるようになりました。これにより、Go言語の型システムにおけるブール型の利用がより直感的になります。
コミット
commit e29d3dfc49f7142d87ab71bd1d8d04e129972dd5
Author: Russ Cox <rsc@golang.org>
Date: Wed Feb 22 00:29:37 2012 -0500
gc: new, less strict bool rules
R=ken2
CC=golang-dev
https://golang.org/cl/5688064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e29d3dfc49f7142d87ab71bd1d8d04e129972dd5
元コミット内容
gc: new, less strict bool rules
R=ken2
CC=golang-dev
https://golang.org/cl/5688064
変更の背景
Go言語の初期のバージョンでは、比較演算子(例: ==
, !=
, <
, >
)の結果は「理想的なブール型(idealbool
)」として扱われました。このidealbool
は、他の理想的な型(idealint
, idealfloat
など)と同様に、具体的な型が決定されるまで型が確定しない特殊な型でした。しかし、idealbool
は、bool
型への暗黙的な変換が厳しく制限されており、特にユーザー定義のbool
型(例: type MyBool bool
)への代入や、interface{}
型への代入において、明示的な型変換が必要となる場面がありました。
この厳格なルールは、開発者にとって不便であり、直感的ではないと認識されていました。例えば、1 != 2
のような比較の結果を直接MyBool
型の変数に代入しようとすると、コンパイルエラーが発生していました。このコミットは、このような不便さを解消し、Go言語の型システムにおけるブール型の扱いをより柔軟で、かつ直感的にするために導入されました。具体的には、idealbool
がより早い段階で具体的なbool
型に解決されるように、コンパイラの型チェックロジックが変更されました。
前提知識の解説
Go言語の型システムと「理想的な型(Ideal Types)」
Go言語の型システムには、数値リテラルや比較演算の結果など、特定のコンテキストで一時的に型が確定しない「理想的な型(Untyped Constants / Ideal Types)」という概念があります。これらは、idealint
(整数リテラル)、idealfloat
(浮動小数点リテラル)、idealstring
(文字列リテラル)、そしてこのコミットで焦点となるidealbool
(比較演算の結果など)などがあります。
- 理想的な型(Untyped Constants): これらの型は、コンパイル時に具体的な型が決定されるまで、その値の範囲内で最も広い型として振る舞います。例えば、
100
という整数リテラルは、int
,int8
,int16
,int32
,int64
など、代入される変数の型に応じて適切な型に解決されます。 idealbool
: 比較演算(例:a == b
,x < y
)の結果は、当初idealbool
として扱われました。これは、true
またはfalse
という値を持つものの、まだ具体的なbool
型に解決されていない状態を指します。
Goコンパイラ(gc
)の主要なフェーズ
Goコンパイラ(gc
)は、ソースコードを機械語に変換する過程で、いくつかの主要なフェーズを経ます。このコミットで変更が加えられているのは、主に以下のフェーズに関連する部分です。
- パーシング(Parsing): ソースコードを抽象構文木(AST)に変換します。
- 型チェック(Type Checking): ASTを走査し、各ノードの型を決定し、型の一貫性を検証します。このフェーズで、理想的な型が具体的な型に解決されることがよくあります。
src/cmd/gc/typecheck.c
: 型チェックの主要なロジックが含まれています。
- ウォーク(Walk): 型チェックが完了した後、ASTを最適化し、バックエンドでのコード生成に適した形に変換します。このフェーズでは、ASTノードの変換や簡略化が行われます。
src/cmd/gc/walk.c
: ウォークフェーズのロジックが含まれています。
- 定数畳み込み(Constant Folding): コンパイル時に計算可能な定数式を評価し、その結果で置き換えます。
src/cmd/gc/const.c
: 定数畳み込みに関連するロジックが含まれています。
- サブルーチン(Subroutines): コンパイラの様々なフェーズで利用されるユーティリティ関数や共通ロジックが含まれています。
src/cmd/gc/subr.c
: 型変換や代入に関するヘルパー関数などが含まれています。
このコミットは、これらのフェーズにおけるidealbool
の扱いを変更することで、より柔軟な型変換を実現しています。
技術的詳細
このコミットの核心は、Goコンパイラがidealbool
型を扱う方法を変更し、より早い段階で具体的なbool
型に解決するようにした点にあります。これにより、比較演算の結果がより広範なコンテキストで直接利用できるようになります。
具体的な変更点は以下のファイルに分散しています。
-
src/cmd/gc/const.c
:convlit1
関数において、idealbool
型のノードがTBOOL
型に変換されるロジックが追加されました。これは、リテラル変換の初期段階でidealbool
を具体的なbool
型に「昇格」させることを意味します。defaultlit
関数において、比較演算子(iscmp[n->op]
)の結果であるidealbool
のデフォルト型解決ロジックが変更されました。特に、ターゲット型が指定されている場合、idealbool
はそのターゲット型(TBOOL
)に解決されるようになりました。defaultlit2
関数において、ブール型のリテラル変換が強制される場合に、両辺がTBOOL
型に変換されるロジックが追加されました。
-
src/cmd/gc/subr.c
:assignconv
関数に、idealbool
がTBOOL
以外の型(例:interface{}
)に代入される場合に、idealbool
を明示的にTBOOL
に変換するロジックが追加されました。これは、idealbool
がインターフェース型に代入される際に、その「理想的な」状態ではなく、具体的なbool
型として扱われるようにするためです。
-
src/cmd/gc/typecheck.c
:typecheck
関数内で、比較演算の結果の型がTBOOL
からidealbool
に変更されました。これは、比較演算の結果がすぐに具体的なbool
型に解決されるのではなく、一旦idealbool
として保持され、その後のコンテキストで型が解決されるという新しいフローを示しています。OPRINTN
(print
組み込み関数)の型チェックにおいて、引数が整数定数の場合、TINT64
にデフォルト解決されるように変更されました。これはブール型とは直接関係ありませんが、型解決ロジックの一般的な改善の一部です。typecheckdef
関数に、OLITERAL
ではないノードで、かつT
(不明な型)ではないideal
型が残っている場合にfatal
エラーを発生させるチェックが追加されました。これは、ideal
型が最終的に具体的な型に解決されることを保証するためのデバッグ/整合性チェックです。
-
src/cmd/gc/walk.c
:walkexpr
関数内で、比較演算の結果のノード(n
)の型がTBOOL
ではない場合にfatal
エラーを発生させるチェックが追加されました。これは、ウォークフェーズに入る前には比較演算の結果がTBOOL
に解決されていることを保証するためのものです。OOROR
(論理OR)のウォーク処理において、結果のノードの型が元のノードの型(n->type
)に設定されるようになりました。これは、型情報が正しく伝播されるようにするための修正です。
これらの変更により、Goコンパイラはidealbool
をより柔軟に扱い、特に比較演算の結果がユーザー定義のブール型やinterface{}
型に代入される際の不必要なエラーを排除し、開発者の利便性を向上させています。
コアとなるコードの変更箇所
src/cmd/gc/const.c
--- a/src/cmd/gc/const.c
+++ b/src/cmd/gc/const.c
@@ -87,6 +87,8 @@ convlit1(Node **np, Type *t, int explicit)\n
switch(n->op) {
default:
+ if(n->type == idealbool)
+ n->type = types[TBOOL];
if(n->type->etype == TIDEAL) {
convlit(&n->left, t);
convlit(&n->right, t);
@@ -1010,6 +1012,10 @@ defaultlit(Node **np, Type *t)\n }\n n->type = t;\n return;\n+ case ONOT:\n+ defaultlit(&n->left, t);\n+ n->type = n->left->type;\n+ return;\n default:\n if(n->left == N) {
dump("defaultlit", n);
@@ -1029,13 +1035,18 @@ defaultlit(Node **np, Type *t)\n } else if(t == T && (n->left->op == OLSH || n->left->op == ORSH)) {\n defaultlit(&n->right, T);\n defaultlit(&n->left, n->right->type);\n+ } else if(iscmp[n->op]) {\n+ defaultlit2(&n->left, &n->right, 1);\
} else {\n defaultlit(&n->left, t);\n defaultlit(&n->right, t);\n }\n- if(n->type == idealbool || n->type == idealstring)\n- n->type = types[n->type->etype];
- else
+ if(n->type == idealbool || n->type == idealstring) {\n+ if(t != T && t->etype == n->type->etype)\n+ n->type = t;\n+ else\n+ n->type = types[n->type->etype];
+ } else
n->type = n->left->type;\n return;\n }\n@@ -1124,6 +1135,10 @@ defaultlit2(Node **lp, Node **rp, int force)\n }\n if(!force)\n return;\n+ if(l->type->etype == TBOOL) {\n+ convlit(lp, types[TBOOL]);\n+ convlit(rp, types[TBOOL]);\n+ }\n if(isconst(l, CTCPLX) || isconst(r, CTCPLX)) {\n convlit(lp, types[TCOMPLEX128]);\n convlit(rp, types[TCOMPLEX128]);
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1354,6 +1354,18 @@ assignconv(Node *n, Type *t, char *context)\n if(t->etype == TBLANK)
return n;
+ // Convert ideal bool from comparison to plain bool
+ // if the next step is non-bool (like interface{}).
+ if(n->type == idealbool && t->etype != TBOOL) {
+ if(n->op == ONAME || n->op == OLITERAL) {
+ r = nod(OCONVNOP, n, N);
+ r->type = types[TBOOL];
+ r->typecheck = 1;
+ r->implicit = 1;
+ n = r;
+ }
+ }
+
if(eqtype(n->type, t))
return n;
src/cmd/gc/typecheck.c
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -526,7 +526,7 @@ reswitch:\n t = l->type;\n if(iscmp[n->op]) {\n evconst(n);\n- t = types[TBOOL];
+ t = idealbool;
if(n->op != OLITERAL) {
defaultlit2(&l, &r, 1);
n->left = l;
@@ -1317,6 +1317,13 @@ reswitch:\n case OPRINTN:\n ok |= Etop;\n typechecklist(n->list, Erv | Eindir); // Eindir: address does not escape\n+ for(args=n->list; args; args=args->next) {\n+ // Special case for print: int constant is int64, not int.\n+ if(isconst(args->n, CTINT))\n+ defaultlit(&args->n, types[TINT64]);\n+ else\n+ defaultlit(&args->n, T);\n+ }\n goto ret;\n
case OPANIC:\
@@ -2887,6 +2894,8 @@ typecheckdef(Node *n)\n }\n
ret:\n+ if(n->op != OLITERAL && n->type != T && isideal(n->type))\n+ fatal("got %T for %N", n->type, n);\n if(typecheckdefstack->n != n)\n fatal("typecheckdefstack mismatch");\n l = typecheckdefstack;
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1055,6 +1055,8 @@ walkexpr(Node **np, NodeList **init)\n walkexpr(&r, nil);\n }\n typecheck(&r, Erv);\n+ if(n->type->etype != TBOOL) fatal("cmp %T", n->type);\n+ r->type = n->type;\n n = r;\n goto ret;\n
@@ -1190,7 +1192,7 @@ walkexpr(Node **np, NodeList **init)\n r = nod(OOROR, nod(ONE, nod(OITAB, n->left, N), nod(OITAB, n->right, N)), r);\n typecheck(&r, Erv);\n walkexpr(&r, nil);\n-\n+\t\tr->type = n->type;\n n = r;\n goto ret;\n```
### `test/named1.go`
```diff
--- a/test/named1.go
+++ b/test/named1.go
@@ -37,8 +37,8 @@ func main() {
asBool(true)
asBool(*&b)
asBool(Bool(true))
- asBool(1 != 2) // ERROR "cannot use.*type bool.*as type Bool"
- asBool(i < j) // ERROR "cannot use.*type bool.*as type Bool"
+ asBool(1 != 2) // ok now
+ asBool(i < j) // ok now
_, b = m[2] // ERROR "cannot .* bool.*type Bool"
コアとなるコードの解説
src/cmd/gc/const.c
の変更
-
convlit1
におけるidealbool
の即時変換:+ if(n->type == idealbool) + n->type = types[TBOOL];
この変更は、リテラル変換の初期段階で、ノードの型が
idealbool
であれば、すぐに具体的なTBOOL
型に変換することを意味します。これにより、idealbool
がより早い段階で具体的な型に解決され、後続の型チェックや代入処理で柔軟性が増します。 -
defaultlit
における比較演算子の結果の型解決:+ } else if(iscmp[n->op]) { + defaultlit2(&n->left, &n->right, 1); } else { defaultlit(&n->left, t); defaultlit(&n->right, t); } - if(n->type == idealbool || n->type == idealstring) - n->type = types[n->type->etype]; - else + if(n->type == idealbool || n->type == idealstring) { + if(t != T && t->etype == n->type->etype) + n->type = t; + else + n->type = types[n->type->etype]; + } else n->type = n->left->type;
比較演算子(
iscmp[n->op]
)の場合にdefaultlit2
を呼び出すことで、比較の両辺の型解決を強制します。また、idealbool
またはidealstring
の場合の型解決ロジックが変更され、ターゲット型t
が指定されていればその型に解決し、そうでなければデフォルトの具体的な型(types[n->type->etype]
、つまりTBOOL
またはTSTRING
)に解決するようにしました。これにより、idealbool
がより適切なタイミングで具体的な型に解決されるようになります。 -
defaultlit2
におけるブール型の強制変換:+ if(l->type->etype == TBOOL) { + convlit(lp, types[TBOOL]); + convlit(rp, types[TBOOL]); + }
defaultlit2
は、2つのノードの型をデフォルト型に解決する関数です。この変更により、左辺の型がTBOOL
であれば、両辺を明示的にTBOOL
に変換するconvlit
が呼び出されます。これは、ブール型の比較において、両辺が確実にbool
型として扱われるようにするためのものです。
src/cmd/gc/subr.c
の変更
assignconv
におけるidealbool
からTBOOL
への変換:
このコードは、+ // Convert ideal bool from comparison to plain bool + // if the next step is non-bool (like interface{}). + if(n->type == idealbool && t->etype != TBOOL) { + if(n->op == ONAME || n->op == OLITERAL) { + r = nod(OCONVNOP, n, N); + r->type = types[TBOOL]; + r->typecheck = 1; + r->implicit = 1; + n = r; + } + }
idealbool
型のノードが、TBOOL
型ではない他の型(特にinterface{}
のような型)に代入される場合に適用されます。idealbool
がONAME
(変数)またはOLITERAL
(リテラル)である場合、OCONVNOP
(変換操作)ノードを挿入し、その型をTBOOL
に設定します。これにより、idealbool
がinterface{}
などに代入される際に、具体的なbool
値としてラップされるようになり、以前のようなコンパイルエラーが回避されます。
src/cmd/gc/typecheck.c
の変更
-
比較演算の結果の型を
idealbool
に変更:- t = types[TBOOL]; + t = idealbool;
以前は比較演算の結果が直接
TBOOL
に設定されていましたが、この変更により、一旦idealbool
として保持されるようになりました。これにより、idealbool
がより柔軟な型解決の機会を持つことができます。 -
typecheckdef
におけるideal
型の最終チェック:+ if(n->op != OLITERAL && n->type != T && isideal(n->type)) + fatal("got %T for %N", n->type, n);
この追加されたチェックは、型チェックの最終段階で、リテラルではないノードがまだ
ideal
型として残っている場合に、コンパイラを異常終了させます。これは、すべてのideal
型が最終的に具体的な型に解決されるべきであるというコンパイラの整合性を保証するためのデバッグ/検証メカニズムです。
src/cmd/gc/walk.c
の変更
-
比較演算の結果の型が
TBOOL
であることを保証:+ if(n->type->etype != TBOOL) fatal("cmp %T", n->type); + r->type = n->type;
ウォークフェーズでは、型チェックが完了していることを前提としています。このチェックは、比較演算の結果のノードの型が
TBOOL
であることを確認し、もしそうでなければ致命的なエラーを発生させます。これは、型チェックフェーズでidealbool
が正しくTBOOL
に解決されたことを保証するためのものです。また、結果のノードr
の型を元のノードn
の型に設定することで、型情報が正しく伝播されるようにします。 -
OOROR
(論理OR)の結果の型設定:-\n+\t\tr->type = n->type;
論理OR演算の結果のノード
r
の型を、元のノードn
の型に設定するように変更されました。これも、ウォークフェーズにおける型情報の正確な伝播を保証するための修正です。
test/named1.go
の変更
- asBool(1 != 2) // ERROR "cannot use.*type bool.*as type Bool"
- asBool(i < j) // ERROR "cannot use.*type bool.*as type Bool"
+ asBool(1 != 2) // ok now
+ asBool(i < j) // ok now
このテストファイルの変更は、上記のコンパイラ変更が意図通りに機能していることを示しています。以前はidealbool
をユーザー定義のBool
型に直接代入しようとするとエラーになっていましたが、コンパイラのルール緩和により、これが許容されるようになりました。これにより、Go言語のブール型の利用がより柔軟になったことが確認できます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/e29d3dfc49f7142d87ab71bd1d8d04e129972dd5
- Go Code Review CL 5688064: https://golang.org/cl/5688064
参考にした情報源リンク
- Go Code Review CL 5688064: https://golang.org/cl/5688064
- Go言語の型システムに関する一般的な情報(理想的な型、型推論など)
- Goコンパイラ(
gc
)の内部構造に関する一般的な情報(型チェック、ウォークフェーズなど)