LoginSignup
11
4

More than 3 years have passed since last update.

nomのパースエラー表示を改善する: nom-greedyerror

Posted at

はじめに

Rustのパーサコンビネータライブラリnomで詳細なパースエラーを出す方法と、その表示を改善するクレートnom-greedyerrorについて解説します。

nomのエラー型

nomに実装済みのエラー型は2種類あります。Iをパーサの入力型とすると(I, ErrorKind)VerboseError<I>です。パーサが返すIResultのエラー型はデフォルトでは(I, ErrorKind)となっていて、これは最も直近で失敗したパーサの情報しか返すことができません。それに対し、VerboseErrorは失敗したパーサの履歴を保持しているので、最終的に失敗するまでにどのような失敗を経てきたのかを知ることができます。

VerboseErrorの使用方法

例えば

    alt((
        tuple((alpha1, digit1, alpha1)),
        tuple((digit1, alpha1, digit1)),
    ))(input)

というパーサを考えます。これは「アルファベット+数値+アルファベット」か「数値+アルファベット+数値」を受理するパーサで、例えば123abc123abc123abcを受理します。
このパーサにVerboseErrorを適用したいときは以下のようにします。

fn parser<'a, E: ParseError<&'a str>>(
    input: &'a str,
) -> IResult<&'a str, (&'a str, &'a str, &'a str), E> {
    alt((
        tuple((alpha1, digit1, alpha1)),
        tuple((digit1, alpha1, digit1)),
    ))(input)
}

型変数E: ParseError<&'a str>>を使うことで、(I, ErrorKind)でもVerboseErrorでもどちらでも使えるパーサができます。(入力型Iも変数にできますが省略します)

let error = parser::<(&str, ErrorKind)>>("abc012:::");
let error = parser::<VerboseError<&str>>("abc012:::");

使うときはこのようにします。またVerboseErrorの場合は、エラーを分かりやすく表示してくれるconvert_errorという関数が提供されています。

match error {
    Err(nom::Err::Error(e)) => println!("{}", convert_error("abc012:::", e)),
    _ => (),
};

これによってどのパーサがどこで失敗したかが分かるようになっています。

0: at line 0, in Digit:
abc012:::
^

1: at line 0, in Alt:
abc012:::
^

エラー表示の問題点

VerboseErrorの表示は一見良さそうなのですが、少し気になる点があります。
感覚的には123abc::::::でエラーが出てほしいです。

0: at line 0, in Digit:
abc012:::
      ^     <--- ここでエラーでは?

1: at line 0, in Alt:
abc012:::
^

VerboseError:::でエラーを出せないのはaltコンビネータにおけるエラー選択方法によります。
altでは子パーサtuple((alpha1, digit1, alpha1))tuple((digit1, alpha1, digit1))をそれぞれ試して両方失敗した場合alt自体も失敗とします。その際VerboseErrorの実装では最後に試したパーサのエラーを返すことになっています。
なのでtuple((digit1, alpha1, digit1))を最後に試して「先頭が数値でない」というエラーになります。
しかし実際にはtuple((alpha1, digit1, alpha1))を試した際にかなり長く受理出来ていて、最後のalpha1だけ失敗している状態なので、こちらのエラーを返す方がエラーメッセージとしては良くなりそうです。

GreedyError

これを改善するためにVerboseErrorのうちaltでのエラー選択方法を変更してGreedyErrorを実装しました。

GreedyErrorではaltで複数のエラーが検知されたときに、最も長く受理されたエラーを選択します。
使い方はVerboseErrorとほぼ同じですが、「最も長い」ことを判定するために入力型から現在位置を取得できる必要があります。
そのため入力型をnom_locateLocatedSpanなどに変更します。

type Span<'a> = LocatedSpan<&'a str>;
let error = parser::<GreedyError<Span>>("abc012:::");

これにより先ほどのエラーは

0: at line 0, in Alpha:
abc012:::
      ^

1: at line 0, in Alt:
abc012:::
^

となって、:::alpha1に失敗したことが分かります。

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4