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

[インデックス 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)は、ソースコードを機械語に変換する過程で、いくつかの主要なフェーズを経ます。このコミットで変更が加えられているのは、主に以下のフェーズに関連する部分です。

  1. パーシング(Parsing): ソースコードを抽象構文木(AST)に変換します。
  2. 型チェック(Type Checking): ASTを走査し、各ノードの型を決定し、型の一貫性を検証します。このフェーズで、理想的な型が具体的な型に解決されることがよくあります。
    • src/cmd/gc/typecheck.c: 型チェックの主要なロジックが含まれています。
  3. ウォーク(Walk): 型チェックが完了した後、ASTを最適化し、バックエンドでのコード生成に適した形に変換します。このフェーズでは、ASTノードの変換や簡略化が行われます。
    • src/cmd/gc/walk.c: ウォークフェーズのロジックが含まれています。
  4. 定数畳み込み(Constant Folding): コンパイル時に計算可能な定数式を評価し、その結果で置き換えます。
    • src/cmd/gc/const.c: 定数畳み込みに関連するロジックが含まれています。
  5. サブルーチン(Subroutines): コンパイラの様々なフェーズで利用されるユーティリティ関数や共通ロジックが含まれています。
    • src/cmd/gc/subr.c: 型変換や代入に関するヘルパー関数などが含まれています。

このコミットは、これらのフェーズにおけるidealboolの扱いを変更することで、より柔軟な型変換を実現しています。

技術的詳細

このコミットの核心は、Goコンパイラがidealbool型を扱う方法を変更し、より早い段階で具体的なbool型に解決するようにした点にあります。これにより、比較演算の結果がより広範なコンテキストで直接利用できるようになります。

具体的な変更点は以下のファイルに分散しています。

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

    • convlit1関数において、idealbool型のノードがTBOOL型に変換されるロジックが追加されました。これは、リテラル変換の初期段階でidealboolを具体的なbool型に「昇格」させることを意味します。
    • defaultlit関数において、比較演算子(iscmp[n->op])の結果であるidealboolのデフォルト型解決ロジックが変更されました。特に、ターゲット型が指定されている場合、idealboolはそのターゲット型(TBOOL)に解決されるようになりました。
    • defaultlit2関数において、ブール型のリテラル変換が強制される場合に、両辺がTBOOL型に変換されるロジックが追加されました。
  2. src/cmd/gc/subr.c:

    • assignconv関数に、idealboolTBOOL以外の型(例: interface{})に代入される場合に、idealboolを明示的にTBOOLに変換するロジックが追加されました。これは、idealboolがインターフェース型に代入される際に、その「理想的な」状態ではなく、具体的なbool型として扱われるようにするためです。
  3. src/cmd/gc/typecheck.c:

    • typecheck関数内で、比較演算の結果の型がTBOOLからidealboolに変更されました。これは、比較演算の結果がすぐに具体的なbool型に解決されるのではなく、一旦idealboolとして保持され、その後のコンテキストで型が解決されるという新しいフローを示しています。
    • OPRINTNprint組み込み関数)の型チェックにおいて、引数が整数定数の場合、TINT64にデフォルト解決されるように変更されました。これはブール型とは直接関係ありませんが、型解決ロジックの一般的な改善の一部です。
    • typecheckdef関数に、OLITERALではないノードで、かつT(不明な型)ではないideal型が残っている場合にfatalエラーを発生させるチェックが追加されました。これは、ideal型が最終的に具体的な型に解決されることを保証するためのデバッグ/整合性チェックです。
  4. 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{}のような型)に代入される場合に適用されます。idealboolONAME(変数)またはOLITERAL(リテラル)である場合、OCONVNOP(変換操作)ノードを挿入し、その型をTBOOLに設定します。これにより、idealboolinterface{}などに代入される際に、具体的な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言語のブール型の利用がより柔軟になったことが確認できます。

関連リンク

参考にした情報源リンク

  • Go Code Review CL 5688064: https://golang.org/cl/5688064
  • Go言語の型システムに関する一般的な情報(理想的な型、型推論など)
  • Goコンパイラ(gc)の内部構造に関する一般的な情報(型チェック、ウォークフェーズなど)