構文解析器にエラー出力機能を実装する
構文エラーが無視されてしまう
前回はlet文の構文解析の一部を実装しました。その過程で ExpectPeek()
で先読み結果を判定する処理を実装し、必要なトークンが存在するかどうかを見て処理を分岐しました。具体的には "=" があるかどうかを以下のコードで判定しています。
Parsing/Parser.cs
public class Parser
{
// ..
public LetStatement ParseLetStatement()
{
// ..
// 等号 =
if (!this.ExpectPeek(TokenType.ASSIGN)) return null;
// ..
}
private bool ExpectPeek(TokenType type)
{
// 次のトークンが期待するものであれば読み飛ばす
if (this.NextToken.Type == type)
{
this.ReadToken();
return true;
}
return false;
}
}
ParseLetStatement()
はlet文をパースして、LetStatement
ノードを作成して返します。しかし正しい構文出なかった場合、今のところ Null を返してみなかったことにしています。これではどのようなエラーが発生したかわからずデバッグが困難になります。
よって構文解析を行う際に発生したエラーを管理できるようにします。とはいっても大げさなものではありません。
エラー出力機能の実装
エラー内容を保持できるように List でエラー内容を管理します。ExpectPeek()
で期待するトークン以外が続く場合に、エラー内容を追加するようにします。
Parsing/Parser.cs
public class Parser
{
// ..
public List<string> Errors { get; set; } = new List<string>();
// ..
private bool ExpectPeek(TokenType type)
{
// 次のトークンが期待するものであれば読み飛ばす
if (this.NextToken.Type == type)
{
this.ReadToken();
return true;
}
this.AddNextTokenError(type, this.NextToken.Type);
return false;
}
private void AddNextTokenError(TokenType expected, TokenType actual)
{
this.Errors.Add($"{actual.ToString()} ではなく {expected.ToString()} が来なければなりません。");
}
}
ExpectPeek()
が false を返す時、構文がおかしいということです。したがって期待するトークンに対して間違ったトークンが来てしまっているということなので、false をリターンする前にそのエラーを追加するようにします。AddNextTokenError()
はエラーメッセージをリストに追加します。
では、テストを修正して動作を確認します。
[TestMethod]
public void TestLetStatement1()
{
var input = @"let x = 5;
let y = 10;
let xyz = 838383;";
var lexer = new Lexer(input);
var parser = new Parser(lexer);
var root = parser.ParseProgram();
this._CheckParserErrors(parser);
// ..
}
private void _CheckParserErrors(Parser parser)
{
if (parser.Errors.Count == 0) return;
var message = "\n" + string.Join("\n", parser.Errors);
Assert.Fail(message);
}
_CheckParserErrors
は構文解析でエラーが発生していればテストでエラーをぶん投げるチェック処理です。エラーがなければ今まで通りテストは通ります。この処理を ParseProgram()
実行直後に呼び出します。
エラーの出力はエラーメッセージすべてを改行区切りで結合して出力しています。個別に出力するスマートな方法がわからなかったのでまとめて出しています。書籍だとループして個別に出力しています..。
構文エラーがなければ分からないので、例えば入力するコードを以下のようにしてみます。
let x 5;
let = 10;
let;
等号がない場合、識別子がない場合、両方ない場合、すべてエラーになります。この入力でテストすると次のようなエラーが発生します。
テスト名: TestLetStatement1
テストの完全名: UnitTestProject.ParserTest.TestLetStatement1
テスト ソース: E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs : 行 14
テスト成果: 失敗
テスト継続時間: 0:00:00.0428278
結果 のスタック トレース:
at UnitTestProject.ParserTest._CheckParserErrors(Parser parser) in E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs:line 68
at UnitTestProject.ParserTest.TestLetStatement1() in E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs:line 23
結果 のメッセージ: Assert.Fail failed.
INT ではなく ASSIGN が来なければなりません。
ASSIGN ではなく IDENT が来なければなりません。
SEMICOLON ではなく IDENT が来なければなりません。
3箇所のlet構文エラーがすべて出力されています。これで対応箇所がすぐにわかるというものです。もしトークンで行番号や列数を管理しているなら、ここに出力することでより詳細なエラー内容が把握できます。
動作が確認出来たらテストコードは戻しておきます。
短いですがここまでにします。次回はreturn文です。これも多分短く済みます。
ここまでのソースです。
mntm0/Gorilla at 2c697f53a631b440a4899227cbaca807e571e582
以上。
コメントを書く