[インデックス 12490] ファイルの概要
このコミットは、Go言語のパーサー(go/parserパッケージ)におけるエラー同期メカニズムを改善し、より正確で単一のエラー報告を実現することを目的としています。特に、gofmtが特定のテストケース(Issue 3106)に対して、以前は複数の不正確なエラーを報告していた問題を解決します。また、一般的なエラーチェックのための新しいテストハーネスと、そのテストケースが追加されています。
コミット
- コミットハッシュ:
c8981c718b50352e66cb4b08ed5682af1c1a5d75 - 作者: Robert Griesemer gri@golang.org
- 日付: 2012年3月7日 (水) 12:24:20 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c8981c718b50352e66cb4b08ed5682af1c1a5d75
元コミット内容
go/parser: better error synchronization
gofmt reports now a single, accurate error for
the test case of issue 3106.
Also: Added test harness for general error
checking and two test cases for now.
Fixes #3106.
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5755062
変更の背景
Go言語のパーサーは、ソースコードを解析して抽象構文木(AST)を構築する役割を担っています。このプロセスにおいて、ソースコードに構文エラーが含まれている場合、パーサーはエラーを検出し、そのエラーを報告する必要があります。しかし、単にエラーを報告するだけでなく、その後の解析を継続し、可能な限り多くのエラーを一度に検出できるように「エラー回復(error recovery)」を行うことが重要です。
このコミットの背景には、Goパーサーのエラー回復メカニズムが不十分であったという問題があります。特に、Issue 3106で報告されたケースでは、特定の構文エラーが発生した際に、パーサーが適切にエラー回復できず、その結果として後続のコードに対しても誤ったエラーを連鎖的に報告してしまう、いわゆる「エラーの滝(error cascade)」が発生していました。これにより、gofmtのようなツールが、単一の根本的な問題に対して多数の無関係なエラーメッセージを出力し、ユーザーが真の原因を特定するのを困難にしていました。
このコミットは、このようなエラーの連鎖を防ぎ、より正確で単一のエラー報告を実現するために、パーサーのエラー同期ロジックを改善することを目的としています。具体的には、エラー発生時にパーサーが次に有効な構文要素(例えば、新しいステートメントや宣言の開始)までスキップすることで、不正確なエラーの報告を減らし、ユーザーにとってより有用なエラーメッセージを提供することを目指しています。
前提知識の解説
1. 字句解析(Lexical Analysis)と構文解析(Parsing)
コンパイラやインタープリタのフロントエンドは、通常、字句解析と構文解析の2つの主要なフェーズに分かれます。
- 字句解析(Lexical Analysis / Scanning): ソースコードを読み込み、意味のある最小単位である「トークン(token)」のストリームに変換します。例えば、
var x = 10;というコードは、var(キーワード),x(識別子),=(演算子),10(整数リテラル),;(区切り文字) といったトークンに分解されます。 - 構文解析(Syntax Analysis / Parsing): 字句解析器から受け取ったトークンのストリームが、言語の文法規則に準拠しているかを検証し、その構造を表現する「抽象構文木(Abstract Syntax Tree: AST)」を構築します。ASTは、プログラムの構造を階層的に表現したもので、コンパイラの後のフェーズ(意味解析、コード生成など)で利用されます。
Go言語では、go/scannerパッケージが字句解析を、go/parserパッケージが構文解析を担当します。
2. パーサーにおけるエラー回復と同期
構文解析中に文法エラーが検出された場合、パーサーはエラーを報告するだけでなく、解析を継続しようとします。このプロセスを「エラー回復(Error Recovery)」と呼びます。エラー回復の目的は、単一の構文エラーが原因で後続の正しいコードまでエラーとして扱われる「エラーの滝」を防ぎ、可能な限り多くのエラーを一度の解析で検出することです。
エラー回復の一般的な戦略の一つに「パニックモード回復(Panic Mode Recovery)」があります。これは、エラーが検出された際に、パーサーが入力ストリームをスキップし、特定の「同期トークン(Synchronization Token)」が見つかるまで読み飛ばす方法です。同期トークンは、通常、文の終わりを示すセミコロン、ブロックの開始/終了を示す括弧、または新しい宣言やステートメントの開始を示すキーワード(func, var, if, forなど)です。パーサーは、これらの同期トークンを見つけることで、構文解析を再開できると期待します。
このコミットにおける「エラー同期(Error Synchronization)」とは、まさにこのパニックモード回復の効率と正確性を向上させることを指します。パーサーがエラー発生時に、次に解析を再開すべき「安全な場所」をより適切に判断できるようにすることで、誤ったエラー報告を減らし、より有用な診断メッセージを提供します。
3. go/astパッケージとast.BadExpr/ast.BadStmt/ast.BadDecl
Go言語のgo/astパッケージは、Goプログラムの抽象構文木(AST)を表現するためのデータ構造を提供します。ASTは、プログラムのソースコードの構造を木構造で表現したものです。
ast.BadExpr, ast.BadStmt, ast.BadDeclは、go/astパッケージで定義されている特別なASTノードです。これらは、パーサーが構文エラーを検出した際に、そのエラーのある部分をAST内で表現するために使用されます。例えば、不正な式が見つかった場合、その部分にはast.BadExprノードが挿入されます。これにより、パーサーはエラーのある部分をマークしつつ、解析を継続してASTを構築することができます。
このコミットでは、これらのBadノードが既に挿入されている場合に、重複してエラーを報告しないようにするロジックが追加されています。これは、エラー回復の改善と密接に関連しており、単一の根本的なエラーに対して複数のエラーメッセージが生成されるのを防ぐのに役立ちます。
技術的詳細
このコミットの技術的な核心は、Goパーサーのparser.goファイルにおけるエラー回復ロジックの強化と、新しいテストハーネスの導入です。
1. エラー回復の改善 (parser.go)
-
expectSemi()関数の変更: 以前のexpectSemi()関数は、セミコロンが期待される場所でtoken.SEMICOLONを期待していました。しかし、Goの文法では、セミコロンは閉じ括弧()や})の前に省略可能です。この変更では、p.tok != token.RPAREN && p.tok != token.RBRACEという条件が追加され、閉じ括弧の前ではセミコロンを必須としないようになりました。 さらに重要なのは、セミコロンが期待されるが実際には存在しない場合に、パーサーがエラーを報告した後、for !isStmtSync(p.tok) { p.next() }というループでトークンをスキップするようになった点です。これは、次に有効なステートメントの開始トークン(同期トークン)が見つかるまで読み飛ばすことで、エラーの連鎖を防ぐためのパニックモード回復の典型的な実装です。 -
新しい同期ヘルパー関数
isStmtSync()とisDeclSync(): これらの関数は、それぞれ現在のトークンが新しいステートメントまたは宣言の開始を示す同期トークンであるかどうかを判断します。isStmtSync(tok token.Token):break,const,continue,defer,fallthrough,for,go,goto,if,return,select,switch,type,var,EOFといったキーワードがステートメントの開始トークンとして認識されます。isDeclSync(tok token.Token):const,type,var,EOFといったキーワードが宣言の開始トークンとして認識されます。 これらの関数は、エラー回復時にパーサーが次に解析を再開すべき「安全な場所」を特定するために使用されます。
-
parseOperand()関数の変更: オペランドが期待される場所でエラーが発生した場合、以前は単にp.next()で次のトークンに進んでいました。変更後、if !isStmtSync(p.tok) { p.next() }という条件が追加され、現在のトークンがステートメントの同期トークンでない場合にのみ次のトークンに進むようになりました。これにより、エラー発生時に不必要に多くのトークンをスキップしすぎたり、逆にスキップが足りずにエラーが連鎖したりするのを防ぎます。 -
parseStmt()およびparseDecl()関数の変更: これらの関数も、エラー回復ロジックが強化されました。parseStmt(): ステートメントが期待される場所でエラーが発生した場合、for !isStmtSync(p.tok) { p.next() }ループを使用して、次のステートメント同期トークンまでスキップします。parseDecl(): 宣言が期待される場所でエラーが発生した場合、for !isSync(p.tok) { p.next() }ループを使用して、引数として渡された同期関数(isDeclSyncまたはisStmtSync)がtrueを返すまでスキップします。これにより、宣言の解析中にエラーが発生しても、パーサーが次の宣言またはステートメントの開始まで適切に回復できるようになります。
-
ast.BadExprなどの重複エラー報告の抑制:makeIdentList(),parseCallExpr(),parseReceiver()などの関数において、既にast.BadExprなどの「不正な」ASTノードが挿入されている場合には、新たにエラーを報告しないようにする条件(if _, isBad := x.(*ast.BadExpr); !isBadなど)が追加されました。これは、単一の構文エラーが原因で複数のエラーメッセージが生成されるのを防ぐための重要な改善です。
2. 新しいテストハーネス (error_test.go)
このコミットでは、Goパーサーのエラー報告の正確性を検証するための新しいテストハーネスが導入されました。
-
error_test.go: この新しいファイルは、パーサーが報告するエラーメッセージを、テストファイル内に埋め込まれた期待されるエラーメッセージと比較するためのフレームワークを提供します。- テストファイルは
.src拡張子を持ち、gofmtによる変更から保護されます。 - 期待されるエラーは、
/* ERROR "rx" */という形式のコメントで指定されます。rxは、期待されるエラーメッセージにマッチする正規表現です。 - テストハーネスは、ソースファイルを解析し、パーサーが報告したエラーと、テストファイルに記述された期待されるエラーを比較します。これにより、エラーメッセージの内容、位置、そしてエラーの数が正確であるかを自動的に検証できます。
- テストファイルは
-
testdata/commas.srcとtestdata/issue3106.src: これらのファイルは、新しいテストハーネスを使用する具体的なテストケースです。commas.src: カンマの欠落に関するエラー報告と回復をテストします。issue3106.src: Issue 3106で報告された特定のシナリオを再現し、パーサーが単一の正確なエラーを報告し、適切に回復できることを検証します。このファイルには、defer ifという不正な構文が含まれており、以前はここで複数のエラーが報告されていました。
これらの変更により、Goパーサーはより堅牢になり、開発者にとってより有用なエラーメッセージを提供するようになりました。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その中のコアとなる変更箇所は以下の通りです。
src/pkg/go/parser/parser.go
func (p *parser) expectSemi():- セミコロンが閉じ括弧の前で省略可能になる条件が追加。
- セミコロンがない場合のエラー回復ロジック(
for !isStmtSync(p.tok) { p.next() })が追加。
func isStmtSync(tok token.Token) bool: 新規追加。ステートメントの同期トークンを判定。func isDeclSync(tok token.Token) bool: 新規追加。宣言の同期トークンを判定。func (p *parser) makeIdentList(list []ast.Expr) []*ast.Ident:ast.BadExprが既に存在する場合、重複してエラーを報告しない条件が追加。
func (p *parser) parseOperand(lhs bool) ast.Expr:- エラー発生時のトークンスキップロジックが
isStmtSyncを使用するように変更。
- エラー発生時のトークンスキップロジックが
func (p *parser) parseCallExpr() *ast.CallExpr:ast.BadExprが既に存在する場合、重複してエラーを報告しない条件が追加。
func (p *parser) parseStmt() (s ast.Stmt):- ステートメントが見つからない場合のエラー回復ロジック(
for !isStmtSync(p.tok) { p.next() })が追加。
- ステートメントが見つからない場合のエラー回復ロジック(
func (p *parser) parseReceiver(scope *ast.Scope) *ast.FieldList:ast.BadExprが既に存在する場合、重複してエラーを報告しない条件が追加。
func (p *parser) parseDecl() ast.Decl:- 引数に
isSync func(token.Token) boolが追加され、汎用的な同期関数を受け取るように変更。 - 宣言が見つからない場合のエラー回復ロジック(
for !isSync(p.tok) { p.next() })が追加。
- 引数に
func (p *parser) parseFile() *ast.File:p.parseDecl()の呼び出しがp.parseDecl(isDeclSync)に変更され、宣言の同期関数が渡されるように変更。
src/pkg/go/parser/error_test.go
- ファイル全体: 新規追加。Goパーサーのエラー報告をテストするためのフレームワーク。
expectedErrors(): テストファイル内の/* ERROR "rx" */コメントから期待されるエラーを抽出。compareErrors(): 報告されたエラーと期待されるエラーを比較し、不一致を報告。checkErrors(): 個々のテストファイルを解析し、エラーを検証するヘルパー関数。TestErrors():testdataディレクトリ内の.srcファイルを走査し、checkErrorsを呼び出すメインのテスト関数。
src/pkg/go/parser/testdata/commas.src
- ファイル全体: 新規追加。カンマの欠落に関するエラー報告をテストするソースファイル。
src/pkg/go/parser/testdata/issue3106.src
- ファイル全体: 新規追加。Issue 3106の特定のシナリオを再現し、パーサーのエラー回復をテストするソースファイル。
コアとなるコードの解説
parser.go の変更点
-
エラー回復の強化:
expectSemi()関数におけるisStmtSyncの導入は、セミコロンの欠落という一般的なエラーに対して、パーサーが次に有効なステートメントの開始まで効率的にスキップできるようにします。これにより、セミコロンの欠落が原因で発生する後続の構文エラーの連鎖を大幅に削減できます。isStmtSyncとisDeclSyncは、パーサーがエラー発生時に「どこまでスキップすれば、次に意味のある構文要素を解析できるか」を判断するための明確な基準を提供します。これにより、パーサーはエラーのある部分を飛び越え、健全な状態から解析を再開できるようになります。これは、パニックモード回復戦略の具体的な実装です。parseOperand(),parseStmt(),parseDecl()におけるisStmtSyncやisSyncの利用は、パーサーが式、ステートメント、宣言の解析中にエラーに遭遇した場合の回復ロジックを統一し、より堅牢にします。エラーが発生しても、パーサーは無闇にトークンを消費するのではなく、次に解析を再開できる安全なポイントまで進むことを試みます。
-
重複エラー報告の抑制:
makeIdentList(),parseCallExpr(),parseReceiver()などで追加されたif _, isBad := x.(*ast.BadExpr); !isBadのようなチェックは非常に重要です。これは、パーサーが一度ast.BadExprなどの「不正な」ASTノードを挿入してエラーをマークした場合、その同じエラー箇所に対して再度エラーメッセージを生成しないようにするためのものです。これにより、単一の根本的な構文エラーが、複数の冗長なエラーメッセージとして報告されるのを防ぎ、gofmtのようなツールがよりクリーンで分かりやすいエラー出力を提供できるようになります。
error_test.go および testdata の追加
- テストハーネスの導入:
error_test.goは、Goパーサーの品質保証において極めて重要な役割を果たします。このテストハーネスは、パーサーが特定のエラーを正確な位置で、かつ期待されるメッセージで報告するかどうかを自動的に検証するメカニズムを提供します。/* ERROR "rx" */コメントによるエラーの指定方法は、テストコードとテスト対象のソースコードを密接に結びつけ、エラーの期待値を非常に明確に記述することを可能にします。これにより、パーサーの変更がエラー報告の挙動に与える影響を容易に確認でき、リグレッション(退行)を防ぐことができます。commas.srcやissue3106.srcのような具体的なテストケースは、パーサーの特定のエラー回復シナリオをカバーし、実際のコードで発生しうる問題を再現して検証します。特にissue3106.srcは、このコミットの直接的な動機となった問題が解決されたことを示す「ゴールデンテストケース」としての役割を果たします。
これらの変更は、Go言語のツールチェーン、特にgofmtのようなツールが、ユーザーに対してより正確で有用なエラーメッセージを提供するための基盤を強化するものです。パーサーのエラー回復能力が向上することで、開発者は構文エラーをより迅速に特定し、修正できるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/c8981c718b50352e66cb4b08ed5682af1c1a5d75
- Go Issue 3106: https://go.dev/issue/3106 (このコミットが修正した問題)
- Gerrit Change-Id:
https://golang.org/cl/5755062
参考にした情報源リンク
- Go言語の公式ドキュメント(
go/parser,go/ast,go/tokenパッケージ) - コンパイラ設計に関する一般的な書籍やオンラインリソース(特に構文解析とエラー回復の章)
- Go言語のIssueトラッカー(Issue 3106の詳細)
- Go言語のソースコード(
src/pkg/go/parserディレクトリ)