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.
- package/README.md +94 -72
- package/dist/analysis/gating.d.ts +66 -1
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/combinators/case-fold.d.ts +39 -0
- package/dist/combinators/case-fold.d.ts.map +1 -0
- package/dist/combinators/expect.d.ts +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +38 -11
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/keywords.d.ts +12 -3
- package/dist/combinators/keywords.d.ts.map +1 -1
- package/dist/combinators/not.d.ts.map +1 -1
- package/dist/combinators/peek.d.ts +27 -0
- package/dist/combinators/peek.d.ts.map +1 -0
- package/dist/combinators/repeat.d.ts +68 -3
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +23 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/codegen.d.ts +40 -0
- package/dist/compiler/codegen.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +5 -0
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/index.cjs +462 -105
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +462 -105
- package/dist/index.js.map +4 -4
- package/dist/language-service/index.cjs.map +1 -1
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +529 -135
- package/dist/plugin/index.cjs.map +4 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +529 -135
- package/dist/plugin/index.js.map +4 -4
- package/dist/run/index.cjs +216 -0
- package/dist/run/index.cjs.map +7 -0
- package/dist/run/index.d.ts +28 -0
- package/dist/run/index.d.ts.map +1 -0
- package/dist/run/index.js +193 -0
- package/dist/run/index.js.map +7 -0
- package/dist/spec/ebnf.d.ts.map +1 -1
- package/dist/spec/index.cjs +79 -17
- package/dist/spec/index.cjs.map +2 -2
- package/dist/spec/index.js +79 -17
- package/dist/spec/index.js.map +2 -2
- package/dist/spec/model.d.ts +23 -3
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/spec/railroad.d.ts.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- 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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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)
|
|
66
|
-
|
|
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)
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
- **[
|
|
80
|
-
|
|
81
|
-
- **[
|
|
82
|
-
|
|
83
|
-
- **[
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|

|
|
120
139
|
|
|
@@ -124,26 +143,29 @@ Compared parsers: **Parséman**, [Peggy](https://peggyjs.org/), [Parsimmon](http
|
|
|
124
143
|
|
|
125
144
|

|
|
126
145
|
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
138
|
-
pnpm typecheck
|
|
139
|
-
pnpm build
|
|
140
|
-
pnpm
|
|
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
|
|
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
|
|
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)
|
|
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;
|
|
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
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
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,
|
|
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')
|
|
17
|
-
* word('color', 'A-Za-z-')
|
|
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;
|
|
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,
|
|
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"}
|