parseman 0.33.0 → 0.35.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 (59) hide show
  1. package/README.md +94 -72
  2. package/dist/analysis/gating.d.ts +66 -1
  3. package/dist/analysis/gating.d.ts.map +1 -1
  4. package/dist/combinators/case-fold.d.ts +39 -0
  5. package/dist/combinators/case-fold.d.ts.map +1 -0
  6. package/dist/combinators/expect.d.ts +1 -1
  7. package/dist/combinators/expect.d.ts.map +1 -1
  8. package/dist/combinators/first-set.d.ts +38 -11
  9. package/dist/combinators/first-set.d.ts.map +1 -1
  10. package/dist/combinators/keywords.d.ts +12 -3
  11. package/dist/combinators/keywords.d.ts.map +1 -1
  12. package/dist/combinators/not.d.ts.map +1 -1
  13. package/dist/combinators/peek.d.ts +27 -0
  14. package/dist/combinators/peek.d.ts.map +1 -0
  15. package/dist/combinators/repeat.d.ts +68 -3
  16. package/dist/combinators/repeat.d.ts.map +1 -1
  17. package/dist/combinators/trivia-skip.d.ts +23 -1
  18. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  19. package/dist/compiler/codegen.d.ts +40 -0
  20. package/dist/compiler/codegen.d.ts.map +1 -1
  21. package/dist/compiler/fields.d.ts.map +1 -1
  22. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  23. package/dist/compiler/linker.d.ts +5 -0
  24. package/dist/compiler/linker.d.ts.map +1 -1
  25. package/dist/compiler/value-usage.d.ts.map +1 -1
  26. package/dist/coverage.d.ts.map +1 -1
  27. package/dist/cst/capture-buffer.d.ts.map +1 -1
  28. package/dist/index.cjs +462 -105
  29. package/dist/index.cjs.map +4 -4
  30. package/dist/index.d.ts +4 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +462 -105
  33. package/dist/index.js.map +4 -4
  34. package/dist/language-service/index.cjs.map +1 -1
  35. package/dist/language-service/index.js.map +1 -1
  36. package/dist/plugin/evaluator.d.ts.map +1 -1
  37. package/dist/plugin/index.cjs +529 -135
  38. package/dist/plugin/index.cjs.map +4 -4
  39. package/dist/plugin/index.d.ts.map +1 -1
  40. package/dist/plugin/index.js +529 -135
  41. package/dist/plugin/index.js.map +4 -4
  42. package/dist/run/index.cjs +216 -0
  43. package/dist/run/index.cjs.map +7 -0
  44. package/dist/run/index.d.ts +28 -0
  45. package/dist/run/index.d.ts.map +1 -0
  46. package/dist/run/index.js +193 -0
  47. package/dist/run/index.js.map +7 -0
  48. package/dist/spec/ebnf.d.ts.map +1 -1
  49. package/dist/spec/index.cjs +79 -17
  50. package/dist/spec/index.cjs.map +2 -2
  51. package/dist/spec/index.js +79 -17
  52. package/dist/spec/index.js.map +2 -2
  53. package/dist/spec/model.d.ts +23 -3
  54. package/dist/spec/model.d.ts.map +1 -1
  55. package/dist/spec/railroad.d.ts.map +1 -1
  56. package/dist/types.d.ts +9 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/version.d.ts +1 -1
  59. package/package.json +11 -1
package/README.md CHANGED
@@ -6,29 +6,17 @@
6
6
 
7
7
  Write parsers as TypeScript functions. Ship them like hand-written parsers.
8
8
 
9
- Parséman is a TypeScript parser-combinator library with an optional compiler/macro path that turns your grammar into flat JavaScript. Use the same grammar interpreted in tests and REPLs, macro-compiled at build time in production, or compiled on demand at runtime with `compile()`.
9
+ Parser combinators are pleasant to write and usually slow. Parser generators are fast and
10
+ usually mean grammar files, generated code, and extra tooling. Parséman is a TypeScript
11
+ combinator library with an optional compiler: write ordinary TypeScript, and a build-time
12
+ macro turns it into flat JavaScript that behaves like a parser you wrote by hand.
10
13
 
11
- Use Parséman when you want:
12
-
13
- - normal TypeScript instead of grammar files
14
- - parser-combinator ergonomics without parser-combinator slowness
15
- - CST/AST nodes with spans and trivia
16
- - error recovery for editor tooling
17
- - incremental re-parsing
18
- - fast parsers for DSLs, config languages, formatters, linters, and language servers
14
+ Reach for it when you want normal TypeScript instead of grammar files, CST/AST nodes with
15
+ spans and trivia, error recovery and incremental re-parsing for editor tooling, or simply
16
+ a fast parser for a DSL, config language, formatter, or linter.
19
17
 
20
18
  > **📖 Full documentation: [matthew-dean.github.io/parseman](https://matthew-dean.github.io/parseman/)**
21
19
 
22
- ## Why Parséman?
23
-
24
- Most parser tools make you choose between ergonomics and performance.
25
-
26
- Parser combinators are pleasant to write, but often slow. Parser generators can be fast, but usually involve grammar files, generated code, and extra tooling. Hand-written parsers are fast, but expensive to design and maintain.
27
-
28
- Parséman aims for the useful middle: write your parser as ordinary TypeScript, then compile it into code that behaves more like a hand-written parser.
29
-
30
- ---
31
-
32
20
  ## Install
33
21
 
34
22
  ```bash
@@ -36,8 +24,8 @@ npm install parseman
36
24
  # pnpm add parseman
37
25
  ```
38
26
 
39
- Parseman is pre-1.0. Minor versions may include breaking changes; check the
40
- [changelog](./CHANGELOG.md) before upgrading.
27
+ Pre-1.0: minor versions may carry breaking changes — check the [changelog](./CHANGELOG.md)
28
+ before upgrading. Requires Node `^20.19.0 || >=22.12.0`.
41
29
 
42
30
  ## Quick start
43
31
 
@@ -57,64 +45,95 @@ parse(requestLine, 'GET /api/v1 HTTP/1.1')
57
45
  // { ok: true, value: { verb: 'GET', path: '/api/v1', version: 'HTTP/1.1' }, span: ... }
58
46
  ```
59
47
 
48
+ New here? **[What is Parséman?](https://matthew-dean.github.io/parseman/guide/getting-started)**
49
+ walks the same ground more slowly.
50
+
60
51
  ## Three modes, one grammar
61
52
 
62
53
  The same combinator code runs three ways, with identical results:
63
54
 
64
55
  - **Interpreter** — zero setup, works anywhere (tests, REPLs, dynamic grammars).
65
- - **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.
66
- - **`compile()`** — the same optimizer, run on demand at runtime.
56
+ - **Macro build** — a [bundler plugin](https://matthew-dean.github.io/parseman/guide/macro-mode)
57
+ evaluates your grammar at build time and inlines the result. The combinator import you
58
+ mark `with { type: 'macro' }` disappears entirely; what ships is flat JavaScript with no
59
+ parseman import in it. (Executing that parser still goes through `run()`/`parse()`, so an
60
+ app keeps parseman as an ordinary dependency — see
61
+ [the three modes](https://matthew-dean.github.io/parseman/guide/modes).)
62
+ - **`compile()`** — the same optimizer, on demand at runtime.
67
63
 
68
64
  ```ts
69
65
  // Add the plugin (vite.config.ts) and one import attribute — that's the whole change:
70
66
  import { literal, sequence, choice } from 'parseman' with { type: 'macro' }
71
67
  ```
72
68
 
73
- See **[The three modes](https://matthew-dean.github.io/parseman/guide/modes)** for the full story.
69
+ See **[The three modes](https://matthew-dean.github.io/parseman/guide/modes)**.
74
70
 
75
71
  ## What's in the box
76
72
 
77
- - **[Combinators](https://matthew-dean.github.io/parseman/guide/combinators)** — `literal`, `regex`, `sequence`, `choice`, `many`, `sepBy`, `token`, `not`, and more.
78
- - **[Whitespace & trivia](https://matthew-dean.github.io/parseman/guide/trivia)** — grammar-defined filler skipping, with per-chunk kind capture.
79
- - **[Recursive rules](https://matthew-dean.github.io/parseman/guide/recursive-rules)** — `rules()` for mutually recursive grammars; fully macro-compilable.
80
- - **[CST / AST nodes](https://matthew-dean.github.io/parseman/guide/ast)** — `node()` captures terminals, named `field()` values, and trivia for you, with `unwrap` for AST/value wrappers, `collapse` for grammar-local CST wrappers, and `cstBuildHost({ collapse })` for public CST policies.
81
- - **[Incremental re-parsing](https://matthew-dean.github.io/parseman/guide/incremental)** — `parseDoc` re-parses just the edited subtree on each keystroke.
82
- - **[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.
83
- - **[Context-sensitive parsing](https://matthew-dean.github.io/parseman/guide/context)** — `withCtx` / `guard` without mutating shared state.
84
-
85
- Full API in the **[reference](https://matthew-dean.github.io/parseman/reference/api)**.
86
-
87
- ---
88
-
89
- ## Compared to other parser tools
90
-
91
- Wondering how Parséman compares to Peggy, Chevrotain, Lezer, tree-sitter, Parsimmon, Nearley, or hand-written parsers?
92
-
93
- See the full comparison: **[How Parséman compares](https://matthew-dean.github.io/parseman/guide/comparison)**
94
-
95
- ## Real grammar example: GraphQL
96
-
97
- Parséman includes a [GraphQL grammar](./examples/graphql/parser.ts) used in the benchmark suite. It parses executable GraphQL documents (queries, mutations, subscriptions, fragments, directives, variables, all value types) into typed AST nodes — not just syntax-validating them.
98
-
99
- This is a real-world example of Parséman on a non-trivial, spec-shaped language, not a toy grammar.
100
-
101
- - Source: [`examples/graphql/parser.ts`](./examples/graphql/parser.ts)
102
- - Benchmark results: [GraphQL benchmarks](https://matthew-dean.github.io/parseman/guide/benchmarks#graphql)
103
- - Benchmark runner: [`bench/run.ts`](./bench/run.ts)
104
-
105
- ---
73
+ **✍️ Writing grammars**
74
+
75
+ - 🧩 **[Combinators](https://matthew-dean.github.io/parseman/guide/combinators)** — `literal`,
76
+ `regex`, `sequence`, `choice`, `many`, `sepBy`, `token`, `peek`, `not`, and more.
77
+ - 🌀 **[Recursive rules](https://matthew-dean.github.io/parseman/guide/recursive-rules)** —
78
+ `rules()` for mutually recursive grammars; fully macro-compilable.
79
+ - 🫧 **[Whitespace & trivia](https://matthew-dean.github.io/parseman/guide/trivia)** —
80
+ grammar-defined filler skipping, with per-chunk kind capture.
81
+ - 🎯 **[Ordered choice, done right](https://matthew-dean.github.io/parseman/guide/keywords)** —
82
+ PEG semantics you control by ordering, with keyword boundaries that don't bite.
83
+ - 🪶 **[Grammars the way you write them](https://matthew-dean.github.io/parseman/guide/natural-grammars)** —
84
+ no FIRST/FOLLOW sets to compute, no left-factoring `choice(a·x, a·y)` into `a·(x | y)`
85
+ to satisfy the tool. The compiler does that for you.
86
+ - 🧬 **[Extending grammars](https://matthew-dean.github.io/parseman/guide/extending)** —
87
+ `compose()` a dialect onto a base grammar instead of forking it.
88
+ - 🎛️ **[Context-sensitive parsing](https://matthew-dean.github.io/parseman/guide/context)** —
89
+ `withCtx` / `gate` without mutating shared state.
90
+
91
+ **🌳 Getting structure out**
92
+
93
+ - 🌲 **[CST / AST nodes](https://matthew-dean.github.io/parseman/guide/ast)** — `node()`
94
+ captures terminals, named fields, and trivia, with policies for wrapping and collapsing.
95
+ - 🩹 **[Error recovery](https://matthew-dean.github.io/parseman/guide/error-recovery)** —
96
+ keep parsing broken input and report every error.
97
+ - ⚡ **[Incremental re-parsing](https://matthew-dean.github.io/parseman/guide/incremental)** —
98
+ `parseDoc` re-parses just the edited subtree on each keystroke.
99
+ - 💡 **[Editor / LSP integration](https://matthew-dean.github.io/parseman/guide/editor-integration)** —
100
+ completions and lint keyed by rule name; the grammar stays pure structure.
101
+
102
+ **🔬 Seeing what your grammar does**
103
+
104
+ - 🚂 **[Railroad diagrams & EBNF](https://matthew-dean.github.io/parseman/guide/spec-generation)** —
105
+ `toRailroadHtml()` and `toEBNF()` generate the grammar reference *from the parser*, so
106
+ the spec can't drift from what actually parses.
107
+ - 🔎 **[Grammar observability](https://matthew-dean.github.io/parseman/guide/grammar-observability)** —
108
+ coverage ("which rules and choice arms ran?") and trace ("what did it try, select, and
109
+ backtrack through?").
110
+ - 🚦 **[First-char gating diagnostics](https://matthew-dean.github.io/parseman/guide/first-char-gating)** —
111
+ the build tells you which `choice` lost its O(1) dispatch, names the overlapping arms,
112
+ and says how to fix it.
113
+ - 📈 **[Performance guide](https://matthew-dean.github.io/parseman/guide/performance)** —
114
+ the levers that actually move a grammar, and [how regexes lower](https://matthew-dean.github.io/parseman/guide/regex-lowering).
115
+
116
+ Full API in the **[reference](https://matthew-dean.github.io/parseman/reference/api)**; how
117
+ it stacks up against Peggy, Chevrotain, Lezer, tree-sitter, Parsimmon, Nearley and
118
+ hand-written parsers in
119
+ **[How Parséman compares](https://matthew-dean.github.io/parseman/guide/comparison)**.
106
120
 
107
121
  ## Benchmarks
108
122
 
109
- Parséman includes benchmarks against several JavaScript/TypeScript parser libraries across JSON, CSV, and GraphQL fixtures. Benchmarks are not universal truth tablets — results depend on grammar shape, input size, runtime, and what each parser is asked to produce. The benchmark suite is included so results can be inspected and reproduced (see [Reproducing the numbers](https://matthew-dean.github.io/parseman/guide/benchmarks#reproducing-the-numbers)).
110
-
111
- **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](https://peggyjs.org/), [Parsimmon](https://github.com/jneen/parsimmon), [Chevrotain](https://chevrotain.io/), [Nearley](https://nearley.js.org/), and [Jison](https://github.com/zaach/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.
112
-
113
- 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**, Parséman's `parseDoc` stores parent-relative spans so in-place value edits are ~110× faster than a full reparse and ~20× ahead of Lezer; structural edits (inserting/removing a whole list element) reuse the collection's untouched tail and land within a few × of Lezer's buffer reuse rather than at full-reparse cost. Full breakdown in the [benchmarks guide](https://matthew-dean.github.io/parseman/guide/benchmarks).
123
+ Benchmarked against [Peggy](https://peggyjs.org/),
124
+ [Parsimmon](https://github.com/jneen/parsimmon), [Chevrotain](https://chevrotain.io/),
125
+ [Nearley](https://nearley.js.org/), [Jison](https://github.com/zaach/jison) and
126
+ [Lezer](https://lezer.codemirror.net/) on JSON, CSV and GraphQL — each building real output
127
+ (objects, row arrays, AST nodes), not validating syntax.
114
128
 
115
- Measured on Apple M4 Pro. Bars show µs per parse — shorter is faster. Refresh: `pnpm bench:svg` (benchmarks chart parsers and updates `assets/bench-*.svg`).
129
+ **Parsing to JS values, the macro build is the fastest general-purpose JS parser in the
130
+ suite** — ahead of every library above at every grammar and size (GraphQL large: **142 µs**
131
+ vs Peggy's 339 µs). Only a purpose-built native edges it out, `JSON.parse` on JSON.
116
132
 
117
- Compared parsers: **Parséman**, [Peggy](https://peggyjs.org/), [Parsimmon](https://github.com/jneen/parsimmon), [Chevrotain](https://chevrotain.io/), [Nearley](https://nearley.js.org/), and [Jison](https://github.com/zaach/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/`).
133
+ Two more results worth calling out: the compiled CST path beats Lezer on the JSON CST
134
+ fixture while producing a richer tree carrying spans and trivia, and `parseDoc` stores
135
+ parent-relative spans so an in-place edit costs a fraction of a full reparse rather than a
136
+ multiple of it.
118
137
 
119
138
  ![JSON parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parseman/main/assets/bench-json.svg)
120
139
 
@@ -124,26 +143,29 @@ Compared parsers: **Parséman**, [Peggy](https://peggyjs.org/), [Parsimmon](http
124
143
 
125
144
  ![JSON CST parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parseman/main/assets/bench-cst-json.svg)
126
145
 
127
- 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.)
146
+ Per-fixture figures, initialization costs, hardware, grammar provenance and how to reproduce
147
+ any of it: **[benchmarks guide](https://matthew-dean.github.io/parseman/guide/benchmarks)**.
148
+ Results move with grammar shape, input size and runtime — which is why the suite ships with
149
+ the library rather than only its conclusions. Speed levers for your own grammars:
150
+ [performance guide](https://matthew-dean.github.io/parseman/guide/performance).
128
151
 
129
- On JSON, CSV, and GraphQL, Parséman macro beats every other library at every fixture size in the charts above — e.g. GraphQL large **142 µs** vs Peggy **339 µs**, JSON large **141 µs** vs Peggy **466 µs** / Chevrotain **250 µs**. Native `JSON.parse` is the one thing faster on JSON (**54.6 µs** large). Even the zero-setup interpreter is highly competitive: on JSON and CSV it's the fastest option after the macro build — ahead of Peggy and every other generator — with Peggy edging it out only on GraphQL. On the CST chart, macro build beats Lezer at every size (**172 µs** vs **619 µ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](./notes/PERF_IDEAS.md).
130
-
131
- ---
152
+ The GraphQL fixture is a [real grammar](./examples/graphql/parser.ts), parsing executable
153
+ documents — queries, mutations, fragments, directives, every value type — into typed AST
154
+ nodes, so the numbers come from a spec-shaped language rather than a toy.
132
155
 
133
156
  ## Developing
134
157
 
135
158
  ```bash
136
159
  pnpm install
137
- pnpm test # Vitest — interpreter + compiler parity + ordered-choice semantics
138
- pnpm typecheck # TypeScript 7
139
- pnpm build # ESM + CJS + .d.ts → dist/
140
- pnpm bench # parser-to-parser comparison
141
- pnpm bench:parseman # Parseman interpreted vs compiled regression report
142
- pnpm bench:svg # chart-only benchmarks + refresh assets/bench-*.svg
143
- pnpm bench:compile-grammars # regenerate Peggy / Nearley / Jison parser output in bench/
144
- pnpm docs:dev # run this documentation site locally
160
+ pnpm test # interpreter + compiled parity, ordered-choice semantics
161
+ pnpm typecheck
162
+ pnpm build # ESM + CJS + .d.ts → dist/
163
+ pnpm docs:dev # this documentation site, locally
145
164
  ```
146
165
 
166
+ Benchmark and chart tasks (`pnpm bench`, `bench:svg`, `bench:parseman`, …) are described in
167
+ the [benchmarks guide](https://matthew-dean.github.io/parseman/guide/benchmarks#reproducing-the-numbers).
168
+
147
169
  ## License
148
170
 
149
171
  MIT © [Matthew Dean](https://github.com/matthew-dean)
@@ -15,8 +15,16 @@
15
15
  * (see `src/compiler/codegen.ts`). Accept a deliberately-ungated choice by listing
16
16
  * its `id` in the gating snapshot allowlist (`analyzeGating(entry, { accept })` /
17
17
  * `compile(g, { gating: { level, accept } })`) — the single suppression mechanism.
18
+ *
19
+ * WHERE the question is asked matters as much as the answer. A SHARED SHAPE — a
20
+ * `rules()` map referencing a rule it doesn't define (`g.Value`) — has no verdict of
21
+ * its own: the hole makes every first-set through it `any`, but that configuration is
22
+ * never executed and its author cannot fix it. Such a choice is `deferred` here, and
23
+ * re-asked with `resolveRef` at the site that BINDS the name (`runFusedGatingDiagnostic`
24
+ * in codegen.ts) — the artifact that really runs, whose author really can fix it.
18
25
  */
19
26
  import type { Combinator, FirstSet } from '../types.ts';
27
+ import { type RefResolver } from '../combinators/first-set.ts';
20
28
  /** Why an arm's (deep) first-set is `any` / over-broad — the poison source. */
21
29
  export type FirstSetCause = 'leading-not' | 'nullable-prefix' | 'cross-artifact-ref' | 'broad-recognizer' | 'opaque-wrapper' | 'ref-cycle';
22
30
  /** An arm whose deep first-set is `any` / over-broad. */
@@ -32,6 +40,16 @@ export type AnyArm = {
32
40
  * genuinely-ungated finding.
33
41
  */
34
42
  shallowAnyOnly: boolean;
43
+ /**
44
+ * True when the poison is a NAMED cross-artifact hole this artifact cannot resolve
45
+ * (`g.Value` in a shared shape). The author of THIS artifact cannot act on it — the
46
+ * arm's real first-set only exists once a consumer binds the name — so a choice
47
+ * whose every `any` arm is one of these is `deferred`, not `ungated`.
48
+ *
49
+ * An UNNAMED unresolved `ref()` is NOT this: nobody can bind it by name, so it is a
50
+ * genuine local finding and stays reportable here.
51
+ */
52
+ unresolvedExternal: boolean;
35
53
  /** Concrete fix, naming a real primitive. */
36
54
  suggestion: string;
37
55
  };
@@ -72,6 +90,17 @@ export type ChoiceGating = {
72
90
  gates: 'yes' | 'recoverable' | 'no';
73
91
  /** True when this ungated choice's `id` is in the accepted-snapshot allowlist. */
74
92
  accepted: boolean;
93
+ /**
94
+ * `gates: 'no'` was decided by cross-artifact HOLES ONLY — every `any` arm is an
95
+ * unresolved NAMED `g.Foo` ref and no two finite arms overlap. The verdict is not
96
+ * this artifact's to make: the shape module can't fix it (the hole has no body
97
+ * here) and the configuration it describes never runs. The answer belongs to the
98
+ * FUSED artifact, where the name is bound — see `runFusedGatingDiagnostic`.
99
+ *
100
+ * Deferred choices are excluded from `ungated`: they neither warn nor fail the
101
+ * `'error'` gate at this site.
102
+ */
103
+ deferred: boolean;
75
104
  combinedFirstSet: {
76
105
  shallow: FirstSet;
77
106
  deep: FirstSet;
@@ -88,6 +117,22 @@ export type AnalyzeGatingOptions = {
88
117
  * SINGLE per-choice suppression mechanism.
89
118
  */
90
119
  accept?: Iterable<string>;
120
+ /**
121
+ * Name to attribute an UNNAMED entry to, instead of the synthetic `<entry>`.
122
+ * The macro plugin passes the binding's own variable name, so a warning on a
123
+ * top-level combinator const reads `choice @ directMixinReferenceAhead`
124
+ * (actionable, and a discriminating `accept` key) rather than `choice @
125
+ * <entry>` repeated once per const. Ignored when the entry already carries a
126
+ * `_ruleName`.
127
+ */
128
+ entryName?: string;
129
+ /**
130
+ * Bind NAMED cross-artifact holes (`g.Foo`) by name — supply the FUSED winner map's
131
+ * lookup. With it, an arm led by a shared shape's hole reports the first-set it
132
+ * really has once bound, so `deferred` collapses to a real `yes`/`no` verdict.
133
+ * Without it (the authoring site) such a choice stays `deferred`.
134
+ */
135
+ resolveRef?: RefResolver;
91
136
  };
92
137
  export type GatingReport = {
93
138
  totalChoices: number;
@@ -97,6 +142,12 @@ export type GatingReport = {
97
142
  ungated: ChoiceGating[];
98
143
  /** Ungated choices whose id was in the accepted allowlist — silent, accepted with intent. */
99
144
  accepted: ChoiceGating[];
145
+ /**
146
+ * Choices whose verdict is NOT this artifact's to make — every `any` arm is an
147
+ * unresolved cross-artifact hole (see `ChoiceGating.deferred`). Silent here; the
148
+ * fused artifact re-asks the question with the hole bound.
149
+ */
150
+ deferred: ChoiceGating[];
100
151
  /** Accepted ids that matched no ungated choice — stale snapshot entries to prune. */
101
152
  acceptedUnused: string[];
102
153
  /** Every choice, for full inspection / CI snapshots. */
@@ -105,11 +156,25 @@ export type GatingReport = {
105
156
  };
106
157
  export declare function firstSetToString(fs: FirstSet): string;
107
158
  export declare function analyzeGating(entry: Combinator<unknown>, opts?: AnalyzeGatingOptions): GatingReport;
159
+ /**
160
+ * Multi-root variant: analyze a WHOLE `rules()` map in one walk, so every choice
161
+ * is attributed to the rule that owns it.
162
+ *
163
+ * The macro build compiles a grammar through `compileRuleMap`/`compileLinkable`,
164
+ * never through the single-entry `compile()`. Analyzing one entry at a time (or
165
+ * not at all) is what produced unnamed `choice @ <entry>` warnings and ZERO
166
+ * anti-patterns for grammars that in fact have dozens: an unnamed warning in a
167
+ * multi-thousand-line grammar is unactionable. Seeding the walk with EVERY named
168
+ * root is what recovers the rule names — and, because the walk is shared, each
169
+ * choice is still analyzed exactly once however many rules reach it.
170
+ */
171
+ export declare function analyzeGatingRules(ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>, opts?: AnalyzeGatingOptions): GatingReport;
108
172
  export type GatingWarnLevel = 'off' | 'warn' | 'error';
109
173
  /**
110
174
  * Format the genuinely-ungated findings + anti-patterns as ready-to-print lines.
111
175
  * Precise by design: only 'no'-gated choices NOT in the accepted allowlist, plus
112
- * the anti-pattern lints. Recoverable / gated / accepted choices produce nothing.
176
+ * the anti-pattern lints. Recoverable / gated / accepted / DEFERRED choices produce
177
+ * nothing (a deferred choice's verdict belongs to the fusing artifact, not here).
113
178
  */
114
179
  export declare function formatGatingWarnings(report: GatingReport): string[];
115
180
  //# sourceMappingURL=gating.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gating.d.ts","sourceRoot":"","sources":["../../src/analysis/gating.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAa,MAAM,aAAa,CAAA;AAGlE,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GACrB,aAAa,GACb,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,GAClB,gBAAgB,GAChB,WAAW,CAAA;AAEf,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,gGAAgG;IAChG,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAA;IACvB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,oEAAoE;AACpE,MAAM,MAAM,OAAO,GAAG;IACpB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,EAAE,EAAE,QAAQ,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,YAAY,GAAG,aAAa,GAAG,eAAe,CAAA;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,gBAAgB,GAAG,sBAAsB,GAAG,cAAc,CAAA;AAEzG,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,iBAAiB,CAAA;IAC3B;;;;;;;OAOG;IACH,KAAK,EAAE,KAAK,GAAG,aAAa,GAAG,IAAI,CAAA;IACnC,kFAAkF;IAClF,QAAQ,EAAE,OAAO,CAAA;IACjB,gBAAgB,EAAE;QAAE,OAAO,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAA;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAA;CACpB,CAAA;AAED,qEAAqE;AACrE,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,uFAAuF;IACvF,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,6FAA6F;IAC7F,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,qFAAqF;IACrF,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,wDAAwD;IACxD,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,YAAY,EAAE,WAAW,EAAE,CAAA;CAC5B,CAAA;AAsCD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAIrD;AAgJD,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,YAAY,CAiDnG;AA4CD,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;AAEtD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,EAAE,CAanE"}
1
+ {"version":3,"file":"gating.d.ts","sourceRoot":"","sources":["../../src/analysis/gating.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The case-fold CLASS of a code point under ECMAScript's NON-Unicode `/i`
3
+ * matching — the exact relation `keywords({ caseInsensitive: true })` compiles to.
4
+ *
5
+ * A first-set drives O(1) `choice` dispatch, so it MUST be a superset of every
6
+ * character the combinator can really start with. For a case-insensitive matcher
7
+ * that means: for every input char X that the matcher would accept as the first
8
+ * char, X is in the set. Under `/i` WITHOUT `u`, two chars match iff they share a
9
+ * canonical form, and that relation is NOT simply `{c, toUpperCase(c),
10
+ * toLowerCase(c)}` — 67 BMP code points sit in classes those three miss:
11
+ *
12
+ * σ (U+03C3) ↔ Σ (U+03A3) ↔ ς (U+03C2) final sigma is in the class but is
13
+ * neither the upper- nor lowercase of σ
14
+ * µ (U+00B5) ↔ μ (U+03BC) ↔ Μ (U+039C) micro sign folds into Greek mu
15
+ * DŽ (U+01C4) ↔ Dž (U+01C5) ↔ dž (U+01C6) titlecase digraphs (also LJLjljNJNjnjDZDzdz)
16
+ * ͅ (U+0345) ↔ ι (U+03B9) ↔ ι (U+1FBE) combining iota subscript
17
+ *
18
+ * so widening a first-set by upper/lower alone is UNSOUND for them.
19
+ *
20
+ * The canonical form below is ECMAScript's `Canonicalize` for non-Unicode mode:
21
+ * uppercase the char; if that is not exactly one char, or if it would cross the
22
+ * ASCII boundary INTO Basic Latin, keep the original. That last clause is why
23
+ * `/s/i` does not match `ſ` (U+017F) and `/k/i` does not match `K` (U+212A) —
24
+ * cross-boundary folds are refused in BOTH directions, which is what keeps an
25
+ * ASCII first-set tight and sound for the common all-ASCII keyword set.
26
+ *
27
+ * The classes are derived from the RUNNING engine's own `toUpperCase`, not a
28
+ * frozen table, so they cannot drift from the regex engine that will do the
29
+ * matching. Verified exhaustively against real `/i` behaviour across the BMP:
30
+ * 1141 multi-member classes, 2307 members, zero disagreements in either
31
+ * direction (see `test/unit/case-fold.test.ts`).
32
+ */
33
+ /**
34
+ * Every code point that a non-Unicode `/i` matcher treats as equal to `cp`,
35
+ * INCLUDING `cp` itself. Union these into a first-set to keep case-insensitive
36
+ * dispatch sound.
37
+ */
38
+ export declare function caseFoldVariants(cp: number): readonly number[];
39
+ //# sourceMappingURL=case-fold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"case-fold.d.ts","sourceRoot":"","sources":["../../src/combinators/case-fold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAuCH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAc9D"}
@@ -7,7 +7,7 @@ export type { ParseError };
7
7
  * literal('}') -> ['"}"']
8
8
  * keywords(['a','b']) -> ['"a"', '"b"']
9
9
  * choice(x, y) -> expected(x) ++ expected(y) (all alternatives)
10
- * sequence(a, b, …) -> expected(a) (only the first term can fail first)
10
+ * sequence(a, b, …) -> expected(a) ++ … through the NULLABLE prefix
11
11
  * label('s', x) -> ['s'] (an explicit name wins)
12
12
  *
13
13
  * This is what lets expect() report a meaningful, IDENTICAL expectation in both
@@ -1 +1 @@
1
- {"version":3,"file":"expect.d.ts","sourceRoot":"","sources":["../../src/combinators/expect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,UAAU,EAAE,MAAM,aAAa,CAAA;AAGhG,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE,CAsC/D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,CA2B/F;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE"}
1
+ {"version":3,"file":"expect.d.ts","sourceRoot":"","sources":["../../src/combinators/expect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,UAAU,EAAE,MAAM,aAAa,CAAA;AAIhG,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE,CAE/D;AAkED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,CA2B/F;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE"}
@@ -1,4 +1,17 @@
1
1
  import type { FirstSet, Combinator } from '../types.ts';
2
+ /**
3
+ * Resolve a NAMED cross-artifact rule reference (`g.Foo`) whose own thunk does not
4
+ * resolve — the shared-shape hole, bound by name at fuse time.
5
+ *
6
+ * A shape module compiles `Ratio: sequence(g.Value, …)` without defining `Value`, so
7
+ * that `lazy`'s thunk throws and every first-set through it degrades to `any`. Once
8
+ * the shape is FUSED with a dialect that defines `Value`, the hole is bound — pass a
9
+ * resolver over the fused winner map and the first-set is the real one.
10
+ *
11
+ * Only NAMED refs are resolvable: an unnamed `ref()` that was never `.define()`d
12
+ * carries no name for anyone to bind, so it stays `any`.
13
+ */
14
+ export type RefResolver = (name: string) => Combinator<unknown> | undefined;
2
15
  export declare function union(a: FirstSet, b: FirstSet): FirstSet;
3
16
  export declare function intersects(a: FirstSet, b: FirstSet): boolean;
4
17
  export declare function fromChar(code: number): FirstSet;
@@ -20,7 +33,7 @@ export declare function empty(): FirstSet;
20
33
  * set, which stays sound; under-estimating would drop valid start chars and make
21
34
  * first-char dispatch skip a matching arm.
22
35
  */
23
- export declare function matchesEmpty(p: Combinator<unknown>, seen?: Set<Combinator<unknown>>): boolean;
36
+ export declare function matchesEmpty(p: Combinator<unknown>, seen?: Set<Combinator<unknown>>, resolve?: RefResolver): boolean;
24
37
  /**
25
38
  * A ZERO-WIDTH ASSERTION never consumes input, so it contributes NOTHING to a
26
39
  * sequence's first-set — the first consumed char comes from the following
@@ -33,17 +46,27 @@ export declare function matchesEmpty(p: Combinator<unknown>, seen?: Set<Combinat
33
46
  * widens the set of possible first chars beyond Y. So firstSet(Y) is a sound (and
34
47
  * tighter) superset.
35
48
  *
36
- * Parseman has NO positive-lookahead combinator (`(?=X)` exists only INSIDE a
37
- * `regex()` pattern, where the regex first-set analyzer handles it). If one is ever
38
- * added, its tight first-set is firstSet(body) ∩ firstSet(Y) when `body` is
39
- * NON-nullable (both must be satisfiable at the same position), else firstSet(Y)
40
- * (a nullable `(?=body)` succeeds on empty, so it imposes no first-char constraint
41
- * and intersecting would UNSOUNDLY exclude valid chars). It must NOT be added to
42
- * this predicate without that intersection logic — a future assertion combinator
43
- * left out of this set merely contributes its shallow `any` first-set, which stays
44
- * sound (over-approximation), only losing tightness.
49
+ * The POSITIVE lookahead `peek(X)` is zero-width too, but it is NOT in this
50
+ * predicate: unlike `not`, it knows what it requires, so its first-set is a real
51
+ * constraint that must be INTERSECTED into the sequence's set rather than dropped
52
+ * (see `isPositiveLookahead` and `sequenceFirstSet`).
45
53
  */
46
54
  export declare function isZeroWidthAssertion(p: Combinator<unknown>): boolean;
55
+ /**
56
+ * A POSITIVE zero-width assertion (`peek(X)`). It consumes nothing, so like
57
+ * `not(X)` it does not contribute a first char of its own — but it does REQUIRE
58
+ * that X match here, so `peek(X) Y` can only start with a char in
59
+ * firstSet(X) ∩ firstSet(Y). That intersection is what makes a leading `peek()`
60
+ * gate its choice arm; `not(not(X))`, the only previous spelling, reports `any()`
61
+ * and poisons the dispatch instead.
62
+ *
63
+ * Soundness: first-sets are SUPERSETS of the true first chars, and
64
+ * (A ⊇ a) ∧ (B ⊇ b) ⇒ A ∩ B ⊇ a ∩ b — so intersecting stays a superset and can
65
+ * never skip a real match. A NULLABLE body succeeds on the empty string and
66
+ * therefore constrains nothing; `peek()` reports `any()` in that case, which the
67
+ * intersection treats as "no constraint".
68
+ */
69
+ export declare function isPositiveLookahead(p: Combinator<unknown>): boolean;
47
70
  /**
48
71
  * First-set of a sequence: union each term's first-set through the NULLABLE
49
72
  * PREFIX — a leading `optional(…)` / `many(…)` / nullable term can be skipped, so
@@ -66,6 +89,10 @@ export declare function sequenceFirstSet(parsers: readonly Combinator<unknown>[]
66
89
  * SOUND ONLY where refs are FINAL (monolithic compile). Under compose OVERRIDE a
67
90
  * referenced rule can be replaced with a WIDER first-set, so a baked deep set
68
91
  * would wrongly skip valid input — the compose path defers dispatch to fuse time.
92
+ *
93
+ * `resolve` binds NAMED cross-artifact holes (`g.Foo`) against a fused winner map —
94
+ * see `RefResolver`. Diagnostic-only today: it is what lets the gating analysis ask
95
+ * the question at the site where the hole actually HAS an answer.
69
96
  */
70
- export declare function firstSetOf(p: Combinator<unknown>, seen?: Set<Combinator<unknown>>): FirstSet;
97
+ export declare function firstSetOf(p: Combinator<unknown>, seen?: Set<Combinator<unknown>>, resolve?: RefResolver): FirstSet;
71
98
  //# sourceMappingURL=first-set.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"first-set.d.ts","sourceRoot":"","sources":["../../src/combinators/first-set.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,QAAQ,EAAE,UAAU,EAAa,MAAM,aAAa,CAAA;AAE7E,wBAAgB,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAKxD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAS5D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnG;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ,CAE1D;AAED,wBAAgB,GAAG,IAAI,QAAQ,CAE9B;AAED,wBAAgB,KAAK,IAAI,QAAQ,CAEhC;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,GAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAa,GAAG,OAAO,CAqCxG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,OAAO,CAEpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,QAAQ,CAOlF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,GAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAa,GAAG,QAAQ,CA2CvG"}
1
+ {"version":3,"file":"first-set.d.ts","sourceRoot":"","sources":["../../src/combinators/first-set.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,QAAQ,EAAE,UAAU,EAAa,MAAM,aAAa,CAAA;AAE7E;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,CAAC,GAAG,SAAS,CAAA;AAS3E,wBAAgB,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAKxD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAS5D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnG;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ,CAE1D;AAED,wBAAgB,GAAG,IAAI,QAAQ,CAE9B;AAED,wBAAgB,KAAK,IAAI,QAAQ,CAEhC;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,EACtB,IAAI,GAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAa,EAC1C,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CA6CT;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,OAAO,CAEpE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,OAAO,CAEnE;AA6BD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,QAAQ,CAalF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CACxB,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,EACtB,IAAI,GAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAa,EAC1C,OAAO,CAAC,EAAE,WAAW,GACpB,QAAQ,CAsDV"}
@@ -13,10 +13,19 @@ export type KeywordsOptions = {
13
13
  * matching `true` inside `trueish`. The boundary defaults to `_0-9A-Za-z`,
14
14
  * which covers most programming-language identifiers.
15
15
  *
16
- * word('true') // matches "true" but not "trueish"
17
- * word('color', 'A-Za-z-') // CSS-style identifier boundary
16
+ * word('true') // matches "true" but not "trueish"
17
+ * word('color', 'A-Za-z-') // CSS-style identifier boundary
18
+ * word('media', 'A-Za-z0-9_-', { caseInsensitive: true })
19
+ * word('media', { caseInsensitive: true }) // default boundary + options
20
+ *
21
+ * `caseInsensitive` matches `keywords()` and exists because it is REQUIRED, not a
22
+ * nicety: CSS at-keywords, function names and units are ASCII case-insensitive per
23
+ * spec (CSS Syntax §3). Without it the only conforming spellings were
24
+ * `regex(/media/i)` — which `analyzeGating` correctly flags as the `keyword-regex`
25
+ * anti-pattern — or `keywords(['media'], …)` for a single word.
18
26
  */
19
- export declare function word(str: string, boundary?: string): Combinator<string>;
27
+ export declare function word(str: string, boundary?: string, opts?: Omit<KeywordsOptions, 'boundary'>): Combinator<string>;
28
+ export declare function word(str: string, opts: Omit<KeywordsOptions, 'boundary'>): Combinator<string>;
20
29
  /**
21
30
  * Create a keyword factory with a fixed word-boundary class. Use when many
22
31
  * keywords share the same boundary (e.g. CSS identifiers). For a single keyword,
@@ -1 +1 @@
1
- {"version":3,"file":"keywords.d.ts","sourceRoot":"","sources":["../../src/combinators/keywords.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAmD,MAAM,aAAa,CAAA;AAK9F,MAAM,MAAM,eAAe,GAAG;IAC5B,gCAAgC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAe,GAAG,UAAU,CAAC,MAAM,CAAC,CAE7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,SAAe,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,CAAC,MAAM,CAAC,CAErF;AAMD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,GAAE,eAAoB,GAAG,UAAU,CAAC,MAAM,CAAC,CA0CjG"}
1
+ {"version":3,"file":"keywords.d.ts","sourceRoot":"","sources":["../../src/combinators/keywords.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAmD,MAAM,aAAa,CAAA;AAM9F,MAAM,MAAM,eAAe,GAAG;IAC5B,gCAAgC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;AAClH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;AAW9F;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,SAAe,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,CAAC,MAAM,CAAC,CAErF;AAMD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,GAAE,eAAoB,GAAG,UAAU,CAAC,MAAM,CAAC,CA8DjG"}
@@ -1 +1 @@
1
- {"version":3,"file":"not.d.ts","sourceRoot":"","sources":["../../src/combinators/not.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAA;AAIpF;;;;;;;;;GASG;AACH,wBAAgB,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CA0BrE"}
1
+ {"version":3,"file":"not.d.ts","sourceRoot":"","sources":["../../src/combinators/not.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAA;AAIpF;;;;;;;;;GASG;AACH,wBAAgB,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAwCrE"}
@@ -0,0 +1,27 @@
1
+ import type { Combinator } from '../types.ts';
2
+ /**
3
+ * Positive lookahead. Succeeds (consuming nothing) when `combinator` matches at
4
+ * this position; fails when it doesn't. The mirror of `not()`.
5
+ *
6
+ * // "a mixin reference, not a class selector": require the punctuation ahead,
7
+ * // then let the real production consume it.
8
+ * const mixinRef = sequence(peek(regex(/[.#]/)), mixinCall)
9
+ *
10
+ * WHY THIS EXISTS AS A PRIMITIVE (not `not(not(X))`):
11
+ *
12
+ * `not(not(X))` is behaviourally a positive lookahead, but `not()` reports
13
+ * `firstSet: any()` — it cannot know what it forbids — so `not(not(X))` reports
14
+ * `any()` too. An arm leading with it therefore POISONS its choice's first-char
15
+ * dispatch (`analyzeGating` flags it as the `double-not` anti-pattern), and among
16
+ * sibling arms that share a first char the hand-rolled gate miscompiles.
17
+ *
18
+ * `peek(X)` carries X's first-set instead, so a leading `peek()` GATES its arm:
19
+ * `choice(sequence(peek(regex(/[.#]/)), …), …)` still emits O(1) dispatch.
20
+ *
21
+ * The first-set is exact only when X is NON-NULLABLE. A nullable `peek(X)`
22
+ * succeeds on the empty string, so it constrains no first character at all — it
23
+ * reports `any()`, the sound over-approximation. (Same rule the sequence
24
+ * first-set uses; see `isPositiveLookahead` in `first-set.ts`.)
25
+ */
26
+ export declare function peek(combinator: Combinator<unknown>): Combinator<null>;
27
+ //# sourceMappingURL=peek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peek.d.ts","sourceRoot":"","sources":["../../src/combinators/peek.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAA;AAIpF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAkCtE"}