Skip to main content

Module chapter_7

Module chapter_7 

Source
Available on crate feature 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.

Re-exports§

pub use super::chapter_6 as previous;
pub use super::chapter_8 as next;
pub use crate::_tutorial as table_of_contents;