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

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

このコミットは、Goコンパイラの型チェックフェーズにおけるOINDノード(間接参照操作)の処理をより堅牢にするための変更です。具体的には、無効な間接参照に関するエラーチェックのロジックを改善し、特定の不正なコードパターンに対してより正確なエラーメッセージが出力されるように修正しています。

コミット

commit c0d9bf5650361191f5e86c4a31176a01a9c2867b
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Fri Jan 18 22:46:10 2013 +0100

    cmd/gc: more robust checking of OIND nodes.
    
    Fixes #4610.
    
    R=golang-dev, remyoudompheng, rsc
    CC=golang-dev, nigeltao
    https://golang.org/cl/7058057

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

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

元コミット内容

このコミットは、Goコンパイラのcmd/gc部分におけるOINDノードのチェックをより堅牢にするものです。これにより、Go言語のIssue #4610で報告された問題が修正されます。

変更の背景

Goコンパイラは、ソースコードを抽象構文木(AST)に変換し、そのASTに対して型チェックなどの様々な処理を行います。このコミットが行われる前は、特定の不正なコードパターン、例えば&foo{}のような形式で、fooが型名ではなく変数名である場合に、コンパイラが不正確なエラーメッセージを出力したり、内部で予期せぬ動作をしたりする可能性がありました。

具体的には、&foo{}という表現は、fooが型名であるかのように複合リテラル({})と組み合わせて使用しようとしていますが、fooは既に宣言された変数であるため、これはGoの文法として誤りです。このような場合、コンパイラは「fooは型ではありません」というような、より適切なエラーを出すべきです。しかし、以前の実装では、OINDノード(間接参照)のチェックロジックが広範すぎたため、この種の誤用が「無効な間接参照」という、文脈にそぐわないエラーとして報告されるか、あるいは型チェックの途中で不適切なパスに進んでしまう可能性がありました。

この問題はGoのIssue #4610として報告されており、このコミットはその修正を目的としています。修正の目的は、コンパイラが不正なコードに対してより正確で分かりやすいエラーメッセージを提供し、開発者のデバッグ体験を向上させることにあります。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの内部構造と概念に関する知識が必要です。

  1. Goコンパイラ (cmd/gc): Go言語の公式コンパイラのフロントエンド部分です。ソースコードの字句解析、構文解析、型チェック、中間コード生成などを行います。
  2. 抽象構文木 (AST): ソースコードの構造を木構造で表現したものです。コンパイラはASTを操作して様々な解析や変換を行います。
  3. ASTノードの種類: ASTの各要素は特定のノードタイプで表現されます。
    • OIND (Opcode for INDirection): ポインタの逆参照(間接参照)操作を表すノードです。例えば、*pのような式に対応します。
    • OADDR (Opcode for ADDRess): 変数のアドレスを取得する操作を表すノードです。例えば、&xのような式に対応します。
    • OLIT (Opcode for LITeral): リテラル値(数値、文字列など)を表すノードです。
    • OCALL (Opcode for CALL): 関数呼び出しを表すノードです。
  4. 型チェック (typecheck.c): cmd/gc内のtypecheck.cファイルは、ASTノードの型がGo言語の型システム規則に適合しているかを検証する役割を担っています。このフェーズで、型の一貫性、正しい演算子の使用、関数呼び出しの引数と戻り値の型の一致などがチェックされます。
  5. Node構造体: Goコンパイラ内部でASTの各ノードを表すC言語の構造体です。Nodeは、そのノードの操作タイプ(OIND, OADDRなど)、関連する型情報(typeフィールド)、子ノード(left, rightなど)などの情報を含みます。
  6. topフラグ: typecheck関数に渡される引数で、現在のノードが評価されるべきコンテキストを示します。
    • Erv (Expression is Rvalue): 式が右辺値(値として評価されるべき)であることを示します。
    • Etop (Expression is TOP-level): 式がトップレベルのステートメントとして評価されるべきであることを示します。
  7. isptr配列: 型のカテゴリを示すブール値の配列で、特定の型がポインタ型であるかどうかを効率的にチェックするために使用されます。t->etypeはノードtの基本型(element type)を示します。
  8. yyerror: コンパイラがエラーメッセージを出力するために使用する関数です。

技術的詳細

このコミットの技術的詳細な変更は、src/cmd/gc/typecheck.cファイル内のOINDノードを処理する部分にあります。

元のコードでは、OINDノードの型チェックにおいて、以下の条件で「無効な間接参照」エラーを発生させていました。

if((top & (Erv | Etop)) && !isptr[t->etype]) {
    yyerror("invalid indirect of %lN", n->left);
    goto error;
}

このロジックは、「もし現在の式が右辺値またはトップレベルのコンテキストにあり、かつその型がポインタ型でない場合」にエラーを出す、というものでした。

しかし、この条件は一部のケースで過剰に厳しく、例えば&foo{}のような、OADDR(アドレス取得)操作と複合リテラルが絡む不正な構文に対して、より適切なエラー(例:「fooは型ではありません」)ではなく、「無効な間接参照」というエラーを出してしまう可能性がありました。これは、コンパイラの内部処理でOADDROINDのチェックロジックと相互作用したり、あるいはOINDのチェックが早すぎる段階で実行され、より詳細な型チェックが妨げられていたためと考えられます。

新しいコードでは、この条件を以下のように変更しました。

if(!isptr[t->etype]) {
    if(top & (Erv | Etop)) {
        yyerror("invalid indirect of %lN", n->left);
        goto error;
    }
    goto ret;
}

この変更のポイントは、if(!isptr[t->etype])(型がポインタでない場合)という条件を外側のif文に移動し、その内部でif(top & (Erv | Etop))(右辺値またはトップレベルのコンテキストにある場合)という条件をネストさせたことです。

これにより、以下の挙動が実現されます。

  1. 型がポインタでない場合 (!isptr[t->etype]):
    • かつ、右辺値またはトップレベルのコンテキストにある場合 (top & (Erv | Etop)): 従来通り「無効な間接参照」エラーを発生させます。これは、本当にポインタでない型に対して間接参照を行おうとしている、かつその結果が値として使われるべき、という明確なエラーケースに対応します。
    • しかし、右辺値またはトップレベルのコンテキストにない場合 (!(top & (Erv | Etop))): エラーを発生させずにgoto ret;(関数から戻る)します。これは、OINDノードが非ポインタ型に対して適用されているものの、それがまだ中間的な状態であるか、あるいは別のより適切なエラーが後続の型チェックフェーズで検出されるべきケースに対応します。例えば、&foo{}のようなケースでは、fooが型ではないというエラーが優先されるべきであり、この変更によってそのエラーが適切に報告されるようになります。

この修正により、コンパイラはOINDノードのチェックをより文脈に即して行い、不正なコードに対してより正確で具体的なエラーメッセージを出力できるようになりました。

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

変更は主に以下の2つのファイルで行われています。

  1. src/cmd/gc/typecheck.c:

    • reswitchラベルの直下、OINDノードの型チェックロジックが変更されました。
    • 元の単一のif文が、ネストされたif文に置き換えられ、エラー発生の条件がより厳密になりました。
    --- a/src/cmd/gc/typecheck.c
    +++ b/src/cmd/gc/typecheck.c
    @@ -482,9 +482,12 @@ reswitch:
     		t->type = n->left->type;
     		if(t->type == T) {
     			n->left = N;
     			goto ret;
     		}
    -		if((top & (Erv | Etop)) && !isptr[t->etype]) {
    -			yyerror("invalid indirect of %lN", n->left);
    -			goto error;
    +		if(!isptr[t->etype]) {
    +			if(top & (Erv | Etop)) {
    +				yyerror("invalid indirect of %lN", n->left);
    +				goto error;
    +			}
    +			goto ret;
     		}
     		ok |= Erv;
     		n->type = t->type;
    
  2. test/fixedbugs/issue4610.go:

    • このコミットによって新しく追加されたテストファイルです。
    • errorcheckディレクティブが含まれており、コンパイル時に特定のエラーメッセージが出力されることを期待しています。
    • テストコードは_ = &foo{} // ERROR "is not a type"という行を含み、fooが型ではない場合に「is not a type」というエラーが正しく検出されることを検証します。
    // errorcheck
    
    // Copyright 2012 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.
    
    package main
    
    type bar struct {
    	x int
    }
    
    func main() {
    	var foo bar
    	_ = &foo{} // ERROR "is not a type"
    }
    

コアとなるコードの解説

src/cmd/gc/typecheck.cの変更は、OINDノードの型チェックロジックをより洗練させるものです。

変更前のコードでは、OINDノードが処理される際に、その左の子ノード(n->left)の型tがポインタ型でない場合、かつそのOINDノードが右辺値またはトップレベルのコンテキストで評価されるべき場合に、即座に「invalid indirect」エラーを発生させていました。

しかし、&foo{}のようなケースでは、fooは変数であり型ではないため、foo{}という複合リテラル自体が不正です。この不正な複合リテラルのアドレスを取ろうとする&演算子(OADDR)が、内部的にOINDのチェックロジックと関連付けられていたか、あるいはOINDのチェックが早すぎる段階で実行され、より根本的な「fooは型ではない」というエラーがマスクされてしまっていたと考えられます。

新しいコードでは、まず!isptr[t->etype](型がポインタでない)という条件で分岐します。

  • もし型がポインタでない場合、さらにtop & (Erv | Etop)(右辺値またはトップレベルのコンテキストにある)という条件をチェックします。
    • この内側の条件が真であれば、それは本当にポインタでないものに対して間接参照を行おうとしている明確なエラーケースなので、「invalid indirect」エラーを出力します。
    • しかし、この内側の条件が偽であれば(つまり、ポインタでないものに対して間接参照を行おうとしているが、それが右辺値やトップレベルのコンテキストではない場合)、エラーを出力せずにgoto ret;で処理を続行させます。

この「エラーを出力せずに処理を続行させる」という変更が重要です。これにより、&foo{}のようなケースで、OINDのチェックが早期にエラーを発生させることなく、コンパイラの他の部分がfooが型ではないことを正しく認識し、「is not a type」というより適切なエラーメッセージを出力できるようになります。

test/fixedbugs/issue4610.goは、この修正が意図通りに機能することを確認するための回帰テストです。このテストは、_ = &foo{}という不正なコードがコンパイルされた際に、コンパイラが「is not a type」というエラーを正確に報告することを保証します。

関連リンク

  • Go言語のIssue #4610: このコミットが修正した問題のトラッキング。
    • https://golang.org/issue/4610 (Web検索で直接見つからなかったため、コミットメッセージのFixes #4610から推測されるリンク)
  • Gerrit Change-Id: 7058057

参考にした情報源リンク

  • Go言語のソースコード (src/cmd/gc/typecheck.c, test/fixedbugs/issue4610.go)
  • Go言語のコンパイラに関する一般的な知識(AST、型チェックなど)
  • Go言語のIssueトラッカーの慣習(Fixes #XXXX
  • Go言語のGerritコードレビューシステム
  • Go言語の複合リテラルとアドレス演算子に関する言語仕様
  • Go言語のerrorcheckテストディレクティブに関する情報
  • Go言語のコンパイラ開発に関するブログ記事やドキュメント(一般的な理解のため)
  • C言語のビット演算子(&, |)とgoto文の一般的な知識
  • Go言語のcmd/gcにおけるNode構造体やtopフラグ、isptr配列などの内部表現に関する情報(Goのソースコードから直接読み解いたもの)