はじめに
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)
というパーサを考えます。これは「アルファベット+数値+アルファベット」か「数値+アルファベット+数値」を受理するパーサで、例えば123abc123やabc123abcを受理します。
このパーサに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_locateのLocatedSpanなどに変更します。
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に失敗したことが分かります。