[インデックス 10785] ファイルの概要
このコミットでは、Goコンパイラ(gc
)における型エラー報告の改善が行われています。具体的には、型が一致しない場合に報告される型情報が、元の型ではなく推論された型を使用するように変更されました。これにより、より正確で理解しやすいエラーメッセージが生成されるようになります。
変更されたファイルは以下の通りです。
src/cmd/gc/fmt.c
: コンパイラのエラーメッセージフォーマットに関する主要な変更が含まれています。test/ddd1.go
: 型推論に関するテストケースが更新され、期待されるエラーメッセージが修正されています。test/fixedbugs/bug386.go
: 新しいテストファイルが追加され、特定のバグ(Issue 2451および2452)が修正されたことを検証しています。test/named1.go
: 型推論に関する別のテストケースが更新され、期待されるエラーメッセージが修正されています。
コミット
- コミットハッシュ:
e14d1d7e41eed928e045961195f160069f6abb2d
- 作者: Luuk van Dijk lvd@golang.org
- コミット日時: 2011年12月14日(水)17:34:35 +0100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e14d1d7e41eed928e045961195f160069f6abb2d
元コミット内容
gc: use inferred type rather than original one when reporting non-assignability.
Fixes #2451
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5372105
変更の背景
このコミットは、Goコンパイラ(gc
)が型不一致のエラーを報告する際に、誤解を招く可能性のある型情報を提供していた問題を解決するために行われました。具体的には、Go言語のIssue 2451で報告された問題に対応しています。
以前のコンパイラは、ある式が別の型に代入できない場合に、その式の「元の型(original type)」をエラーメッセージに含めていました。しかし、Goコンパイラは型推論(type inference)を行うため、多くの場合、式にはコンパイラが推論した「推論された型(inferred type)」が存在します。この推論された型が、プログラマが意図した、または期待する型と異なる場合、エラーメッセージに元の型が表示されると、なぜ型エラーが発生したのかが分かりにくくなることがありました。
例えば、sum("hello")
のようなコードで、sum
関数が int
型の引数を期待している場合、以前は "hello"
が「ideal string型」として報告されることがありました。しかし、プログラマにとっては、それが最終的に string
型として扱われているという情報の方が、なぜ int
に代入できないのかを理解する上で重要です。
この変更の目的は、エラーメッセージの精度と分かりやすさを向上させ、開発者が型エラーの原因をより迅速に特定できるようにすることです。
前提知識の解説
Goコンパイラ (gc)
Go言語の公式コンパイラは gc
と呼ばれます。これは、Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成などが含まれます。このコミットが関連するのは、主に意味解析とエラー報告のフェーズです。
型推論 (Type Inference)
型推論とは、プログラミング言語のコンパイラが、変数や式の型を明示的に宣言しなくても、その使用方法から自動的に型を決定する機能です。Go言語では、変数の初期化時に型を省略できるなど、強力な型推論機能を持っています。例えば、x := 10
と書くと、コンパイラは x
を int
型と推論します。
代入不可能性 (Non-assignability)
Go言語では、異なる型の値を直接代入できない場合があります。例えば、int
型の変数に string
型の値を代入しようとすると、型エラーが発生します。これを「代入不可能性」と呼びます。コンパイラは、このような型不一致を検出し、エラーとして報告します。
コンパイラのAST (Abstract Syntax Tree) と Node
、Type
構造体
Goコンパイラは、ソースコードを解析して抽象構文木(AST)を構築します。ASTは、プログラムの構造を木構造で表現したものです。ASTの各ノードは、式、文、宣言などのプログラム要素を表します。
Node
構造体: ASTの各要素を表すコンパイラ内部のデータ構造です。Node
には、そのノードが表す式の型情報や、元の式(orig
フィールド)などのメタデータが含まれることがあります。Type
構造体: Go言語の型システムにおける型(int
,string
,struct
など)を表すコンパイラ内部のデータ構造です。
src/cmd/gc/fmt.c
の役割
src/cmd/gc/fmt.c
は、Goコンパイラのエラーメッセージやデバッグ情報のフォーマットを担当するC言語のソースファイルです。コンパイラが型エラーなどを検出した際に、ユーザーに表示されるメッセージの生成ロジックが含まれています。fmtprint
関数は、C言語の printf
のような機能を提供し、%N
や %T
といったカスタムフォーマット指定子を使って、Node
や Type
の情報を整形して出力します。
技術的詳細
このコミットの核心は、src/cmd/gc/fmt.c
内の nodefmt
関数と Nconv
関数の変更にあります。
nodefmt
関数の変更
nodefmt
関数は、ASTの Node
をフォーマットして文字列として出力する役割を担っています。以前のバージョンでは、FmtLong
フラグが設定されている(詳細なフォーマットが要求されている)場合、n->type
を直接使用して型情報を出力していました。しかし、n->type
はそのノードの現在の型を表しますが、コンパイラが型推論を行った結果、元の式が持っていた型とは異なる、より具体的な型になっている場合があります。
この変更では、Node *n
の type
フィールドを Type *t
に一時的に保存し、もし n->orig
(元のノード) が存在すれば、n
をその n->orig
に置き換えるロジックが追加されました。そして、型情報の出力には、元の n->type
を保存しておいた t
を使用するように変更されました。
// 変更前
- if(f->flags&FmtLong && n->type != T) {
- if(n->type->etype == TNIL)
+ if(f->flags&FmtLong && t != T) { // t を使用
+ if(t->etype == TNIL) // t を使用
return fmtprint(f, "nil");
else
- return fmtprint(f, "%N (type %T)", n, n->type);
+ return fmtprint(f, "%N (type %T)", n, t); // t を使用
この変更により、エラーメッセージで表示される型は、その式が最終的に推論された型(t
)となり、より正確な情報が提供されます。同時に、%N
で表示されるノード自体は、もし元のノードが存在すればそちらが使われることで、エラーが発生した元のコードのコンテキストをより正確に反映できるようになります。
Nconv
関数の変更
Nconv
関数は、fmtprint
の %N
フォーマット指定子を処理する関数です。以前のバージョンでは、エラー報告モード(FErr
または FExp
)の場合に、n->orig
が存在すれば n
を n->orig
に置き換えていました。
// 変更前
case FErr:
case FExp:
- if(n->orig != N)
- n = n->orig;
r = nodefmt(fp, n);
break;
このコミットでは、この if(n->orig != N) n = n->orig;
の行が削除されました。これは、nodefmt
関数内で既に n->orig
の考慮が行われるようになったため、Nconv
で重複して処理する必要がなくなったためです。これにより、コードの重複が解消され、ロジックがより一貫性を持つようになりました。
テストファイルの変更
test/ddd1.go
とtest/named1.go
では、sum("hello")
や1 != 2
のような式に対するエラーメッセージの期待値が更新されています。以前は「ideal string」や「ideal bool」といった「ideal type」が報告されていましたが、変更後は「string」や「bool」といった推論された具体的な型が報告されるようになっています。test/fixedbugs/bug386.go
は新規追加されたテストファイルで、func f() error { return 0 }
のように、error
型を返す関数がint
型の値を返そうとした場合に、int
型が正しくエラーメッセージに表示されることを検証しています。これはIssue 2451と2452(関連する別のバグ)の両方に対応しています。
コアとなるコードの変更箇所
src/cmd/gc/fmt.c
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1278,13 +1278,17 @@ exprfmt(Fmt *f, Node *n, int prec)
static int
nodefmt(Fmt *f, Node *n)
{
+ Type *t;
+
+ t = n->type;
+ if(n->orig != N)
+ n = n->orig;
+
- if(f->flags&FmtLong && n->type != T) {
- if(n->type->etype == TNIL)
+ if(f->flags&FmtLong && t != T) {
+ if(t->etype == TNIL)
return fmtprint(f, "nil");
else
- return fmtprint(f, "%N (type %T)", n, n->type);
-
+ return fmtprint(f, "%N (type %T)", n, t);
}
// TODO inlining produces expressions with ninits. we can't print these yet.
@@ -1479,8 +1483,6 @@ Nconv(Fmt *fp)
switch(fmtmode) {
case FErr:
case FExp:
- if(n->orig != N)
- n = n->orig;
r = nodefmt(fp, n);
break;
case FDbg:
test/ddd1.go
--- a/test/ddd1.go
+++ b/test/ddd1.go
@@ -15,7 +15,7 @@ var (
_ = sum()
_ = sum(1.0, 2.0)
_ = sum(1.5) // ERROR "integer"
- _ = sum("hello") // ERROR ".hello. .type ideal string. as type int|incompatible"
+ _ = sum("hello") // ERROR ".hello. .type string. as type int|incompatible"
_ = sum([]int{1}) // ERROR "[]int literal.*as type int|incompatible"
)
test/fixedbugs/bug386.go
(新規ファイル)
// errchk $G $D/$F.go
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Issue 2451, 2452
package foo
func f() error { return 0 } // ERROR "cannot use 0 .type int."
func g() error { return -1 } // ERROR "cannot use -1 .type int."
test/named1.go
--- a/test/named1.go
+++ b/test/named1.go
@@ -37,7 +37,7 @@ func main() {
asBool(true)
asBool(*&b)
asBool(Bool(true))
- asBool(1 != 2) // ERROR "cannot use.*type ideal bool.*as type Bool"
+ asBool(1 != 2) // ERROR "cannot use.*type bool.*as type Bool"
asBool(i < j) // ERROR "cannot use.*type bool.*as type Bool"
_, b = m[2] // ERROR "cannot .* bool.*type Bool"
コアとなるコードの解説
src/cmd/gc/fmt.c
の nodefmt
関数
この関数の変更は、エラーメッセージにおける型の表示方法を改善するためのものです。
static int
nodefmt(Fmt *f, Node *n)
{
Type *t; // 新しく導入されたTypeポインタ
t = n->type; // 現在のノードの型をtに保存
if(n->orig != N) // もし元のノードが存在すれば
n = n->orig; // nを元のノードに置き換える
// ... (中略) ...
if(f->flags&FmtLong && t != T) { // 型情報の出力にtを使用
if(t->etype == TNIL)
return fmtprint(f, "nil");
else
return fmtprint(f, "%N (type %T)", n, t); // %T にtを使用
}
// ... (後略) ...
}
この変更のポイントは以下の通りです。
Type *t;
の導入:nodefmt
関数の冒頭で、Type
型のポインタt
が宣言されました。t = n->type;
:n->type
(現在のノードの型) がt
に保存されます。これは、n
がn->orig
に置き換えられた後でも、元のn
が持っていた型情報を参照できるようにするためです。if(n->orig != N) n = n->orig;
: ここが重要な変更点です。もし現在のNode
n
にorig
(元のノード) が設定されている場合、n
をそのorig
ノードに置き換えます。これにより、%N
フォーマット指定子でノードが出力される際に、型推論や最適化によって変更された後のノードではなく、ソースコードに書かれた元のノードが参照されるようになります。これは、エラーメッセージがより元のコードに即したものになることを意味します。fmtprint(f, "%N (type %T)", n, t);
: 型情報の出力部分では、%N
には変更された可能性のあるn
(元のノードを指す場合がある) が渡され、%T
にはt
(元のn->type
を保存したもの) が渡されます。これにより、エラーメッセージは「元のコードのこの部分(%N
)は、この型(%T
)として扱われています」という、より正確な情報を提供するようになります。
src/cmd/gc/fmt.c
の Nconv
関数
Nconv
関数は、fmtprint
の %N
フォーマット指定子を処理する際に呼び出されます。
// 変更前
case FErr:
case FExp:
- if(n->orig != N)
- n = n->orig;
r = nodefmt(fp, n);
break;
この部分が削除されたのは、nodefmt
関数内で既に n->orig
の処理が行われるようになったためです。Nconv
で再度 n->orig
をチェックして n
を置き換える必要がなくなったため、コードの重複が解消され、ロジックが簡素化されました。
これらの変更により、Goコンパイラは型エラーを報告する際に、より正確で開発者にとって分かりやすい型情報を提供するようになりました。特に、型推論が関与する複雑なケースにおいて、エラーメッセージの有用性が大幅に向上しています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/e14d1d7e41eed928e045961195f160069f6abb2d
- Go Code Review: https://golang.org/cl/5372105
参考にした情報源リンク
- Go Code Review 5372105: https://golang.org/cl/5372105
- Go Issue 2451 (関連する可能性のある情報源): 検索結果から直接的なGo言語のIssueトラッカーのリンクは見つかりませんでしたが、コミットメッセージとCLの記述から、このコミットがGoコンパイラの型エラー報告に関するIssue 2451を修正していることが確認できます。