[インデックス 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): ソースコードを構成する最小単位です。例えば、
if、x、=、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) を解析しているかどうかを追跡し、そのコンテキストに基づいて = トークンの解釈を変更する点にあります。
-
inRhsフィールドの導入:parser構造体にinRhs boolという新しいフィールドが追加されました。このフィールドは、パーサーが現在、式の右辺を解析している最中である場合にtrueに設定されます。初期値はfalseです。 -
parseLhsListとparseRhsListでのinRhsの管理:parseLhsList(): 左辺のリストを解析する関数です。この関数に入る前にp.inRhsの現在の値をoldに保存し、p.inRhsをfalseに設定します。これは、左辺の解析中は代入の右辺ではないためです。解析が完了したら、p.inRhsをoldに戻します。parseRhsList(): 右辺のリストを解析する関数です。この関数に入る前にp.inRhsの現在の値をoldに保存し、p.inRhsをtrueに設定します。これは、右辺の解析中であるためです。解析が完了したら、p.inRhsをoldに戻します。
-
parseRhsとparseRhsOrTypeでのinRhsの管理: これらの関数も同様に、式の右辺を解析する際にinRhsをtrueに設定し、解析後に元の値に戻すロジックが追加されました。 -
tokPrec()関数の導入とparseBinaryExprの変更:tokPrec(): 新しく導入されたヘルパー関数です。現在のトークンとその優先順位を返します。この関数が重要なのは、p.inRhsがtrue(つまり、右辺を解析中) であり、かつ現在のトークンがtoken.ASSIGN(=) である場合に、トークンをtoken.EQL(==) に「見せかける」点です。これにより、パーサーは右辺で=を見ても、あたかも==がそこにあるかのように振る舞います。parseBinaryExpr(): 二項演算子を含む式を解析する関数です。この関数内で、従来のp.tok.Precedence()の代わりにp.tokPrec()を呼び出すように変更されました。これにより、右辺のコンテキストで=が==として扱われ、その優先順位で解析が試みられます。
-
エラーメッセージの改善:
parseBinaryExpr内でp.expect(op)が呼び出されるようになりました。p.expectは、期待されるトークンが現在のトークンと一致しない場合にエラーを報告する関数です。inRhsがtrueの場合に=が==として扱われるため、もし実際に=が存在した場合、p.expect(token.EQL)が呼び出され、token.ASSIGN(=) が見つかったときに「expected '=='」という具体的なエラーメッセージが生成されるようになります。
これらの変更により、パーサーは文脈を認識し、= と == の誤用に対してより的確なエラーメッセージを提供できるようになりました。
コアとなるコードの変更箇所
src/pkg/go/parser/parser.go
parser構造体にinRhs boolフィールドが追加されました。parseLhsList()関数とparseRhsList()関数で、inRhsフィールドの保存と復元、および設定が行われるようになりました。tokPrec()という新しいヘルパー関数が追加されました。この関数は、inRhsがtrueかつ現在のトークンが=の場合に、トークンを==として扱います。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 の場合、パーサーは特定の文脈で = を == として解釈しようとします。
parseLhsList と parseRhsList における 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()
}
この関数は、現在のトークンとその優先順位を返します。最も重要なのは、inRhs が true であり、かつ現在のトークンが 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) を使用することで、期待されるトークンと実際のトークンが異なる場合に、より具体的なエラーメッセージが生成されるようになります。例えば、inRhs が true の状態で = が現れた場合、tokPrec() は == を返し、p.expect(token.EQL) が呼び出されます。しかし、実際のトークンは = なので、p.expect は「expected '=='」というエラーを報告します。
parseRhs と parseRhsOrType における 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 CL: https://golang.org/cl/7733044
- GitHub Commit: https://github.com/golang/go/commit/916f4cfa64822c9b7f407544a73c44fc51248a5c
参考にした情報源リンク
- Go言語の公式ドキュメント (go.dev)
- Go言語のソースコード (github.com/golang/go)
- コンパイラの設計に関する一般的な知識