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

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

このコミットは、Goコンパイラのcmd/gcにおけるwalkcompare関数内のバグ修正に関するものです。具体的には、以前のコミット(リビジョン c0e0467635ec)によって引き起こされた、一時変数(temporary)の型推論の誤り(ideal typeとして扱われる問題)と、比較結果をカスタムのブーリアン型に代入する際の破損を修正しています。これにより、構造体の比較やマップの要素比較など、特定の比較操作が正しく機能するようになります。

コミット

commit 14b0af4272aa6c638e97cb3364a81962d69dbfc6
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Feb 24 19:51:59 2014 +0100

    cmd/gc: fix walkcompare bugs.
    
    Revision c0e0467635ec (cmd/gc: return canonical Node* from temp)
    exposed original nodes of temporaries, allowing callers to mutate
    their types.
    
    In walkcompare a temporary could be typed as ideal because of
    this. Additionnally, assignment of a comparison result to
    a custom boolean type was broken.
    
    Fixes #7366.
    
    LGTM=rsc
    R=rsc, iant, khr
    CC=golang-codereviews
    https://golang.org/cl/66930044

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

https://github.com/golang/go/commit/14b0af4272aa6c638e97cb3364a81962d69dbfc6

元コミット内容

このコミットが修正しているバグは、リビジョン c0e0467635ec(コミットメッセージ: cmd/gc: return canonical Node* from temp)によって露呈しました。この元のコミットは、一時変数(temporary)から「canonical Node*」を返すように変更しました。ここでいう「canonical Node*」とは、コンパイラ内部で一時変数を表現する際に、その一時変数が元々参照していた元のノード(式や変数など)を指すポインタを返すようになったことを意味します。

この変更の意図は、コンパイラが一時変数をより効率的に、かつ正確に扱うためであったと考えられます。しかし、その副作用として、呼び出し元が一時変数の元のノードを介してその型を誤って変更できてしまうという問題を引き起こしました。

変更の背景

Goコンパイラは、コードの最適化や中間表現の変換を行う過程で、一時的な変数(temporary variables)を生成することがよくあります。これらの変数は、複雑な式の中間結果を保持したり、特定の操作のために一時的なストレージを提供したりするために使用されます。

元のコミット c0e0467635ec は、一時変数の内部表現を改善し、その「canonical Node*」を返すようにしました。これは、コンパイラが一時変数をより一貫性のある方法で扱うことを目的としていましたが、意図しない副作用として、一時変数の元のノードが外部に「露出」する形となりました。

この露出により、walkcompare関数内で以下の2つの問題が発生しました。

  1. 一時変数の型がidealになる問題: walkcompare関数は、比較操作(==!=など)を処理するGoコンパイラの重要な部分です。この関数内で一時変数が生成される際、元のコミットの変更により、その一時変数の型が誤って「ideal type」(Goにおける型推論の初期段階で使われる、具体的な型がまだ決定されていない数値型など)として扱われる可能性が生じました。これは、型チェックの段階で問題を引き起こし、コンパイルエラーや不正なコード生成につながる可能性があります。
  2. カスタムブーリアン型への代入の破損: Goでは、type MyBool boolのように基底型がboolであるカスタムのブーリアン型を定義できます。walkcompareが生成する比較結果(これは通常bool型)を、このようなカスタムブーリアン型に代入しようとすると、コンパイラが正しく処理できず、バグが発生していました。これは、型変換(conversion)のロジックが、一時変数の型が誤って扱われることによって影響を受けたためと考えられます。

これらの問題は、GoのIssue #7366として報告され、このコミットによって修正されました。

前提知識の解説

このコミットを理解するためには、Goコンパイラの内部構造と、特にcmd/gc(Goコンパイラのフロントエンド)がどのように動作するかについての基本的な知識が必要です。

  • cmd/gc: Go言語の公式コンパイラのフロントエンド部分です。ソースコードのパース、型チェック、中間表現(IR)の生成、最適化、コード生成など、コンパイルプロセスの大部分を担います。
  • Node*: Goコンパイラ内部で、プログラムの抽象構文木(AST)や中間表現の各要素(式、ステートメント、型など)を表すデータ構造へのポインタです。コンパイラはこれらのNodeを操作してコードを変換します。
  • walk関数群: cmd/gcには、ASTやIRを走査(walk)し、変換や最適化を行うための関数群があります。walkcompareもその一つで、比較演算子(==, !=など)を含む式を処理します。
  • typecheck: コンパイラの型チェックフェーズで呼び出される関数です。式の型を決定し、型の一貫性を検証します。Goは静的型付け言語であるため、このステップは非常に重要です。
  • walkexpr: 式を走査し、必要に応じて最適化やコード生成のための準備を行う関数です。
  • Erv: typecheck関数に渡される引数の一つで、式の評価コンテキストを示します。Ervは「expression value」を意味し、式が値として評価されることを示します。
  • N: コンパイラ内部で使われる、nilに相当する特別なNodeポインタです。
  • OEQ / ONE: コンパイラ内部で比較演算子を表すオペレーションコードです。OEQは等価(==)、ONEは不等価(!=)を表します。
  • TSTRUCT: コンパイラ内部で構造体型を表す型コードです。
  • nodbool: ブーリアンリテラル(trueまたはfalse)を表すNodeを生成する関数です。
  • OCONVNOP: コンパイラ内部で「no-op conversion」(何もしない型変換)を表すオペレーションコードです。これは、型は異なるが、値の表現は同じである場合に、コンパイラが型チェックを通過させるために使用されます。例えば、基底型が同じカスタム型と組み込み型の間などで使われます。
  • ideal type: Goの型システムにおける概念で、具体的な型がまだ決定されていない数値リテラル(例: 1003.14)などが持つ「型」です。これらのリテラルは、使用される文脈によってintfloat64など、異なる具体的な型に推論されます。一時変数が誤ってideal typeとして扱われると、型推論のロジックが混乱し、問題を引き起こす可能性があります。
  • canonical Node*: コンパイラ内部で、あるエンティティ(例えば一時変数)が参照する、そのエンティティの「真の」または「標準的な」表現を指すポインタ。
  • temporary (一時変数): コンパイラが内部的に生成する、ソースコードには直接現れない変数。中間計算結果の保持などに使われます。
  • custom boolean type: type MyBool boolのように、boolを基底型とするユーザー定義型。

技術的詳細

このコミットの技術的詳細は、src/cmd/gc/walk.c内のwalkcompare関数の変更に集約されています。

walkcompare関数は、Goの比較演算子(==!=)を処理する際に呼び出されます。この関数は、比較されるオペランドの型や性質(例えば、構造体、インターフェース、ポインタなど)に基づいて、比較をどのように実装するかを決定します。

元のコードでは、比較結果を表すexprノードが生成された後、以下の処理が行われていました。

// 構造体やその他の型の比較ロジックの後に共通して存在したパターン
typecheck(&expr, Erv); // 型チェック
walkexpr(&expr, init); // 式の走査
expr->type = n->type;  // 比較結果の型を元の比較式の型に設定
*np = expr;            // 結果ノードを更新
return;

このパターンは、walkcompareの複数の箇所(例えば、インライン化された比較や、memequalのようなヘルパー関数を呼び出す比較)で繰り返されていました。

問題は、リビジョンc0e0467635ecが一時変数の「canonical Node*」を返すように変更したことで、exprが一時変数の元のノードを指すようになり、その結果、expr->type = n->type;という行が、一時変数の型を誤って上書きしてしまう可能性が生じたことです。特に、一時変数がideal typeとして生成された場合、この上書きによって型システムが混乱し、後続の処理で問題が発生しました。

また、比較結果をカスタムブーリアン型に代入する際に問題が発生したのは、expr->type = n->type;の行が、比較結果の型を強制的に元の比較式の型(例えば、T型の構造体同士の比較であればT型)に設定しようとしたためと考えられます。しかし、比較結果は常にブーリアン値であるべきであり、カスタムブーリアン型への代入には、明示的または暗黙的な型変換が必要になります。この不整合が、カスタムブーリアン型への代入を破損させました。

このコミットでは、これらの問題を解決するために、goto ret;という新しい制御フローと、OCONVNOPという新しいノードタイプを導入しています。

変更の核心は、比較結果のrノードが最終的に決定された後、共通のretラベルにジャンプするようにしたことです。retラベルでは、以下の処理が行われます。

ret:
	typecheck(&r, Erv); // 結果ノードの型チェック
	walkexpr(&r, init); // 結果ノードの走査
	if(r->type != n->type) { // 結果ノードの型が元の比較式の型と異なる場合
		r = nod(OCONVNOP, r, N); // OCONVNOPノードを挿入
		r->type = n->type;       // OCONVNOPノードの型を元の比較式の型に設定
		r->typecheck = 1;        // 型チェック済みとしてマーク
	}
	*np = r; // 結果ノードを更新
	return;

この変更により、以下の点が改善されました。

  1. 型の上書きの防止: expr->type = n->type;のような直接的な型の上書きが削除されました。代わりに、typecheck(&r, Erv);walkexpr(&r, init);がまず実行され、rノードの型が正しく推論・処理されます。
  2. カスタムブーリアン型への対応: if(r->type != n->type)のチェックが追加されました。ここでn->typeは元の比較式全体の期待される型(例えば、b = x == yという式におけるbの型、つまりmybool)を指します。もし比較結果rの型(通常はbool)がn->typeと異なる場合、OCONVNOPノードが挿入されます。OCONVNOPは「no-op conversion」を意味し、コンパイラが型は異なるが互換性のある型(例: boolmybool)の間で、値の表現を変えずに型チェックを通過させるためのメカニズムです。これにより、比較結果をカスタムブーリアン型に正しく代入できるようになりました。
  3. コードの重複排除: 複数の箇所で繰り返されていた型チェックと走査のロジックがretラベルに集約され、コードの重複が減り、保守性が向上しました。

test/cmp.goに追加されたテストケースは、カスタムブーリアン型への比較結果の代入が正しく行われることを検証しています。また、test/fixedbugs/issue7366.goは、小さな構造体の比較時に一時変数がideal typeとして生成されるバグが修正されたことを確認するためのコンパイルテストです。

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

src/cmd/gc/walk.cwalkcompare関数が主な変更箇所です。

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -3171,13 +3171,10 @@ walkcompare(Node **np, NodeList **init)
 		}
 		if(expr == N)
 			expr = nodbool(n->op == OEQ);
-		typecheck(&expr, Erv);
-		walkexpr(&expr, init);
-		expr->type = n->type;
-		*np = expr;
-		return;
+		r = expr;
+		goto ret;
 	}
-	
+
 	if(t->etype == TSTRUCT && countfield(t) <= 4) {
 		// Struct of four or fewer fields.
 		// Inline comparisons.
@@ -3194,13 +3191,10 @@ walkcompare(Node **np, NodeList **init)
 		}
 		if(expr == N)
 			expr = nodbool(n->op == OEQ);
-		typecheck(&expr, Erv);
-		walkexpr(&expr, init);
-		expr->type = n->type;
-		*np = expr;
-		return;
+		r = expr;
+		goto ret;
 	}
-	
+
 	// Chose not to inline, but still have addresses.
 	// Call equality function directly.
 	// The equality function requires a bool pointer for
@@ -3233,10 +3227,7 @@ walkcompare(Node **np, NodeList **init)
 
 	if(n->op != OEQ)
 		r = nod(ONOT, r, N);
-	typecheck(&r, Erv);
-	walkexpr(&r, init);
-	*np = r;
-	return;
+	goto ret;
 
 hard:
 	// Cannot take address of one or both of the operands.
@@ -3252,7 +3243,16 @@ hard:
 	r = mkcall1(fn, n->type, init, typename(n->left->type), l, r);
 	if(n->op == ONE) {
 		r = nod(ONOT, r, N);
-		typecheck(&r, Erv);
+	}
+	goto ret;
+
+ret:
+	typecheck(&r, Erv);
+	walkexpr(&r, init);
+	if(r->type != n->type) {
+		r = nod(OCONVNOP, r, N);
+		r->type = n->type;
+		r->typecheck = 1;
 	}
 	*np = r;
 	return;

コアとなるコードの解説

変更の主要なパターンは、typecheck(&expr, Erv); walkexpr(&expr, init); expr->type = n->type; *np = expr; return;という一連の処理が、r = expr; goto ret;またはgoto ret;に置き換えられたことです。

  1. r = expr; goto ret;:

    • exprは比較結果を表すノードです。このノードを一時変数rに代入します。
    • goto ret;によって、関数の末尾近くに新しく追加されたret:ラベルに制御が移ります。これにより、以前は各比較ロジックの分岐で個別に実行されていた型チェックと走査の処理が、一元化された共通の場所で実行されるようになります。
  2. goto ret;:

    • rが既に比較結果のノードを指している場合(例えば、ONOTノードが生成された後など)は、直接retラベルにジャンプします。
  3. ret:ラベル内の新しいロジック:

    • typecheck(&r, Erv);: 最終的な比較結果ノードrに対して型チェックを実行します。これにより、ideal typeの問題が修正され、rの型が正しく確定されます。
    • walkexpr(&r, init);: rノードを走査し、必要に応じてさらに変換や最適化を行います。
    • if(r->type != n->type) { ... }: ここがカスタムブーリアン型への代入問題を解決する鍵です。
      • r->typeは、typecheckwalkexprによって確定された比較結果の実際の型(通常はbool)です。
      • n->typeは、元の比較式全体が期待される型です。例えば、var b mybool; b = x == y;というコードの場合、n->typemybool型になります。
      • もしこれら2つの型が異なる場合(例: boolmybool)、コンパイラは明示的な型変換が必要であると判断します。
      • r = nod(OCONVNOP, r, N);: ここでOCONVNOP(no-op conversion)ノードが挿入されます。これは、rノードの値を変更せずに、その型をn->typeに「変換」するためのコンパイラ内部の指示です。これにより、bool型の比較結果がmybool型として扱われるようになり、カスタムブーリアン型への代入が正しく行われます。
      • r->type = n->type;: 新しく作成されたOCONVNOPノードの型を、期待されるn->typeに設定します。
      • r->typecheck = 1;: このノードが型チェック済みであることをマークします。
    • *np = r;: 最終的に処理されたrノードを、呼び出し元が期待する結果ノードポインタ*npに設定します。

この変更により、walkcompareはより堅牢になり、一時変数の型推論の誤りや、カスタムブーリアン型への代入に関するバグが修正されました。

関連リンク

  • Go CL 66930044: https://golang.org/cl/66930044
  • Go Issue #7366: https://golang.org/issue/7366

参考にした情報源リンク

  • Goコンパイラのソースコード(特にsrc/cmd/gc/walk.c
  • Go言語のIssueトラッカー
  • Go言語の型システムに関するドキュメント(ideal typeなど)
  • Goコンパイラの内部構造に関する一般的な情報(AST, IR, Nodeなど)