[インデックス 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)
- コンパイラの設計に関する一般的な知識