Better nom Error Messages with nom_locate
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 word "target" 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: "target",
code: Tag,
},
),
)-
The
input: "target"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 "target"
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
Err(
Error(
Error {
input: LocatedSpan {
offset: 0,
line: 1,
fragment: "target",
extra: "some_parser",
},
code: Tag,
},
),
)The input now contains the LocatedSpan with
more details about where the error occurred.
The value of the extra field is what we
defined on line 17. Using it as a reference, we
know in which function the failure occurred.
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 examples. 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 value with the name of the parser we added in those functions.e.input.location_line()on line 20 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 21 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 23-25 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:
ERROR: second_parser failed on line 2 column 7
----------------------------------------------
spacer target
^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 of 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.