06 構文解析器にエラー出力機能を実装する [オリジナル言語インタプリタを作る]

06 構文解析器にエラー出力機能を実装する [オリジナル言語インタプリタを作る]

構文解析器にエラー出力機能を実装する

構文エラーが無視されてしまう

前回は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

以上。

開発いろいろカテゴリの最新記事