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

[インデックス 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 と書くと、コンパイラは xint 型と推論します。

代入不可能性 (Non-assignability)

Go言語では、異なる型の値を直接代入できない場合があります。例えば、int 型の変数に string 型の値を代入しようとすると、型エラーが発生します。これを「代入不可能性」と呼びます。コンパイラは、このような型不一致を検出し、エラーとして報告します。

コンパイラのAST (Abstract Syntax Tree) と NodeType 構造体

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 といったカスタムフォーマット指定子を使って、NodeType の情報を整形して出力します。

技術的詳細

このコミットの核心は、src/cmd/gc/fmt.c 内の nodefmt 関数と Nconv 関数の変更にあります。

nodefmt 関数の変更

nodefmt 関数は、ASTの Node をフォーマットして文字列として出力する役割を担っています。以前のバージョンでは、FmtLong フラグが設定されている(詳細なフォーマットが要求されている)場合、n->type を直接使用して型情報を出力していました。しかし、n->type はそのノードの現在の型を表しますが、コンパイラが型推論を行った結果、元の式が持っていた型とは異なる、より具体的な型になっている場合があります。

この変更では、Node *ntype フィールドを 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 が存在すれば nn->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.gotest/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.cnodefmt 関数

この関数の変更は、エラーメッセージにおける型の表示方法を改善するためのものです。

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を使用
	}
	// ... (後略) ...
}

この変更のポイントは以下の通りです。

  1. Type *t; の導入: nodefmt 関数の冒頭で、Type 型のポインタ t が宣言されました。
  2. t = n->type;: n->type (現在のノードの型) が t に保存されます。これは、nn->orig に置き換えられた後でも、元の n が持っていた型情報を参照できるようにするためです。
  3. if(n->orig != N) n = n->orig;: ここが重要な変更点です。もし現在の Node norig (元のノード) が設定されている場合、n をその orig ノードに置き換えます。これにより、%N フォーマット指定子でノードが出力される際に、型推論や最適化によって変更された後のノードではなく、ソースコードに書かれた元のノードが参照されるようになります。これは、エラーメッセージがより元のコードに即したものになることを意味します。
  4. fmtprint(f, "%N (type %T)", n, t);: 型情報の出力部分では、%N には変更された可能性のある n (元のノードを指す場合がある) が渡され、%T には t (元の n->type を保存したもの) が渡されます。これにより、エラーメッセージは「元のコードのこの部分(%N)は、この型(%T)として扱われています」という、より正確な情報を提供するようになります。

src/cmd/gc/fmt.cNconv 関数

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コンパイラは型エラーを報告する際に、より正確で開発者にとって分かりやすい型情報を提供するようになりました。特に、型推論が関与する複雑なケースにおいて、エラーメッセージの有用性が大幅に向上しています。

関連リンク

参考にした情報源リンク

  • Go Code Review 5372105: https://golang.org/cl/5372105
  • Go Issue 2451 (関連する可能性のある情報源): 検索結果から直接的なGo言語のIssueトラッカーのリンクは見つかりませんでしたが、コミットメッセージとCLの記述から、このコミットがGoコンパイラの型エラー報告に関するIssue 2451を修正していることが確認できます。