parseman
v0.48.1
Published
Parser combinators that compile to optimized JavaScript — use as a library or as a build-time macro
Maintainers
Readme
Parséman (PAR-zə-mahn)
Write parsers as ordinary functions. Ship them like hand-written parsers.
Parser combinators are pleasant to write and usually slow. Parser generators are fast and usually mean grammar files, generated code, and extra tooling. Parséman is a combinator library with an optional compiler, and it gives you both.
Parsing to JS values, the runtime-compiled artifact is the fastest general-purpose JS
parser in the suite — ahead of every other library measured, at every grammar and every
input size in that suite.
Every parser in the suite builds real output: objects, row arrays, AST nodes. On a 7.7 kB
GraphQL document Parséman takes 188 µs; Peggy takes 323 µs. Only a purpose-built native
edges it out: JSON.parse on JSON.
Two more results. The compiled CST path beats Lezer on the JSON CST fixture — 242 µs vs
570 µs at 11.9 kB — while producing a richer tree carrying spans and trivia. And parseDoc
stores parent-relative spans, so an in-place edit costs a fraction of a full reparse rather
than a multiple of it. Results move with grammar shape, input size and runtime — which is
why the suite ships with the library rather than only its conclusions.
You get there by writing normal code. Add the bundler plugin, mark one import, and the
combinators you already wrote compile to an optimized TableProgram artifact — the compiler
computes first sets, links direct bodies for proven shapes, and left-factors choices for you.
Grammars work the same in plain JavaScript: the macro compiles a .js grammar to the same
output as a .ts one. The package ships one ESM implementation, loadable through either
import or require() on the supported Node versions. Write in TypeScript and result
types are inferred across the whole combinator chain — types you didn't have to write out.
Reach for it when you want CST/AST nodes with spans and trivia, error recovery and incremental re-parsing for editor tooling, or simply a fast parser for a DSL, config language, formatter, or linter.
Parséman is scannerless without giving up token-style routing. Literal or first-set-disjoint
alternatives are a natural fit for choice(...); when several branches share a broad opener,
dispatch(selector, when(...), otherwise(...)) parses that head once, then routes by the
returned value or structural marker. CSS-like grammars use that for at-rules,
identifier-or-function values, media features, and dialect routes where interpolation and
dedicated syntax overlap.
📖 Full documentation: matthew-dean.github.io/parseman
Install
npm install parseman
# pnpm add parsemanPre-1.0: minor versions may carry breaking changes — check the changelog
before upgrading. Requires Node ^20.19.0 || >=22.13.0.
Quick start
import { literal, sequence, choice, regex, transform, parse } from 'parseman'
const method = choice(literal('GET'), literal('POST'), literal('PUT'), literal('DELETE'))
const target = regex(/[^\s]+/)
const version = regex(/1\.[01]/)
const requestLine = transform(
sequence(method, literal(' '), target, literal(' HTTP/'), version),
([verb, , path, , ver]) => ({ verb, path, version: `HTTP/${ver}` })
)
parse(requestLine, 'GET /api/v1 HTTP/1.1')
// { ok: true, value: { verb: 'GET', path: '/api/v1', version: 'HTTP/1.1' }, span: ... }New here? What is Parséman? walks the same ground more slowly.
Three modes, one grammar
The same combinator code runs three ways, with identical results:
- Interpreter — zero setup, works anywhere (tests, REPLs, dynamic grammars).
- Macro build — a bundler plugin
evaluates your grammar at build time and inlines the result. The combinator import you
mark
with { type: 'macro' }disappears entirely; what ships is a compact table artifact using the sharedparseman/tableruntime. Executing it still goes throughrun()/parse(), so an app keeps Parseman as an ordinary dependency — see the three modes.) compile()— the same optimizer, on demand at runtime.
// Add the plugin (vite.config.ts) and one import attribute — that's the whole change:
import { literal, sequence, choice } from 'parseman' with { type: 'macro' }See The three modes.
What's in the box
✍️ Writing grammars
- 🧩 Combinators —
literal,regex,sequence,choice,many,sepBy,token,peek,not, and more. - 🌀 Recursive rules —
rules()for mutually recursive grammars; fully macro-compilable. - 🫧 Whitespace & trivia — grammar-defined filler skipping, with per-chunk kind capture.
- 🎯 Ordered choice, done right — PEG semantics you control by ordering, with keyword boundaries that don't bite.
- 🪶 Grammars the way you write them —
no FIRST/FOLLOW sets to compute, no left-factoring
choice(a·x, a·y)intoa·(x | y)to satisfy the tool. The compiler does that for you. - 🧬 Extending grammars —
compose()a dialect onto a base grammar instead of forking it. - 🎛️ Context-sensitive parsing —
withCtx/gatewithout mutating shared state. - 🧭 Scannerless routing —
dispatchparses a broad shared head once and routes at the grammar boundary that matters.
🌳 Getting structure out
- 🌲 CST / AST nodes —
node()captures terminals, named fields, and trivia, with policies for wrapping and collapsing. - 🩹 Error recovery — keep parsing broken input and report every error.
- ⚡ Incremental re-parsing —
parseDocre-parses just the edited subtree on each keystroke. - 💡 Editor / LSP integration — completions and lint keyed by rule name; the grammar stays pure structure.
🔬 Seeing what your grammar does
- 🚂 Railroad diagrams & EBNF —
toRailroadHtml()andtoEBNF()generate the grammar reference from the parser, so the spec can't drift from what actually parses. - 🔎 Grammar observability — coverage ("which rules and choice arms ran?") and trace ("what did it try, select, and backtrack through?").
- 🚦 First-char gating diagnostics —
diagnoseGrammar()tells you whichchoicelost its O(1) dispatch, names the overlapping arms, and says how to fix it. Run it in a test or a CI job — compiling reports nothing. - 📈 Performance guide — the levers that actually move a grammar, and how regexes lower.
Full API in the reference; how it stacks up against Peggy, Chevrotain, Lezer, tree-sitter, Parsimmon, Nearley and hand-written parsers in How Parséman compares.
Benchmarks
Benchmarked against Peggy, Parsimmon, Chevrotain, Nearley, Jison and Lezer on JSON, CSV and GraphQL, at three input sizes each. Each chart's legend names the libraries measured for that grammar.
Largest fixture of each, runtime compile against the fastest other library on that chart:
GraphQL 188 µs vs Peggy's 323 µs, JSON 223 µs vs Chevrotain's 238 µs, CSV
114 µs vs Peggy's 422 µs. Native JSON.parse does JSON large in 50.3 µs. On the CST
chart, runtime compile runs 242 µs against Lezer's 570 µs parse-only.
Those are the committed 0.48.0 charts, regenerated on 2026-08-14 on an M4 Pro with Node 25.9.0. They include the restored small rows for JSON, CSV, GraphQL, and CST.
Per-fixture figures, initialization costs, hardware, grammar provenance and how to reproduce any of it: benchmarks guide. Results move with grammar shape, input size and runtime — which is why the suite ships with the library rather than only its conclusions. Speed levers for your own grammars: performance guide.
The GraphQL fixture is a real grammar, parsing executable documents — queries, mutations, fragments, directives, every value type — into typed AST nodes, so the numbers come from a spec-shaped language rather than a toy.
Developing
pnpm install
pnpm test # interpreter + compiled parity, ordered-choice semantics
pnpm typecheck
pnpm build # ESM + .d.ts → dist/
pnpm docs:dev # this documentation site, locallyBenchmark and chart tasks (pnpm bench, bench:svg, bench:parseman, …) are described in
the benchmarks guide.
The frozen 0.48 runtime architecture and release specification is
docs/design/parseman-0.48.md. Detailed measurements
and rejected experiments remain in the linked evidence registers rather than being
treated as current design.
License
MIT © Matthew Dean
