Rust用PEGパーサジェネレータのrust-pegの紹介 [2]


2023年 11月 24日

この記事ではrust-pegでパーサを書くにあたってのtipsを紹介します。

空白文字の扱い

前回の記事で書いたパーサは数式の数字と演算子の間に空白を置くことができません.置けるように改善します.
通常のルールは呼び出す時にカッコが必要ですが,rust-pegでは___などのアンダースコアで構成されたルールはカッコ無しで記述できます.
文法の本質に関わらない非常に多く呼び出されるルールはアンダースコアで記述すると可読性の高いコードが書けます.

peg::parser! {
    pub grammar parser() for str {
        pub rule calc() -> f64
            = precedence! {
                l:(@) _ "+" _ r:@ { l + r }
                --
                l:(@) _ "*" _ r:@ { l * r }
                --
                n:number() { n }
                "(" _ c:calc() _ ")" { c }
            }
// ...省略...
        rule _()
            = quiet!{ " "* }
    }
}

空白文字は_,改行も許される部分には__などと使い分けると良いでしょう.

繰り返し

yacc等では配列の宣言のような繰り返しを含む文法はルールの再帰によってパーサを定義しますが,rust-pegでは繰り返しを表現する文法があります.
array()ルールではカンマで区切られた数字の配列をパースしVecを返します.先に紹介した_ルールで空白文字を入れることが可能ですが,配列最後の区切り文字(ケツカンマ)は許容されません.最後のカンマを許容するパーサは下記array2()のように?を使うと書けます.

pub rule array() -> Vec<f64>
    = "[" _ n:(number() ** (_ "," _)) _ "]" { n }

pub rule array2() -> Vec<f64>
    = "[" _ n:(number() ** (_ "," _)) _ ","? _ "]" { n }

#[test]
fn test2() {
    assert!(parser::array("[1,2,3]").is_ok());
    assert!(parser::array("[ 1 , 2 , 3 ]").is_ok());
    assert!(parser::array("[1,2,3,]").is_err());

    assert!(parser::array2("[1,2,3]").is_ok());
    assert!(parser::array2("[1,2,3,]").is_ok());
}

左再帰の問題

rust-pegでは,precedence!{}マクロや**などにより左再帰問題が発生することは少ないですが,問題になる際は#[cache_left_rec]を利用できます.(参考
次の例は左結合の割り算の演算子を定義するルールです.左再帰を定義しても無限ループしません.

// パーサー定義内
#[cache_left_rec]
 pub rule left_recursion() -> f64
     = l:left_recursion() "/" r:number() { l / r } / n:number() { n }

dbg!(parser::left_recursion("8/2/2").unwrap()); // => 2

エラーレポート

パーサは返り値としてResult型の値を返します.Okの場合はルールの返り値の値になりますが,Errの場合はエラーの発生した位置・期待されるルールの入った構造体が入っています.
例えば,先に定義したcalc()ルールに"1++2"という入力をすると次のような値が返ります.

dbg!(parser::calc("1++2"));

// エラーの内容
[src/main.rs:12] parser::calc("1++2") = Err(
    ParseError {
        location: LineCol {
            line: 1,
            column: 3,
            offset: 2,
        },
        expected: ExpectedSet {
            expected: {
                "\"(\"",
                "['0' ..= '9']", // この記述が何を意味しているか分かりにくい
            },
        },
    },
)

expectedフィールドには次の入力で期待されるルールの一覧が表示されますがこの状態ではそれぞれの定義のみが表示されるだけで,なにを表しているか分かりにくいです.
そこでnumber()ルールの定義を変更してこの表示を分かりやすくしましょう.

pub rule number() -> f64
    = quiet!{
        n:$(['0'..='9']+ ("." ['0'..='9']+)?) {?
        n.parse().or(Err("can't parse number"))
        }
    } / expected!("number")

quiet!{}マクロは囲われたルールのマッチが失敗してもエラーに表示しないようにするマクロです.上の例では、quiet!{} マクロに続けて expected!() マクロを書くことで、エラーメッセージに "number" が表示されるようにしています。 このコードを実行すると、以下のようなエラーが返ります。

// エラーの内容
[src/main.rs:12] parser::calc("1++2") = Err(
  ParseError {
    location: LineCol {
      line: 1,
      column: 3,
      offset: 2,
    },
    expected: ExpectedSet {
      expected: {
        "\"(\"",
        "number", // "number"が次に期待されていることが分かりやすい
      },
    },
  },
)