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

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

このコミットは、Goコンパイラのnew組み込み関数に関連するエラーメッセージの修正に関するものです。具体的には、new(bool)のような無効な型に対してnew関数が呼び出された際に表示されるエラーメッセージが、より正確な情報を示すように改善されています。

コミット

commit cd40cd243535e1662dda14f238adf59535cc045c
Author: Russ Cox <rsc@golang.org>
Date:   Fri Dec 19 12:18:44 2008 -0800

    fix new(bool) error message
    
    R=ken
    OCL=21616
    CL=21616

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

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

元コミット内容

fix new(bool) error message

このコミットは、new(bool)のような無効な型に対してnew関数が使用された場合に表示されるエラーメッセージを修正することを目的としています。

変更の背景

Go言語のnew組み込み関数は、指定された型のゼロ値へのポインタを割り当てて返します。しかし、new関数はポインタ型ではない型(例えば、boolstringなど)に対して直接使用することはできません。このような誤った使用があった場合、コンパイラはエラーを報告する必要があります。

このコミット以前は、new(bool)のようなコードが書かれた際に、コンパイラが出力するエラーメッセージが不正確でした。具体的には、エラーメッセージがnew(*bool)のように、あたかもポインタ型を期待しているかのような表示になっていました。これは、コンパイラ内部で型情報を処理する際に、元の型ではなく、そのポインタ型(または内部で一時的に生成されたポインタ型)を参照してしまっていたためと考えられます。

開発者は、ユーザーがより理解しやすい、正確なエラーメッセージを受け取るべきであると考え、この修正が行われました。正確なエラーメッセージは、開発者がコードの誤りを迅速に特定し、修正するのに役立ちます。

前提知識の解説

Go言語のnew組み込み関数

Go言語には、メモリ割り当てを行うための2つの組み込み関数、newmakeがあります。

  • new(Type): newは、指定されたTypeのゼロ値を格納するのに十分なメモリを割り当て、そのメモリへのポインタを返します。例えば、p := new(int)int型のゼロ値(0)を格納するメモリを割り当て、そのアドレスをpに代入します。pの型は*intになります。newは、スライス、マップ、チャネル以外の任意の型に使用できます。
  • make(Type, ...): makeは、スライス、マップ、チャネルといった参照型を初期化するために使用されます。これらは内部的にデータ構造を必要とするため、newでは不十分です。例えば、s := make([]int, 10)は、要素数10のスライスを初期化します。

このコミットで問題となっているのはnew関数であり、特にnew(bool)のように、newがポインタ型を返すにもかかわらず、その引数にポインタ型ではない基本型を直接渡した場合の挙動です。Goの設計上、new(T)*Tを返すため、new(bool)*boolを返しますが、newの引数として渡される型が、newが内部的に処理するポインタ型と混同されることがありました。

Goコンパイラの構造(src/cmd/gc/walk.cの役割)

Goコンパイラは複数のステージで構成されており、src/cmd/gcディレクトリはGoコンパイラの主要な部分(フロントエンドと一部のバックエンド)を含んでいます。

  • walk.c: このファイルは、コンパイラの「ウォーカー(Walker)」または「ツリーウォーカー(Tree Walker)」と呼ばれる部分の実装を含んでいます。ウォーカーは、抽象構文木(AST: Abstract Syntax Tree)を走査し、型チェック、定数畳み込み、最適化、コード生成のための準備など、様々な変換や処理を行います。
    • newcompat関数は、new組み込み関数の呼び出しを処理する部分です。この関数は、newに渡された引数の型を検証し、適切なメモリ割り当てコードを生成するか、無効な型が渡された場合にはエラーを報告します。

型システムとポインタ

Go言語は静的型付け言語であり、すべての変数には型があります。ポインタは、メモリ上の特定のアドレスを指す変数です。*Typeという構文は、Typeへのポインタを表します。

このコミットの文脈では、newcompat関数内で、n->typenewに渡された元の型を表し、t->typeがその型がポインタ型であった場合にそのポインタが指す先の型(つまり、デリファレンスされた型)を表すという区別が重要になります。

技術的詳細

このコミットの核心は、src/cmd/gc/walk.c内のnewcompat関数における型変数の使用方法の修正です。

newcompat関数は、new組み込み関数の呼び出しを処理します。この関数は、new(T)Tの部分、つまりnewに渡された引数の型を検証し、有効な場合はメモリ割り当てのためのコードを生成し、無効な場合はエラーを報告します。

修正前のコードでは、newcompat関数内でn->typenewに渡された元の型)を変数tに代入し、その後、tをポインタのデリファレンスのようにt = t->typeとして更新していました。これにより、tは元の型ではなく、そのポインタが指す先の型(または、ポインタではない型の場合は未定義の動作)を示すようになっていました。

問題は、エラーメッセージを出力するyyerror関数が、この更新されたt変数を使用していた点にありました。例えば、new(bool)という呼び出しがあった場合、n->typebool型を指します。しかし、t = t->typeの行が実行されると、tbool型から別の(おそらく無効な)型に変わってしまいます。その結果、エラーメッセージはcannot new(*%T)のように、boolではなく*bool(または他の不正確な型)を表示してしまっていました。

この修正では、新しい変数t0が導入されました。

  1. t0 = n->type;newに渡された元の型をt0に保存します。
  2. t = t0->type;tは引き続き、ポインタのデリファレンス後の型を処理するために使用されます。
  3. エラーメッセージを出力するyyerrorの呼び出し箇所で、tの代わりにt0を使用するように変更されました。

これにより、new(bool)のような無効な呼び出しがあった場合でも、エラーメッセージはcannot new(bool)のように、newに渡された元の型を正確に表示するようになります。これは、ユーザーにとってより明確で役立つエラーメッセージとなります。

isptr配列は、Goコンパイラ内部で型がポインタであるかどうかを判断するために使用されるフラグの配列です。TARRAYTSTRINGは、Goコンパイラ内部で配列型や文字列型を表す定数です。これらのチェックは、new関数が特定の型に対してどのように振る舞うべきかを決定するために行われます。

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

src/cmd/gc/walk.cファイルのnewcompat関数が変更されました。

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2000,19 +2000,19 @@ Node*
 newcompat(Node *n)
 {
 	Node *r, *on;
-	Type *t;\n+	Type *t, *t0;
 
-	t = n->type;
-	if(t == T)
+	t0 = n->type;
+	if(t0 == T)
 		goto bad;
 
-	if(t->etype == TARRAY)
+	if(t0->etype == TARRAY)
 		return arrayop(n, Erv);
 
-	if(!isptr[t->etype] || t->type == T)
+	if(!isptr[t0->etype] || t0->type == T)
 		goto bad;
 
-	t = t->type;
+	t = t0->type;
 	switch(t->etype) {
 	case TSTRING:
 	\tgoto bad;
@@ -2031,7 +2031,7 @@ newcompat(Node *n)
 
 	default:
 	\tif(n->left != N)
-\t\t\tyyerror(\"cannot new(*%T, expr)\", t);
+\t\t\tyyerror(\"cannot new(%T, expr)\", t0);
 	\tdowidth(t);
 	\ton = syslook(\"mal\", 1);
 	\targtype(on, t);
@@ -2044,7 +2044,7 @@ newcompat(Node *n)
 	return r;
 
 bad:
-\tyyerror(\"cannot new(*%T)\", t);
+\tyyerror(\"cannot new(%T)\", t0);
 	return n;
 }
 

コアとなるコードの解説

変更は主に以下の点に集約されます。

  1. 新しい型変数t0の導入:

    • 変更前: Type *t;
    • 変更後: Type *t, *t0;
    • newcompat関数の冒頭で、Type型のポインタ変数t0が新しく宣言されました。これは、newに渡された元の型を保持するために使用されます。
  2. 元の型の保存:

    • 変更前: t = n->type;
    • 変更後: t0 = n->type;
    • nnew呼び出しのASTノードを表し、n->typeはそのノードの型(つまり、newに渡された引数の型)を指します。この元の型が、新しく導入されたt0に代入されるようになりました。これにより、t0newに渡された実際の型を常に保持します。
  3. 型チェックの対象変更:

    • 変更前: if(t == T)if(t->etype == TARRAY)if(!isptr[t->etype] || t->type == T)
    • 変更後: if(t0 == T)if(t0->etype == TARRAY)if(!isptr[t0->etype] || t0->type == T)
    • これらの型チェックは、newに渡された型が有効であるか、または特定の特性(配列型であるか、ポインタ型であるかなど)を持っているかを判断するためのものです。これらのチェックが、元の型を保持するt0に対して行われるようになりました。これにより、チェックがより正確になります。
  4. デリファレンスされた型の処理:

    • 変更前: t = t->type;
    • 変更後: t = t0->type;
    • この行は、newに渡された型がポインタ型であった場合に、そのポインタが指す先の型(デリファレンスされた型)をtに代入します。このtは、その後のswitch文で、デリファレンスされた型に基づいて追加の処理を行うために使用されます。t0からtへの代入に変更されたことで、元の型t0は保持されつつ、tはデリファレンス後の型を正しく参照できるようになります。
  5. エラーメッセージの修正:

    • 変更前: yyerror("cannot new(*%T, expr)", t); および yyerror("cannot new(*%T)", t);
    • 変更後: yyerror("cannot new(%T, expr)", t0); および yyerror("cannot new(%T)", t0);
    • これがこのコミットの最も重要な変更点です。エラーメッセージを出力するyyerror関数に渡される型引数が、tからt0に変更されました。また、エラーメッセージのフォーマット文字列も*が削除され、cannot new(%T)となりました。
    • これにより、new(bool)のような無効な呼び出しがあった場合、以前はcannot new(*bool)のように表示されていたエラーメッセージが、cannot new(bool)と、newに渡された元の型を正確に表示するようになりました。これは、ユーザーがエラーの原因をより直感的に理解するのに役立ちます。

この変更により、Goコンパイラはnew組み込み関数の誤用に対するエラーメッセージをより正確かつ分かりやすく表示できるようになり、開発者のデバッグ体験が向上しました。

関連リンク

参考にした情報源リンク