nom (Rust)
A byte-oriented, zero-copy, streaming parser-combinator library for Rust whose parsers are ordinary functions of the form Input -> IResult<Input, Output, Error>, assembled bottom-up from small reusable pieces.
| Field | Value |
|---|---|
| Language | Rust (the library is #![no_std]-capable; std is the default feature) |
| License | MIT |
| Repository | rust-bakery/nom |
| Documentation | docs.rs/nom · crates.io · doc/ design notes |
| Key authors | Geoffroy Couprie (original author) and contributors; org maintainers at rust-bakery |
| Category | Parser combinator (recursive-descent, scannerless) |
| Algorithm class | Hand-rolled top-down / recursive descent with ordered choice; not a generated table parser |
| Lexing model | Scannerless — parsers consume &[u8]/&str directly; no separate lexer or token stream |
| Grammar class | Effectively PEG-like (ordered choice, no built-in left-recursion, unbounded lookahead via the input) |
| Latest release | 8.0.0 (2025-01-25) |
NOTE
nom is the canonical byte-oriented binary/network parser-combinator in the Rust ecosystem. It is the data point against which the Haskell parsec lineage, the chumsky error-recovery library, and the modern fork winnow (covered below) are compared in this survey. See the comparison capstone for the cross-subject synthesis.
Overview
What it solves
A parser combinator turns parsing from a code-generation problem into a composition problem. Where yacc/bison and ANTLR take a grammar in a separate DSL and emit a table-driven state machine, nom asks you to write tiny Rust functions — "take 5 bytes", "recognize the word HTTP" — and glue them together with higher-order combinators. The crate-root documentation states the contrast directly (src/lib.rs):
"Parser combinators are an approach to parsers that is very different from software like lex and yacc. Instead of writing the grammar in a separate syntax and generating the corresponding code, you use very small functions with very specific purposes, like 'take 5 bytes', or 'recognize the word HTTP', and assemble them in meaningful patterns."
The library's stated goal (README.md) is narrow and performance-led:
"nom is a parser combinators library written in Rust. Its goal is to provide tools to build safe parsers without compromising the speed or memory consumption."
nom's design centre of gravity is binary and network formats: it is byte-oriented and bit-oriented first, string-oriented second. It was created by Geoffroy Couprie to write safe replacements for the kind of memory-unsafe C parsers that historically sit at the root of security vulnerabilities — the thesis of the LangSec/SSTIC paper "Writing parsers like it is 2017" (Chifflier & Couprie), whose nom-based parsers shipped into Suricata (the IDS/IPS engine, Rust parsers since Suricata 4.0) and were prototyped against media formats in VLC.
Design philosophy
Three properties define nom, each a verbatim claim from its own documentation.
Zero-copy. A nom parser never duplicates the bytes it recognizes; recognized spans are returned as slices that borrow the original input (README.md):
"If a parser returns a subset of its input data, it will return a slice of that input, without copying."
This falls directly out of Rust's borrow checker: a &[u8] or &str output is tied by lifetime to the input buffer, so "parse" for a span-shaped result is just pointer + length, allocation-free.
Streaming-correct by construction. nom distinguishes "I cannot decide yet, give me more bytes" from "this input is wrong" (README.md):
"nom has been designed for a correct behaviour with partial data: If there is not enough data to decide, nom will tell you it needs more instead of silently returning a wrong result."
That signal is the Err::Incomplete(Needed) variant (see Algorithm & grammar class), the feature that makes nom suitable for parsing a network byte stream as it arrives rather than only a fully-buffered message.
Speed competitive with handwritten C. Because each combinator monomorphizes into a concrete function with no dynamic dispatch on the hot path, the optimizer collapses a tower of combinators into tight code (README.md):
"Benchmarks have shown that nom parsers often outperform many parser combinators library like Parsec and attoparsec, some regular expression engines and even handwritten C parsers."
Couprie summarizes the family resemblance in src/lib.rs: parsers are "small and easy to write", "easy to reuse", "easy to test separately", and "the parser combination code looks close to the grammar you would have written".
How it works
Core abstractions and types
| Concept | Type / item | Role |
|---|---|---|
| Parser result | IResult<I, O, E = Error<I>> | Result<(I, O), Err<E>> — remaining input + output, or an error/Incomplete |
| Error wrapper | Err<E> | Three-way: Incomplete(Needed), Error(E), Failure(E) |
| The parser abstraction | Parser<Input> trait | Anything callable as a parser; all combinators and methods hang off it |
| Input abstraction | Input trait (8.x; was InputIter/InputTake/…) | What makes &[u8], &str, &[T], and custom types parseable |
| Error contract | ParseError<I>, ContextError<I>, FromExternalError | Lets a user error type plug into every combinator |
| Concrete error types | Error<I> (default), VerboseError<I> | The minimal error vs. the diagnostic, context-accumulating error |
| Error tag | ErrorKind | Which combinator failed (Tag, Alt, Many1, …) |
| Insufficiency | Needed::{Unknown, Size(NonZeroUsize)} | How many more bytes a streaming parser wants |
Parsers are functions
The foundational idea is that a parser is just a function from input to an IResult (doc/making_a_new_parser_from_scratch.md):
"nom parsers are functions that use the
nom::IResulttype everywhere. … a parser taking a byte slice&[u8]and returning a 32 bits unsigned integeru32would have this signature:fn parse_u32(input: &[u8]) -> IResult<&[u8], u32>."
IResult is a thin alias over the standard library's Result:
// nom: src/internal.rs
pub type IResult<I, O, E = Error<I>> = Result<(I, O), Err<E>>;
pub enum Err<E> {
/// There was not enough data
Incomplete(Needed),
/// The parser had an error (recoverable)
Error(E),
/// The parser had an unrecoverable error
Failure(E),
}The success arm carries (I, O) — the remaining input alongside the output — so a caller (or the next combinator) resumes exactly where the previous parser stopped. This "thread the leftover input through" convention is the spine of the whole library, and is the single design point that winnow later inverted (it returns the leftover by mutating a &mut I instead; see The winnow fork).
Combinators: parsers that take parsers
Building blocks are assembled bottom-up (doc/making_a_new_parser_from_scratch.md): "Parsers are usually built from the bottom up, by first writing parsers for the smallest elements, then assembling them in more complex parsers by using combinators." A combinator is a function that generates a parser; the crate root gives the canonical signature (src/lib.rs):
"nom is based on functions that generate parsers, with a signature like this:
(arguments) -> impl Fn(Input) -> IResult<Input, Output, Error>. The arguments of a combinator can be direct values (liketakewhich uses a number of bytes or character as argument) or even other parsers."
The post-5.0 API is functions, not the macros nom originally shipped (see Interface & composition model). The frequently-used combinators, grouped by purpose (docs.rs/nom):
| Group | Combinators | Meaning |
|---|---|---|
| Basic elements | tag, take, take_while, take_until, char, one_of | Recognize literals / spans / single tokens |
| Numbers | be_u16, le_u32, u8, … (in number::{streaming,complete}) | Fixed-width big/little-endian integers |
| Sequence | preceded, terminated, delimited, separated_pair, tuples | Run parsers in order, keep some/all results |
| Choice | alt | Ordered choice — first child that succeeds wins |
| Repetition | many0, many1, many_m_n, separated_list0, fold_many0 | Apply a parser repeatedly, collecting results |
| General-purpose | opt, map, map_res, value, verify, recognize, cut, peek | Transform, validate, commit, or look ahead |
A small, representative example — recognizing #rrggbb hex colours — straight from the nom README.md (README.md):
// nom: README.md
fn hex_color(input: &str) -> IResult<&str, Color> {
let (input, _) = tag("#")(input)?;
let (input, (red, green, blue)) =
(hex_primary, hex_primary, hex_primary).parse(input)?;
Ok((input, Color { red, green, blue }))
}Several conventions are on display at once: tag("#") returns a parser that is immediately called on input; the ? operator short-circuits on Err; the threaded input is rebound at each step; and a tuple of parsers is itself a parser (run in sequence, output is the tuple of outputs). A length-prefixed binary read shows the byte-oriented side (doc/making_a_new_parser_from_scratch.md):
// nom: doc/making_a_new_parser_from_scratch.md
pub fn length_value(input: &[u8]) -> IResult<&[u8], &[u8]> {
let (input, length) = be_u16(input)?;
take(length)(input) // returns a *slice* of `input` — zero-copy
}take(length)(input) returns a &[u8] borrowing the original buffer: no allocation, no copy. That is the zero-copy property made concrete.
The Parser trait and the 8.0 rewrite
From the start every combinator was duck-typed as "a function returning IResult", but nom also exposes a Parser trait so combinators can be invoked as methods. nom 8.0 (2025-01-25) re-founded the library on this trait. The 8.0 CHANGELOG (CHANGELOG.md) describes it as "a significant refactoring of nom to reduce the amount of code generated by parsers, and reduce the API surface", merging the old menagerie of input traits — "InputIter, InputTakeAtPosition, InputLength, InputTake and Slice are now merged in the Input trait" — and switching the idiomatic call style: "Instead of writing combinator(arg)(input), we now write combinator(arg).parse(input)."
The trait's required method became mode-parametric, so a single parser body can be compiled in different "output modes" (produce the value, or just check that it matches) (docs.rs Parser):
// nom 8.0: src/internal.rs (Parser trait, abridged)
pub trait Parser<Input> {
type Output;
type Error: ParseError<Input>;
fn process<OM: OutputMode>(
&mut self,
input: Input,
) -> PResult<OM, Input, Self::Output, Self::Error>;
// provided combinator methods:
fn parse(&mut self, input: Input) -> IResult<Input, Self::Output, Self::Error> { /* … */ }
// map, and_then, flat_map, map_res, map_opt, and, or, …
}The OutputMode type parameter is nom's answer to the "parse modes" optimization (run the same grammar in a value-producing mode vs. a recognize-only mode without building outputs) — the same idea chumsky realizes with GATs and winnow deliberately side-steps. It is the deepest internal change in nom's history and the reason 8.x is a breaking release.
Streaming vs complete
For every primitive that can run out of input, nom ships two versions in parallel modules — nom::bytes::streaming / nom::bytes::complete, nom::character::streaming / …::complete, nom::number::streaming / …::complete. The crate docs state the difference precisely (docs.rs/nom):
"A streaming parser assumes that we might not have all of the input data. This can happen with some network protocol or large file parsers, where the input buffer can be full and need to be resized or refilled. … A complete parser assumes that we already have all of the input data. This will be the common case with small files that can be read entirely to memory."
The behavioural fork is at end-of-input. Ask take(4) for four bytes when only two remain: the streaming parser returns Err::Incomplete(Needed::Size(2)) (buffer more, retry); the complete parser returns Err::Error(..) (this input genuinely ends here). Picking the wrong module is the most common nom bug — a complete parser fed a half-message reports a spurious syntax error; a streaming parser fed a finished buffer asks forever for bytes that will never come.
Algorithm & grammar class
nom is a scannerless, hand-rolled recursive-descent engine. There is no grammar compilation step, no parse table, and no separate tokenizer — combinators consume the raw &[u8]/&str directly, so lexing and parsing are one fused pass. Operationally it behaves like a PEG: alt is ordered choice (it tries children left to right and commits to the first success), there is no built-in support for left-recursion (a left-recursive combinator simply recurses forever), and there is no declarative grammar to analyze for ambiguity — the order you write the alternatives in is the disambiguation. Lookahead is unbounded but explicit: peek parses without consuming, and a child of alt may consume arbitrarily far before failing and being retried from the saved input position.
The streaming model is the genuinely distinctive algorithmic feature. The three-way Err (Incomplete / Error / Failure) lets a parser be a partial function over a growing prefix: Incomplete(Needed) is a first-class result meaning "this prefix is consistent with a valid parse but underdetermined", which is precisely what a byte-stream protocol decoder needs and what a Result<T, E>-only design (or a fully-buffered combinator library) cannot express without conflating "need more" with "wrong".
NOTE
nom does not memoize. Unlike a packrat PEG parser, a child of alt that consumes a long prefix and then fails is simply re-run from the saved position by the next alternative; there is no per-position result cache. In practice nom grammars are written to factor out common prefixes (or to cut early) so that pathological backtracking does not arise, trading the linear-time guarantee of packrat for lower constant factors and zero cache memory.
Interface & composition model
The grammar is expressed as host-language Rust code — an internal, embedded DSL of combinator functions, not an external grammar file. This is the defining contrast with the generator family (bison/yacc, ANTLR, Menhir): there is no .y/.g4 file and no build-time code-generation step; a parser is a normal fn you can name, document, unit-test, and #[inline]. Composition is by ordinary function application and the higher-order combinators above.
The AST/CST is whatever the closures build. nom imposes no tree type. A combinator's output is produced by map/map_res closures, so you assemble your own domain structs as you go (the Color { red, green, blue } above), or — exploiting zero-copy — keep outputs as borrowed &str/&[u8] slices into the source and defer interpretation. There is no generic CST and no notion of trivia/whitespace preservation; nom is firmly an AST-building tool, not a lossless-syntax-tree tool like tree-sitter.
A note on API history: nom originally shipped a macro DSL (named!, tag!, alt!, do_parse!). The 5.0 release (2019) rewrote the internals around functions — the CHANGELOG describes it as "a complete rewrite of nom internals to use functions as a base for parsers, instead of macros", with the old macros kept as thin shims — and 7.0 (2021) was "the first release without the macros that were used since nom's beginning" (CHANGELOG.md). New code uses the function/method API exclusively; the macro era is the most common source of stale tutorials.
Input genericity is achieved through the Input trait (and its 8.x predecessors). A parser written against I: Input runs over &[u8], &str, &[T], or any user type implementing the trait — the mechanism third-party crates like nom_locate (line/column-tracking spans) and [bytes] integrations exploit.
Performance
Performance is nom's headline and its principal reason for existing. The characteristics:
- Time complexity is that of the grammar you write — typically linear in input for well-factored grammars, but with no memoization an
altwith overlapping, back-tracking alternatives can be super-linear in the worst case (the same trap as any unmemoized PEG). Mitigations are factoring andcut(commit to a branch, convertingErrortoFailuresoaltstops retrying). - Zero-copy outputs (
&[u8]/&strslices borrowing the input) mean a recognizer allocates nothing; allocation happens only where you build owned structures (Vec,String) in amap. - Monomorphization, no dynamic dispatch. Each combinator is a concrete generic; the compiler inlines the tower into straight-line code, which is why the README can claim parity with handwritten C.
- Bit-level and SIMD-adjacent. nom has dedicated bit parsers (
nom::bits) and abitvecintegration (added in 6.0, split out to thenom-bitveccrate in 7.0). It does not itself vectorize, but it is the glue layer often paired with hand-vectorized primitives; it is not a data-parallel engine likesimdjson. - Published benchmarks. The README's "outperform … even handwritten C parsers" claim is backed by
rust-bakery/parser_benchmarks. The most illuminating external numbers come from thewinnowfork's author, Ed Page, who benchmarked nom against its successors on chumsky's JSON benchmark (Winnow 0.5 blog): nom 7.1.3 at 341.28 µs, chumsky (zero-copy) at 191.06 µs, winnow 0.3.6 at 125.54 µs, winnow 0.5.0 at 97.328 µs. nom is fast, but the fork demonstrated headroom nom's pure-function signature left on the table (below).
Error handling & recovery
nom's error story has two layers: a control-flow layer and a diagnostics layer.
Control flow — the three-way error. The Err enum is the mechanism (doc/error_management.md). Error(E) is "a normal parser error" and is recoverable: inside alt it makes the combinator "try another child parser."Failure(E) is "an error from which we cannot recover" and is unrecoverable: "the alt combinator will not try other branches if a child parser returns Failure." The bridge between them is the cut() combinator, which "transform[s] an Error into Failure" once "we know we were in the right branch" — the standard way to turn "try the next alternative" into "this is definitely an if-statement, so a parse error here is a real syntax error, report it." Incomplete is the third arm, orthogonal to the other two (see streaming).
Diagnostics — VerboseError and context. The default Error<I> is minimal (an input position + an ErrorKind tag) for speed. For human-readable messages nom offers VerboseError<I>, which accumulates a Vec of (input, VerboseErrorKind) frames as the error unwinds the call stack, where VerboseErrorKind is Context(&'static str), Char(char), or Nom(ErrorKind) (doc/error_management.md). The context("…") combinator pushes a named frame, so the final error reads like a breadcrumb trail ("while parsing array, while parsing value, expected ]"). Custom error types plug in through three traits: ParseError<I> (the core contract: from_error_kind, append, from_char, or), ContextError<I> (adds add_context for the context combinator), and FromExternalError (wraps a foreign error surfaced by map_res). The popular [nom-supreme] crate builds richer, ready-made error trees on these traits.
IMPORTANT
nom is a parser, not an error-recovery framework. Its model is "fail fast with a good message", not "synthesize a placeholder node and keep going to report many errors at once." There is no automatic error recovery and no incremental reparsing: a nom parse is a single pass that stops at the first Failure. For multi-error recovery, IDE-grade resilience, or incremental editing, the relevant tools in this survey are chumsky (recovery-first combinators) and tree-sitter (incremental, error-tolerant CSTs). nom's Incomplete gives it streaming resumption, which is a different axis from edit resumption.
Ecosystem & maturity
nom is one of the most-depended-upon non-trivial crates in the Rust ecosystem. As of this review crates.io reports 576,482,486 total downloads for nom and 2,531 reverse dependencies (crates.io). It is battle-tested in security-sensitive production: the rusticata family of protocol parsers (tls-parser, der-parser, x509-parser, asn1-rs, ipsec-parser, …) is built on nom and is shipped inside Suricata (Rust parsers since Suricata 4.0); the video encoder [rav1e], the C-expression parser [cexpr] used by bindgen, iso8601, hdrhistogram, and unsigned-varint are all direct dependents visible in nom's reverse-dependency list. The library is mature and stable, released under the permissive MIT license, #![no_std]-capable, and still actively maintained by the rust-bakery org after Couprie's original authorship.
Its most consequential derivative is winnow (next section) — a hard fork that is itself now enormously adopted (it is the parser under toml_edit/toml, and therefore under Cargo's manifest parsing). Other notable companions are nom_locate (span/line-column tracking) and [nom-supreme] / [nom-language] (better errors and language-oriented helpers, the latter extracted as its own crate in 8.0).
The winnow fork (the modern successor)
winnow is Ed Page's fork of nom, created to back the toml_edit crate (it began life as Page's private nom8 fork before becoming a standalone library). It keeps nom's combinator philosophy but changes the foundational types for ergonomics and speed. Page's own framing (Winnow 0.5 blog):
"Winnow started as a fork of nom as I had found its toolbox model of parsers worked much better for me than the framework model other parser libraries used like combine. The original goals for the fork were to improve the developer experience and to remove a corner case that had a performance cliff you could fall off of."
The four substantive differences a nom user must understand when migrating (winnow _topic::nom, winnow _topic::why):
| Axis | nom | winnow |
|---|---|---|
| Parser signature | Fn(I) -> IResult<I, O, E> (returns leftover I) | Fn(&mut I) -> Result<O, E> (advances I in place, by mutation) |
| Why | thread the remaining input through every call | "Cleaner code … No forgetting to chain i … I need not be Copy" |
| Error/backtracking | Err::{Error, Failure, Incomplete} | ErrMode::{Backtrack, Cut, Incomplete} (modality, made optional) |
| Commit combinator | cut(...) (Error → Failure) | cut_err(...) (Backtrack → Cut) |
| Streaming | separate streaming/complete modules | one set of parsers; partiality is a property of the Partial<I> input |
| Sequence of parsers | tuple (a, b, c) / tuple((a, b, c)) | seq! macro / (a, b, c) |
The signature change is the heart of it (winnow _topic::nom): "winnow switched from pure-function parser (Fn(I) -> (I, O) to Fn(&mut I) -> O)", on the grounds of "Cleaner code: Removes need to pass i everywhere", "Correctness: No forgetting to chain i through a parser", "Flexibility: I does not need to be Copy or even Clone", and "Performance: Result::Ok is smaller without i". On error, the mutated input is "left pointing at where the error happened", which simplifies error reporting. winnow also folds Incomplete into an optional modality: if you use neither cut_err nor Partial, you can drop ErrMode entirely and parse with a plain winnow::Result<O>.
The fork is faster on the same benchmarks (the JSON numbers above show winnow 0.5.0 at 97 µs vs. nom 7.1.3 at 341 µs), partly from the smaller Ok payload, partly from "sprinkling some #[inline]s", removing a &str token-set implementation that was "7x slower", and "switching to imperative, rather than pure-functional parsing" (Winnow 0.5 blog). winnow also pulls the ecosystem in-house — span tracking, better errors, and tutorials live in the one crate rather than across nom_locate / nom-supreme, aiming (per its docs) to "include all the fundamentals for parsing to ensure the experience is cohesive and high quality."
NOTE
Which to reach for. nom and winnow are siblings, not rivals with a clear winner. nom is the long-established, MIT-licensed, byte-oriented incumbent with the larger reverse-dependency graph and the rusticata security pedigree. winnow is the ergonomics-and-speed-forward successor, now load-bearing under Cargo via toml_edit. The migration guide notes that "where names diverge, a doc alias exists", so porting is mostly mechanical. For a green-field Rust parser in 2025, winnow is the more actively-evolving choice; for an existing nom codebase or one in the security-parser ecosystem, nom remains fully supported.
Strengths
- Zero-copy, allocation-free recognition. Recognized spans are borrowed
&[u8]/&strslices; a pure recognizer allocates nothing. - First-class streaming.
Err::Incomplete(Needed)cleanly separates "need more bytes" from "wrong input" — exactly right for network protocols and large-file decoders; very few combinator libraries model this. - Byte/bit-oriented. Big/little-endian integers, bit-level parsers, and a
bitvecintegration make binary formats first-class, not an afterthought. - Fast. Monomorphized, inlined combinators reach handwritten-C territory on the project's own benchmarks.
- Safe. All the speed lives inside Rust's memory-safety guarantees — the entire reason nom was created (replacing unsafe C parsers in security-critical paths).
- Composable and testable. Each parser is an ordinary, individually-unit-testable function; the grammar lives in normal Rust with full IDE/
rustcsupport, no codegen step. - Battle-tested. 576M+ downloads, 2,531 reverse deps, shipping inside Suricata via
rusticata.
Weaknesses
- No memoization → backtracking foot-guns. Overlapping
altbranches can be super-linear; the author must factor common prefixes orcutdeliberately. - No left recursion. Like any PEG-style engine, left-recursive grammars must be rewritten (e.g. with
many0/fold_many0for left-associative operators). - No error recovery, no incremental reparsing. A parse stops at the first
Failure; there is no resynchronization to collect multiple errors and no edit-incremental mode — usechumskyor tree-sitter for those. streamingvscompleteis a sharp edge. Choosing the wrong module silently changes end-of-input behaviour; a perennial source of confusion.- Error ergonomics are opt-in. Good messages require
VerboseError+context(ornom-supreme); the defaultError<I>is terse by design. - API churn across majors. The macro→function (
5.0/7.0) and the8.0OutputMode/.parse()rewrite mean tutorials rot; much online material targets the dead macro API.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Parser = Fn(I) -> IResult<I, O, E> (thread leftover) | Pure functions: trivially composable, testable, no hidden state | Caller must chain i everywhere; larger Ok payload; I must be Copy/Clone — the precise points winnow inverted |
| Zero-copy slice outputs borrowing the input | No allocation for recognizers; speed parity with C | Output lifetimes tie results to the input buffer; owned data needs an explicit map |
Three-way Err (Error/Failure/Incomplete) | Separates recoverable, committed, and "need more data" — enables true streaming | Extra concept to learn; streaming vs complete module split is easy to get wrong |
Ordered choice (alt), no ambiguity analysis | Determinism and speed; no grammar-compilation phase | Author owns disambiguation; no left recursion; unmemoized backtracking can be quadratic |
Function-combinator API (post-5.0), macros retired (7.0) | First-class IDE/type support, readable errors, no macro magic | Two API generations of churn; legacy macro tutorials are misleading |
8.0 Parser trait + OutputMode | Recognize-only vs value-producing modes; less generated code, smaller API surface | Breaking release; advanced trait signatures (process<OM>) are harder to read |
Minimal default Error<I>, opt-in VerboseError | Keep the fast path allocation-free and small | Human-readable diagnostics require extra wiring (context, VerboseError, nom-supreme) |
Sources
- rust-bakery/nom — GitHub repository
- nom on docs.rs (8.0.0) · crates.io download stats
src/lib.rs— crate-root docs: combinator model, function-generating-parser signature, streaming vs completeREADME.md— zero-copy / streaming / performance claims,hex_colorexampledoc/making_a_new_parser_from_scratch.md— parsers as functions,IResult,length_valuedoc/error_management.md—Errthree-way model,cut,VerboseError,context, error traitsCHANGELOG.md—5.0macro→function rewrite,7.0macro removal,8.0Parser/OutputModerefactorParsertrait — docs.rs (8.0process<OM: OutputMode>)rust-bakery/parser_benchmarks— the project's own benchmark suite- Chifflier & Couprie, "Writing parsers like it is 2017" (LangSec/SSTIC) — security motivation, VLC/Suricata
rusticata— nom-based protocol parsers shipped in Suricata- winnow-rs/winnow — the fork · winnow
_topic::why· winnow_topic::nommigration guide - Ed Page, "Winnow 0.5: The Fastest Rust Parser-Combinator Library?" — fork rationale + benchmarks
- Related deep-dives in this survey: PEG / packrat theory · top-down / recursive descent ·
chumsky· Haskellparsec·simdjson· tree-sitter · the comparison capstone