エラー回復によるパーサーの堅牢化

標準設定では、パーサーは入力にエラーが見つかると即座に処理を中断します。しかし実際には、複数の問題を一度に検出したいケースも多く存在します。LALRPOPではこの要件に対応できますが、文法定義内で明示的に「エラー回復ポイント」を指定する必要があります。これは特別な!トークンを使用して実現します。このトークンはパーサーが入力エラーを検出した際にのみ出現し、エラー発生時にパーサーはこのトークンをストリームに挿入し、可能なアクションを実行した後、再開可能なトークンが見つかるまで不要な入力をスキップします。

複数のエラーを検出するためには、まず複数の解析エラーを格納する手段が必要です。LALRPOP単体ではこのような機能を提供しないため、新たにVec型の変数を用意して検出されたエラーを蓄積します。!トークンの結果にはトークン情報が含まれるため、エラー回復処理を行うにはトークンがクローン可能である必要があります。そこで、LALRPOPファイル内のgrammar宣言行を次のように書き換えます:

use lalrpop_util::ErrorRecovery;

grammar<'err>(errors: &'err mut Vec<ErrorRecovery<usize, Token<'input>, &'static str>>);

ErrorRecovery構造体はParseErrorをラップし、スキップされた文字列への参照を持つ追加フィールドを持っています。

エラー回復用の選択肢も他の生成規則と同様の戻り値型を返す必要があるため、エラー発生を示すためにExpr列挙型に新しいバリアントを追加します:

pub enum Expr {
    Number(i32),
    Op(Box<Expr>, Opcode, Box<Expr>),
    Error,
}

文法定義を修正し、!トークンを含む第3の選択肢を追加します。この選択肢では、!から受け取ったErrorRecovery値をerrorsに保存し、Expr::Errorを返却します。!トークンの値はParseError型となります。完全なソースコードはcalculator7で確認できます。

Term: Box<Expr> = {
    Num => Box::new(Expr::Number(<>)),
    "(" <Expr> ")",
    ! => { errors.push(<>); Box::new(Expr::Error) },
};

これにより、欠落したオペランドなど様々なエラーを含んだテストを作成できます。なお、パースメソッドはLALRPOPファイルのgrammar行の変更により、引数が一つ増えています。!トークンが適切に挿入されることで、パーサーがエラー状態から回復していることが確認できます。

#[test]
fn test_error_recovery() {
    let mut error_list = Vec::new();

    let result = calculator7::ExprsParser::new()
        .parse(&mut error_list, "22 * + 3")
        .unwrap();
    assert_eq!(&format!("{:?}", result), "[((22 * error) + 3)]");

    let result = calculator7::ExprsParser::new()
        .parse(&mut error_list, "22 * 44 + 66, *3")
        .unwrap();
    assert_eq!(&format!("{:?}", result), "[((22 * 44) + 66), (error * 3)]");

    let result = calculator7::ExprsParser::new()
        .parse(&mut error_list, "*")
        .unwrap();
    assert_eq!(&format!("{:?}", result), "[(error * error)]");

    assert_eq!(error_list.len(), 4);
}

ただし、エラー回復機能はパーサーレベルで報告されたエラーのみに対応しており、レキサーレベルのエラー(無効なトークンなど)は回復できません。レキサーレベルでのエラー回復が必要な場合、カスタムレクサーにErrorトークンを定義し、エラーを返す代わりに有効なErrorトークンを返すようにします。この手法の詳細な実装例は関連ドキュメントで提供されています。同様のアプローチは組み込みレクサーでも利用可能です。

タグ: lalrpop error-recovery parsing rust

8月2日 16:49 投稿