winnow

repository·main·Indexed 21 days ago

https://github.com/winnow-rs/winnow

A byte-oriented, zero-copy parser combinators library for Rust (v1.0.4) that provides building blocks to construct parsers for any data format. It includes specialized modules for ASCII data processing, binary and bit data processing, and tools for handling various endianness (Big, Little, Native) and numeric types.

Tokens
23.3K
Snippets
87
Records
103
Agent score
75%

What's inside winnow

  1. Overview of winnow

    main
    winnow is a Rust library designed to make building parsers for any format easy and efficient. It provides a set of building blocks (combinators) that allow you to compose complex parsers from simpler ones.
  2. Navigate the winnow Parser API organization

    main

    The Parser API is organized into specific modules to help you find the right tools for your parsing task:

    • combinator: Contains parser composition tools and basic building blocks.
    • token: Provides Stream agnostic token and token slice processing.
    • ascii: Specialized for ASCII data processing.
    • binary: Specialized for byte and bit data processing.
    • Parser::*: Limited to non-grammatical output or error conversions.
    • impls: Opaque types that implement the Parser trait but offer no additional behavior.

    Naming Conventions for Parsers:

    • Use take when a parser produces slices.
    • Use offset when referring to indices into a Stream to access a token.
    • Use count or occurrences when referring to the number of values processed, returned, or expected.
  3. Understand the winnow release cadence and compatibility

    main

    Winnow follows a structured release cycle to manage breaking changes and stability:

    • Major releases (breaking changes): Every 6-9 months.
    • Minor releases (minor incompatibilities or MSRV bumps): Every 2 months.
    • Patch releases: Triggered by user-facing, user-contributed PRs. These contain no breaking changes and no MSRV (Minimum Supported Rust Version) bumps.

    If you encounter a feature marked as unstable-<name>, it is a large feature currently under development and is not yet stabilized.

  4. Understand ModalResult and ErrMode

    main

    In winnow, parsers typically return a ModalResult<O, E> rather than a standard Result<O, E>. This allows the parser to communicate not just that an error occurred, but how the caller should respond to it via the ErrMode<E> enum.

    ErrMode<E> has three variants:

    • Backtrack(E): A recoverable error. The parser failed, but the caller (like an alt combinator) should try other branches.
    • Cut(E): An unrecoverable error. The parser has committed to a specific branch, and any further error should be reported directly to the user instead of backtracking.
    • Incomplete(Needed): Not enough data is available to complete the parse. This is used with partial streams to signal that more data should be buffered before retrying.

    You can manipulate these modes using .cut() to prevent backtracking or .backtrack() to re-enable it.

    /// Ok(O) is the parsed value
    /// Err(ErrMode<E>) is the error along with how to respond to it
    pub type ModalResult<O, E = ContextError> = Result<O, ErrMode<E>>;
  5. How to thread global state through parsers using `Stateful`

    main

    The Stateful<I, S> wrapper allows you to attach user-provided state S to an input stream I. This is useful for tracking information across multiple parser calls, such as:

    • Recursion checks: Tracking depth to prevent infinite loops.
    • Error recovery: Storing context for better error reporting or recovery strategies.
    • Debugging: Maintaining counters or logs during the parsing process.

    Stateful implements Deref to the inner input I, so you can call most stream methods directly on the Stateful instance. When creating a Checkpoint for a Stateful stream, the checkpoint will also include the state, allowing you to restore both the input position and the user state simultaneously.

    Example

    # #[cfg(feature = "ascii")] {
    # use std::cell::Cell;
    # use winnow::prelude::*;
    # use winnow::stream::Stateful;
    # use winnow::ascii::alpha1;
    # type Error = ();
    #
    # #[derive(Debug)]
    # struct State<'s>(&'s mut u32);
    #
    # impl<'s> State<'s> {
    #     fn count(&mut self) {
    #         *self.0 += 1;
    #     }
    # }
    #
    # type Stream<'is> = Stateful<&'is str, State<'is>>;
    #
    # fn word<'s>(i: &mut Stream<'s>) -> ModalResult<&'s str> {
    #   i.state.count();
    #   alpha1.parse_next(i)
    # }
    #
    # let data = "Hello";
    # let mut state = 0;
    # let input = Stream { input: data, state: State(&mut state) };
    # let output = word.parse(input).unwrap();
    # assert_eq!(state, 1);
    # }
    # #[cfg(feature = "ascii")] {
    # use std::cell::Cell;
    # use winnow::prelude::*;
    # use winnow::stream::Stateful;
    # use winnow::ascii::alpha1;
    # type Error = ();
    #
    # #[derive(Debug)]
    # struct State<'s>(&'s mut u32);
    #
    # impl<'s> State<'s> {
    #     fn count(&mut self) { 
    #         *self.0 += 1; 
    #     }
    # }
    #
    # type Stream<'is> = Stateful<&'is str, State<'is>>;
    #
    # fn word<'s>(i: &mut Stream<'is>) -> ModalResult<&'s str> {
    #   i.state.count();
    #   alpha1.parse_next(i)
    # }
    #
    # let data = "Hello";
    # let mut state = 0;
    # let input = Stream { input: data, state: State(&mut state) };
    # let output = word.parse(input).unwrap();
    # assert_eq!(state, 1);
    # }
  6. How to use `Partial` for streaming input

    main

    The Partial<I> wrapper is used to mark an input as a partial buffer, which is essential for streaming scenarios (e.g., network protocols or large files) where the entire input might not be available at once.

    When using Partial, parsers that require a specific amount of data will return ErrMode::Incomplete(Needed) instead of a standard error if the input is insufficient. This allows the caller to wait for more data and retry.

    Key behaviors:

    • ErrMode::Incomplete: Reports how much more data is needed.
    • Parser::complete_err: Can be used to transform Incomplete errors into Backtrack errors if preferred.
    • Ambiguity: For parsers with no clear limit (like alpha0 which matches 0 or more characters), the Partial version will return Incomplete if the end of the buffer is reached, because it cannot know if more valid characters are coming. A 'complete' parser (non-wrapped) would instead return a successful match of the current buffer.
    # #[cfg(feature = "ascii")] {
    # use winnow::{Result, error::ErrMode, error::Needed, error::ContextError, token, ascii, stream::Partial};
    # use winnow::prelude::*;
    
    // A parser using a partial stream
    fn take_partial<'s>(i: &mut Partial<&'s [u8]>) -> ModalResult<&'s [u8], ContextError> {
      token::take(4u8).parse_next(i)
    }
    
    // A parser using a complete stream
    fn take_complete<'s>(i: &mut &'s [u8]) -> ModalResult<&'s [u8], ContextError> {
      token::take(4u8).parse_next(i)
    }
    
    // If input is 4+ bytes, both succeed
    assert_eq!(take_partial.parse_peek(Partial::new(&b"abcde"[..])), Ok((Partial::new(&b"e"[..]), &b"abcd"[..])));
    assert_eq!(take_complete.parse_peek(&b"abcde"[..]), Ok((&b"e"[..], &b"abcd"[..])));
    
    // If input is < 4 bytes, partial returns Incomplete, complete returns an error
    assert_eq!(take_partial.parse_peek(Partial::new(&b"abc"[..])), Err(ErrMode::Incomplete(Needed::new(1))));
    assert!(take_complete.parse_peek(&b"abc"[..]).is_err());
    # }
  7. Use ContextError to accumulate parsing context

    main

    The ContextError<C> type is used to accumulate context (like labels or expected tokens) as an error bubbles up the parser chain. This is primarily driven by the Parser::context combinator.

    Key features:

    • Context Types: By default, it uses StrContext, which can hold Label (a description of what is being parsed) or Expected (the grammar item that was expected).
    • Accumulation: As parsers fail and backtrack, they can use add_context to push new information onto the error stack.
    • Customization: You can define your own context type C to store more structured data if StrContext is insufficient.
    pub struct ContextError<C = StrContext> {
        #[cfg(feature = "alloc")]
        context: alloc::vec::Vec<C>,
        // ...
    }
  8. Trace error paths with TreeError

    main

    For advanced debugging and testing, TreeError<I, C> allows you to trace the entire path of a failure. Unlike a standard error that only shows the final failure point, a TreeError captures the full tree of failed branches and the context added at each level.

    It consists of:

    • Base: The initial error that triggered the failure.
    • Stack: A trace of frames containing either the error kind or the context added during the walk back up the stack.
    • Alt: A collection of all failed branches when using an alt combinator, allowing you to see why every alternative failed.
    pub enum TreeError<I, C = StrContext> {
        Base(TreeErrorBase<I>),
        Stack {
            base: Box<Self>,
            stack: Vec<TreeErrorFrame<I, C>>,
        },
        Alt(Vec<Self>),
    }
  9. Implement the Parser trait

    main

    The Parser trait is the core abstraction in winnow. You can implement it in two primary ways:

    1. Using a function: The simplest way is to define a function that takes a mutable reference to a Stream and returns a Result. Any function with the signature FnMut(&mut I) -> Result<O, E> automatically implements Parser.

    2. Using stateful objects: You can return a function that captures state, allowing for more complex parsing logic.

    Basic types like u8, char, &[u8], and &str also implement Parser for literal matching.

    use winnow::prelude::*;
    
    // 1. Function-based parser
    fn empty(input: &mut &str) -> ModalResult<()> {
        let output = ();
        Ok(output)
    }
    
    // 2. Stateful parser
    fn empty_stateful<O: Clone>(output: O) -> impl FnMut(&mut &str) -> ModalResult<O> {
        move |input: &mut &str| {
            let output = output.clone();
            Ok(output)
        }
    }
    
    let (input, output) = empty.parse_peek("Hello").unwrap();
  10. Compose parsers using tuples

    main

    Winnow allows you to compose multiple parsers into a single parser using Rust tuples. When you wrap several parsers in a tuple, the resulting parser will execute them sequentially and return a tuple containing the outputs of each individual parser.

    Note that the unit type () can also be used as a parser that always succeeds without consuming input or producing output.

    // Example of composing parsers using a tuple
    // (be_u16, take(3u8), "fg")
    // This will return a tuple of (u16, &[u8], &str)
    let mut parser = (be_u16, take(3u8), "fg");
    let result = parser.parse_next(input);
  11. Manage error modes with `cut_err` and `backtrack_err`

    main

    Winnow uses error modes to distinguish between recoverable (Backtrack) and unrecoverable (Cut) errors. This is critical when using combinators like alt.

    • cut_err: Converts a Backtrack error into a Cut error. This 'commits' the parser, preventing alt from trying other branches. Use this when you have matched a prefix that guarantees a specific syntax, and a subsequent failure should be treated as a hard error rather than a reason to try something else.
    • backtrack_err: Converts a Cut error back into a Backtrack error, allowing alt to continue trying other branches.
    # // Example of cut_err committing a branch
    # use winnow::{error::ErrMode, error::ContextError};
    # use winnow::prelude::*;
    # use winnow::token::one_of;
    # use winnow::ascii::digit1;
    # use winnow::combinator::alt;
    # use winnow::combinator::preceded;
    # use winnow::combinator::cut_err;
    
    fn parser<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
      alt((
        preceded(one_of(['+', '-']), cut_err(digit1)),
        rest
      )).parse_next(input)
    }
    
    // If input is "+", it fails at digit1. 
    // Because of cut_err, it returns Err(ErrMode::Cut) instead of backtracking to 'rest'.
    assert_eq!(parser.parse_peek("+",), Err(ErrMode::Cut(ContextError::new())));