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

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

このコミットは、Go言語のパーサーにおけるエラーメッセージの改善を目的としています。具体的には、== (等価演算子) が期待される文脈で誤って = (代入演算子) が使用された場合に、より分かりやすいエラーメッセージを生成するように変更が加えられています。これにより、開発者がコードの誤りを迅速に特定し、修正する手助けとなります。

コミット

commit 916f4cfa64822c9b7f407544a73c44fc51248a5c
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Mar 11 15:23:18 2013 -0700

    go/parser: better error message if = is seen instead of ==
    
    Fixes #4519.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7733044

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

https://github.com/golang/go/commit/916f4cfa64822c9b7f407544a73c44fc51248a5c

元コミット内容

go/parser: better error message if = is seen instead of ==

Fixes #4519.

R=rsc
CC=golang-dev
https://golang.org/cl/7733044

変更の背景

Go言語のパーサーは、ソースコードを解析して抽象構文木 (AST) を構築する役割を担っています。このプロセスにおいて、文法的に誤ったコードが入力された場合、パーサーはエラーを報告します。本コミットの背景には、if文の条件式やその他の右辺 (RHS: Right Hand Side) の式において、プログラミングの一般的なミスとして == (等価演算子) の代わりに = (代入演算子) を使用してしまうケースがありました。

従来のパーサーでは、このような誤りに対して必ずしも明確なエラーメッセージを提供できていませんでした。例えば、if x = 0 {} のようなコードは、Go言語の文法では if 文の条件式として代入を行うことは許可されていないため、構文エラーとなります。しかし、そのエラーメッセージが「== が期待される」という直接的なものではなく、より一般的な構文エラーとして報告される可能性がありました。

このコミットは、Go言語のIssue #4519 に対応するもので、このような一般的な誤りに対して、より具体的で分かりやすいエラーメッセージ「expected '=='」を生成することで、開発者のデバッグ体験を向上させることを目的としています。

前提知識の解説

このコミットの理解には、以下のGo言語およびコンパイラの基本的な概念が役立ちます。

  • Go言語のパーサー (go/parser): Go言語のソースコードを読み込み、その文法構造を解析して抽象構文木 (AST) を生成するコンポーネントです。ASTは、コンパイラの次の段階(型チェック、コード生成など)で利用されます。
  • トークン (Token): ソースコードを構成する最小単位です。例えば、ifx= 0{} などがそれぞれトークンとして識別されます。Go言語の go/token パッケージで定義されています。
  • 演算子の優先順位 (Operator Precedence): 複数の演算子が存在する場合に、どの演算子が先に評価されるかを決定するルールです。例えば、1 + 2 * 3 では、乗算 (*) が加算 (+) よりも優先されるため、結果は 7 になります。
  • 代入演算子 (=): 変数に値を割り当てるために使用されます。例: x = 10
  • 等価演算子 (==): 2つの値が等しいかどうかを比較するために使用されます。結果はブール値 (true または false) になります。例: x == 10
  • 左辺 (LHS: Left Hand Side) と右辺 (RHS: Right Hand Side): 代入式 a = b において、a が左辺、b が右辺です。一般的に、左辺には値を格納できる変数や式が来ますが、右辺には評価されて値になる式が来ます。
  • 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。パーサーによって生成され、プログラムの意味を理解するための基盤となります。

技術的詳細

このコミットの核心は、Goパーサーが式の右辺 (RHS) を解析しているかどうかを追跡し、そのコンテキストに基づいて = トークンの解釈を変更する点にあります。

  1. inRhs フィールドの導入: parser 構造体に inRhs bool という新しいフィールドが追加されました。このフィールドは、パーサーが現在、式の右辺を解析している最中である場合に true に設定されます。初期値は false です。

  2. parseLhsListparseRhsList での inRhs の管理:

    • parseLhsList(): 左辺のリストを解析する関数です。この関数に入る前に p.inRhs の現在の値を old に保存し、p.inRhsfalse に設定します。これは、左辺の解析中は代入の右辺ではないためです。解析が完了したら、p.inRhsold に戻します。
    • parseRhsList(): 右辺のリストを解析する関数です。この関数に入る前に p.inRhs の現在の値を old に保存し、p.inRhstrue に設定します。これは、右辺の解析中であるためです。解析が完了したら、p.inRhsold に戻します。
  3. parseRhsparseRhsOrType での inRhs の管理: これらの関数も同様に、式の右辺を解析する際に inRhstrue に設定し、解析後に元の値に戻すロジックが追加されました。

  4. tokPrec() 関数の導入と parseBinaryExpr の変更:

    • tokPrec(): 新しく導入されたヘルパー関数です。現在のトークンとその優先順位を返します。この関数が重要なのは、p.inRhstrue (つまり、右辺を解析中) であり、かつ現在のトークンが token.ASSIGN (=) である場合に、トークンを token.EQL (==) に「見せかける」点です。これにより、パーサーは右辺で = を見ても、あたかも == がそこにあるかのように振る舞います。
    • parseBinaryExpr(): 二項演算子を含む式を解析する関数です。この関数内で、従来の p.tok.Precedence() の代わりに p.tokPrec() を呼び出すように変更されました。これにより、右辺のコンテキストで === として扱われ、その優先順位で解析が試みられます。
  5. エラーメッセージの改善: parseBinaryExpr 内で p.expect(op) が呼び出されるようになりました。p.expect は、期待されるトークンが現在のトークンと一致しない場合にエラーを報告する関数です。inRhstrue の場合に === として扱われるため、もし実際に = が存在した場合、p.expect(token.EQL) が呼び出され、token.ASSIGN (=) が見つかったときに「expected '=='」という具体的なエラーメッセージが生成されるようになります。

これらの変更により、パーサーは文脈を認識し、=== の誤用に対してより的確なエラーメッセージを提供できるようになりました。

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

src/pkg/go/parser/parser.go

  • parser 構造体に inRhs bool フィールドが追加されました。
  • parseLhsList() 関数と parseRhsList() 関数で、inRhs フィールドの保存と復元、および設定が行われるようになりました。
  • tokPrec() という新しいヘルパー関数が追加されました。この関数は、inRhstrue かつ現在のトークンが = の場合に、トークンを == として扱います。
  • parseBinaryExpr() 関数内で、トークンの優先順位を取得する際に p.tok.Precedence() の代わりに p.tokPrec() が使用されるようになりました。また、p.next() の代わりに p.expect(op) が使用されるようになりました。
  • parseRhs() 関数と parseRhsOrType() 関数で、inRhs フィールドの保存と復元、および設定が行われるようになりました。

src/pkg/go/parser/short_test.go

  • invalids 変数に、=== の代わりに使った場合の新しいテストケースが3つ追加されました。これらのテストケースは、期待されるエラーメッセージが「expected '=='」であることを検証します。

    • package p; func f() { if x := g(); x = /* ERROR "expected '=='" */ 0 {}};
    • package p; func f() { _ = x = /* ERROR "expected '=='" */ 0 {}};
    • package p; func f() { _ = 1 == func()int { var x bool; x = x = /* ERROR "expected '=='" */ true; return x }() };

コアとなるコードの解説

parser 構造体への inRhs フィールドの追加

type parser struct {
	// ...
	exprLev int  // < 0: in control clause, >= 0: in expression
	inRhs   bool // if set, the parser is parsing a rhs expression
	// ...
}

inRhs は、パーサーが現在、式の右辺を解析しているかどうかを示すブール値のフラグです。このフラグが true の場合、パーサーは特定の文脈で === として解釈しようとします。

parseLhsListparseRhsList における inRhs の管理

func (p *parser) parseLhsList() []ast.Expr {
	old := p.inRhs
	p.inRhs = false // 左辺の解析中は右辺ではない
	list := p.parseExprList(true)
	// ...
	p.inRhs = old // 復元
	return list
}

func (p *parser) parseRhsList() []ast.Expr {
	old := p.inRhs
	p.inRhs = true // 右辺の解析中
	list := p.parseExprList(false)
	p.inRhs = old // 復元
	return list
}

これらの関数は、それぞれ代入の左辺と右辺の式リストを解析します。inRhs フラグを適切に設定・復元することで、パーサーが現在の解析コンテキストを正確に把握できるようにします。

tokPrec() 関数の導入

func (p *parser) tokPrec() (token.Token, int) {
	tok := p.tok
	if p.inRhs && tok == token.ASSIGN {
		tok = token.EQL // 右辺で = が見つかったら == として扱う
	}
	return tok, tok.Precedence()
}

この関数は、現在のトークンとその優先順位を返します。最も重要なのは、inRhstrue であり、かつ現在のトークンが token.ASSIGN (=) である場合に、返されるトークンを token.EQL (==) に変更する点です。これにより、パーサーは右辺のコンテキストで === と誤認し、その後の解析で == が期待されるというエラーを生成する準備をします。

parseBinaryExpr の変更

func (p *parser) parseBinaryExpr(lhs bool, prec1 int) ast.Expr {
	// ...
	x := p.parseUnaryExpr(lhs)
	// 変更前: for prec := p.tok.Precedence(); prec >= prec1; prec-- {
	for _, prec := p.tokPrec(); prec >= prec1; prec-- { // tokPrec() を使用
		for {
			// 変更前: for p.tok.Precedence() == prec {
			op, oprec := p.tokPrec() // tokPrec() を使用
			if oprec != prec {
				break
			}
			// 変更前: pos, op := p.pos, p.tok; p.next()
			pos := p.expect(op) // p.expect(op) を使用
			// ...
		}
	}
	// ...
}

parseBinaryExpr は二項演算子を解析する主要な関数です。ここで p.tok.Precedence() の代わりに p.tokPrec() が呼び出されることで、inRhs の状態に応じたトークンの解釈が適用されます。また、p.expect(op) を使用することで、期待されるトークンと実際のトークンが異なる場合に、より具体的なエラーメッセージが生成されるようになります。例えば、inRhstrue の状態で = が現れた場合、tokPrec()== を返し、p.expect(token.EQL) が呼び出されます。しかし、実際のトークンは = なので、p.expect は「expected '=='」というエラーを報告します。

parseRhsparseRhsOrType における inRhs の管理

func (p *parser) parseRhs() ast.Expr {
	old := p.inRhs
	p.inRhs = true
	x := p.checkExpr(p.parseExpr(false))
	p.inRhs = old
	return x
}

func (p *parser) parseRhsOrType() ast.Expr {
	old := p.inRhs
	p.inRhs = true
	x := p.checkExprOrType(p.parseExpr(false))
	p.inRhs = old
	return x
}

これらの関数も、式の右辺を解析する際に inRhs フラグを一時的に true に設定し、解析後に元の状態に戻すことで、tokPrec() 関数が正しく機能するためのコンテキストを提供します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (go.dev)
  • Go言語のソースコード (github.com/golang/go)
  • コンパイラの設計に関する一般的な知識