parseman 0.1.7 → 0.3.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.
Files changed (54) hide show
  1. package/README.md +192 -155
  2. package/dist/combinators/choice.d.ts.map +1 -1
  3. package/dist/combinators/completions.d.ts +17 -0
  4. package/dist/combinators/completions.d.ts.map +1 -0
  5. package/dist/combinators/grammar.d.ts +20 -4
  6. package/dist/combinators/grammar.d.ts.map +1 -1
  7. package/dist/combinators/guard.d.ts +2 -2
  8. package/dist/combinators/guard.d.ts.map +1 -1
  9. package/dist/combinators/keywords.d.ts +22 -0
  10. package/dist/combinators/keywords.d.ts.map +1 -0
  11. package/dist/combinators/literal.d.ts.map +1 -1
  12. package/dist/combinators/node.d.ts +24 -0
  13. package/dist/combinators/node.d.ts.map +1 -0
  14. package/dist/combinators/parser.d.ts +7 -7
  15. package/dist/combinators/parser.d.ts.map +1 -1
  16. package/dist/combinators/probe.d.ts +13 -0
  17. package/dist/combinators/probe.d.ts.map +1 -0
  18. package/dist/combinators/recover.d.ts.map +1 -1
  19. package/dist/combinators/regex.d.ts.map +1 -1
  20. package/dist/combinators/repeat.d.ts.map +1 -1
  21. package/dist/combinators/scanTo.d.ts.map +1 -1
  22. package/dist/combinators/sequence.d.ts.map +1 -1
  23. package/dist/combinators/trivia-skip.d.ts +33 -0
  24. package/dist/combinators/trivia-skip.d.ts.map +1 -0
  25. package/dist/combinators/withCtx.d.ts +1 -1
  26. package/dist/compiler/codegen.d.ts +13 -4
  27. package/dist/compiler/codegen.d.ts.map +1 -1
  28. package/dist/cst/grammar.d.ts +33 -2
  29. package/dist/cst/grammar.d.ts.map +1 -1
  30. package/dist/cst/incremental.d.ts +4 -2
  31. package/dist/cst/incremental.d.ts.map +1 -1
  32. package/dist/cst/trivia-index.d.ts +50 -0
  33. package/dist/cst/trivia-index.d.ts.map +1 -0
  34. package/dist/cst/types.d.ts +7 -7
  35. package/dist/cst/types.d.ts.map +1 -1
  36. package/dist/functional/doc.d.ts +53 -0
  37. package/dist/functional/doc.d.ts.map +1 -0
  38. package/dist/index.cjs +953 -221
  39. package/dist/index.cjs.map +4 -4
  40. package/dist/index.d.ts +12 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +6519 -226
  43. package/dist/index.js.map +4 -4
  44. package/dist/plugin/evaluator.d.ts +26 -6
  45. package/dist/plugin/evaluator.d.ts.map +1 -1
  46. package/dist/plugin/index.cjs +1164 -215
  47. package/dist/plugin/index.cjs.map +4 -4
  48. package/dist/plugin/index.d.ts +5 -2
  49. package/dist/plugin/index.d.ts.map +1 -1
  50. package/dist/plugin/index.js +1164 -215
  51. package/dist/plugin/index.js.map +4 -4
  52. package/dist/types.d.ts +51 -6
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +5 -3
package/README.md CHANGED
@@ -6,6 +6,8 @@
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
+ _Note: Not necessarily production-ready! I still have test cases and sample parsers I want to build to more rigorously test the API and performance._
10
+
9
11
  ## Benchmarks
10
12
 
11
13
  Measured on Apple M2 Pro. Bars show µs per parse — shorter is faster.
@@ -14,7 +16,11 @@ Measured on Apple M2 Pro. Bars show µs per parse — shorter is faster.
14
16
 
15
17
  ![CSV parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parsecraft/main/assets/bench-csv.svg)
16
18
 
17
- Parséman compiled edges out Peggy on small and medium JSON. At 12 kB Peggy pulls ahead by ~10% — it's been doing this a while. On CSV, where the grammar is non-recursive and fully inlines, Parséman compiled is the clear winner.
19
+ ![GraphQL parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parsecraft/main/assets/bench-graphql.svg)
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 85–680 µs depending on grammar size; Chevrotain always costs 950–1,300 µs. Parsers not listed there start for free.
22
+
23
+ On JSON, Parséman macro beats Peggy at small and medium sizes; Peggy pulls ahead by ~19% at 12 kB — it's been doing this a while. On CSV and GraphQL, where the grammar is non-recursive or fully inlineable, Parséman macro is the clear winner — CSV macro is now ~6× faster than Peggy on the 500-row fixture. 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.
18
24
 
19
25
  ---
20
26
 
@@ -106,7 +112,7 @@ If `with { type: 'macro' }` is stripped (older bundlers, test runners), the attr
106
112
 
107
113
  ### What gets compiled
108
114
 
109
- Pure combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `oneOrMore`, `optional`, `sepBy`, `transform`, `skip`. Parsers using `ref()` for recursion or that close over external variables stay as-is. The plugin compiles what it can and quietly leaves the rest alone.
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 })` 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.
110
116
 
111
117
  ---
112
118
 
@@ -115,6 +121,8 @@ Pure combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `one
115
121
  | Combinator | Description |
116
122
  |---|---|
117
123
  | `literal(s, opts?)` | Exact string match. `opts.caseInsensitive` for locale-aware comparison. |
124
+ | `word(s, boundary?)` | Single keyword with automatic word-boundary guard. |
125
+ | `wordContext(boundary?)` | Returns a keyword factory with a baked-in boundary (e.g. CSS `A-Za-z0-9_-`). |
118
126
  | `regex(pattern)` | Match a regex at the current position. Patterns are optimized via `regexp-tree`. |
119
127
  | `sequence(...parsers)` | Match all in order; returns a tuple `[v1, v2, ...]`. Skips trivia between terms when trivia is set. |
120
128
  | `choice(...parsers)` | Ordered alternatives (PEG — first match wins). Disjoint first chars → O(1) dispatch. |
@@ -124,29 +132,53 @@ Pure combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `one
124
132
  | `sepBy(parser, sep)` | Zero or more `parser` separated by `sep`. |
125
133
  | `transform(parser, fn)` | Map the result: `fn(value, span) → newValue`. |
126
134
  | `skip(main, skipped)` | Match `main` then `skipped`; return `main`'s value. |
127
- | `parser(factory)` | Mutually recursive grammar rules — no forward declarations needed. |
128
- | `ref<T>()` | Low-level forward declaration slot (use `parser()` in most cases). |
135
+ | `rules(factory)` | Named grammar rules — no forward declarations needed, handles mutual recursion. |
136
+ | `node(type, parser, build)` | CST/AST rule: captures `parser`'s terminals into `children`/`rawChildren` and trivia offsets into a flat `triviaLog`, then calls `build(children, rawChildren, span, triviaLog)`. |
137
+ | `ref<T>()` | Low-level forward declaration slot (use `rules()` in most cases). |
129
138
  | `not(parser)` | Negative lookahead — succeeds (consuming nothing) when `parser` fails. |
130
139
  | `guard(predicate)` | Succeeds only when `predicate(ctx)` returns true; used for context-sensitive rules. |
131
140
  | `withCtx(extra, parser)` | Merge `extra` into the user context for the duration of `parser`. |
132
141
  | `recover(parser, sentinel)` | On failure, skip input until `sentinel` matches; returns a `CSTError` node. |
133
- | `scanTo(sentinel, skips?, opts?)` | Consume input up to (and including) `sentinel`, optionally skipping balanced pairs. |
134
- | `balanced(open, close)` | Match a balanced pair (e.g. `(…)`, `[…]`). Used as a `skip` argument to `scanTo`. |
142
+ | `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. |
143
+ | `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. |
144
+
145
+ ---
146
+
147
+ ## Performance: collapse opaque shapes into one regex
148
+
149
+ 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.
150
+
151
+ Measured on a repeated 3-shape group (`name1 1px #111 …`, ~29 KB), parsing the same content three ways:
152
+
153
+ | Approach | Interpreted | Compiled |
154
+ |---|---|---|
155
+ | `oneOrMore(sequence(ident, sp, num, sp, hex, sp))` | 0.289 ms | 0.167 ms |
156
+ | same, with **inline** `regex(…)` instead of shared refs | 0.281 ms | 0.183 ms |
157
+ | **one** `regex(/…ident…num…hex…/)` per group | **0.055 ms** | **0.042 ms** |
158
+
159
+ Two takeaways:
160
+
161
+ - **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.
162
+ - **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.
163
+
164
+ **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.
165
+
166
+ **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.
135
167
 
136
168
  ---
137
169
 
138
170
  ## Whitespace and comment skipping
139
171
 
140
- Pass `trivia` to `parse()` and `sequence()` will automatically skip it between terms:
172
+ Wrap your root combinator with `parser()` to declare trivia — whitespace and comments to skip between tokens. This bakes the setting into the combinator tree so all modes (interpreter, `compile()`, macro build) behave identically:
141
173
 
142
174
  ```ts
143
- import { regex, sepBy, literal, parse } from 'parseman'
175
+ import { parser, regex, trivia, sepBy, literal } from 'parseman'
144
176
 
145
- const ws = regex(/\s*/)
177
+ const ws = trivia(regex(/\s*/))
146
178
  const word = regex(/[a-z]+/)
147
- const list = sepBy(word, literal(','))
179
+ const list = parser({ trivia: ws }, sepBy(word, literal(',')))
148
180
 
149
- parse(list, 'foo , bar , baz', { trivia: ws })
181
+ list.parse('foo , bar , baz')
150
182
  // { ok: true, value: ['foo', 'bar', 'baz'], ... }
151
183
  ```
152
184
 
@@ -155,15 +187,7 @@ Multiple trivia types — whitespace and comments — combine with `choice()` an
155
187
  ```ts
156
188
  const lineComment = sequence(literal('//'), regex(/[^\n]*/))
157
189
  const blockComment = sequence(literal('/*'), scanTo(literal('*/'), []))
158
- const trivia = many(choice(regex(/\s+/), lineComment, blockComment))
159
- ```
160
-
161
- Use `grammar(opts, root)` instead of the `parse()` trivia option when you want trivia only for a subtree within a larger parse:
162
-
163
- ```ts
164
- import { grammar } from 'parseman'
165
-
166
- const jsonValue = grammar({ trivia: ws }, choice(object, array, str, num, bool, nil))
190
+ const ws = trivia(many(choice(regex(/\s+/), lineComment, blockComment)))
167
191
  ```
168
192
 
169
193
  ---
@@ -172,7 +196,20 @@ const jsonValue = grammar({ trivia: ws }, choice(object, array, str, num, bool,
172
196
 
173
197
  `choice()` uses PEG ordered-choice semantics: first match wins. **Order matters.**
174
198
 
175
- For keywords — where `if` should not match the prefix of `ifdef` — use `not()`:
199
+ For keywords — where `if` should not match the prefix of `ifdef` — use `word()` or `wordContext()`:
200
+
201
+ ```ts
202
+ const kw = wordContext() // bakes in a default identifier boundary once
203
+
204
+ const token = choice(
205
+ kw('if'),
206
+ kw('else'),
207
+ kw('return'),
208
+ regex(/[a-zA-Z_]\w*/), // ident fallback
209
+ )
210
+ ```
211
+
212
+ Or build the boundary guard by hand with `not()`:
176
213
 
177
214
  ```ts
178
215
  const wordChar = regex(/\w/)
@@ -196,20 +233,20 @@ const op = choice(literal('instanceof'), literal('in'), literal('if'))
196
233
 
197
234
  ---
198
235
 
199
- ## Recursive grammars
236
+ ## Named and recursive rules
200
237
 
201
- Use `parser()` for mutually recursive rules. Pass a factory that receives all rule names as ready-to-use references and returns the definitions:
238
+ 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:
202
239
 
203
240
  ```ts
204
- import { parser, choice, sequence, literal, sepBy, transform, regex } from 'parseman'
241
+ import { rules, parser, choice, sequence, literal, sepBy, transform, trivia, regex } from 'parseman'
205
242
  import type { Combinator } from 'parseman'
206
243
 
207
244
  type JSON = null | boolean | number | string | JSON[] | Record<string, JSON>
208
245
 
209
- const ws = regex(/[ \t\n\r]*/)
246
+ const ws = trivia(regex(/[ \t\n\r]*/))
210
247
 
211
- const { value } = parser<{ value: Combinator<JSON> }>(g => {
212
- const comma = sequence(ws, literal(','), ws)
248
+ const { value } = rules<{ value: Combinator<JSON> }>(g => {
249
+ const comma = literal(',')
213
250
 
214
251
  const array = transform(
215
252
  sequence(literal('['), sepBy(g.value, comma), literal(']')),
@@ -225,151 +262,92 @@ const { value } = parser<{ value: Combinator<JSON> }>(g => {
225
262
  )
226
263
 
227
264
  return {
228
- value: grammar(
229
- { trivia: ws },
230
- choice(object, array, jsonString, jsonNumber, jsonBool, jsonNull)
231
- ) as Combinator<JSON>,
265
+ value: choice(object, array, jsonString, jsonNumber, jsonBool, jsonNull) as Combinator<JSON>,
232
266
  }
233
267
  })
268
+
269
+ export const jsonParser = parser({ trivia: ws }, value)
270
+ jsonParser.parse('{ "a": 1 }')
234
271
  ```
235
272
 
236
- `g.value` is a parser 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`.
273
+ `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`.
237
274
 
238
- > **Macro note:** Recursive parsers can't be inlined as a single expression, so the plugin leaves them as-is — but you can still call `compile()` at runtime for the same speedups. The codegen emits mutually recursive named functions and handles cycles just fine. Non-recursive leaf parsers in the same file still get inlined.
275
+ > **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).
276
+ >
277
+ > 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.
239
278
 
240
279
  ### `ref<T>()` — low-level forward declaration
241
280
 
242
- `parser()` is the right tool for most recursive grammars. `ref<T>()` is the lower-level primitive it uses internally, exposed for cases where you need a single forward slot outside of a `parser()` call:
281
+ `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:
243
282
 
244
283
  ```ts
245
284
  const value = ref<JSON>()
246
285
  // ... build parsers that use value ...
247
- value.define(grammar({ trivia: ws }, choice(object, array, str, num, bool, nil)))
286
+ value.define(choice(object, array, str, num, bool, nil))
248
287
  ```
249
288
 
250
289
  ---
251
290
 
252
- ## Class-based grammars
291
+ ## Grammars that build an AST
253
292
 
254
- For grammars that need automatic CST construction, incremental re-parsing, or custom AST nodes, extend `Parser`. Capital-letter rules produce named CST nodes; lowercase rules are transparent helpers whose terminals surface as leaves of the nearest enclosing rule.
293
+ For grammars that produce a typed CST/AST, support incremental re-parsing, or care about trivia, wrap each rule in `node(type, parser, build)`. parseman captures the rule's terminals into `children` / `rawChildren` and records trivia as flat `[start, end, insertIdx, …]` triples in `triviaLog`, 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.
255
294
 
256
295
  ```ts
257
- import { Parser, parse, regex, literal, choice, sequence, many, sepBy } from 'parseman'
258
- import type { Refs } from 'parseman'
259
-
260
- class ExprParser extends Parser {
261
- ws = regex(/\s*/)
262
- digits = regex(/[0-9]+/)
263
- ident = regex(/[a-zA-Z_]\w*/)
264
-
265
- // Plain initializer when no cross-reference needed
266
- Str = sequence(literal('"'), regex(/[^"]*/), literal('"'))
267
-
268
- // Thunk form for forward / mutual references
269
- Num = (g: Refs<ExprParser>) => g.digits
270
- Id = (g: Refs<ExprParser>) => g.ident
271
- Add = (g: Refs<ExprParser>) => sequence(g.Num, many(sequence(literal('+'), g.Num)))
272
- Expr = (g: Refs<ExprParser>) => choice(g.Add, g.Num, g.Id)
273
- }
274
-
275
- const expr = new ExprParser()
276
- const r = parse(expr.rule('Expr'), '1+2+3')
277
- // r.value is a CSTNode { _tag: 'node', type: 'Expr', span, children, savedContext }
278
- ```
296
+ import { rules, parser, node, regex, literal, sequence, many, trivia } from 'parseman'
297
+ import type { Combinator } from 'parseman'
279
298
 
280
- ### Inheritance
299
+ // Any node shape works as long as it satisfies NodeLike (see below).
300
+ type N = { _tag: 'node'; type: string; span: { start: number; end: number }; state: unknown; children: unknown[] }
301
+ const ws = trivia(regex(/\s+/))
281
302
 
282
- Override any rule by redeclaring it in a subclass — subclass initializers run after the parent's, so the override wins automatically:
303
+ export const { Expr, Num } = rules<{ Expr: Combinator<N>; Num: Combinator<N> }>(g => {
304
+ const num = node('Num', regex(/[0-9]+/),
305
+ (children, raw, span, _triviaLog) => ({ _tag: 'node', type: 'Num', span, state: null, children: [...children] }))
306
+ const expr = node('Expr', parser({ trivia: ws }, sequence(g.Num, many(sequence(literal('+'), g.Num)))),
307
+ (children, raw, span, _triviaLog) => ({ _tag: 'node', type: 'Expr', span, state: null, children: [...children] }))
308
+ return { Expr: expr, Num: num }
309
+ })
283
310
 
284
- ```ts
285
- class JSXParser extends ExprParser {
286
- // replace just the ident rule; everything else stays
287
- ident = regex(/[a-zA-Z_$][\w$]*/)
288
- }
311
+ Expr.parse('1 + 2 + 3', 0, { trackLines: false })
312
+ // value is a Node whose children are the captured Num sub-nodes and '+' leaves
289
313
  ```
290
314
 
291
- ### Custom AST nodes (`buildNode`)
292
-
293
- Override `buildNode` to return your own node type instead of the default `CSTNode`:
294
-
295
- ```ts
296
- import type { CSTLeaf, CSTError, CSTRawChild, Span } from 'parseman'
297
-
298
- type MyNode = { _tag: 'node'; type: string; span: Span; savedContext: unknown; children: MyNode[]; text: string }
299
-
300
- class MyParser extends Parser<MyNode> {
301
- // ... rules ...
315
+ - **`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.
316
+ - **`rawChildren`** — structural children only (same items as `children`, without trivia tokens).
317
+ - **`triviaLog`** — flat `[start, end, insertIdx, …]` triples 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`).
302
318
 
303
- protected buildNode(
304
- type: string,
305
- span: Span,
306
- children: ReadonlyArray<MyNode | CSTLeaf | CSTError>,
307
- savedContext: unknown,
308
- rawChildren: ReadonlyArray<CSTRawChild>,
309
- ): MyNode {
310
- return { _tag: 'node', type, span, savedContext, children: children as MyNode[], text: '...' }
311
- }
312
- }
313
- ```
319
+ 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 parser in `parser({ trivia })` 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.
314
320
 
315
- `children` contains the structural children (sub-nodes and leaf tokens, no trivia). `rawChildren` contains everything in parse order including trivia tokens — useful for whitespace-sensitive grammars.
316
-
317
- ### Whitespace-sensitive rules with `rawChildren`
318
-
319
- When whitespace is semantically meaningful (e.g. CSS where `div p` is a descendant combinator but `div+p` is adjacent), inspect `rawChildren` inside `buildNode`:
320
-
321
- ```ts
322
- import type { CSTTrivia } from 'parseman'
323
-
324
- class CssParser extends Parser<SelectorNode> {
325
- ident = regex(/[a-zA-Z-]+/)
326
- Selector = (g: Refs<CssParser>) => sequence(g.ident, g.ident)
327
-
328
- protected buildNode(type, span, children, savedContext, rawChildren) {
329
- if (type === 'Selector') {
330
- // rawChildren: [Ident("div"), CSTTrivia(" "), Ident("p")]
331
- const hasDescendant = rawChildren.some(c => c._tag === 'trivia')
332
- }
333
- return ...
334
- }
335
- }
336
-
337
- // Trivia is set on parse() — the whitespace skip happens globally
338
- parse(css.rule('Stylesheet'), src, { trivia: many(choice(regex(/\s+/), comment)) })
339
- ```
340
-
341
- `CSTTrivia` nodes only appear in `rawChildren`, never in `children`. Zero-length trivia matches (e.g. `\s*` at a non-whitespace position) are not emitted.
321
+ > `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.
342
322
 
343
323
  ### Incremental re-parsing
344
324
 
345
- `Parser.parse(ruleName, input)` returns a `ParseDoc` — an object holding the tree, any parse errors, and an `edit()` method for incremental re-parsing. The parser itself stays stateless; all the incremental state lives in the doc.
325
+ `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.
346
326
 
347
327
  ```ts
348
- const css = new CSSParser()
328
+ import { makeFunctionalDoc } from 'parseman'
349
329
 
350
- const doc = css.parse('Stylesheet', src)
351
- doc.tree // CSTNode root, or null on failure
330
+ const registry = { Expr, Num } // straight from rules()
331
+ let doc = makeFunctionalDoc(registry, 'Expr', src)
332
+ doc.tree // your Node root, or null on failure
352
333
  doc.errors // ParseFail[], empty on success
353
334
  doc.input // the source string
354
335
 
355
- // subsequent edits return a new doc — old one is untouched
356
- // edit(from, to, replacement): select from→to in the old text, replace with replacement
357
- const doc2 = doc.edit(changeStart, changeStart + changeLength, newText)
336
+ // edit(from, to, replacement) — two byte offsets into the OLD text + the
337
+ // replacement. "Select from→to, type replacement" — the three things every
338
+ // editor knows on each keystroke. Returns a new doc; the old one is untouched.
339
+ doc = doc.edit(changeStart, changeStart + changeLength, newText)
358
340
  ```
359
341
 
360
- `edit(from, to, replacement)` takes two byte offsets into the **old** text (`from` and `to`), plus the replacement string. Think of it as "highlight characters from→to, type replacement" — the same three things any editor already knows on every keystroke. Internally it runs `old.slice(0, from) + replacement + old.slice(to)`. It finds the smallest node containing the change, re-parses just that subtree using its saved context, and stops early when the new span end matches the expected position. O(changed region) amortized for typical edits. Nodes unaffected by the edit are structurally shared between old and new docs.
361
-
362
- Context-sensitive grammars work correctly: each CST node records a `ctx.user` snapshot at parse time (`savedContext`), so re-parsing resumes from the exact same state. Solid enough for a language server.
342
+ `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.
363
343
 
364
- **In an IDE extension**, hold one parser instance per language, one `ParseDoc` per open document. On each keystroke your editor gives you the changed range as byte offsets — pass those straight to `edit()`:
344
+ **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()`:
365
345
 
366
346
  ```ts
367
- // VS Code example
368
- const parser = new CSSParser()
369
- const docs = new Map<string, ParseDoc<CSTNode>>()
347
+ const docs = new Map<string, ReturnType<typeof makeFunctionalDoc<Node>>>()
370
348
 
371
- vscode.workspace.onDidOpenTextDocument(document => {
372
- docs.set(document.uri.toString(), parser.parse('Stylesheet', document.getText()))
349
+ vscode.workspace.onDidOpenTextDocument(d => {
350
+ docs.set(d.uri.toString(), makeFunctionalDoc(registry, 'Stylesheet', d.getText()))
373
351
  })
374
352
 
375
353
  vscode.workspace.onDidChangeTextDocument(event => {
@@ -379,7 +357,23 @@ vscode.workspace.onDidChangeTextDocument(event => {
379
357
  doc = doc.edit(change.rangeOffset, change.rangeOffset + change.rangeLength, change.text)
380
358
  }
381
359
  docs.set(uri, doc)
382
- // walk doc.tree to emit diagnostics, folding ranges, semantic tokens, etc.
360
+ // walk doc.tree for diagnostics, folding ranges, semantic tokens, etc.
361
+ })
362
+ ```
363
+
364
+ ### Inheritance — by composition
365
+
366
+ Share rules across grammar variants by composition: factor common rules into helpers, and let each variant swap the ones it needs. Because every rule is a plain value, a variant is just different pieces passed into the same `rules()` shape — and it all stays macro-compilable.
367
+
368
+ ```ts
369
+ // shared building blocks
370
+ const makeIdent = () => regex(/[a-zA-Z_]\w*/)
371
+
372
+ export const { Expr } = rules<{ Expr: Combinator<Node> }>(g => {
373
+ const ident = makeIdent() // base
374
+ // a JSX variant would use regex(/[a-zA-Z_$][\w$]*/) here instead
375
+ /* ...rules referencing ident... */
376
+ return { Expr: /* ... */ }
383
377
  })
384
378
  ```
385
379
 
@@ -387,28 +381,26 @@ vscode.workspace.onDidChangeTextDocument(event => {
387
381
 
388
382
  ## Context-sensitive parsing
389
383
 
390
- `withCtx` and `guard` implement context-sensitive rules without mutating shared state.
384
+ `withCtx` and `guard` implement context-sensitive rules without mutating shared state, and they compose into `rules()` like any other combinator.
391
385
 
392
- `withCtx(extra, parser)` merges `extra` into the user context for the duration of `parser`. `guard(predicate)` succeeds only when `predicate(ctx)` returns true, effectively gating a rule behind runtime context.
386
+ `withCtx(extra, parser)` merges `extra` into the user context for the duration of `parser`. `guard(predicate)` succeeds only when `predicate(ctx)` returns true, gating a rule behind runtime context.
393
387
 
394
388
  ```ts
395
- import { withCtx, guard, many, sequence, choice, literal, regex } from 'parseman'
389
+ import { rules, withCtx, guard, many, sequence, choice, literal, regex, trivia, parser } from 'parseman'
390
+ import type { Combinator } from 'parseman'
396
391
 
397
- class LangParser extends Parser {
398
- ws = regex(/\s*/)
392
+ const ws = trivia(regex(/\s*/))
399
393
 
400
- Expr = regex(/[a-z]+/)
401
- Return = (g: Refs<LangParser>) => sequence(
402
- guard((ctx: { inFn?: boolean }) => ctx.inFn === true),
403
- literal('return'),
404
- )
405
- Stmt = (g: Refs<LangParser>) => choice(g.Return, g.Expr)
406
- Body = (g: Refs<LangParser>) => withCtx({ inFn: true }, many(sequence(g.Stmt, g.ws)))
407
- Program = (g: Refs<LangParser>) => many(g.Body)
408
- }
394
+ export const { Program } = rules<{ Program: Combinator<unknown> }>(g => {
395
+ const expr = regex(/[a-z]+/)
396
+ const ret = sequence(guard((ctx: { inFn?: boolean }) => ctx.inFn === true), literal('return'))
397
+ const stmt = choice(ret, expr)
398
+ const body = withCtx({ inFn: true }, many(sequence(stmt, ws)))
399
+ return { Program: parser({ trivia: ws }, many(body)) }
400
+ })
409
401
  ```
410
402
 
411
- `Return` is only reachable inside a `Body` because `guard` rejects it when `inFn` is not set. `ParseDoc.edit()` replays the correct context on incremental edits because `savedContext` captures the `inFn: true` snapshot at the node that originally set it.
403
+ `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.
412
404
 
413
405
  ---
414
406
 
@@ -426,7 +418,35 @@ const stmt = recover(g.Stmt, literal(';'))
426
418
  const block = scanTo(literal('}'), [balanced(literal('('), literal(')')), balanced(literal('['), literal(']'))])
427
419
  ```
428
420
 
429
- `scanTo(sentinel, skips?, opts?)` consumes input character-by-character until `sentinel` matches. Pass `skips` to skip over balanced pairs that might contain the sentinel character. Pass `opts.orEOF: true` to succeed at end-of-input if the sentinel is never found.
421
+ ### `scanTo` vs `balanced` — when to use each
422
+
423
+ 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.
424
+
425
+ **`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:
426
+
427
+ ```ts
428
+ // Consume a CSS at-rule prelude up to '{' or ';',
429
+ // but don't stop inside parentheses or strings.
430
+ const prelude = scanTo(choice(literal('{'), literal(';')), {
431
+ skip: [balanced('(', ')'), singleStr, doubleStr],
432
+ })
433
+ ```
434
+
435
+ **`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:
436
+
437
+ ```ts
438
+ const parenGroup = balanced('(', ')') // matches (a + b) including the parens
439
+ const bracketExpr = balanced('[', ']') // matches [0] including the brackets
440
+ ```
441
+
442
+ `balanced` can itself accept a `skip` list for deeply nested structures:
443
+
444
+ ```ts
445
+ // Match (…) allowing strings inside to contain unbalanced parens
446
+ const parenWithStrings = balanced('(', ')', { skip: [singleStr, doubleStr] })
447
+ ```
448
+
449
+ **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.
430
450
 
431
451
  ---
432
452
 
@@ -479,18 +499,35 @@ type Span = {
479
499
  }
480
500
  ```
481
501
 
482
- ### CST types
502
+ ### Node and document types
503
+
504
+ Any AST your `transform` callbacks produce participates in incremental re-parsing as long as it satisfies `NodeLike` — that's the whole contract:
483
505
 
484
506
  ```ts
485
- type CSTNode = { _tag: 'node'; type: string; span: Span; children: CSTChild[]; savedContext: unknown }
486
- type CSTLeaf = { _tag: 'leaf'; value: string; span: Span }
487
- type CSTError = { _tag: 'error'; type: string; span: Span; expected: string[]; children: CSTChild[]; savedContext: unknown }
488
- type CSTTrivia = { _tag: 'trivia'; value: string; span: Span } // only in rawChildren
507
+ type NodeLike = {
508
+ readonly _tag: 'node'
509
+ readonly type: string // the rule name — used as the registry key on re-parse
510
+ readonly span: Span
511
+ readonly state: unknown // ctx.state snapshot at parse time; replayed on edit
512
+ readonly children: ReadonlyArray<{ readonly _tag: string }>
513
+ }
489
514
 
490
- type CSTChild = CSTNode | CSTLeaf | CSTError
491
- type CSTRawChild = CSTNode | CSTLeaf | CSTTrivia | CSTError
515
+ // makeFunctionalDoc<N>(registry, rootRule, input, opts?) → FunctionalDoc<N>
516
+ type Registry<N> = Record<string, (input: string, pos: number, ctx: ParseContext) => ParseResult<N>>
517
+ interface FunctionalDoc<N extends NodeLike> {
518
+ readonly tree: N | null
519
+ readonly errors: ParseFail[]
520
+ readonly input: string
521
+ edit(from: number, to: number, replacement: string): FunctionalDoc<N>
522
+ }
523
+ type FunctionalDocOptions<N> = {
524
+ state?: unknown // initial ctx.state for the root parse
525
+ rebuild?: (node: N, children: ReadonlyArray<unknown>) => N // override for class-instance ASTs
526
+ }
492
527
  ```
493
528
 
529
+ `children` only needs items carrying a `_tag` so traversal can tell sub-nodes (`_tag: 'node'`) from anything else. The `type` string must match the rule name in the registry so `edit()` can re-parse the right rule.
530
+
494
531
  ---
495
532
 
496
533
  ## Developing
@@ -1 +1 @@
1
- {"version":3,"file":"choice.d.ts","sourceRoot":"","sources":["../../src/combinators/choice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAC+B,QAAQ,EAClD,MAAM,aAAa,CAAA;AAGpB,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;AACvH,KAAK,SAAS,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI;KACrE,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAChC,CAAC,MAAM,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAEjD,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EACxH,GAAG,IAAI,EAAE,CAAC,GACT,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAqH1B;AAwFD,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAKzE;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKhG"}
1
+ {"version":3,"file":"choice.d.ts","sourceRoot":"","sources":["../../src/combinators/choice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAC+B,QAAQ,EAClD,MAAM,aAAa,CAAA;AAGpB,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;AACvH,KAAK,SAAS,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI;KACrE,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAChC,CAAC,MAAM,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAEjD,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EACxH,GAAG,IAAI,EAAE,CAAC,GACT,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAiI1B;AAwFD,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAKzE;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKhG"}
@@ -0,0 +1,17 @@
1
+ import type { Combinator } from '../types.ts';
2
+ /**
3
+ * Returns the set of expected tokens at the given cursor offset.
4
+ * Useful for implementing IDE completions.
5
+ *
6
+ * Runs the parser on input truncated at `offset` with a probe that tracks the
7
+ * highest-position failure seen, even when sepBy/many backtrack past the cursor.
8
+ * Returns the expected tokens from that deepest failure.
9
+ *
10
+ * Returns an empty array when the input up to `offset` parses completely
11
+ * with no failures at or before the cursor.
12
+ *
13
+ * The returned strings use the same labels as ParseFail.expected:
14
+ * quoted literals like `"\"{\""` and regex patterns like `"/[0-9]+/"`.
15
+ */
16
+ export declare function completionsAt(parser: Combinator<unknown>, input: string, offset: number): string[];
17
+ //# sourceMappingURL=completions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/combinators/completions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,aAAa,CAAA;AAEtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,MAAM,EAAE,CAYV"}
@@ -1,8 +1,24 @@
1
- import type { Combinator, ParseResult } from '../types.ts';
1
+ import type { Combinator, ParseContext, ParseResult } from '../types.ts';
2
2
  export type ParseOptions = {
3
- trivia?: Combinator<unknown>;
4
3
  trackLines?: boolean;
4
+ /**
5
+ * Enable error recovery. When true, recover() nodes collect their ParseErrors
6
+ * into a side-channel array rather than (only) embedding them in the value tree.
7
+ * The returned ParseOk will have an `errors` field listing all recovered errors.
8
+ * Top-level parse failures (where no recover() node caught the error) still
9
+ * return ParseFail as usual.
10
+ */
11
+ recover?: boolean;
12
+ };
13
+ export type ParserOptions = ParseOptions & {
14
+ trivia?: Combinator<unknown>;
15
+ /** Record consumed trivia as CSTTrivia tokens in rawChildren. Default: skip. */
16
+ captureTrivia?: boolean;
5
17
  };
6
- export declare function grammar<T>(opts: ParseOptions, root: Combinator<T>): Combinator<T>;
7
- export declare function parse<T>(parser: Combinator<T>, input: string, opts?: ParseOptions): ParseResult<T>;
18
+ export interface ParsemanParser<T> extends Combinator<T> {
19
+ parse(input: string): ParseResult<T>;
20
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T>;
21
+ }
22
+ export declare function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanParser<T>;
23
+ export declare function parse<T>(combinator: Combinator<T>, input: string, opts?: ParseOptions): ParseResult<T>;
8
24
  //# sourceMappingURL=grammar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/combinators/grammar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAgB,WAAW,EAAE,MAAM,aAAa,CAAA;AAGxE,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAuBjF;AAED,wBAAgB,KAAK,CAAC,CAAC,EACrB,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,EACrB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,YAAiB,GACtB,WAAW,CAAC,CAAC,CAAC,CAWhB"}
1
+ {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/combinators/grammar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGxE,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IACzC,MAAM,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAC5B,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,CAAA;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,UAAU,CAAC,CAAC,CAAC;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;IACpC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;CACrE;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CA6BrF;AAED,wBAAgB,KAAK,CAAC,CAAC,EACrB,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,YAAiB,GACtB,WAAW,CAAC,CAAC,CAAC,CAehB"}
@@ -1,7 +1,7 @@
1
1
  import type { Combinator } from '../types.ts';
2
2
  /**
3
3
  * Zero-width assertion: succeeds (consuming nothing) only when `predicate`
4
- * returns true for `ctx.user`. Fails otherwise.
4
+ * returns true for `ctx.state`. Fails otherwise.
5
5
  *
6
6
  * Intended for use inside sequence() to gate subsequent parsing on runtime
7
7
  * context set with withCtx().
@@ -11,5 +11,5 @@ import type { Combinator } from '../types.ts';
11
11
  * literal('return'), optional(expr)
12
12
  * )
13
13
  */
14
- export declare function guard(predicate: (user: unknown) => boolean): Combinator<null>;
14
+ export declare function guard(predicate: (state: unknown) => boolean): Combinator<null>;
15
15
  //# sourceMappingURL=guard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/combinators/guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAA;AAEpF;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAgB7E"}
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/combinators/guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAA;AAEpF;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAgB9E"}