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

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

このコミットは、Goコンパイラの型変換ロジックにおけるバグ修正を目的としています。具体的には、src/cmd/gc/const.c 内の convlit 関数が convlit1 にリファクタリングされ、型変換の挙動を制御するための新しい引数 conv が導入されました。これにより、特定のコンテキストでの不適切な文字列リテラル変換が防止されます。

コミット

commit e683fb7a54ab60530e8c1e06c416cc646dbf519b
Author: Ken Thompson <ken@golang.org>
Date:   Fri Dec 12 13:10:36 2008 -0800

    bug104
    
    R=r
    OCL=21082
    CL=21082
---
 src/cmd/gc/const.c | 42 ++++++++++++++++++++++++++----------------
 src/cmd/gc/go.h    |  1 +
 src/cmd/gc/walk.c  |  2 +-\
 3 files changed, 28 insertions(+), 17 deletions(-)

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

https://github.com/golang/go/commit/e683fb7a54ab60530e8c1e06c416cc646dbf519b

元コミット内容

bug104

変更の背景

このコミットは、Goコンパイラにおける「bug104」として知られる問題を修正するために行われました。Goコンパイラは、ソースコードを機械語に変換する過程で、様々な型変換(型キャストやリテラルの型推論など)を行います。特に、数値リテラルから文字列への変換は、Go言語の仕様上、特定の文脈でのみ許容されるべき挙動です。

元の実装では、convlit 関数がリテラルの型変換を処理していましたが、この関数が呼び出される全てのコンテキストにおいて、数値リテラルから文字列への変換が常に試みられてしまう可能性がありました。これにより、コンパイラが予期しない型変換を行い、結果として不正なコード生成やコンパイルエラーを引き起こす「bug104」が発生していたと考えられます。

このバグは、Go言語の初期開発段階で発見されたものであり、コンパイラの堅牢性と正確性を確保するために修正が必要でした。特に、Go言語が静的型付け言語である以上、型変換のルールは厳密に適用されるべきであり、コンパイラはそのルールを正確に反映する必要があります。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの内部構造とGo言語の基本的な型システムに関する知識が必要です。

  • Goコンパイラ (gc): Go言語の公式コンパイラは、歴史的に gc (Go Compiler) と呼ばれるツールチェーンの一部です。これは、ソースコードの解析、抽象構文木 (AST) の構築、型チェック、中間コード生成、最適化、最終的な機械語コード生成といった一連のコンパイルプロセスを担当します。
  • 抽象構文木 (AST): ソースコードは、コンパイラによって解析され、プログラムの構造を木構造で表現したASTに変換されます。コンパイラの各フェーズ(型チェック、最適化など)はこのASTを走査(walk)しながら処理を進めます。
  • リテラル: プログラム中に直接記述される値のことです。例えば、123 (整数リテラル)、3.14 (浮動小数点リテラル)、"hello" (文字列リテラル) などがあります。
  • 型変換 (Type Conversion): ある型の値を別の型の値に変換することです。Go言語では、明示的な型変換(例: int(f))と、コンパイラが自動的に行う暗黙的な型変換(例: 定数リテラルの型推論)があります。
  • Node 構造体: Goコンパイラの内部でASTの各ノードを表すデータ構造です。Node には、そのノードが表す式や文の種類、型情報、値などが含まれます。
  • Type 構造体: Goコンパイラの内部で型情報を表すデータ構造です。
  • Mpflt, Mpint: Goコンパイラが内部で多倍長浮動小数点数や多倍長整数を扱うための構造体です。リテラル値はこれらの形式で表現されることがあります。
  • Rune: Go言語におけるUnicodeコードポイントを表す型(int32 のエイリアス)。文字列はルーンのシーケンスとして扱われます。
  • String 構造体: Goコンパイラが内部で文字列を表現するための構造体。

このコミットは、特に convlit 関数がリテラル(特に数値リテラル)を他の型に変換する際の挙動に焦点を当てています。Go言語では、整数リテラルはデフォルトで型を持たず、使用される文脈によって適切な型に推論されます。しかし、string(10) のように整数を明示的に文字列に変換する構文も存在します。このコミットは、このような明示的な変換と、それ以外の文脈での変換を区別するためのものです。

技術的詳細

このコミットの主要な変更点は、src/cmd/gc/const.c にある convlit 関数のリファクタリングです。

  1. convlit から convlit1 へのリネームと引数追加: 元の void convlit(Node *n, Type *t) 関数が void convlit1(Node *n, Type *t, int conv) に名前変更され、新たに int conv という引数が追加されました。この conv 引数は、型変換が「明示的な変換」であるかどうかを示すフラグとして機能します。conv1 の場合は明示的な変換、0 の場合はそれ以外の文脈での変換(例えば、リテラルの型推論など)を意味します。

  2. 文字列変換ロジックの条件付き実行: convlit1 関数内で、数値リテラルから文字列への変換を行うコードブロック(isptrto(t, TSTRING) で判定される部分)が、if(!conv) goto bad1; という条件文の後に移動されました。

    • 変更前は、この文字列変換ロジックは常に実行される可能性がありました。
    • 変更後は、conv0(つまり明示的な変換ではない)の場合、この文字列変換ロジックはスキップされ、bad1 ラベルにジャンプします。これは、明示的な string(int) のような変換でない限り、数値リテラルが自動的に文字列に変換されるべきではないという意図を反映しています。
  3. 新しい convlit 関数の導入: void convlit(Node *n, Type *t) という新しい関数が導入されました。この関数は単に convlit1(n, t, 0); を呼び出します。これにより、既存のコードベースで convlit を呼び出している箇所は、デフォルトで conv=0 の挙動(つまり、数値から文字列への変換を抑制する挙動)を維持できます。

  4. src/cmd/gc/walk.c の変更: src/cmd/gc/walk.c 内の walk 関数(ASTを走査する主要な関数の一つ)において、convlit(l, t); の呼び出しが convlit1(l, t, 1); に変更されました。この変更は非常に重要です。walk.c のこの箇所は、おそらく明示的な型変換(例: string(expr))を処理するコンテキストであるため、conv フラグを 1 に設定することで、数値から文字列への変換が正しく許可されるようにしています。

これらの変更により、Goコンパイラは、数値リテラルから文字列への変換を、明示的に要求された場合にのみ実行し、それ以外の暗黙的な文脈では行わないように制御できるようになりました。これにより、「bug104」が修正され、コンパイラの型変換の挙動がより正確で予測可能になります。

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

src/cmd/gc/const.c

--- a/src/cmd/gc/const.c
+++ b/src/cmd/gc/const.c
@@ -34,7 +34,7 @@ truncfltlit(Mpflt *fv, Type *t)
 }
 
 void
-convlit(Node *n, Type *t)
+convlit1(Node *n, Type *t, int conv)
 {
 	int et, wt;
 
@@ -92,21 +92,6 @@ convlit(Node *n, Type *t)
 			defaultlit(n);
 			return;
 		}
-		if(isptrto(t, TSTRING)) {
-			Rune rune;
-			int l;
-			String *s;
-
-			rune = mpgetfix(n->val.u.xval);
-			l = runelen(rune);
-			s = mal(sizeof(*s)+l);
-			s->len = l;
-			runetochar((char*)(s->s), &rune);
-
-			n->val.u.sval = s;
-			n->val.ctype = CTSTR;
-			break;
-		}
 		if(isint[et]) {
 			// int to int
 			if(mpcmpfixfix(n->val.u.xval, minintval[et]) < 0)
@@ -132,6 +117,25 @@ convlit(Node *n, Type *t)
 			truncfltlit(fv, t);
 			break;
 		}
+		if(!conv)
+			goto bad1;
+
+		// only done as string(CONST)
+		if(isptrto(t, TSTRING)) {
+			Rune rune;
+			int l;
+			String *s;
+
+			rune = mpgetfix(n->val.u.xval);
+			l = runelen(rune);
+			s = mal(sizeof(*s)+l);
+			s->len = l;
+			runetochar((char*)(s->s), &rune);
+
+			n->val.u.sval = s;
+			n->val.ctype = CTSTR;
+			break;
+		}
 		goto bad1;
 
 	case Wlitfloat:
@@ -186,6 +190,12 @@ bad3:
 	return;
 }
 
+void
+convlit(Node *n, Type *t)
+{
+	convlit1(n, t, 0);
+}
+
 void
 evconst(Node *n)
 {

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -822,6 +822,7 @@ Node*	old2new(Node*, Type*);
 /*
  *	const.c
  */
+void	convlit1(Node*, Type*, int);
 void	convlit(Node*, Type*);
 void	evconst(Node*);
 int	cmpslit(Node *l, Node *r);

src/cmd/gc/walk.c

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -560,7 +560,7 @@ loop:
 		goto ret;
 
 	if(!iscomposite(t))
-		convlit(l, t);
+		convlit1(l, t, 1);
 
 	// nil conversion
 	if(eqtype(t, l->type, 0)) {

コアとなるコードの解説

src/cmd/gc/const.c の変更

  • convlit から convlit1 への変更: convlit 関数は、リテラルノード n をターゲット型 t に変換する役割を担っていました。この関数が convlit1 にリネームされ、int conv という新しい引数が追加されました。この conv 引数は、変換が明示的な型変換(例: string(10))であるか、それとも暗黙的な型推論や他のコンテキストでの変換であるかを区別するために使用されます。

  • 文字列変換ロジックの移動と条件付け: 元の convlit 関数内にあった、整数リテラルを文字列に変換するロジック(isptrto(t, TSTRING) で判定されるブロック)が、convlit1 の中で if(!conv) goto bad1; の後に移動されました。

    • isptrto(t, TSTRING) は、ターゲット型 t が文字列型へのポインタ(Go言語では文字列は値型ですが、内部的にはポインタのように扱われることがあります)であるかをチェックします。
    • rune = mpgetfix(n->val.u.xval); は、リテラルノード n の整数値を Rune 型(Unicodeコードポイント)として取得します。
    • l = runelen(rune); は、そのルーンをUTF-8エンコードした場合のバイト長を計算します。
    • s = mal(sizeof(*s)+l); s->len = l; runetochar((char*)(s->s), &rune); は、新しい String 構造体を割り当て、ルーンをUTF-8エンコードして文字列データとして格納します。
    • n->val.u.sval = s; n->val.ctype = CTSTR; は、ノード n の値を新しく作成した文字列に設定し、その型を文字列リテラル (CTSTR) に変更します。
    • このブロックが if(!conv) goto bad1; の後に配置されたことで、conv0 の場合(つまり、明示的な変換ではない場合)は、この文字列変換ロジックは実行されずに bad1 にジャンプし、エラーとして扱われるか、他の変換パスが試されます。これにより、不適切な文字列変換が防止されます。
  • 新しい convlit のラッパー関数: void convlit(Node *n, Type *t) という新しい関数が定義され、内部で convlit1(n, t, 0); を呼び出しています。これは、既存のコードベースで convlit を呼び出している箇所が、この変更によって意図せず数値から文字列への変換を行うことを防ぐための互換性レイヤーです。デフォルトでは conv=0 となり、文字列変換は抑制されます。

src/cmd/gc/go.h の変更

  • convlit1 関数のプロトタイプ宣言が追加されました。これにより、他のファイルから convlit1 を呼び出すことができるようになります。

src/cmd/gc/walk.c の変更

  • walk 関数内で、convlit(l, t); の呼び出しが convlit1(l, t, 1); に変更されました。
    • walk 関数はASTを走査し、型チェックやコード生成の準備を行います。この特定の呼び出し箇所は、おそらく明示的な型変換(例: var s string = string(10) のようなコード)を処理するコンテキストに該当します。
    • ここで conv1 に設定することで、convlit1 内の文字列変換ロジックが実行され、数値リテラル l がターゲット型 t(この場合は文字列型)に正しく変換されるようになります。

これらの変更は、Goコンパイラの型変換ロジックをより細かく制御し、特定のバグ(bug104)を修正するとともに、将来的な型変換の挙動の予測可能性と堅牢性を向上させるものです。

関連リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Go言語のコンパイラに関するドキュメント(Goのソースコード内や関連する設計ドキュメントを参照)

参考にした情報源リンク

  • Go言語のソースコード(特に src/cmd/gc ディレクトリ)
  • Go言語の型システムに関する公式ドキュメントや仕様
  • Goコンパイラの内部構造に関する技術記事や解説(例: "Go Compiler Internals" など)
  • Go言語のバグトラッカーやメーリングリストアーカイブ("bug104" の詳細な議論がある場合)
  • Web検索結果: "Go compiler bug104" (ただし、具体的なバグの詳細は公開されていないことが多い)