unstable-doc only.Expand description
§Chapter 7: Error Reporting
§Context
With Parser::parse we get errors that point to the failure but don’t explain the reason for
the failure:
// ...
fn main() {
let input = "0xZZ";
let error = "\
0xZZ
^
";
assert_eq!(input.parse::<Hex>().unwrap_err(), error);
}Back in chapter_1, we glossed over the Err variant of Result. Result<O> is
actually short for Result<O, E=ContextError> where ContextError is a relatively cheap
way of building up reasonable errors for humans.
You can use Parser::context to annotate the error with custom types
while unwinding to further clarify the error:
use winnow::error::StrContext;
use winnow::error::StrContextValue;
fn parse_digits<'s>(input: &mut &'s str) -> Result<(&'s str, &'s str)> {
alt((
("0b", parse_bin_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("binary"))),
("0o", parse_oct_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("octal"))),
("0d", parse_dec_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("decimal"))),
("0x", parse_hex_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("hexadecimal"))),
)).parse_next(input)
}
// ...
fn main() {
let input = "0xZZ";
let error = "\
0xZZ
^
invalid digit
expected hexadecimal";
assert_eq!(input.parse::<Hex>().unwrap_err(), error);
}If you remember back to chapter_3, alt will only report the last error.
So if the parsers fail for any reason, like a bad radix, it will be reported as an invalid
hexadecimal value:
fn main() {
let input = "100";
let error = "\
100
^
invalid digit
expected hexadecimal";
assert_eq!(input.parse::<Hex>().unwrap_err(), error);
}We can improve this with fail:
use winnow::error::StrContext;
use winnow::error::StrContextValue;
fn parse_digits<'s>(input: &mut &'s str) -> Result<(&'s str, &'s str)> {
alt((
("0b", parse_bin_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("binary"))),
("0o", parse_oct_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("octal"))),
("0d", parse_dec_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("decimal"))),
("0x", parse_hex_digits)
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("hexadecimal"))),
fail
.context(StrContext::Label("radix prefix"))
.context(StrContext::Expected(StrContextValue::StringLiteral("0b")))
.context(StrContext::Expected(StrContextValue::StringLiteral("0o")))
.context(StrContext::Expected(StrContextValue::StringLiteral("0d")))
.context(StrContext::Expected(StrContextValue::StringLiteral("0x"))),
)).parse_next(input)
}
// ...
fn main() {
let input = "100";
let error = "\
100
^
invalid radix prefix
expected `0b`, `0o`, `0d`, `0x`";
assert_eq!(input.parse::<Hex>().unwrap_err(), error);
}§Error Cuts
We still have the issue that we are falling-through when the radix is valid but the digits don’t match it:
fn main() {
let input = "0b5";
let error = "\
0b5
^
invalid radix prefix
expected `0b`, `0o`, `0d`, `0x`";
assert_eq!(input.parse::<Hex>().unwrap_err(), error);
}Winnow provides an error wrapper, ErrMode<ContextError>, so different failure modes can affect parsing.
ErrMode is an enum with Backtrack and Cut variants (ignore Incomplete as its only
relevant for streaming). By default, errors are Backtrack, meaning that
other parsing branches will be attempted on failure, like the next case of an alt. Cut
shortcircuits all other branches, immediately reporting the error.
To make ErrMode more convenient, Winnow provides ModalResult:
pub type ModalResult<O, E = ContextError> = Result<O, ErrMode<E>>;So we can get the correct context by changing to ModalResult and adding cut_err:
use winnow::combinator::cut_err;
fn parse_digits<'s>(input: &mut &'s str) -> ModalResult<(&'s str, &'s str)> {
alt((
("0b", cut_err(parse_bin_digits))
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("binary"))),
("0o", cut_err(parse_oct_digits))
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("octal"))),
("0d", cut_err(parse_dec_digits))
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("decimal"))),
("0x", cut_err(parse_hex_digits))
.context(StrContext::Label("digit"))
.context(StrContext::Expected(StrContextValue::Description("hexadecimal"))),
fail
.context(StrContext::Label("radix prefix"))
.context(StrContext::Expected(StrContextValue::StringLiteral("0b")))
.context(StrContext::Expected(StrContextValue::StringLiteral("0o")))
.context(StrContext::Expected(StrContextValue::StringLiteral("0d")))
.context(StrContext::Expected(StrContextValue::StringLiteral("0x"))),
)).parse_next(input)
}
// ...
fn main() {
let input = "0b5";
let error = "\
0b5
^
invalid digit
expected binary";
assert_eq!(input.parse::<Hex>().unwrap_err(), error);
}§Error Adaptation and Rendering
While Winnow can provide basic rendering of errors, your application can have various demands beyond the basics provided like
- Correctly reporting columns with unicode
- Conforming to a specific layout
For example, to get rustc-like errors with annotate-snippets:
#[derive(Debug, PartialEq, Eq)]
pub struct Hex(usize);
impl std::str::FromStr for Hex {
type Err = HexError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
// ...
.parse(input)
.map_err(|e| HexError::from_parse(e))
}
}
#[derive(Debug)]
pub struct HexError {
message: String,
// Byte spans are tracked, rather than line and column.
// This makes it easier to operate on programmatically
// and doesn't limit us to one definition for column count
// which can depend on the output medium and application.
span: std::ops::Range<usize>,
input: String,
}
impl HexError {
// Avoiding `From` so `winnow` types don't become part of our public API
fn from_parse(error: ParseError<&str, ContextError>) -> Self {
// The default renderer for `ContextError` is still used but that can be
// customized as well to better fit your needs.
let message = error.inner().to_string();
let input = (*error.input()).to_owned();
// Assume the error span is only for the first `char`.
let span = error.char_span();
Self {
message,
span,
input,
}
}
}
impl std::fmt::Display for HexError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = annotate_snippets::Level::Error.title(&self.message)
.snippet(annotate_snippets::Snippet::source(&self.input)
.fold(true)
.annotation(annotate_snippets::Level::Error.span(self.span.clone()))
);
let renderer = annotate_snippets::Renderer::plain();
let rendered = renderer.render(message);
rendered.fmt(f)
}
}
impl std::error::Error for HexError {}
fn main() {
let input = "0b5";
let error = "\
error: invalid digit
expected binary
|
1 | 0b5
| ^
|";
assert_eq!(input.parse::<Hex>().unwrap_err().to_string(), error);
}To add spans to your parsed data for inclusion in semantic errors, see Parser::with_span.
For richer syntactic errors with spans,
consider separating lexing and parsing and annotating your tokens with Parser::with_span.