Skip to content

chumsky (Rust)

A Rust parser-combinator library built around first-class error recovery and rich diagnostics: a parser is an ordinary Rust value, but a failing parse yields a partial AST and a list of errors rather than bailing on the first mistake — the property that makes it suited to building real language frontends and LSPs.

FieldValue
LanguageRust (no_std-capable)
LicenseMIT
Repositoryzesterer/chumsky (now primary on Codeberg; GitHub mirror archived)
Documentationdocs.rs/chumsky · crates.io · lib.rs
Key authorsJoshua Barretto (zesterer) and contributors
CategoryParser combinator (internal DSL, host-language-embedded)
Algorithm / grammar classRecursive-descent over PEG (ordered choice, no ambiguity); opt-in left-recursion + memoization; context-sensitive combinators
Lexing modelScannerless or two-stage — generic over Input, so the same combinators run over &str, &[u8], or a &[Token] slice
Latest release0.13.0 (published line; the 1.0.0-alpha.0 tag marks where the zero-copy rewrite incubated)

NOTE

The published 0.x line carries the zero-copy rewrite since 0.10.0 (2025-03-22), with 0.13.0 current in the cloned Codeberg source. The earlier 1.0.0-alpha.0 tag is where the rewrite incubated before landing in the published line. This deep-dive describes the zero-copy API — the Parser trait parameterised by an input lifetime 'src — not the pre-rewrite 0.9.x API, which differs substantially.


Overview

What it solves

A parser generator (Bison, ANTLR, Menhir, tree-sitter) consumes a grammar in a separate DSL and emits parsing code. A parser combinator library — the Parsec lineage in Haskell, nom and chumsky in Rust — takes the opposite stance: a parser is a first-class value of the host language, and bigger parsers are built from smaller ones with ordinary functions ("combinators"), so the full power of Rust is available inside the grammar. chumsky's README states the goal plainly (README.md):

"Chumsky is a parser library for Rust that makes writing expressive, high-performance parsers easy."

What sets chumsky apart from its combinator siblings is the problem it makes central: error recovery. Most parsers — generated or hand-written, including the existing Rust combinator libraries — are designed to accept valid input and produce, in the words of the author, "either a syntax tree or a single error message", not both. chumsky's author Joshua Barretto argues in the library's founding blog post that this framing is backwards (Why can't error-tolerant parsers also be easy to write?):

"Error reporting is, therefore, not simply an unusual deviation from the happy path: it is the happy path, and it is the one that we should prioritise thinking about when writing parsers."

The motivation is the modern compiler / IDE workload (blog):

"Modern software is complex and modern programming languages even more so. With rich static type systems and complex, heavyweight build systems, it's suddenly important that compilers can produce errors that can be fixed in batches to maintain developer productivity."

A parser that gives up at the first syntax error forces a slow edit-compile-fix-recompile loop and produces no AST for later stages (name resolution, type-checking, autocomplete) to work on. chumsky instead encounters an error, reports it, resynchronises with the input, and resumes — so one run yields many errors and a best-effort AST. The crate's own recovery guide names those three steps and the payoff (guide::_03_error_and_recovery):

"This approach yields significant benefits, it allows us to generate multiple errors per each run of our parser, which means less back and forth with the user."

Design philosophy

chumsky's thesis is that declarative ergonomics and error resilience are not in tension — that you should not have to choose between a terse combinator grammar and the hand-written, error-recovering parsers shipped by mature compilers (Rust's, Elm's). From the blog (blog):

"I'm convinced that it's not only possible, but also practical, to write high-quality parsers that perform reliable error recovery using more declarative approaches to parser development."

The key claim is that recovery costs almost nothing at the call site, because all the machinery — generating, recording, and selecting between error sets on different parse branches — is hidden behind the combinator API (blog):

"Despite this fact, the logic that generates errors, records errors, performs recovery, and selects between error sets on different parse branches is all hidden behind the declarative parser API. … None of these features come with any significant syntax overhead, nor implementation complexity. The code required to perform error recovery in this JSON parser is just 3 lines, for example."

Three further commitments shape the whole API:

  1. Generic over everything. A chumsky parser is parameterised over its input, token, output, span, and error types. The README's first feature is "Fully generic across input, token, output, span, and error types" (README.md). The same combinators parse a scannerless &str, a byte slice, or a slice of pre-lexed tokens.
  2. Zero-copy by default. Outputs can borrow directly from the input rather than copying it. The README: "Zero-copy parsing minimises allocation by having outputs hold references/slices of the input" (README.md). This is the headline of the rewrite that produced today's Parser<'src, …> trait.
  3. PEG semantics, no ambiguity. chumsky's parsers are recursive-descent and accept Parsing Expression Grammars — ordered choice, deterministic, no parse forests — extended with explicit combinators for the context-sensitive constructs (below) that a pure CFG cannot express.

chumsky's natural foils within this survey are nom (the other major Rust combinator library, tuned for byte/streaming throughput) and the Parsec lineage (the functional ancestor of the model). The explicit chumsky-vs-nom contrast is drawn out below.


How it works

The Parser trait and the zero-copy signature

Everything in chumsky is a value implementing the Parser trait. In the zero-copy API its signature carries four type parameters and an input lifetime (src/lib.rs):

rust
// chumsky::Parser (zero-copy line) — type parameters
pub trait Parser<'src, I, O, E = extra::Default>
where
    I: Input<'src>,            // the input stream (&str, &[u8], &[Token], …)
    E: ParserExtra<'src, I>,   // error type + parser State + Context
{
    // run the parser, producing BOTH an output and accumulated errors
    fn parse(&self, input: I) -> ParseResult<O, E::Error>
    where
        Self: Sized,
        E::State: Default,
        E::Context: Default;

    // "check-only" mode: validate without building the output (faster)
    fn check(&self, input: I) -> ParseResult<(), E::Error>
    where
        Self: Sized,
        O: 'src;
    // … plus the combinator methods below
}
Type parameterRole
'srcLifetime of the borrowed input — lets O hold references/slices into the input (zero-copy)
I: Input<'src>The input stream: &str, &[u8], &[T] token slices, or a nested/streamed source
OThe output the parser produces (an AST node, a token, a Vec, …)
E: ParserExtraA bundle of the error type, a mutable State (for arenas/interners), and a Context type

The crucial return type is ParseResult<O, E::Error>. Unlike a Result<O, E>, it carries an output and a list of errors at the same time — the type-level shape of "a partial AST plus a list of problems". This is what makes recovery a first-class outcome rather than an exceptional one.

extra::Default is the zero-config error/state bundle; a real language frontend swaps in a Rich<'src, char> error type (chumsky's batteries-included error that records spans, found/expected tokens, and labels) via extra::Err<Rich<…>>.

Building parsers: the combinator surface

A parser is assembled from primitives (leaf parsers) wrapped in combinator methods (which take a parser and return a bigger one). The canonical leaf is just — match a specific token/string — and the canonical combinators are methods on the Parser trait. The README's complete Brainfuck example shows the core surface in one screen (README.md):

rust
use chumsky::prelude::*;

#[derive(Clone)]
enum Instr {
    Left, Right,
    Incr, Decr,
    Read, Write,
    Loop(Vec<Self>),
}

fn brainfuck<'a>() -> impl Parser<'a, &'a str, Vec<Instr>> {
    recursive(|bf| choice((
        just('<').to(Instr::Left),
        just('>').to(Instr::Right),
        just('+').to(Instr::Incr),
        just('-').to(Instr::Decr),
        just(',').to(Instr::Read),
        just('.').to(Instr::Write),
        bf.delimited_by(just('['), just(']')).map(Instr::Loop),
    ))
        .repeated()
        .collect())
}

brainfuck().parse("--[>--->->->++>-<<<<<-------]>--.>---------.>--..+++.");

The combinators it touches generalise to every grammar:

Combinator / primitiveMeaning
just(x)Match exactly token/string x
any(), filter(pred)Match any token / a token satisfying a predicate
choice((a, b, …)) / a.or(b)Ordered choice: try each in turn, take the first that succeeds
a.then(b) / ignore_then / then_ignoreSequence two parsers; keep both (tuple), or only the second / only the first
a.repeated()Zero-or-more; an IterParser you .collect() into a Vec (or fold)
a.separated_by(sep)Repetition with a separator (commas, semicolons)
a.delimited_by(l, r)Parse a between an opening and closing delimiter
a.or_not()Optional — yields Option<O>
a.map(f) / a.to(v)Transform the output / replace it with a constant
a.foldl(b, f)Left-fold a parser's repeated outputs — the idiom for left-associative operator chains
a.labelled(name)Attach a human name used in "expected …" error messages
recursive with a closure argument pTie a recursive knot so a grammar can refer to itself

foldl is worth singling out: a naive left-recursive rule (expr := expr "+" term) would loop forever in a recursive-descent parser, so chumsky expresses left association as term.foldl(op.then(term).repeated(), …) — parse one term, then fold each following op term into an accumulator. (For deep operator-precedence grammars chumsky also offers Pratt parsing, which is more ergonomic still.)

NOTE

Because a chumsky parser is built from generic methods over the Input/Output types, the resulting closure tree is monomorphised and inlined by rustc. The README credits an "Internal optimiser leverages the power of GATs to optimise your parser for you" — generic associated types let the trait carry the borrowed-output lifetime through the whole combinator chain, which is what made the zero-copy rewrite possible.

Error recovery: recover_with and the recovery strategies

The signature feature is recover_with, a Parser method that wraps a parser with a recovery Strategy invoked only when the wrapped parser fails (src/lib.rs):

rust
// chumsky::Parser::recover_with
fn recover_with<S: Strategy<'src, I, O, E>>(self, strategy: S) -> RecoverWith<Self, S>;

When the inner parser fails, the strategy gets a chance to consume some input, synthesise a placeholder output (an Expr::Error node, say), and let the parse continue past the malformed region — so the error is recorded into ParseResult rather than aborting the parse. chumsky ships three built-in strategies, all in src/recovery.rs:

StrategyWhat it does
via_parser(p)Recover by running an arbitrary parser p; whatever p matches becomes the recovered region. The general-purpose primitive.
nested_delimiters(open, close, others, fb)Skip a balanced bracketed region, respecting nesting, and substitute a fallback output
skip_then_retry_until(skip, until)Skip tokens (one strategy) until a resync token (until) is seen, then retry the parser
skip_until(skip, until, fallback)Skip input until a resync token, then emit a fallback. The blunt last resort.

nested_delimiters is the workhorse for language frontends — if an expression inside (…)/[…]/{…} is broken, it lets the parser swallow the whole balanced group and keep going on the tokens after the closing bracket. Its verbatim doc comment (src/recovery.rs):

"A recovery parser that searches for a start and end delimiter, respecting nesting. … It is possible to specify additional delimiter pairs that are valid in the pattern's context for better errors. For example, you might want to also specify [('[', ']'), ('{', '}')] when recovering a parenthesized expression as this can aid in detecting delimiter mismatches. A function that generates a fallback output on recovery is also required."

The blunter skip_until carries an explicit health warning in its own doc comment, codifying the recovery-quality trade-off (src/recovery.rs):

"A recovery parser that skips input until one of several inputs is found. This strategy is very 'stupid' and can result in very poor error generation in some languages. Place this strategy after others as a last resort, and be careful about over-using it."

The recommended discipline is to stack strategies specific-to-general and to place recovery high in the parser stack so the real grammar gets every chance to parse before a fallback fires. From the recover_with doc-comment:

"There is no silver bullet for error recovery, so this function allows you to specify one of several different strategies at the location of your choice. Prefer an error recovery strategy that more precisely mirrors valid syntax where possible to make error recovery more reliable."

In practice a recovering expression parser reads:

rust
// recover a broken parenthesised expression, then keep parsing
expr.recover_with(via_parser(nested_delimiters(
    '(', ')',
    [('[', ']'), ('{', '}')],   // sibling pairs, for mismatch detection
    |span| Expr::Error,         // fallback AST node for the bad region
)))

Because the recovered region becomes an Expr::Error node inside an otherwise-real AST, downstream stages keep working: the LSP can still offer completions in the valid parts of a file with a syntax error in one function.

Built-in Pratt expression parsing

Operator-precedence grammars are the classic pain point for recursive-descent parsers. chumsky bakes in a Pratt parser via the pratt module, whose doc opens "Utilities for parsing expressions using Pratt parsing." (src/pratt.rs). Operators are declared with prefix, infix, and postfix constructors, each carrying a binding power and (for infix) an associativity — left(n), right(n), or none(n), higher n binding tighter (pratt docs):

rust
// chumsky::pratt — precedence-correct expression parsing in one combinator
let expr = atom.pratt((
    postfix(4, op('!'), |lhs, _, _| Expr::Factorial(Box::new(lhs))),
    infix(right(3), op('^'), |l, _, r, _| Expr::Pow(Box::new(l), Box::new(r))),
    prefix(2, op('-'), |_, rhs, _| Expr::Neg(Box::new(rhs))),
    infix(left(1), op('+'), |l, _, r, _| Expr::Add(Box::new(l), Box::new(r))),
));

This replaces the multi-level foldl precedence ladder a combinator parser would otherwise spell out by hand, parsing -2 + 3 ^ 4! with the conventional precedence (factorial > exponent > negation > addition) in a single declaration.

Context-sensitive parsing: stateful and nested

Pure PEGs are not simply "context-free": PEGs and CFGs are best treated as incomparable, with deterministic CFLs as common ground and syntactic predicates pushing PEGs beyond CFGs in other directions. Some real syntaxes need still more state. chumsky's rewrite added dedicated combinators for those context-sensitive cases. The 0.10.0 changelog records both the context-sensitive support and the state mechanism (CHANGELOG.md):

"Support for parsing context-sensitive grammars such as Python-style indentation, Rust-style raw strings, and much more""Support for manipulating shared state during parsing, elegantly allowing support for arena allocators, cstrees, interners, and much more"

Two mechanisms underpin this:

  • Context in the ParserExtra bundle lets an earlier parse result parameterise a later parser — e.g. read the # count of a Rust raw-string opener, then require exactly that many # to close it.
  • State (the mutable E::State) threads a &mut value (an interner, an arena, an indentation stack) through the parse, accessed via parse_with_state.

chumsky also supports nested inputs: a parser can run over a &[Token] slice where some tokens are themselves trees of tokens (Rust-style token trees), parsing the outer stream and recursing into bracketed groups without a separate flattening pass. The README lists "Nested inputs such as token trees are fully supported both as inputs and outputs" (README.md).

Diagnostics: the ariadne sister crate

chumsky deliberately stops at producing Rich error values; rendering them as beautiful terminal diagnostics is the job of ariadne, the same author's companion crate, whose README opens "A fancy compiler diagnostics crate." (ariadne README). The two are designed to complement each other without a hard dependency — ariadne is "a sister project" to chumsky, inspired by codespan and modelled on rustc's output. Given a Rich error's span and labels, ariadne renders multi-line, multi-file reports with colour-coded label arcs, overlap-avoiding layout heuristics, and Unicode/tab-aware column handling. The standard language-frontend pipeline is therefore: chumsky parses (recovering, accumulating Rich errors) → map each error to an ariadne::Report → ariadne prints it. This pairing is what gives a hobby language rustc-quality diagnostics for a few hours' work.


Algorithm & grammar class

chumsky is a recursive-descent parser with PEG semantics: choice is ordered (choice/or take the first alternative that matches), so a chumsky grammar is unambiguous by construction — there are no parse forests and no GLR-style ambiguity to resolve, in contrast to the general parsers (Earley/GLR). The README states the project's broad practical claim directly:

"Chumsky's parsers are recursive descent parsers and are capable of parsing parsing expression grammars (PEGs), which includes all known context-free languages. However, chumsky doesn't stop there: it also supports context-sensitive grammars via a set of dedicated combinators."

Read that as an upstream capability claim, not as a settled formal theorem that PEGs strictly contain CFGs. This survey's PEG theory deep-dive follows Ford's more careful position: PEGs express all deterministic LR-class CFLs, can express some non-CFLs such as aⁿbⁿcⁿ, and are believed but not proven to be incomparable with CFGs overall. Two refinements push chumsky past textbook recursive descent in practice. First, left recursion — normally fatal to recursive descent — has opt-in support via memoization (the README: "Left recursion and memoization have opt-in support"), so a directly left-recursive rule can be written without the foldl rewrite. Second, the context-sensitive combinators (above) handle indentation-sensitive and delimiter-counting syntaxes that a plain CFG cannot express. Ambiguity, the central concern of bottom-up and general parsing, simply does not arise here — ordered choice resolves every overlap by source order, the same determinism the PEG/packrat theory guarantees.

Interface & composition model

chumsky is an internal DSL: the grammar is Rust code, composed from Parser-trait methods, with no external grammar file and no code-generation step (the generator camp's model). This buys the full power of Rust inside the grammar — closures in map, ordinary functions returning impl Parser, recursive for self-reference — and ordinary cargo tooling, at the cost (shared with all combinator libraries) that the grammar is opaque to static analysis: there is no separate artefact to lint for ambiguity or to generate a railroad diagram from a grammar file (though chumsky can emit railroad diagrams from the parser itself, per its feature list).

The AST is built inline: each combinator's map/to/foldl/Pratt fold constructs output nodes as it goes, and zero-copy means those nodes can borrow slices of the input (identifiers, string literals) instead of allocating owned Strings. Host-language integration is total — a chumsky parser is just a value of an impl Parser<'src, I, O, E> type, returnable from a function, storable, and (with the caching feature) build-once-reuse-many.

Performance

chumsky's performance story is the zero-copy rewrite. The pre-0.10 0.9.x line allocated owned outputs; the rewrite (incubated across the 1.0.0-alpha.x line, shipped to stable in 0.10.0) reworked the entire Parser trait around a borrowed input lifetime so outputs hold references into the input. The changelog's verdict is blunt — "Performance has radically improved" (CHANGELOG.md) — and the README frames the target as parity with the throughput-focused libraries: chumsky has "performance comparable to a hand-written parser" and stays competitive with nom (README.md). The enabling mechanisms:

  • Zero-copy outputs"Zero-copy parsing minimises allocation by having outputs hold references/slices of the input" (README.md) — eliminate the per-token String allocations that dominated the old line.
  • GAT-based internal optimiser — the trait machinery monomorphises and inlines the combinator tree, so a chumsky parser compiles down to roughly the nested-match code a hand-written recursive-descent parser would be.
  • check() mode — a second evaluation path that validates input without constructing the output, for when you only need yes/no (the README: "Check-only mode for fast verification of inputs"). In the JSON example this is measurably faster than the full parse.
  • Opt-in memoization — packrat-style caching is available where a grammar needs it (left recursion, heavy backtracking), but is not paid by default, unlike a classic packrat parser that memoizes unconditionally.

Complexity is the usual recursive-descent profile: linear time on grammars without pathological backtracking, but PEG ordered choice can backtrack, so a poorly-factored grammar can degrade — memoization is the escape hatch. There is no SIMD/data-parallel path; that niche belongs to simdjson. chumsky's 0.x inputs are slice-based (not streaming/incremental like attoparsec's Partial), so the whole input is in memory.

IMPORTANT

chumsky's performance posture is "fast enough to not need a separate lexer-generator, while keeping rich errors", not "fastest possible". For raw byte-shovelling throughput on machine formats, nom (and certainly simdjson) still win; chumsky's bet is that AST-building human-language frontends value error quality far more than the last increment of throughput — and that the rewrite closed enough of the gap to make that trade painless.

Error handling & recovery

This is chumsky's reason to exist, and the dimension where it leads the field. Three layers:

  1. Rich errors by default. The built-in Rich error type records the span, the token found, the set of tokens expected, and any labelled grammar-production names — enough to render a "found }, expected expression" message without hand-rolling an error type.
  2. Recovery as a first-class outcome. recover_with plus the via_parser / nested_delimiters / skip_then_retry_until strategies let the parser produce a partial AST and a list of errors in one run — the ParseResult<O, E::Error> return type encodes exactly that. This is what no other mainstream Rust combinator library does declaratively.
  3. IDE-readiness. Because a syntax error in one function leaves the rest of the file's AST intact (recovery substitutes an Error node and continues), chumsky is well-suited to LSP servers and incremental compiler frontends, which must keep functioning on perpetually-incomplete, perpetually-invalid buffers. The author's own Tao language uses chumsky for both lexer and parser specifically to surface many lexer, parser, and type errors in a single run.

What chumsky does not do is incremental reparsing — re-parsing only the edited subtree of a previous parse, the way tree-sitter does. A chumsky parse is whole-input each time; its IDE story is error-resilience and partial ASTs, not sub-linear edit reparsing. For an editor that needs both, the two are complementary, not competing.

Ecosystem & maturity

chumsky is a widely-adopted, single-maintainer-led project (Joshua Barretto / zesterer, with contributors) — among the two best-known Rust parser-combinator libraries alongside nom, and the default recommendation for programming-language and DSL frontends in Rust where errors matter. Its flagship production user is the author's own statically-typed functional language Tao, used as the "dog food" project that drives the library's error-reporting work. It pairs with the sibling ariadne diagnostics crate (also widely used independently). The maturity caveat is API churn around the post-0.10 zero-copy redesign: the 0.13.0 published line is current, but the rewrite changed the trait shape substantially from 0.9.x and the road to a final 1.0 remains visible in the old alpha tag and branch history. The project recently moved its primary home to Codeberg, with the GitHub repository archived as a mirror. Tooling beyond the core is light: railroad-diagram generation and parser debugging utilities are built in; there is no separate grammar-workbench or generator IDE (there is nothing to generate from).


Strengths

  • Best-in-class declarative error recovery. recover_with + nested_delimiters produce a partial AST and a list of errors in one run, with (per the author) ~3 lines of recovery code for a JSON parser — no other mainstream Rust combinator library offers this.
  • Rich diagnostics for free. The default Rich error plus the ariadne sister crate give a hobby language rustc-grade terminal errors with spans, labels, and colour.
  • Genuinely generic. One combinator set runs over &str, &[u8], or &[Token] slices — scannerless or two-stage, your choice — and over your own span/error/output types.
  • Zero-copy outputs. Borrowed slices in the AST cut allocation; the rewrite brought throughput into the same league as nom for JSON-like inputs.
  • Built-in Pratt parsing makes operator-precedence expression grammars a one-combinator declaration instead of a hand-coded precedence ladder.
  • Beyond pure PEG: opt-in left recursion + memoization, and dedicated combinators for context-sensitive syntaxes (indentation, raw strings, token trees) and stateful parsing (arenas, interners).
  • no_std-capable, so it runs in embedded environments.
  • IDE/LSP-friendly: error-resilience and partial ASTs keep a frontend working on invalid, in-progress buffers.

Weaknesses

  • API instability. The recommended API is the post-0.10 zero-copy line, but 1.0 is not yet finalised and the rewrite changed the trait shape substantially from 0.9.x.
  • Compile-time and type complexity. Deeply nested combinator types and the GAT-based machinery can produce intimidating type errors and non-trivial rustc build times — a known cost of the heavily-generic combinator approach.
  • No incremental reparsing. Every parse is whole-input; unlike tree-sitter it does not re-parse only the edited subtree, so for very large always-changing buffers it leans on full re-parse speed rather than edit-locality.
  • Not the throughput champion. For machine/binary formats and maximum bytes/second, nom and simdjson remain faster; chumsky trades the last increment of speed for error quality.
  • Slice-based, not streaming. The whole input must be in memory; there is no attoparsec-style incremental/Partial input.
  • Single-maintainer bus factor, and a recent home move to Codeberg that downstreams must track.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Make error recovery a first-class outcome (ParseResult carries output and errors)"Error reporting … is the happy path"; language frontends/LSPs need partial ASTs + many errorsMore machinery behind the API; recovery placement is a real grammar-design concern
Combinator (internal DSL), no generator stepFull Rust inside the grammar; cargo-native; rapid syntax iterationGrammar opaque to static analysis; no external grammar artefact to lint for ambiguity
PEG ordered choice, unambiguous by constructionDeterministic, no parse forests, predictable; simple mental modelSource order silently resolves overlaps; pathological grammars can backtrack (memoization needed)
Generic over input/token/output/span/errorOne library for scannerless &str, byte slices, and pre-lexed token streamsHeavy generics → complex types and longer compile times
Zero-copy rewrite (input lifetime 'src, GAT optimiser)Outputs borrow input → far less allocation; throughput competitive with nomA breaking redesign of the trait; the long-lived 1.0.0-alpha churn it caused
Recovery strategies as opt-in, stacked specific→generalReal grammar gets every chance before a fallback fires; quality stays highskip_until-style fallbacks are "stupid" and degrade errors if over-used
Diagnostics split into the ariadne sister crateSeparation of concerns — parse vs render; ariadne is reusable independentlyTwo crates to wire together; an extra mapping step from Rich error → ariadne::Report
Opt-in left recursion / memoization (not packrat-by-default)Don't pay packrat's memory cost unless the grammar needs itLeft recursion / heavy backtracking need explicit opt-in; not automatically linear like a full packrat parser
Slice input, no streaming/incrementalSimpler, faster random access; fits whole-file compiler frontendsWhole input in memory; no Partial-style streaming, no tree-sitter-style edit reparsing

Sources