はじめに
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
に失敗したことが分かります。