winnow
repository·main·Indexed 21 days ago
https://github.com/winnow-rs/winnowA 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.
What's inside winnow
- 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.
Navigate the winnow Parser API organization
mainThe
ParserAPI 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: ProvidesStreamagnostic 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 theParsertrait but offer no additional behavior.
Naming Conventions for Parsers:
- Use
takewhen a parser produces slices. - Use
offsetwhen referring to indices into aStreamto access a token. - Use
countoroccurrenceswhen referring to the number of values processed, returned, or expected.
Understand the winnow release cadence and compatibility
mainWinnow 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.Understand ModalResult and ErrMode
mainIn
winnow, parsers typically return aModalResult<O, E>rather than a standardResult<O, E>. This allows the parser to communicate not just that an error occurred, but how the caller should respond to it via theErrMode<E>enum.ErrMode<E>has three variants:Backtrack(E): A recoverable error. The parser failed, but the caller (like analtcombinator) 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>>;How to thread global state through parsers using `Stateful`
mainThe
Stateful<I, S>wrapper allows you to attach user-provided stateSto an input streamI. 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.
StatefulimplementsDerefto the inner inputI, so you can call most stream methods directly on theStatefulinstance. When creating aCheckpointfor aStatefulstream, 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); # }Configure endianness in winnow
mainThe
Endiannessenum allows you to specify how multi-byte integers should be interpreted during parsing.Big: Big-endian byte order.Little: Little-endian byte order.Native: Matches the host's endianness.
How to use `Partial` for streaming input
mainThe
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 returnErrMode::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 transformIncompleteerrors intoBacktrackerrors if preferred.- Ambiguity: For parsers with no clear limit (like
alpha0which matches 0 or more characters), thePartialversion will returnIncompleteif 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()); # }Use ContextError to accumulate parsing context
mainThe
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 theParser::contextcombinator.Key features:
- Context Types: By default, it uses
StrContext, which can holdLabel(a description of what is being parsed) orExpected(the grammar item that was expected). - Accumulation: As parsers fail and backtrack, they can use
add_contextto push new information onto the error stack. - Customization: You can define your own context type
Cto store more structured data ifStrContextis insufficient.
pub struct ContextError<C = StrContext> { #[cfg(feature = "alloc")] context: alloc::vec::Vec<C>, // ... }- Context Types: By default, it uses
Trace error paths with TreeError
mainFor 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, aTreeErrorcaptures 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 analtcombinator, 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>), }Implement the Parser trait
mainThe
Parsertrait is the core abstraction in winnow. You can implement it in two primary ways:Using a function: The simplest way is to define a function that takes a mutable reference to a
Streamand returns aResult. Any function with the signatureFnMut(&mut I) -> Result<O, E>automatically implementsParser.Using stateful objects: You can return a function that captures state, allowing for more complex parsing logic.
Basic types like
u8,char,&[u8], and&stralso implementParserfor 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();Compose parsers using tuples
mainWinnow 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);Manage error modes with `cut_err` and `backtrack_err`
mainWinnow uses error modes to distinguish between recoverable (
Backtrack) and unrecoverable (Cut) errors. This is critical when using combinators likealt.cut_err: Converts aBacktrackerror into aCuterror. This 'commits' the parser, preventingaltfrom 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 aCuterror back into aBacktrackerror, allowingaltto 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())));