parseman 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -551
- package/dist/combinators/node.d.ts +15 -1
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +26 -0
- package/dist/compiler/build-arity.d.ts.map +1 -0
- package/dist/compiler/codegen.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/index.cjs +102 -12
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -12
- package/dist/index.js.map +4 -4
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +89 -13
- package/dist/plugin/index.cjs.map +3 -3
- package/dist/plugin/index.js +89 -13
- package/dist/plugin/index.js.map +3 -3
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -6,21 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Write parsers in TypeScript — fast enough to run as-is, and blazing fast when the bundler macro kicks in. Same code either way; no grammar files, no generated output to check in. Drop the plugin in tests or anywhere a bundler isn't around and everything still works.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Measured on Apple M2 Pro. Bars show µs per parse — shorter is faster. Refresh: `pnpm bench`, then `pnpm bench:svg` (updates `assets/bench-*.svg`).
|
|
12
|
-
|
|
13
|
-
Compared parsers: **Parséman**, Peggy, Parsimmon, Chevrotain, Nearley, and Jison (plus `JSON.parse` on JSON). Each implements the same parsing work on the bench fixtures — building JS values / row arrays / GraphQL AST nodes, not syntax-only validation. Peggy grammars in `bench/*.pegjs` are the reference; Nearley JSON uses [kach/nearley `examples/json.ne`](https://github.com/kach/nearley/blob/master/examples/json.ne); other Nearley and Jison grammars are ports of those Peggy files (`bench/vendor/`).
|
|
14
|
-
|
|
15
|
-

|
|
16
|
-
|
|
17
|
-

|
|
18
|
-
|
|
19
|
-

|
|
20
|
-
|
|
21
|
-
Parséman has three modes — **interpreter** (zero setup, works anywhere), **macro build** (compiled by the bundler plugin at build time, zero runtime cost), and **`.compile()`** (optional runtime JIT). Most production use lands on one of the first two. The initialization section only shows parsers with a nonzero setup cost: `.compile()` costs 75–650 µs depending on grammar size; Chevrotain always costs 840–1,400 µs. Parsers not listed there start for free. (Init numbers are pinned on the charts — they're noisy run-to-run; warm-parse bars are the meaningful comparison.)
|
|
22
|
-
|
|
23
|
-
On JSON, CSV, and GraphQL, Parséman macro beats Peggy at every fixture size in the charts above (e.g. GraphQL large: **142 µs** vs Peggy **400 µs**). For CST-building grammars (`node()` rules with trivia capture), the interpreter runs ~2× faster than Chevrotain on the JSON CST fixture while building an equivalent tree.
|
|
9
|
+
> **📖 Full documentation: [matthew-dean.github.io/parseman](https://matthew-dean.github.io/parseman/)**
|
|
24
10
|
|
|
25
11
|
---
|
|
26
12
|
|
|
@@ -31,8 +17,6 @@ npm install parseman
|
|
|
31
17
|
# pnpm add parseman
|
|
32
18
|
```
|
|
33
19
|
|
|
34
|
-
---
|
|
35
|
-
|
|
36
20
|
## Quick start
|
|
37
21
|
|
|
38
22
|
```ts
|
|
@@ -51,559 +35,56 @@ parse(requestLine, 'GET /api/v1 HTTP/1.1')
|
|
|
51
35
|
// { ok: true, value: { verb: 'GET', path: '/api/v1', version: 'HTTP/1.1' }, span: ... }
|
|
52
36
|
```
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
## Macro mode
|
|
38
|
+
## Three modes, one grammar
|
|
57
39
|
|
|
58
|
-
|
|
40
|
+
The same combinator code runs three ways, with identical results:
|
|
59
41
|
|
|
60
|
-
|
|
42
|
+
- **Interpreter** — zero setup, works anywhere (tests, REPLs, dynamic grammars).
|
|
43
|
+
- **Macro build** — a [bundler plugin](https://matthew-dean.github.io/parseman/guide/macro-mode) evaluates your grammar at build time and replaces it with inline JS. Zero runtime cost; the `parseman` import disappears from the bundle.
|
|
44
|
+
- **`.compile()`** — the same optimizer, run on demand at runtime.
|
|
61
45
|
|
|
62
46
|
```ts
|
|
63
|
-
// vite.config.ts
|
|
64
|
-
import
|
|
65
|
-
export default { plugins: [parseman()] }
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
```js
|
|
69
|
-
// rollup.config.js
|
|
70
|
-
import parseman from 'parseman/plugin'
|
|
71
|
-
export default { plugins: [parseman.rollup()] }
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
```js
|
|
75
|
-
// webpack.config.js
|
|
76
|
-
const parseman = require('parseman/plugin')
|
|
77
|
-
module.exports = { plugins: [parseman.webpack()] }
|
|
47
|
+
// Add the plugin (vite.config.ts) and one import attribute — that's the whole change:
|
|
48
|
+
import { literal, sequence, choice } from 'parseman' with { type: 'macro' }
|
|
78
49
|
```
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
import { literal, sequence, choice, regex, transform } from 'parseman' with { type: 'macro' }
|
|
84
|
-
```
|
|
51
|
+
See **[The three modes](https://matthew-dean.github.io/parseman/guide/modes)** for the full story.
|
|
85
52
|
|
|
86
|
-
|
|
53
|
+
## What's in the box
|
|
87
54
|
|
|
88
|
-
|
|
55
|
+
- **[Combinators](https://matthew-dean.github.io/parseman/guide/combinators)** — `literal`, `regex`, `sequence`, `choice`, `many`, `sepBy`, `not`, and more.
|
|
56
|
+
- **[Whitespace & trivia](https://matthew-dean.github.io/parseman/guide/trivia)** — grammar-defined filler skipping, with per-chunk kind capture.
|
|
57
|
+
- **[Recursive rules](https://matthew-dean.github.io/parseman/guide/recursive-rules)** — `rules()` for mutually recursive grammars; fully macro-compilable.
|
|
58
|
+
- **[CST / AST nodes](https://matthew-dean.github.io/parseman/guide/ast)** — `node()` captures terminals and trivia for you, with a `collapse` option for transparent wrapper rules.
|
|
59
|
+
- **[Incremental re-parsing](https://matthew-dean.github.io/parseman/guide/incremental)** — `makeFunctionalDoc` re-parses just the edited subtree on each keystroke.
|
|
60
|
+
- **[Error recovery](https://matthew-dean.github.io/parseman/guide/error-recovery)** — `recover`, `expect`, and a `{ recover: true }` channel keep parsing broken input and report every error.
|
|
61
|
+
- **[Context-sensitive parsing](https://matthew-dean.github.io/parseman/guide/context)** — `withCtx` / `guard` without mutating shared state.
|
|
89
62
|
|
|
90
|
-
|
|
91
|
-
// Before (source):
|
|
92
|
-
const method = choice(literal('GET'), literal('POST'), literal('PUT'), literal('DELETE'))
|
|
93
|
-
|
|
94
|
-
// After (bundle output):
|
|
95
|
-
const method = function(input, _pos, _ctx) {
|
|
96
|
-
const _code = _pos < input.length ? input.codePointAt(_pos) : -1
|
|
97
|
-
if (_code === 71) { /* G-E-T */ }
|
|
98
|
-
else if (_code === 80) { /* P-O-S-T */ }
|
|
99
|
-
else if (_code === 68) { /* D-E-L-E-T-E */ }
|
|
100
|
-
else return { ok: false, expected: ['"GET"', '"POST"', ...], span: { start: _pos, end: _pos } }
|
|
101
|
-
...
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Disjoint first characters → single `codePointAt` dispatch. Regex parsers → sticky `/pattern/y` hoisted to closure scope. No objects allocated on failure paths.
|
|
106
|
-
|
|
107
|
-
### Debugging still works
|
|
108
|
-
|
|
109
|
-
The plugin emits a precise source map via [magic-string](https://github.com/Rich-Harris/magic-string). Breakpoints set on the original `choice(...)` lines are hit when the compiled function runs; step-through shows original combinator source, not emitted charCode checks.
|
|
110
|
-
|
|
111
|
-
If `with { type: 'macro' }` is stripped (older bundlers, test runners), the attribute is silently ignored and the interpreter runs instead — identical results, no errors.
|
|
112
|
-
|
|
113
|
-
### What gets compiled
|
|
114
|
-
|
|
115
|
-
Combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `oneOrMore`, `optional`, `sepBy`, `transform`, `skip`, `not`, `scanTo`, `balanced` — plus `rules()` factories (including mutually recursive ones), `parser({ trivia })` / `noTrivia()` wrappers, and `node()` rules (CST capture and all). A full grammar built as a `rules()` factory of `node()` rules compiles end to end: each rule becomes an independently-callable function, terminal/trivia capture is emitted inline, and every `build`/`transform` callback is inlined with its source span. Grammars with no `node()` emit zero capture code, so they compile byte-identically. Parsers that close over external variables the evaluator can't resolve stay as-is — the plugin compiles what it can and quietly leaves the rest alone.
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## Combinators
|
|
120
|
-
|
|
121
|
-
**Terminology:** In this README, **combinator** means any building block that matches input (`literal`, `choice`, `sequence`, …). The `parser()` **function** is different — it wraps a root combinator with document-level options (trivia, line tracking) and gives you `.parse(input)`. Same word family, two roles: combinators compose the grammar; `parser()` configures how you run it.
|
|
122
|
-
|
|
123
|
-
A combinator reads from the input at the current position and succeeds or fails (returning a value on success). Composing combinators with `sequence`, `choice`, `many`, etc. is how you express parsing decisions.
|
|
124
|
-
|
|
125
|
-
`makeWord`, `rules`, and `parser` are **helpers**: definition-time factories or wrappers that *produce* combinators. They never match input themselves.
|
|
126
|
-
|
|
127
|
-
| Combinator | Description |
|
|
128
|
-
|---|---|
|
|
129
|
-
| `literal(s, opts?)` | Exact string match. `opts.caseInsensitive` for locale-aware comparison. |
|
|
130
|
-
| `word(s, boundary?)` | Single keyword with automatic word-boundary guard. |
|
|
131
|
-
| `keywords(words, opts?)` | Match one of many keywords (longest-first), with optional boundary and case folding. |
|
|
132
|
-
| `regex(pattern)` | Match a regex at the current position. Patterns are optimized via `regexp-tree`. |
|
|
133
|
-
| `sequence(...combinators)` | Match all in order; returns a tuple `[v1, v2, ...]`. Skips trivia between terms when trivia is set. |
|
|
134
|
-
| `choice(...combinators)` | Ordered alternatives (PEG — first match wins). Disjoint first chars → O(1) dispatch. |
|
|
135
|
-
| `many(combinator)` | Zero or more; compiles to a `while` loop. |
|
|
136
|
-
| `oneOrMore(combinator)` | One or more; fails if nothing matches. |
|
|
137
|
-
| `optional(combinator)` | Zero or one; returns `null` on no match. |
|
|
138
|
-
| `sepBy(combinator, sep)` | Zero or more combinator matches separated by `sep`. |
|
|
139
|
-
| `transform(combinator, fn)` | Map the result: `fn(value, span) → newValue`. |
|
|
140
|
-
| `skip(main, skipped)` | Match `main` then `skipped`; return `main`'s value. |
|
|
141
|
-
| `label(name, combinator)` | Attach a string label to a combinator arm (metadata). Used on trivia `choice` branches for per-chunk kind capture — see [Whitespace and comment skipping](#whitespace-and-comment-skipping). |
|
|
142
|
-
| `node(type, combinator, build)` | CST/AST rule: captures the combinator's terminals into `children`/`rawChildren` and trivia offsets into a flat `triviaLog`, then calls `build(children, rawChildren, span, triviaLog)`. |
|
|
143
|
-
| `ref<T>()` | Low-level forward declaration slot (use `rules()` in most cases). |
|
|
144
|
-
| `not(combinator)` | Negative lookahead — succeeds (consuming nothing) when the combinator fails. |
|
|
145
|
-
| `guard(predicate)` | Succeeds only when `predicate(ctx)` returns true; used for context-sensitive rules. |
|
|
146
|
-
| `withCtx(extra, combinator)` | Merge `extra` into the user context for the duration of the combinator. |
|
|
147
|
-
| `recover(combinator, sentinel)` | On failure, skip input until `sentinel` matches; returns a `CSTError` node. |
|
|
148
|
-
| `scanTo(sentinel, opts?)` | Scan forward until `sentinel` matches (sentinel not consumed). Pass `opts.skip` to treat certain patterns as opaque blobs that may contain the sentinel character. Pass `opts.orEOF: true` to succeed at end-of-input. |
|
|
149
|
-
| `balanced(open, close, opts?)` | Match a single balanced delimited region — e.g. `(…)` or `[…]` — including the delimiters. Primarily used as an element of `scanTo`'s `opts.skip` list. |
|
|
150
|
-
|
|
151
|
-
### Helpers (produce combinators at definition time)
|
|
152
|
-
|
|
153
|
-
| Helper | Description |
|
|
154
|
-
|---|---|
|
|
155
|
-
| `trivia(combinator)` | Label a combinator as skippable filler (whitespace, comments). Does not skip by itself — pass the result to `parser({ trivia })` to turn on auto-skipping between tokens. |
|
|
156
|
-
| `makeWord(boundary?)` | Returns `(str) => Combinator` with a fixed word-boundary class. Not a combinator — see [keyword disambiguation](#ordered-choice-and-keyword-disambiguation). |
|
|
157
|
-
| `rules(factory)` | Named, mutually-recursive rule bundle. See [Named and recursive rules](#named-and-recursive-rules). |
|
|
158
|
-
| `parser({ trivia }, combinator)` | Wrap a root combinator with document-level trivia skipping. `trivia: null` clears trivia for that scope (see `noTrivia`). See [Whitespace and comment skipping](#whitespace-and-comment-skipping). |
|
|
159
|
-
| `noTrivia(combinator)` | Run `combinator` with the active trivia cleared — no trivia is skipped between its `sequence`/`many` terms, so they must be **contiguous** in the input. (Trivia is whatever `parser({ trivia })` installed — often whitespace/comments, but it's grammar-defined.) The inverse of `parser({ trivia })`; re-enable trivia in a nested region with `parser({ trivia }, …)`. For a *static* glued token just use one `literal`/`regex` — reach for `noTrivia` when a glued part is itself a structured sub-rule (e.g. a head that must touch a `[subscript]` or `(args)`). See [Contiguous tokens](#contiguous-tokens-no-trivia-between-them). |
|
|
160
|
-
| `triviaEntries(log, labels?, opts?)` | Indexed view over a trivia log — `.start(i)`, `.end(i)`, `.kind(i)`, `.text(i, input)`. The log is a **flat number array** (no per-entry objects); each entry is a fixed-size run of consecutive numbers, so entry `i` starts at `i * stride`. Root `_triviaLog`: `[start, end]` (stride 2). Per-node `triviaLog` (pass `{ nodeLog: true }`): `[start, end, insertIdx]` (stride 3). Passing `labels` appends a kind index per entry (+1 to stride). |
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## Performance: collapse opaque shapes into one regex
|
|
165
|
-
|
|
166
|
-
The single biggest grammar-level perf lever is **the number of combinator boundaries on the hot path**. Every `sequence` / `regex` / `oneOrMore` step is a function call plus a result-object allocation plus — in a `node()` rule — a leaf push. Fewer, fatter combinators beat many thin ones.
|
|
167
|
-
|
|
168
|
-
Measured on a repeated 3-shape group (`name1 1px #111 …`, ~29 KB), parsing the same content three ways:
|
|
169
|
-
|
|
170
|
-
| Approach | Interpreted | Compiled |
|
|
171
|
-
|---|---|---|
|
|
172
|
-
| `oneOrMore(sequence(ident, sp, num, sp, hex, sp))` | 0.289 ms | 0.167 ms |
|
|
173
|
-
| same, with **inline** `regex(…)` instead of shared refs | 0.281 ms | 0.183 ms |
|
|
174
|
-
| **one** `regex(/…ident…num…hex…/)` per group | **0.055 ms** | **0.042 ms** |
|
|
175
|
-
|
|
176
|
-
Two takeaways:
|
|
177
|
-
|
|
178
|
-
- **Shared combinator ref vs inline `regex(…)` literal makes no difference.** Both produce the identical runtime structure (one `regex` combinator either way), and the compiler inlines a `regex` test at every use site regardless of sharing — there's no function-call indirection to eliminate (that only exists for `ref()` / `rules()` entries). Factor out shared terminals for readability; it costs nothing.
|
|
179
|
-
- **Collapsing a fixed multi-token shape into a single `regex` is 4–5× faster** in both the interpreter and compiled output, because it erases the per-step call + allocation overhead. `compile()` is a real but smaller win (~1.7×) and **stacks** with collapsing.
|
|
180
|
-
|
|
181
|
-
**When to collapse:** only where the CST treats the group as opaque text — a dimension `\d+px`, a hex color, an `nth` expression, a simple ident-run. A single regex yields **one leaf**, not structured sub-nodes.
|
|
182
|
-
|
|
183
|
-
**When NOT to collapse:** keep the parts as separate combinators wherever the builder needs them as distinct CST children — for per-field source spans (`fieldSpans`/`valueSpans`), for trivia recovered *between* the parts, or for distinct typed nodes. Correctness first; collapse only the genuinely opaque runs.
|
|
184
|
-
|
|
185
|
-
For library-level codegen and macro optimizations (not grammar authoring), see [PERF_IDEAS.md](./PERF_IDEAS.md).
|
|
63
|
+
Full API in the **[reference](https://matthew-dean.github.io/parseman/reference/api)**.
|
|
186
64
|
|
|
187
65
|
---
|
|
188
66
|
|
|
189
|
-
##
|
|
190
|
-
|
|
191
|
-
Skipping filler between tokens is a two-step setup:
|
|
192
|
-
|
|
193
|
-
1. **Define** what counts as filler — usually `regex(/\s+/)`, comments, or both — and wrap it with `trivia()`. This only sets a metadata flag (`isTrivia`); it does not change when the parser runs.
|
|
194
|
-
2. **Activate** skipping by passing that combinator to `parser({ trivia }, combinator)`. That installs it on the parse context so `sequence`, `sepBy`, `choice`, etc. consume matching filler automatically between terms.
|
|
195
|
-
|
|
196
|
-
```ts
|
|
197
|
-
import { parser, regex, trivia, sepBy, literal } from 'parseman'
|
|
198
|
-
|
|
199
|
-
const ws = trivia(regex(/\s*/)) // "this pattern is filler" — not skipped yet
|
|
200
|
-
const word = regex(/[a-z]+/)
|
|
201
|
-
const list = parser({ trivia: ws }, sepBy(word, literal(','))) // skipping on
|
|
202
|
-
|
|
203
|
-
list.parse('foo , bar , baz')
|
|
204
|
-
// { ok: true, value: ['foo', 'bar', 'baz'], ... }
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Multiple trivia types — whitespace and comments — combine with `choice()` and `many()`:
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
const lineComment = sequence(literal('//'), regex(/[^\n]*/))
|
|
211
|
-
const blockComment = sequence(literal('/*'), scanTo(literal('*/'), []))
|
|
212
|
-
const ws = trivia(many(choice(regex(/\s+/), lineComment, blockComment)))
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Label trivia arms so each captured chunk records its kind in `_triviaLog` / per-node `triviaLog`:
|
|
216
|
-
|
|
217
|
-
```ts
|
|
218
|
-
const rw = trivia(oneOrMore(choice(
|
|
219
|
-
label('whitespace', regex(/[ \t\n\r\f]+/)),
|
|
220
|
-
label('blockComment', regex(/\/\*(?:[^*]|\*(?!\/))*\*\//)),
|
|
221
|
-
)))
|
|
222
|
-
|
|
223
|
-
// After parse, resolve kinds lazily:
|
|
224
|
-
import { triviaEntries } from 'parseman'
|
|
225
|
-
const entries = triviaEntries(triviaLog, rw._meta.triviaKindLabels)
|
|
226
|
-
entries.kind(0) // 'whitespace'
|
|
227
|
-
entries.text(0, input) // slice on demand
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
`label(name, parser)` names a trivia arm; `node('Ruleset', …)` names a CST node — different namespaces, no conflict.
|
|
231
|
-
|
|
232
|
-
When all trivia arms are labeled, each captured chunk appends a **kind index** to the log (stride +1). Root `_triviaLog` entries are `[start, end, kind]`; per-node `triviaLog` entries are `[start, end, insertIdx, kind]`. Without labels, strides are 2 and 3 respectively.
|
|
233
|
-
|
|
234
|
-
### Contiguous tokens (no trivia between them)
|
|
235
|
-
|
|
236
|
-
Trivia skipping is ambient: once `parser({ trivia })` installs it, `sequence`/`many`/`choice` skip filler between **every** term. (Filler is whatever you passed as `trivia` — usually whitespace/comments, but it's grammar-defined.) Sometimes you need the opposite — parts that must touch.
|
|
237
|
-
|
|
238
|
-
If the whole thing is *static*, don't reach for `noTrivia` — just write one `literal`/`regex`: a decimal is `regex(/[0-9]+\.[0-9]+/)`, an operator is `literal('>=')`. `noTrivia` earns its keep only when a glued part is itself a **structured sub-rule** you can't fold into one pattern — usually recursive, or with its own trivia-enabled interior. The classic case is a head that must touch a bracket, wrapping a sub-expression that *does* allow spaces:
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
const ws = trivia(regex(/[ \t\n]+/))
|
|
242
|
-
|
|
243
|
-
// `arr[i + 1]` — `arr` is glued to `[` (no `arr [i]`), but the bracketed
|
|
244
|
-
// subscript is a full, space-tolerant expression. A regex can't express that:
|
|
245
|
-
// `expr` is recursive. Turn trivia back on for a region by nesting another
|
|
246
|
-
// parser({ trivia }) — the innermost one wins, and reverts on exit.
|
|
247
|
-
const indexed = noTrivia(sequence(
|
|
248
|
-
name,
|
|
249
|
-
parser({ trivia: ws }, sequence(literal('['), expr, literal(']'))),
|
|
250
|
-
))
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
Put the **whole** trivia-tolerant region inside the nested `parser({ trivia })` — including its leading `[`. `sequence` skips trivia only *between* terms, never before its first, so a `[` left *outside* the nested parser would glue fine but then reject a space right after it (`arr[ i ]`). Two rules of thumb: wrap the whole contiguous run in `noTrivia` (an enclosing `sequence` skips trivia *before* a term, so wrapping just the inner part leaks leading trivia), and wrap the whole spaced region in the nested `parser({ trivia })`. `noTrivia(child)` is exactly `parser({ trivia: null }, child)`.
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## Ordered choice and keyword disambiguation
|
|
258
|
-
|
|
259
|
-
`choice()` uses PEG ordered-choice semantics: first match wins. **Order matters.**
|
|
260
|
-
|
|
261
|
-
For keywords — where `if` should not match the prefix of `ifdef` — use the `word` combinator. Pass a custom **boundary** (the character class that must *not* follow the match) per call, or use `makeWord` to bake one boundary into a small factory:
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
import { word, makeWord, choice, regex } from 'parseman'
|
|
265
|
-
|
|
266
|
-
word('true') // combinator — default boundary
|
|
267
|
-
word('color', 'A-Za-z0-9_-') // combinator — one-off custom boundary
|
|
268
|
-
|
|
269
|
-
// Helper — returns a function that produces combinators (not a combinator itself)
|
|
270
|
-
const kw = makeWord()
|
|
271
|
-
const cssKw = makeWord('A-Za-z0-9_-')
|
|
272
|
-
|
|
273
|
-
const token = choice(
|
|
274
|
-
kw('if'), // each call yields a combinator
|
|
275
|
-
kw('else'),
|
|
276
|
-
cssKw('color'),
|
|
277
|
-
regex(/[a-zA-Z_]\w*/), // ident fallback
|
|
278
|
-
)
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
`makeWord` is optional — `(s) => word(s, 'A-Za-z0-9_-')` is equivalent.
|
|
282
|
-
|
|
283
|
-
Or build the boundary guard by hand with `not()`:
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
const wordChar = regex(/\w/)
|
|
287
|
-
const keyword = (s: string) => transform(sequence(literal(s), not(wordChar)), ([kw]) => kw)
|
|
288
|
-
const ident = regex(/[a-zA-Z_]\w*/)
|
|
289
|
-
|
|
290
|
-
const token = choice(
|
|
291
|
-
keyword('if'),
|
|
292
|
-
keyword('else'),
|
|
293
|
-
keyword('return'),
|
|
294
|
-
ident,
|
|
295
|
-
)
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
When alternatives share a prefix, put the longer one first:
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
// Wrong: choice(literal('in'), literal('instanceof')) — 'instanceof' never reached
|
|
302
|
-
const op = choice(literal('instanceof'), literal('in'), literal('if'))
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
## Named and recursive rules
|
|
308
|
-
|
|
309
|
-
Use `rules()` when your combinators need to reference each other by name. Pass a factory that receives all rule names as ready-to-use references and returns the definitions:
|
|
310
|
-
|
|
311
|
-
```ts
|
|
312
|
-
import { rules, parser, choice, sequence, literal, sepBy, transform, trivia, regex } from 'parseman'
|
|
313
|
-
import type { Combinator } from 'parseman'
|
|
314
|
-
|
|
315
|
-
type JSON = null | boolean | number | string | JSON[] | Record<string, JSON>
|
|
316
|
-
|
|
317
|
-
const ws = trivia(regex(/[ \t\n\r]*/))
|
|
318
|
-
|
|
319
|
-
const { value } = rules<{ value: Combinator<JSON> }>(g => {
|
|
320
|
-
const comma = literal(',')
|
|
321
|
-
|
|
322
|
-
const array = transform(
|
|
323
|
-
sequence(literal('['), sepBy(g.value, comma), literal(']')),
|
|
324
|
-
([, items]) => items as JSON[]
|
|
325
|
-
)
|
|
326
|
-
const pair = transform(
|
|
327
|
-
sequence(jsonString, literal(':'), g.value),
|
|
328
|
-
([key,, val]) => [key, val] as [string, JSON]
|
|
329
|
-
)
|
|
330
|
-
const object = transform(
|
|
331
|
-
sequence(literal('{'), sepBy(pair, comma), literal('}')),
|
|
332
|
-
([, pairs]) => Object.fromEntries(pairs) as Record<string, JSON>
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
value: choice(object, array, jsonString, jsonNumber, jsonBool, jsonNull) as Combinator<JSON>,
|
|
337
|
-
}
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
export const jsonParser = parser({ trivia: ws }, value)
|
|
341
|
-
jsonParser.parse('{ "a": 1 }')
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
`g.value` is a reference that works anywhere inside the factory regardless of order. Local helpers (`comma`, `pair`, `object`) that don't need to be cross-referenced can be plain `const`. Only put a rule in the returned object if other rules need to reach it as `g.xxx`.
|
|
345
|
-
|
|
346
|
-
> **Macro and `rules()`:** The plugin fully compiles `rules()` factories, including recursive ones. It emits mutually recursive named functions (`_pf0` etc.) so the cycle is broken. Add `with { type: 'macro' }` to your import and the entire grammar — recursive rules included — is inlined at build time. Both binding forms compile: `const { value } = rules(...)` (each rule becomes a top-level function) and `const grammar = rules(...)` (an object literal of compiled rules, so `grammar.value(...)` works).
|
|
347
|
-
>
|
|
348
|
-
> If the plugin meets a macro-imported declaration it can't compile statically (it closes over a runtime value, or isn't a recognized combinator shape), it leaves that declaration for the interpreter, strips the `with { type: 'macro' }` attribute so the import stays valid, and emits a build **warning** (`[parseman] file:line — …`) pointing at it — so a silent fallback never goes unnoticed.
|
|
349
|
-
|
|
350
|
-
### `ref<T>()` — low-level forward declaration
|
|
351
|
-
|
|
352
|
-
`rules()` handles forward references automatically. `ref<T>()` is the lower-level primitive it uses internally, exposed for cases where you need a single forward slot outside of a `rules()` call:
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
const value = ref<JSON>()
|
|
356
|
-
// ... build parsers that use value ...
|
|
357
|
-
value.define(choice(object, array, str, num, bool, nil))
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
## Grammars that build an AST
|
|
363
|
-
|
|
364
|
-
For grammars that produce a typed CST/AST, support incremental re-parsing, or care about trivia, wrap each rule in `node(type, combinator, build)`. parseman captures the rule's terminals into `children` / `rawChildren` and records trivia as flat `[start, end, insertIdx, …]` **entries** in `triviaLog` (three numbers per whitespace/comment chunk), then hands all four to `build(children, rawChildren, span, triviaLog)`. **Capture is the library's job:** you don't wrap terminals to recover their spans, and you don't reconstruct trivia — it's collected as the parser runs, in both the interpreter and the compiled build. Use `buildTriviaIndex(tree, input)` to turn `triviaLog` into a before/after lookup table.
|
|
365
|
-
|
|
366
|
-
```ts
|
|
367
|
-
import { rules, parser, node, regex, literal, sequence, many, trivia } from 'parseman'
|
|
368
|
-
import type { Combinator } from 'parseman'
|
|
369
|
-
|
|
370
|
-
// Any node shape works as long as it satisfies NodeLike (see below).
|
|
371
|
-
type N = { _tag: 'node'; type: string; span: { start: number; end: number }; state: unknown; children: unknown[] }
|
|
372
|
-
const ws = trivia(regex(/\s+/))
|
|
373
|
-
|
|
374
|
-
export const { Expr, Num } = rules<{ Expr: Combinator<N>; Num: Combinator<N> }>(g => {
|
|
375
|
-
const num = node('Num', regex(/[0-9]+/),
|
|
376
|
-
(children, raw, span, _triviaLog) => ({ _tag: 'node', type: 'Num', span, state: null, children: [...children] }))
|
|
377
|
-
const expr = node('Expr', parser({ trivia: ws }, sequence(g.Num, many(sequence(literal('+'), g.Num)))),
|
|
378
|
-
(children, raw, span, _triviaLog) => ({ _tag: 'node', type: 'Expr', span, state: null, children: [...children] }))
|
|
379
|
-
return { Expr: expr, Num: num }
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
Expr.parse('1 + 2 + 3', 0, { trackLines: false })
|
|
383
|
-
// value is a Node whose children are the captured Num sub-nodes and '+' leaves
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
- **`children`** — structural items in source order: spanned `CSTLeaf` terminals (`{ _tag:'leaf', value, span }`) and sub-nodes (whatever a nested `node()`'s `build` returned). A `build` that returns a bare string is recorded by the parent as a spanned leaf, so single-item "collapsing" rules keep their source span.
|
|
387
|
-
- **`rawChildren`** — structural children only (same items as `children`, without trivia tokens).
|
|
388
|
-
- **`triviaLog`** — flat `[start, end, insertIdx, …]` entries for whitespace/comments consumed between terms. `insertIdx` is the `rawChildren` index before which the trivia was consumed. Pass the tree to `buildTriviaIndex(tree, input)` for a `before`/`after` map of trivia tokens — useful for whitespace-sensitive syntax (e.g. CSS `div p` vs `div.p`).
|
|
389
|
-
|
|
390
|
-
Each rule returned from the factory is independently callable — `Expr`, `Num` above are the **rule registry** incremental re-parsing needs. Wrap a rule's inner combinator in `parser({ trivia }, combinator)` so trivia-skipping is baked in regardless of which rule you start from; the macro compiles the wrapper (and all capture) away to flat JS.
|
|
391
|
-
|
|
392
|
-
> `transform(p, fn)` is still the tool for plain value-mapping (no children/trivia). `node()` is for CST/AST rules — it adds the capture `transform` doesn't. Both compile under the macro.
|
|
393
|
-
|
|
394
|
-
### Incremental re-parsing
|
|
395
|
-
|
|
396
|
-
`makeFunctionalDoc(registry, rootRule, input, opts?)` wraps a parse in a document that re-parses incrementally on edits. The `registry` is the object `rules()` returns (rule name → parser fn); the parser functions stay stateless, all incremental state lives in the doc.
|
|
397
|
-
|
|
398
|
-
```ts
|
|
399
|
-
import { makeFunctionalDoc } from 'parseman'
|
|
400
|
-
|
|
401
|
-
const registry = { Expr, Num } // straight from rules()
|
|
402
|
-
let doc = makeFunctionalDoc(registry, 'Expr', src)
|
|
403
|
-
doc.tree // your Node root, or null on failure
|
|
404
|
-
doc.errors // ParseFail[], empty on success
|
|
405
|
-
doc.input // the source string
|
|
406
|
-
|
|
407
|
-
// edit(from, to, replacement) — two byte offsets into the OLD text + the
|
|
408
|
-
// replacement. "Select from→to, type replacement" — the three things every
|
|
409
|
-
// editor knows on each keystroke. Returns a new doc; the old one is untouched.
|
|
410
|
-
doc = doc.edit(changeStart, changeStart + changeLength, newText)
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
`edit()` finds the smallest node containing the change, re-parses just that rule from its start offset using the node's saved `state`, and grafts the result back in when the new span end lands where the edit's delta predicts. Nodes unaffected by the edit are shared by reference between old and new docs. For class-instance ASTs that can't be shallow-spread, pass `opts.rebuild(node, children)` to control how a parent is reconstructed when a child is replaced.
|
|
414
|
-
|
|
415
|
-
**In an IDE extension**, keep one registry per language and one doc per open document. Each keystroke gives you the changed range as byte offsets — pass them straight to `edit()`:
|
|
416
|
-
|
|
417
|
-
```ts
|
|
418
|
-
const docs = new Map<string, ReturnType<typeof makeFunctionalDoc<Node>>>()
|
|
419
|
-
|
|
420
|
-
vscode.workspace.onDidOpenTextDocument(d => {
|
|
421
|
-
docs.set(d.uri.toString(), makeFunctionalDoc(registry, 'Stylesheet', d.getText()))
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
vscode.workspace.onDidChangeTextDocument(event => {
|
|
425
|
-
const uri = event.document.uri.toString()
|
|
426
|
-
let doc = docs.get(uri)!
|
|
427
|
-
for (const change of event.contentChanges) {
|
|
428
|
-
doc = doc.edit(change.rangeOffset, change.rangeOffset + change.rangeLength, change.text)
|
|
429
|
-
}
|
|
430
|
-
docs.set(uri, doc)
|
|
431
|
-
// walk doc.tree for diagnostics, folding ranges, semantic tokens, etc.
|
|
432
|
-
})
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Extending grammars
|
|
436
|
-
|
|
437
|
-
Don't copy a `rules()` factory to make a variant — export what stays the same and pass in (or wrap) what changes. `examples/json/` is the template:
|
|
438
|
-
|
|
439
|
-
| File | What changes |
|
|
440
|
-
|------|----------------|
|
|
441
|
-
| `parser.ts` | Base grammar + `makeJSONParser(customWs)` |
|
|
442
|
-
| `jsonc.ts` | Trivia only — `makeJSONParser(jsoncWs)` |
|
|
443
|
-
| `jsonl.ts` | Document shape — `sepBy(jsonValue, '\n')` with tighter trivia |
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
// jsonc.ts — same recursive grammar, different trivia
|
|
447
|
-
export const jsoncValue = makeJSONParser(jsoncWs)
|
|
448
|
-
|
|
449
|
-
// jsonl.ts — reuse jsonValue, wrap at the top
|
|
450
|
-
export const jsonl = parser({ trivia: lineWs }, sepBy(jsonValue, literal('\n')))
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
`makeJSONParser` in `parser.ts` is the knob: one `rules()` body, trivia passed in, `parser({ trivia }, value)` returned. Unchanged core → parameterize; unchanged rule, different document → wrap the export.
|
|
454
|
-
|
|
455
|
-
---
|
|
456
|
-
|
|
457
|
-
## Context-sensitive parsing
|
|
458
|
-
|
|
459
|
-
`withCtx` and `guard` implement context-sensitive rules without mutating shared state, and they compose into `rules()` like any other combinator.
|
|
460
|
-
|
|
461
|
-
`withCtx(extra, combinator)` merges `extra` into the user context for the duration of the combinator. `guard(predicate)` succeeds only when `predicate(ctx)` returns true, gating a rule behind runtime context.
|
|
462
|
-
|
|
463
|
-
```ts
|
|
464
|
-
import { rules, withCtx, guard, many, sequence, choice, literal, regex, trivia, parser } from 'parseman'
|
|
465
|
-
import type { Combinator } from 'parseman'
|
|
466
|
-
|
|
467
|
-
const ws = trivia(regex(/\s*/))
|
|
468
|
-
|
|
469
|
-
export const { Program } = rules<{ Program: Combinator<unknown> }>(g => {
|
|
470
|
-
const expr = regex(/[a-z]+/)
|
|
471
|
-
const ret = sequence(guard((ctx: { inFn?: boolean }) => ctx.inFn === true), literal('return'))
|
|
472
|
-
const stmt = choice(ret, expr)
|
|
473
|
-
const body = withCtx({ inFn: true }, many(sequence(stmt, ws)))
|
|
474
|
-
return { Program: parser({ trivia: ws }, many(body)) }
|
|
475
|
-
})
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
`return` is only reachable inside a body because `guard` rejects it when `inFn` is not set. Incremental `edit()` replays the correct context because each node records the `ctx.state` snapshot (`node.state`) active when it was parsed.
|
|
479
|
-
|
|
480
|
-
---
|
|
481
|
-
|
|
482
|
-
## Error recovery
|
|
483
|
-
|
|
484
|
-
`recover(combinator, sentinel)` wraps a combinator so that on failure it skips forward until `sentinel` matches, then returns a `CSTError` node instead of bailing on the whole parse. Error recovery is never pretty, but at least you can keep going.
|
|
485
|
-
|
|
486
|
-
```ts
|
|
487
|
-
import { recover, scanTo, balanced, literal } from 'parseman'
|
|
488
|
-
|
|
489
|
-
// Skip to ';' if a statement fails to parse
|
|
490
|
-
const stmt = recover(g.Stmt, literal(';'))
|
|
491
|
-
|
|
492
|
-
// Consume everything up to '}', skipping balanced () and [] pairs
|
|
493
|
-
const block = scanTo(literal('}'), [balanced(literal('('), literal(')')), balanced(literal('['), literal(']'))])
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
### `scanTo` vs `balanced` — when to use each
|
|
497
|
-
|
|
498
|
-
Both are **position arithmetic**: pure cursor-advance with zero CST allocation. Neither pushes terminals into the enclosing `node()`'s child list; only the final scanned span appears as a single leaf.
|
|
499
|
-
|
|
500
|
-
**`scanTo(sentinel, opts?)`** — use when you need to consume an open-ended region whose boundary is a specific token. It scans character-by-character, stopping the moment `sentinel` matches. Pass `opts.skip` to declare patterns that should be treated as opaque blobs so their content never accidentally looks like the sentinel:
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
// Consume a CSS at-rule prelude up to '{' or ';',
|
|
504
|
-
// but don't stop inside parentheses or strings.
|
|
505
|
-
const prelude = scanTo(choice(literal('{'), literal(';')), {
|
|
506
|
-
skip: [balanced('(', ')'), singleStr, doubleStr],
|
|
507
|
-
})
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
**`balanced(open, close, opts?)`** — use when you want to match a single self-contained delimited region and get its full text back (delimiters included). This is the natural building block for `scanTo`'s `skip` list:
|
|
511
|
-
|
|
512
|
-
```ts
|
|
513
|
-
const parenGroup = balanced('(', ')') // matches (a + b) including the parens
|
|
514
|
-
const bracketExpr = balanced('[', ']') // matches [0] including the brackets
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
`balanced` can itself accept a `skip` list for deeply nested structures:
|
|
518
|
-
|
|
519
|
-
```ts
|
|
520
|
-
// Match (…) allowing strings inside to contain unbalanced parens
|
|
521
|
-
const parenWithStrings = balanced('(', ')', { skip: [singleStr, doubleStr] })
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
**The key difference**: `scanTo` scans *until* a boundary; `balanced` matches *across* a known delimited region. Use `balanced` inside `scanTo`'s `skip` to prevent the scanner from stopping at a sentinel that appears inside a nested structure.
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
## Line / column tracking
|
|
529
|
-
|
|
530
|
-
```ts
|
|
531
|
-
const r = parse(myParser, 'hello\nworld', { trackLines: true })
|
|
532
|
-
|
|
533
|
-
if (r.ok) {
|
|
534
|
-
r.span.startLine // 1
|
|
535
|
-
r.span.startColumn // 1
|
|
536
|
-
r.span.endLine // 2
|
|
537
|
-
r.span.endColumn // 6
|
|
538
|
-
}
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
Line lookup is O(log n) via binary search on a precomputed newline index built once per input string. When `trackLines` is false (the default), no index is built and spans carry only byte offsets.
|
|
542
|
-
|
|
543
|
-
---
|
|
544
|
-
|
|
545
|
-
## `compile()` — runtime compilation
|
|
546
|
-
|
|
547
|
-
`compile()` runs the same optimizer as the plugin, but at runtime — handy when you're assembling a grammar dynamically, or just want the speed without a build step:
|
|
67
|
+
## Benchmarks
|
|
548
68
|
|
|
549
|
-
|
|
550
|
-
import { choice, literal, compile } from 'parseman'
|
|
69
|
+
**When parsing to JS values** — objects, row arrays, AST nodes — **Parséman's macro build is the fastest general-purpose JS parser we benchmark**, beating Peggy, Parsimmon, Chevrotain, Nearley, and Jison at every grammar and size. The only thing that edges it out is a purpose-built native like `JSON.parse`; for anything that *doesn't* have a built-in, Parséman is the one to beat.
|
|
551
70
|
|
|
552
|
-
|
|
553
|
-
compiled.parse('yes', 0, { trackLines: false }) // { ok: true, value: 'yes', ... }
|
|
554
|
-
compiled.source // generated JS source string
|
|
555
|
-
compiled.inlineExpression // self-contained expression (what the plugin inlines)
|
|
556
|
-
```
|
|
71
|
+
For **syntax tree building**, the compiled CST path (macro build) beats [Lezer](https://lezer.codemirror.net/) on the JSON CST fixture too — while producing a richer object tree with spans and trivia. For **incremental re-parse**, the two trade places by edit type: Parséman's `makeFunctionalDoc` is ~300× faster than a full reparse on in-place value edits (and ahead of Lezer), while Lezer's buffer reuse wins on structural edits. Full breakdown in the [benchmarks guide](https://matthew-dean.github.io/parseman/guide/benchmarks).
|
|
557
72
|
|
|
558
|
-
|
|
73
|
+
Measured on Apple M2 Pro. Bars show µs per parse — shorter is faster. Refresh: `pnpm bench`, then `pnpm bench:svg` (updates `assets/bench-*.svg`).
|
|
559
74
|
|
|
560
|
-
|
|
75
|
+
Compared parsers: **Parséman**, Peggy, Parsimmon, Chevrotain, Nearley, and Jison (plus `JSON.parse` on JSON). Each implements the same parsing work on the bench fixtures — building JS values / row arrays / GraphQL AST nodes, not syntax-only validation. Peggy grammars in `bench/*.pegjs` are the reference; Nearley JSON uses [kach/nearley `examples/json.ne`](https://github.com/kach/nearley/blob/master/examples/json.ne); other Nearley and Jison grammars are ports of those Peggy files (`bench/vendor/`).
|
|
561
76
|
|
|
562
|
-
|
|
77
|
+

|
|
563
78
|
|
|
564
|
-
|
|
565
|
-
type ParseOk<T> = { ok: true; value: T; span: Span }
|
|
566
|
-
type ParseFail = { ok: false; expected: string[]; span: Span }
|
|
567
|
-
type ParseResult<T> = ParseOk<T> | ParseFail
|
|
568
|
-
|
|
569
|
-
type Span = {
|
|
570
|
-
start: number // byte offset, inclusive
|
|
571
|
-
end: number // byte offset, exclusive
|
|
572
|
-
startLine?: number // 1-based; only when trackLines: true
|
|
573
|
-
startColumn?: number
|
|
574
|
-
endLine?: number
|
|
575
|
-
endColumn?: number
|
|
576
|
-
}
|
|
577
|
-
```
|
|
79
|
+

|
|
578
80
|
|
|
579
|
-
|
|
81
|
+

|
|
580
82
|
|
|
581
|
-
|
|
83
|
+

|
|
582
84
|
|
|
583
|
-
|
|
584
|
-
type NodeLike = {
|
|
585
|
-
readonly _tag: 'node'
|
|
586
|
-
readonly type: string // the rule name — used as the registry key on re-parse
|
|
587
|
-
readonly span: Span
|
|
588
|
-
readonly state: unknown // ctx.state snapshot at parse time; replayed on edit
|
|
589
|
-
readonly children: ReadonlyArray<{ readonly _tag: string }>
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// makeFunctionalDoc<N>(registry, rootRule, input, opts?) → FunctionalDoc<N>
|
|
593
|
-
type Registry<N> = Record<string, (input: string, pos: number, ctx: ParseContext) => ParseResult<N>>
|
|
594
|
-
interface FunctionalDoc<N extends NodeLike> {
|
|
595
|
-
readonly tree: N | null
|
|
596
|
-
readonly errors: ParseFail[]
|
|
597
|
-
readonly input: string
|
|
598
|
-
edit(from: number, to: number, replacement: string): FunctionalDoc<N>
|
|
599
|
-
}
|
|
600
|
-
type FunctionalDocOptions<N> = {
|
|
601
|
-
state?: unknown // initial ctx.state for the root parse
|
|
602
|
-
rebuild?: (node: N, children: ReadonlyArray<unknown>) => N // override for class-instance ASTs
|
|
603
|
-
}
|
|
604
|
-
```
|
|
85
|
+
Parséman has three modes — **interpreter** (zero setup, works anywhere), **macro build** (compiled by the bundler plugin at build time, zero runtime cost), and **`.compile()`** (optional runtime JIT). Most production use lands on one of the first two. The initialization section only shows parsers with a nonzero setup cost: `.compile()` costs 75–650 µs depending on grammar size; Chevrotain always costs 840–1,400 µs. Parsers not listed there start for free. (Init numbers are pinned on the charts — they're noisy run-to-run; warm-parse bars are the meaningful comparison.)
|
|
605
86
|
|
|
606
|
-
|
|
87
|
+
On JSON, CSV, and GraphQL, Parséman macro beats every other library at every fixture size in the charts above — e.g. GraphQL large **154 µs** vs Peggy **423 µs**, JSON large **125 µs** vs Peggy **472 µs** / Chevrotain **1,946 µs**. Native `JSON.parse` is the one thing faster on JSON (**45 µs** large). Even the zero-setup interpreter beats all five libraries at large sizes. On the CST chart, macro build beats Lezer at every size (**344 µs** vs **587 µs** large, parse-only). Full write-up and how to refresh the charts: **[benchmarks guide](https://matthew-dean.github.io/parseman/guide/benchmarks)**. Grammar-level speed levers: [performance guide](https://matthew-dean.github.io/parseman/guide/performance); library-level codegen: [PERF_IDEAS.md](./PERF_IDEAS.md).
|
|
607
88
|
|
|
608
89
|
---
|
|
609
90
|
|
|
@@ -611,12 +92,13 @@ type FunctionalDocOptions<N> = {
|
|
|
611
92
|
|
|
612
93
|
```bash
|
|
613
94
|
pnpm install
|
|
614
|
-
pnpm test
|
|
615
|
-
pnpm typecheck
|
|
616
|
-
pnpm build
|
|
95
|
+
pnpm test # Vitest — interpreter + compiler parity + ordered-choice semantics
|
|
96
|
+
pnpm typecheck # TypeScript 7
|
|
97
|
+
pnpm build # ESM + CJS + .d.ts → dist/
|
|
617
98
|
pnpm bench # Parséman vs Peggy, Parsimmon, Chevrotain, Nearley, Jison
|
|
618
99
|
pnpm bench:svg # refresh assets/bench-*.svg after bench
|
|
619
100
|
pnpm bench:compile-grammars # regenerate Peggy / Nearley / Jison parser output in bench/
|
|
101
|
+
pnpm docs:dev # run this documentation site locally
|
|
620
102
|
```
|
|
621
103
|
|
|
622
104
|
## License
|