Better nom Error Messages with nom_locate
UPDATE: The original version of this post only passed the name of the last parser in the chain. This version is updated to show all the parsers that lead to the error.
Parsing Parser Errors
I designed a note taking format called NeoDocnd. It currently exists only as an embedded feature of my personal static site generator. I'm in the process of extracting it to its own library. That means digging into the nomnom parser/combinator and its cryptic as hell error messages.
"Just how cryptic?" you ask. Let me give you an example.
A First Look
This codes makes a tiny rust app that
uses nom's alt feature to attempt
to use two child parsers to process
a piece of text.
Cargo.toml
The parsers attempt to process the words "charlie delta" as input. They both fail since neither of their patterns (i.e. "xxx" or "yyy") match the word. The result is that nom throws this error:
Err(
Error(
Error {
input: "charlie delta",
code: Tag,
},
),
)-
The
input: "charlie delta"shows us what the parser attempted to process. -
The
code: Taglets us know that the error was generated because on of the attempts to use a.tag()failed.Critically, it doesn't tell us which one of them failed.
Figuring out the issue in this example is
no big deal. It's quick to see that
neither the alfa nor the bravo
parsers match the "charlie delta"
input passed to them.
Things aren't so obvious in
real world parsers. There are tons
of different functions branching
out and interacting with
each other. It doesn't take long to cross a
threshold where figuring out where
an error occurred is a non-trivial
task.
I used to use a crate called nom_supremens to help with this. It provided errors with more details about where they came from. Unfortunately, it doesn't work with the most recent version of nom.
That's where the nom_locate crate enters the scene.
Locating Errors
nom_locatenl works by adding a
LocatedSpan struct to the mix.
Your input goes inside and
gets passed around as a passenger.
For example:
Cargo.toml
That codes contains an intentional error
where the bravo_parser fails.
This is the error that gets produced:
Err(
Error(
Error {
input: LocatedSpan {
offset: 5,
line: 1,
fragment: "not bravo",
extra: [
"alfa_parser",
"bravo_parser",
],
},
code: Tag,
},
),
)The input of the Error now contains a
LocatedSpan with more details about where
the error occurred. The extra
field contains the values we appended
to its Vec on lines 19 and 28. Collectively
they give us the path to the parser
that failed.
That's a huge improvement. And, it gets even better.
Showing Off Errors
We'll use a little more complicated example to demonstrate what nom_locate can do. It starts by defining two parser functions:
Cargo.toml
and
These are the same basic type of parsers from the
prior example. The big difference is that first_parser
does a little more work to get us down to a second
line before passing off to second_parser.
That'll help demonstrate the error output
of this report function:
Here's the overview of how it works:
-
The
type ResultHolder<'a>lines just keep us from having to put the entire signature in the function definition. -
The
result.finish()call on line 14 combines a couple of different nom error types so they can be matched in a single call. -
Line 15 prints a success message of everything parsed properly.
-
The
Errbranch that starts on line 16 does a bunch of formatting to print out a nice error message. -
The
e.inputfield from the error contains the LocatedSpan struct that was passed in to the report.e.input.extraon line 19 pulls the vec with the values of the names of the parsers we added withcontent.extra.push()in the parsers.e.input.fragment()on line 20 gets the text that was attempting to be parsed where the failure occurred.e.input.location_line()on line 21 gets the line number from the original input where the error occured (which will be 2 in our case since first_parser got us to the second line).e.input.get_utf8_column()on line 22 give us the column of the.location_line()where the error starts. (There's a similar.get_column()that can be used if all you've got is ASCII, but I don't mess with that.) -
Lines 24-27 contain
e.input.get_line_beginning()that pulls in the full line that the error occured on. It comes in as&[u8]so the.to_vec()andString::from_utf8()calls are used to turn it into text. -
The rest of the funciton is the formatting that shows the line where the error occurred and puts a
^under the column where the error started.
We tie all that toghter in this main.rs file:
And, when we run it, we get this:
PARSING ERROR:
-> first_parser
-> second_parser
failed at:
charlie delta
on line 2 column 6:
bravo charlie delta
^Orders of magnititude more useful than the original.
Outro
It took an hour or two to figure out the approach and dial things in. It took the rest of the day to write this post. At 8pm I've done zero work on the parser itself. I'm cool with that. I'll be able to move so much faster with these upgraded error messages. I'll make up the time in nothing flat.
-a
Endnote
-
I'm not knocking nom for the terse error messages. It's designed to be as fast as possible. You can use the error payload to figure out where issues are. It just takes a lot more effort.
I expect I'm taking a hit on the parsing speed by using nom_locate. I can't tell though. It rips through blog posts so fast it might as well be instentaneous.
Footnotes
-
nd NeoDoc is a note taking format. It's similar to Markdown, but much more powerful. It's currently embedded in my personal static site generator. I want to be able to do more with it so I'll pulling it out to its own library.
-
nom nom is a parser combinator (aka text processig on steriods). The learning curve is significant. I recommend against learning it at the same time you're learning Rust. Ask me how I know.
-
ns nom_supreme was my go to helper for improving nom error messages in nom v7. It was a bit tricky to set up though. Even if it was available in the current nom v8 I'd probably still use the nom_locate approach as it's simpler to get going.
-
nl nom_locate is my new best friend. Judging by the 30 million downloads on crates.io it looks like I'm not alone. It's going to save me sooo much time. It almost feels like cheating.