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

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

このコミットは、Goコンパイラ(cmd/gc)における、型が壊れている(未定義など)場合にdefergoステートメント内で発生する、誤解を招くエラーメッセージを修正するものです。具体的には、未定義の型に対するメソッド呼び出しがdefergoの引数として使用された際に、コンパイラが不適切な型変換エラーを報告する問題を解決します。

コミット

commit 6f5af9c0b1c242ac74f415a3ce9f9a8437c54324
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Tue May 21 18:35:47 2013 +0200

    cmd/gc: fix confusing error with broken types and defer/go
    
    Fixes #5172.
    
    R=golang-dev, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/9614044

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

https://github.com/golang/go/commit/6f5af9c0b1c242ac74f415a3ce9f9a8437c54324

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)が、型が壊れている(例えば、未定義のフィールドを持つ構造体など)場合にdeferまたはgoステートメント内で発生する、誤解を招くエラーメッセージを修正します。

具体的には、以下のような状況で問題が発生していました。

package main

type foo struct {
	x bar // bar は未定義
}

func main() {
	var f foo
	go f.bar()    // ここでエラーが発生するが、エラーメッセージが不適切
	defer f.bar() // ここでも同様
}

このコードでは、foo構造体のフィールドxが未定義の型barを持っているため、foo型自体が「壊れた型」と見なされます。f.bar()という呼び出しは、実際にはf.x.bar()のような形を意図している可能性がありますが、barが未定義であるため、この式は不正です。

Goコンパイラは、defergoの引数が関数呼び出しでない場合に、型変換が行われていると解釈しようとします。しかし、上記のような壊れた型の場合、コンパイラは「型変換」に関するエラーメッセージを出力してしまい、ユーザーにとってはなぜ型変換エラーが発生しているのかが分かりにくいという問題がありました。本来は、barが未定義であること、つまり型が壊れていることが根本原因であるべきです。

このコミットは、このような状況で、コンパイラが既に壊れた型について警告を発している場合、defergoステートメント内での追加の、そして誤解を招く可能性のある型変換エラーメッセージの出力を抑制することで、エラー報告の明確性を向上させます。

変更の背景

この変更の背景には、Goコンパイラのエラーメッセージの品質向上という目的があります。Go言語は、そのシンプルさと明確さで知られていますが、コンパイラのエラーメッセージもまた、開発者が問題を迅速に特定し、修正できるように、できる限り分かりやすくあるべきです。

Go言語のIssue #5172("cmd/gc: spurious warn about type conversion on broken type inside go and defer")で報告された問題は、まさにこのエラーメッセージの明確性に関するものでした。ユーザーは、未定義の型が原因で発生するエラーに対して、コンパイラが「型変換」に関するエラーを報告することに混乱していました。これは、根本原因(型の未定義)とは異なる、表面的な問題(型変換の失敗)を指摘しているため、デバッグを困難にしていました。

コンパイラは、コードの解析中に型の健全性をチェックし、型が壊れていることを検出した場合には、その時点で適切なエラー(例: "undefined")を報告します。しかし、その後の処理でdefergoステートメントの引数を解析する際に、壊れた型に対するメソッド呼び出しを、誤って型変換と解釈し、さらに別のエラーメッセージを出力してしまうことがありました。この「二重のエラー」または「誤解を招くエラー」を避けることが、このコミットの主な動機です。

開発者は、コンパイラが最も根本的な問題、つまり型の未定義を明確に指摘することを望んでいました。この修正により、コンパイラは既に型が壊れていることを認識している場合、defergoのコンテキストで発生する可能性のある、関連性の低い型変換エラーの報告を抑制するようになります。これにより、開発者はより正確で、根本原因に焦点を当てたエラーメッセージを受け取ることができるようになります。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。

  1. Go言語のdefergoステートメント:

    • defer: deferステートメントは、それが含まれる関数がリターンする直前に、指定された関数呼び出しを実行することをスケジュールします。これは、リソースの解放(ファイルのクローズ、ロックの解除など)によく使用されます。
    • go: goステートメントは、指定された関数呼び出しを新しいゴルーチン(軽量な並行実行スレッド)で実行することを開始します。これにより、並行処理が実現されます。
    • これらのステートメントの引数は、関数呼び出しである必要があります。コンパイラは、引数が関数呼び出しの形式になっているかをチェックします。
  2. Goコンパイラ(cmd/gc:

    • cmd/gcは、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。
    • コンパイラの主要なフェーズには、字句解析、構文解析、型チェック、最適化、コード生成などがあります。
    • このコミットが関連するのは、主に**型チェック(typecheck)**フェーズです。型チェックは、プログラムがGo言語の型システム規則に準拠していることを確認します。
  3. 型システムと「壊れた型(broken type)」:

    • Go言語は静的型付け言語であり、すべての変数と式には型があります。
    • 「壊れた型」とは、コンパイラがその型を正しく解決できない、またはその定義に問題がある状態を指します。例えば、未定義の型を参照している構造体や、存在しないフィールドにアクセスしようとしている場合などがこれに該当します。
    • コンパイラは、このような壊れた型を検出すると、通常は「undefined」などのエラーメッセージを出力します。
  4. typecheck.c:

    • Goコンパイラのソースコードにおいて、src/cmd/gc/typecheck.cは、型チェックロジックの大部分を実装しているC言語のファイルです(Goコンパイラの一部はC言語で書かれています)。
    • このファイルには、式の型を決定し、型の一貫性を検証するための関数が含まれています。
  5. Node構造体とAST (Abstract Syntax Tree):

    • Goコンパイラは、ソースコードを解析する際に、抽象構文木(AST)と呼ばれるツリー構造を構築します。ASTの各ノードは、プログラムの要素(変数、関数呼び出し、演算子など)を表します。
    • Node構造体は、ASTのノードを表すために使用されます。各Nodeは、その種類(Opフィールド)、関連する型(typeフィールド)、子ノード(left, rightなど)などの情報を持っています。
    • n->left->type == Tn->left->type->brokeは、ASTノードの左の子の型が未定義(TはGoコンパイラ内部で未定義の型を表す定数)であるか、またはその型が「壊れている」状態であるかをチェックしています。
  6. エラー報告のメカニズム:

    • コンパイラは、プログラムにエラーがある場合、そのエラーの種類、発生箇所(ファイル名、行番号)、および説明を含むメッセージを出力します。
    • このコミットは、既存のエラー報告ロジックに条件を追加し、特定の状況下でのエラーメッセージの出力を抑制することで、エラー報告の質を向上させています。

これらの概念を理解することで、コミットがGoコンパイラのどの部分に影響を与え、どのような問題を解決しようとしているのかがより明確になります。

技術的詳細

このコミットの技術的詳細は、Goコンパイラの型チェックフェーズにおけるエラー報告のロジックに焦点を当てています。修正はsrc/cmd/gc/typecheck.cファイルのcheckdefergo関数内で行われています。

checkdefergo関数は、deferおよびgoステートメントの引数(関数呼び出し)を型チェックするために使用されます。この関数は、引数が有効な関数呼び出しであること、およびその型が正しいことを確認します。

元のコードでは、defergoの引数が関数呼び出しでない場合(例えば、単なる変数や型変換のように見える場合)、コンパイラはそれを型変換と解釈しようとし、conv:ラベルにジャンプして型変換に関するエラーを報告していました。

問題は、引数として渡された式が、既に「壊れた型」に関連している場合でした。例えば、f.bar()のような式で、fの型が未定義のフィールドbarを持っているためにfの型自体が壊れている場合です。この場合、コンパイラは既にbarが未定義であることについてのエラー(例: ERROR "undefined")を報告しているはずです。しかし、checkdefergo関数は、この壊れた型を考慮せずに、さらに「型変換」に関する誤解を招くエラーを報告していました。

このコミットによる修正は、conv:ラベルにジャンプする前に、以下の条件を追加することでこの問題を解決します。

		// type is broken or missing, most likely a method call on a broken type
		// we will warn about the broken type elsewhere. no need to emit a potentially confusing error
		if(n->left->type == T || n->left->type->broke)
			break;

このコードスニペットは、以下のロジックを導入しています。

  1. n->left->type == T: これは、deferまたはgoステートメントの引数(n->left)の型が、コンパイラ内部で「未定義の型」を表す特殊な定数Tであるかどうかをチェックします。これは、型が全く解決できなかった場合に発生します。
  2. n->left->type->broke: これは、n->leftの型が「壊れている」状態であるかどうかをチェックします。brokeフラグは、コンパイラが型の定義に問題があることを検出したときに設定されます。例えば、構造体内のフィールドの型が未定義である場合、その構造体型全体がbrokeとマークされることがあります。

これらの条件のいずれかが真である場合、つまり、引数の型が未定義であるか、または既に壊れているとマークされている場合、breakステートメントが実行されます。これにより、conv:ラベルへのジャンプがスキップされ、結果として誤解を招く型変換エラーメッセージの出力が抑制されます。

この修正の背後にある考え方は、「既に根本的な型エラーが報告されている場合、その型エラーに起因する二次的な、そして誤解を招く可能性のあるエラーメッセージは出力しない」というものです。これにより、コンパイラのエラー報告がより簡潔で、根本原因に焦点を当てたものになります。

テストケースtest/fixedbugs/issue5172.goは、この修正が意図通りに機能することを確認するために追加されました。このテストケースは、未定義の型を持つ構造体と、その構造体のフィールドに対するメソッド呼び出しをgoおよびdeferステートメント内で使用するシナリオを再現し、コンパイラが適切なエラー(undefined)のみを報告し、誤解を招く型変換エラーを報告しないことを検証します。

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

このコミットによるコードの変更は、主に以下の2つのファイルに集中しています。

  1. src/cmd/gc/typecheck.c: Goコンパイラの型チェックロジックを実装しているC言語のファイルです。

    • 変更前:
      @@ -1793,6 +1793,11 @@ checkdefergo(Node *n)
       		break;
       	default:
       	conv:
       	if(!n->diag) {
       		// The syntax made sure it was a call, so this must be
       		// a conversion.
      
    • 変更後:
      @@ -1793,6 +1793,11 @@ checkdefergo(Node *n)
       		break;
       	default:
       	conv:
      +		// type is broken or missing, most likely a method call on a broken type
      +		// we will warn about the broken type elsewhere. no need to emit a potentially confusing error
      +		if(n->left->type == T || n->left->type->broke)
      +			break;
      +
       	if(!n->diag) {
       		// The syntax made sure it was a call, so this must be
       		// a conversion.
      

    この変更は、checkdefergo関数内のconv:ラベルの直前に、新しい条件分岐を追加しています。この条件は、n->leftdeferまたはgoの引数)の型が未定義であるか、または壊れている場合に、それ以上のエラーチェックをスキップするように指示します。

  2. test/fixedbugs/issue5172.go: このコミットによって追加された新しいテストファイルです。

    • 新規ファイル:
      // errorcheck
      
      // Copyright 2013 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 5172: spurious warn about type conversion on broken type inside go and defer
      
      package main
      
      type foo struct {
      	x bar // ERROR "undefined"
      }
      
      func main() {
      	var f foo
      	go f.bar()
      	defer f.bar()
      }
      

    このテストファイルは、foo構造体内で未定義の型barを使用し、その結果としてfoo型が壊れるシナリオを意図的に作成しています。go f.bar()defer f.bar()の行では、コンパイラが"undefined"エラーのみを報告し、他の誤解を招くエラーを報告しないことをerrorcheckディレクティブによって検証しています。

コアとなるコードの解説

src/cmd/gc/typecheck.cの変更は、Goコンパイラの型チェックロジックの特定のパスを修正しています。

checkdefergo関数は、defergoステートメントの引数が関数呼び出しであるかどうかを検証します。もし引数が関数呼び出しの形式でない場合、コンパイラはそれを型変換と見なし、conv:ラベルにジャンプして型変換に関するエラーを報告しようとします。

追加されたコードブロック:

		if(n->left->type == T || n->left->type->broke)
			break;

この行は、conv:ラベルに到達した直後に評価されます。

  • n->leftは、deferまたはgoステートメントの引数として渡された式を表すASTノードです。
  • n->left->typeはその式の型です。
  • Tは、Goコンパイラ内部で「未定義の型」を示す特別な値です。
  • n->left->type->brokeは、その型が何らかの理由で「壊れている」(例えば、未解決の参照がある)ことを示すフラグです。

この条件文は、「もし引数の型が未定義であるか、または既に壊れているとマークされているならば」ということを意味します。このような状況では、コンパイラは既にその型の問題についてのエラー(例: undefined)を報告しているはずです。したがって、このdefergoのコンテキストで、さらに型変換に関するエラーを報告することは、冗長であり、ユーザーを混乱させる可能性があります。

breakステートメントは、現在のswitch文(checkdefergo関数の一部)から抜け出し、それ以上のエラーチェックを行わないようにします。これにより、誤解を招く型変換エラーメッセージの出力が抑制され、コンパイラはより正確で、根本原因に焦点を当てたエラーメッセージのみを報告するようになります。

要するに、この修正は、コンパイラが既に根本的な型エラーを検出している場合に、そのエラーに起因する二次的な、そして誤解を招く可能性のあるエラーメッセージの報告を避けるための「早期終了」メカニズムを導入しています。

関連リンク

参考にした情報源リンク