parseman 0.46.0 → 0.47.1
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 +8 -3
- package/dist/analysis/choice-cost-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +10339 -7787
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +10339 -7787
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10924 -8506
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10924 -8506
- package/dist/cli/index.js.map +3 -4
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +45 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +47 -53
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +9290 -7181
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9300 -7191
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +313 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +313 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11175 -9534
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11175 -9534
- package/dist/plugin/index.js.map +3 -4
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2934 -1611
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2934 -1611
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11463 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11440 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +453 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +496 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +26 -2
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +348 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +237 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +852 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +3006 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1777 -0
- package/src/table/emit.ts +429 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1680 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +467 -0
- package/src/table/program.ts +876 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
/** ECMAScript `Canonicalize` for non-Unicode `/i`. */
|
|
35
|
+
function canonical(ch: string): string {
|
|
36
|
+
const u = ch.toUpperCase()
|
|
37
|
+
// A fold that expands (`ß` → `SS`) is not a single-char fold: no canonicalization.
|
|
38
|
+
if (u.length !== 1) return ch
|
|
39
|
+
// Refuse a fold that would move a non-ASCII char INTO Basic Latin (`ſ` → `S`).
|
|
40
|
+
if (ch.codePointAt(0)! >= 128 && u.codePointAt(0)! < 128) return ch
|
|
41
|
+
return u
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lazily-built reverse index over the non-ASCII BMP, keyed by canonical form.
|
|
46
|
+
* Only classes with more than one member are kept (a singleton class needs no
|
|
47
|
+
* widening), which is ~2.3k entries rather than 65k. The ~12ms scan is paid ONCE
|
|
48
|
+
* per process and only when a non-ASCII case-insensitive first char actually
|
|
49
|
+
* appears — the all-ASCII path below never touches it.
|
|
50
|
+
*/
|
|
51
|
+
let foldIndex: Map<number, readonly number[]> | null = null
|
|
52
|
+
|
|
53
|
+
function buildFoldIndex(): Map<number, readonly number[]> {
|
|
54
|
+
const byCanonical = new Map<string, number[]>()
|
|
55
|
+
for (let cp = 0; cp < 0x10000; cp++) {
|
|
56
|
+
// Lone surrogates have no case and never canonicalize with anything.
|
|
57
|
+
if (cp >= 0xd800 && cp <= 0xdfff) continue
|
|
58
|
+
const key = canonical(String.fromCharCode(cp))
|
|
59
|
+
const bucket = byCanonical.get(key)
|
|
60
|
+
if (bucket) bucket.push(cp)
|
|
61
|
+
else byCanonical.set(key, [cp])
|
|
62
|
+
}
|
|
63
|
+
const index = new Map<number, readonly number[]>()
|
|
64
|
+
for (const members of byCanonical.values()) {
|
|
65
|
+
if (members.length < 2) continue
|
|
66
|
+
for (const cp of members) index.set(cp, members)
|
|
67
|
+
}
|
|
68
|
+
return index
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every code point that a non-Unicode `/i` matcher treats as equal to `cp`,
|
|
73
|
+
* INCLUDING `cp` itself. Union these into a first-set to keep case-insensitive
|
|
74
|
+
* dispatch sound.
|
|
75
|
+
*/
|
|
76
|
+
export function caseFoldVariants(cp: number): readonly number[] {
|
|
77
|
+
// Basic Latin: the fold class is exactly the a↔A twin, no index needed. (The
|
|
78
|
+
// cross-boundary refusal above is what guarantees nothing outside ASCII joins
|
|
79
|
+
// an ASCII char's class, so this fast path is exact, not an approximation.)
|
|
80
|
+
if (cp < 128) {
|
|
81
|
+
if (cp >= 0x41 && cp <= 0x5a) return [cp, cp + 32]
|
|
82
|
+
if (cp >= 0x61 && cp <= 0x7a) return [cp, cp - 32]
|
|
83
|
+
return [cp]
|
|
84
|
+
}
|
|
85
|
+
// Above the BMP there is nothing to fold: in non-Unicode mode the pattern is
|
|
86
|
+
// matched as UTF-16 code units, and no surrogate has a case mapping.
|
|
87
|
+
if (cp > 0xffff) return [cp]
|
|
88
|
+
foldIndex ??= buildFoldIndex()
|
|
89
|
+
return foldIndex.get(cp) ?? [cp]
|
|
90
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Combinator, ParseContext, ParseResult, ParserMeta, FirstSet,
|
|
3
|
+
AutoNotCheck, CharRange, ChoiceStrategy, GatedArm,
|
|
4
|
+
} from '../types.ts'
|
|
5
|
+
import { union, intersects, matchesEmpty } from './first-set.ts'
|
|
6
|
+
import { deriveExpected } from './expect.ts'
|
|
7
|
+
import { rollbackTrivia, saveTriviaMark } from './trivia-skip.ts'
|
|
8
|
+
|
|
9
|
+
type ArmParser<T> = T extends GatedArm<infer U> ? Combinator<U> : T extends Combinator<infer U> ? Combinator<U> : never
|
|
10
|
+
type UnionArms<T extends (Combinator<unknown> | GatedArm<unknown>)[]> = {
|
|
11
|
+
[K in keyof T]: ArmParser<T[K]>
|
|
12
|
+
}[number] extends Combinator<infer U> ? U : never
|
|
13
|
+
|
|
14
|
+
export function choice<T extends [Combinator<unknown> | GatedArm<unknown>, ...(Combinator<unknown> | GatedArm<unknown>)[]]>(
|
|
15
|
+
...args: T
|
|
16
|
+
): Combinator<UnionArms<T>> {
|
|
17
|
+
// Unwrap gated arms into (parser, gate) pairs
|
|
18
|
+
const parsers = args.map(a => ('gate' in a ? a.combinator : a)) as Combinator<unknown>[]
|
|
19
|
+
const gates = args.map(a => ('gate' in a ? a.gate : null)) as (((state: unknown) => boolean) | null)[]
|
|
20
|
+
|
|
21
|
+
const hasGates = gates.some(g => g !== null)
|
|
22
|
+
|
|
23
|
+
// O(1) first-char dispatch is sound when arms are pairwise-disjoint in their
|
|
24
|
+
// first char AND no arm can match empty. Non-nullability is what makes a GATED
|
|
25
|
+
// arm keep its dispatch slot: a gated arm is normally *skipped* so a later arm
|
|
26
|
+
// can retry the same position — but if every arm is non-nullable and first-sets
|
|
27
|
+
// are disjoint, no later arm can match this first char, so "skip the gate then
|
|
28
|
+
// retry" is exactly "dispatch to this arm, check its gate, and fail the choice
|
|
29
|
+
// if the gate is false". Every OTHER first char dispatches as before and never
|
|
30
|
+
// touches the gate. A nullable arm matches at ANY position (zero-width), so
|
|
31
|
+
// first-char dispatch can't represent it → such choices stay on firstMatch.
|
|
32
|
+
// (Requiring non-nullability universally also tightens the ungated path, which
|
|
33
|
+
// was already unsound for a nullable-but-first-set-disjoint arm; in practice no
|
|
34
|
+
// ungated disjoint choice has a nullable arm, so codegen output is unchanged.)
|
|
35
|
+
const disjoint = areDisjoint(parsers.map(p => p._meta.firstSet))
|
|
36
|
+
&& parsers.every(p => !matchesEmpty(p))
|
|
37
|
+
|
|
38
|
+
let combined: FirstSet = { kind: 'empty' }
|
|
39
|
+
for (const p of parsers) combined = union(combined, p._meta.firstSet)
|
|
40
|
+
|
|
41
|
+
const meta: ParserMeta = {
|
|
42
|
+
firstSet: combined,
|
|
43
|
+
canMatchNewline: parsers.some(p => p._meta.canMatchNewline),
|
|
44
|
+
isTrivia: false,
|
|
45
|
+
disjoint,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A disjoint choice dispatches by first char (gated arms check their gate inside
|
|
49
|
+
// the dispatched branch); a NON-disjoint gated choice falls to firstMatch — the
|
|
50
|
+
// greedy/longest-first strategies are incompatible with per-arm predicates.
|
|
51
|
+
const strategy = (disjoint || hasGates) ? null : detectStrategy(parsers)
|
|
52
|
+
// `sharedPrefix` is a `firstMatch` specialization — the interpreter runs it via
|
|
53
|
+
// the firstMatch loop below, and codegen falls back to firstMatch under
|
|
54
|
+
// coverage/recovery/linkable — so it needs the same auto-not table computed.
|
|
55
|
+
const autoNot = (!disjoint && !hasGates && (strategy?.tag === 'firstMatch' || strategy?.tag === 'sharedPrefix'))
|
|
56
|
+
? computeAutoNot(parsers)
|
|
57
|
+
: parsers.map(() => null)
|
|
58
|
+
|
|
59
|
+
// Runtime state for each strategy (built once, reused on every parse call):
|
|
60
|
+
let greedyLitMap: Map<string, number> | null = null
|
|
61
|
+
let sortedParsers: Combinator<unknown>[] | null = null
|
|
62
|
+
const asciiDispatch = disjoint ? buildAsciiDispatch(parsers) : null
|
|
63
|
+
|
|
64
|
+
if (strategy?.tag === 'greedyClassify') {
|
|
65
|
+
greedyLitMap = new Map()
|
|
66
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
67
|
+
if (i === strategy.superIndex) continue
|
|
68
|
+
const litVal = getCoreLiteralValue(parsers[i]!)
|
|
69
|
+
if (litVal !== null) greedyLitMap.set(litVal, i)
|
|
70
|
+
}
|
|
71
|
+
} else if (strategy?.tag === 'literalsLongestFirst') {
|
|
72
|
+
sortedParsers = strategy.sortedIndices.map(i => parsers[i]!)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
_tag: 'choice',
|
|
77
|
+
_meta: meta,
|
|
78
|
+
_def: {
|
|
79
|
+
tag: 'choice',
|
|
80
|
+
parsers,
|
|
81
|
+
gates,
|
|
82
|
+
disjoint,
|
|
83
|
+
strategy: strategy ?? { tag: 'firstMatch' },
|
|
84
|
+
autoNot,
|
|
85
|
+
},
|
|
86
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<UnionArms<T>> {
|
|
87
|
+
const expected: string[] = []
|
|
88
|
+
|
|
89
|
+
// ── Disjoint: O(1) first-char dispatch (arms may be gated) ────────────
|
|
90
|
+
//
|
|
91
|
+
// EOF IS A DISPATCH MISS, not a reason to leave the disjoint path. Every arm
|
|
92
|
+
// of a disjoint choice is non-nullable (that is a precondition of `disjoint`
|
|
93
|
+
// above), so at EOF no arm can match and the answer is the same "nothing
|
|
94
|
+
// could have started here" the in-bounds miss below gives. Falling through to
|
|
95
|
+
// firstMatch instead made this position the ONE place a gated-off arm was
|
|
96
|
+
// dropped from the report — firstMatch `continue`s past it and contributes
|
|
97
|
+
// nothing — while the in-bounds miss runs `parsers.flatMap` ignoring gates and
|
|
98
|
+
// DOES name it. Same gate state, same "no arm can match", two different
|
|
99
|
+
// answers; codegen and both table drivers only ever gave the union. Accept and
|
|
100
|
+
// reject are untouched: every arm fails at EOF on either path.
|
|
101
|
+
if (disjoint) {
|
|
102
|
+
const code = pos < input.length ? input.codePointAt(pos)! : -1
|
|
103
|
+
let idx = code >= 0 && code < 128 ? asciiDispatch![code]! : -1
|
|
104
|
+
if (idx < 0 && code >= 0) {
|
|
105
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
106
|
+
if (inFirstSet(code, parsers[i]!._meta.firstSet)) { idx = i; break }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (idx >= 0) {
|
|
110
|
+
const gate = gates[idx]
|
|
111
|
+
if (gate && !gate(ctx.state)) {
|
|
112
|
+
// Gate blocks this arm. Disjointness + non-nullable arms guarantee no
|
|
113
|
+
// OTHER arm can match this first char, so skip-and-retry is exactly
|
|
114
|
+
// fail-the-choice — we must not fall through to another arm.
|
|
115
|
+
return { ok: false, expected: deriveExpected(parsers[idx]!), span: { start: pos, end: pos } }
|
|
116
|
+
}
|
|
117
|
+
const result = parsers[idx]!.parse(input, pos, ctx)
|
|
118
|
+
if (result.ok) return result as ParseResult<UnionArms<T>>
|
|
119
|
+
expected.push(...result.expected)
|
|
120
|
+
if (result.committed) return { ok: false, expected, span: { start: pos, end: pos }, committed: true }
|
|
121
|
+
return { ok: false, expected, span: { start: pos, end: pos } }
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
expected: parsers.flatMap(p => {
|
|
126
|
+
const r = p.parse(input, pos, ctx)
|
|
127
|
+
return r.ok ? [] : r.expected
|
|
128
|
+
}),
|
|
129
|
+
span: { start: pos, end: pos },
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── greedyClassify: run one regex, classify by string equality ─────────
|
|
134
|
+
// One parse call total. No backtracking.
|
|
135
|
+
if (strategy?.tag === 'greedyClassify') {
|
|
136
|
+
const superResult = parsers[strategy.superIndex]!.parse(input, pos, ctx)
|
|
137
|
+
if (!superResult.ok) return superResult as ParseResult<UnionArms<T>>
|
|
138
|
+
|
|
139
|
+
const end = superResult.span.end
|
|
140
|
+
const litIdx = greedyLitMap!.get(input.slice(pos, end))
|
|
141
|
+
if (litIdx !== undefined) {
|
|
142
|
+
const litVal = getCoreLiteralValue(parsers[litIdx]!)!
|
|
143
|
+
const value = applyTransforms(parsers[litIdx]!, litVal, { start: pos, end })
|
|
144
|
+
return { ok: true, value: value as UnionArms<T>, span: { start: pos, end } }
|
|
145
|
+
}
|
|
146
|
+
return superResult as ParseResult<UnionArms<T>>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── literalsLongestFirst: sorted descending by length, no backtracking ─
|
|
150
|
+
if (strategy?.tag === 'literalsLongestFirst') {
|
|
151
|
+
for (const p of sortedParsers!) {
|
|
152
|
+
const r = p.parse(input, pos, ctx)
|
|
153
|
+
if (r.ok) return r as ParseResult<UnionArms<T>>
|
|
154
|
+
expected.push(...r.expected)
|
|
155
|
+
}
|
|
156
|
+
return { ok: false, expected, span: { start: pos, end: pos } }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── firstMatch (+ gated arms): try each arm in order, skipping gated-off arms ──
|
|
160
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
161
|
+
if (gates[i] && !gates[i]!(ctx.state)) continue // gate blocks this arm
|
|
162
|
+
// Save leaf-array lengths so a failed/rejected arm can be rolled back.
|
|
163
|
+
const mark = saveTriviaMark(ctx)
|
|
164
|
+
const result = parsers[i]!.parse(input, pos, ctx)
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
rollbackTrivia(ctx, mark)
|
|
167
|
+
expected.push(...result.expected)
|
|
168
|
+
if (result.committed) return { ok: false, expected, span: result.span, committed: true }
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
const checks = autoNot[i]
|
|
172
|
+
if (checks && autoNotFires(input, result.span.end, checks)) {
|
|
173
|
+
rollbackTrivia(ctx, mark)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
return result as ParseResult<UnionArms<T>>
|
|
177
|
+
}
|
|
178
|
+
return { ok: false, expected, span: { start: pos, end: pos } }
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Strategy detection
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
function detectStrategy(parsers: Combinator<unknown>[]): ChoiceStrategy {
|
|
188
|
+
// greedyClassify: exactly one regex arm whose regex matches every literal arm's
|
|
189
|
+
// value exactly (and can potentially match more). All other arms must be literals.
|
|
190
|
+
const regexIndices: number[] = []
|
|
191
|
+
const literalIndices: number[] = []
|
|
192
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
193
|
+
if (getCoreRegexDef(parsers[i]!) !== null) regexIndices.push(i)
|
|
194
|
+
else if (getCoreLiteralValue(parsers[i]!) !== null) literalIndices.push(i)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
regexIndices.length === 1 &&
|
|
199
|
+
literalIndices.length === parsers.length - 1 &&
|
|
200
|
+
literalIndices.length > 0
|
|
201
|
+
) {
|
|
202
|
+
const superIndex = regexIndices[0]!
|
|
203
|
+
const regexDef = getCoreRegexDef(parsers[superIndex]!)!
|
|
204
|
+
const flags = 'y' + regexDef.flags.replace(/[gy]/g, '')
|
|
205
|
+
const re = new RegExp(regexDef.source, flags)
|
|
206
|
+
const allSubsumed = literalIndices.every(i => {
|
|
207
|
+
const litVal = getCoreLiteralValue(parsers[i]!)!
|
|
208
|
+
re.lastIndex = 0
|
|
209
|
+
const m = re.exec(litVal)
|
|
210
|
+
return m !== null && m[0] === litVal
|
|
211
|
+
})
|
|
212
|
+
if (allSubsumed) return { tag: 'greedyClassify', superIndex }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// literalsLongestFirst: every arm is a literal — try longest first, no backtracking
|
|
216
|
+
if (parsers.length === literalIndices.length) {
|
|
217
|
+
const sortedIndices = [...literalIndices].sort((a, b) =>
|
|
218
|
+
getCoreLiteralValue(parsers[b]!)!.length - getCoreLiteralValue(parsers[a]!)!.length
|
|
219
|
+
)
|
|
220
|
+
return { tag: 'literalsLongestFirst', sortedIndices }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// sharedPrefix: EVERY arm is a bare sequence beginning with the SAME concrete
|
|
224
|
+
// leading literal/regex — the compiler parses that left factor once. Detected
|
|
225
|
+
// last (its shape is disjoint from the literal-only strategies above).
|
|
226
|
+
const shared = detectSharedPrefix(parsers)
|
|
227
|
+
if (shared !== null) return shared
|
|
228
|
+
|
|
229
|
+
return { tag: 'firstMatch' }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A stable structural key for a bare leading terminal (concrete literal/regex),
|
|
234
|
+
* or `null` when the term is NOT a shareable concrete prefix. Deliberately
|
|
235
|
+
* conservative: only an UNWRAPPED literal (case-sensitive) or regex qualifies —
|
|
236
|
+
* a transform/label/ref/node wrapper changes the value/capture shape and must NOT
|
|
237
|
+
* be left-factored (correctness first; the strategy is opt-in like greedyClassify).
|
|
238
|
+
*/
|
|
239
|
+
function bareLeadingTermKey(term: Combinator<unknown>): string | null {
|
|
240
|
+
const d = term._def
|
|
241
|
+
if (d.tag === 'literal' && !d.caseInsensitive) return `L:${d.value}`
|
|
242
|
+
// `JSON.stringify` of the pair, not a delimiter join. `source` is arbitrary regex text
|
|
243
|
+
// and can contain any character, so a collision here would judge two structurally
|
|
244
|
+
// DIFFERENT regexes identical and left-factor arms that must not share a prefix — a
|
|
245
|
+
// correctness bug in the generated parser, not a missed optimization. A JSON array is
|
|
246
|
+
// injective by construction, so no argument about which delimiter cannot occur in a
|
|
247
|
+
// regex source is needed. It is also printable: this was a RAW 0x00 byte, which made
|
|
248
|
+
// the whole file binary to `git diff` and invisible to `grep -rn` (see
|
|
249
|
+
// `scripts/check-control-bytes.mjs`). Runs once per choice when its strategy is chosen,
|
|
250
|
+
// never per match.
|
|
251
|
+
if (d.tag === 'regex') return `R:${JSON.stringify([d.source, d.flags])}`
|
|
252
|
+
// Two lead shapes look eligible and are deliberately NOT. Both were tried and
|
|
253
|
+
// measured; see test/unit/routed-fallback.test.ts, which pins the exclusions.
|
|
254
|
+
//
|
|
255
|
+
// `routed()` — a bare routed() IS a single-leaf terminal (it reads the
|
|
256
|
+
// dispatch-consumed token, pushes exactly ONE leaf, skips no trivia, runs no
|
|
257
|
+
// sub-parse), so it is replay-safe and admitting it here is correct. It is
|
|
258
|
+
// excluded because it does not PAY. The strategy trades a duplicated lead scan for
|
|
259
|
+
// a prescan plus a prefix-matched flag; routed()'s emission is a context read and
|
|
260
|
+
// one comparison, so there is nothing worth factoring out. Measured on
|
|
261
|
+
// `dispatch(name, when(k, choice(sequence(routed(), tail_i)...)))`, emitted bytes
|
|
262
|
+
// with the strategy MINUS without it: +242 (2 arms), +260 (4), +296 (8) — it costs
|
|
263
|
+
// more at every width and never crosses over. The same measurement on the
|
|
264
|
+
// regex-lead shape the strategy was built for: -468 (2 arms), -1597 (4), -3846 (8).
|
|
265
|
+
// A routed lead also shares only the ROUTED token; when the expensive shared work
|
|
266
|
+
// is the next term (a prelude), that term is what needs factoring, not this one.
|
|
267
|
+
//
|
|
268
|
+
// `routed(fallback)` — not a leaf at all: on the fallback path it runs a whole
|
|
269
|
+
// sub-parse, so replaying it as a once-recognized leaf would be wrong, and two
|
|
270
|
+
// routed() with DIFFERENT fallbacks are not interchangeable to begin with.
|
|
271
|
+
//
|
|
272
|
+
// A `lazy` ref lead — excluded for correctness, not for want of a key. The
|
|
273
|
+
// once-only prescan runs with `ctx.capturing = false`, which suppresses capture
|
|
274
|
+
// only for code emitted HERE (emitLeafCapture, codegen.ts). A ref compiles to a
|
|
275
|
+
// call into a function body generated under its OWN ctx (emitLazy), so its
|
|
276
|
+
// captures and trivia writes would happen during the prescan and then AGAIN in
|
|
277
|
+
// each arm. Replaying a ref needs recorded-and-spliced capture state, not the
|
|
278
|
+
// variable reuse a terminal replay gets away with.
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Detect the pure single-group shared-prefix shape: at least two arms, EVERY arm
|
|
284
|
+
* a bare `sequence` with ≥2 terms, and every arm's FIRST term the same concrete
|
|
285
|
+
* literal/regex (identical source/flags or literal string). Returns the plan with
|
|
286
|
+
* a representative prefix combinator (the first arm's leading term — all members'
|
|
287
|
+
* leading terms are structurally identical, so any one emits identical code),
|
|
288
|
+
* else `null`. Mixed / multi-group / wrapped shapes are conservatively skipped and
|
|
289
|
+
* fall through to `firstMatch`.
|
|
290
|
+
*/
|
|
291
|
+
function detectSharedPrefix(parsers: Combinator<unknown>[]): ChoiceStrategy | null {
|
|
292
|
+
if (parsers.length < 2) return null
|
|
293
|
+
let key: string | null = null
|
|
294
|
+
let prefix: Combinator<unknown> | null = null
|
|
295
|
+
const members: number[] = []
|
|
296
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
297
|
+
const term0 = leadingTermOfArm(parsers[i]!)
|
|
298
|
+
if (term0 === null) return null
|
|
299
|
+
const k = bareLeadingTermKey(term0)
|
|
300
|
+
if (k === null) return null
|
|
301
|
+
if (key === null) { key = k; prefix = term0 }
|
|
302
|
+
else if (k !== key) return null
|
|
303
|
+
members.push(i)
|
|
304
|
+
}
|
|
305
|
+
if (prefix === null || members.length < 2) return null
|
|
306
|
+
return { tag: 'sharedPrefix', prefix, members }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Peel wrappers that neither consume input nor skip trivia before their inner
|
|
311
|
+
* sequence -- `node`, `parser`/`grammar`, `transform`, `label` -- down to the core
|
|
312
|
+
* `sequence` and return its FIRST term (the candidate shared prefix), or null when
|
|
313
|
+
* the arm is not a wrapped-or-bare sequence with >=2 terms. Because none of these
|
|
314
|
+
* wrappers advance the position before the sequence's first term, the term is parsed
|
|
315
|
+
* at the arm's entry position in every arm, so the once-recognized prefix can be
|
|
316
|
+
* replayed there. `attempt`/`optional`/`many`/`choice`/etc. are NOT peeled, so such
|
|
317
|
+
* arms are conservatively excluded.
|
|
318
|
+
*/
|
|
319
|
+
export function leadingTermOfArm(arm: Combinator<unknown>): Combinator<unknown> | null {
|
|
320
|
+
let d = arm._def
|
|
321
|
+
for (;;) {
|
|
322
|
+
if (d.tag === 'node' || d.tag === 'grammar' || d.tag === 'transform' || d.tag === 'label') {
|
|
323
|
+
d = (d as { parser: Combinator<unknown> }).parser._def
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
break
|
|
327
|
+
}
|
|
328
|
+
if (d.tag !== 'sequence' || d.parsers.length < 2) return null
|
|
329
|
+
return d.parsers[0]!
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Auto-not analysis (firstMatch fallback only)
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
function computeAutoNot(parsers: Combinator<unknown>[]): (AutoNotCheck[] | null)[] {
|
|
337
|
+
return parsers.map((p, i) => {
|
|
338
|
+
const litVal = getCoreLiteralValue(p)
|
|
339
|
+
if (litVal === null) return null
|
|
340
|
+
|
|
341
|
+
const checks: AutoNotCheck[] = []
|
|
342
|
+
for (let j = i + 1; j < parsers.length; j++) {
|
|
343
|
+
const other = parsers[j]!
|
|
344
|
+
const otherLit = getCoreLiteralValue(other)
|
|
345
|
+
if (otherLit !== null && otherLit.startsWith(litVal) && otherLit.length > litVal.length) {
|
|
346
|
+
checks.push({ kind: 'startsWith', value: otherLit.slice(litVal.length) })
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
const regexDef = getCoreRegexDef(other)
|
|
350
|
+
if (regexDef !== null) {
|
|
351
|
+
const contSet = continuationFirstSet(litVal, regexDef.source, regexDef.flags)
|
|
352
|
+
if (contSet !== null) checks.push({ kind: 'firstSet', set: contSet })
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return checks.length > 0 ? checks : null
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function autoNotFires(input: string, end: number, checks: AutoNotCheck[]): boolean {
|
|
360
|
+
for (const check of checks) {
|
|
361
|
+
if (check.kind === 'firstSet') {
|
|
362
|
+
const code = end < input.length ? (input.codePointAt(end) ?? -1) : -1
|
|
363
|
+
if (inFirstSet(code, check.set)) return true
|
|
364
|
+
} else {
|
|
365
|
+
if (input.startsWith(check.value, end)) return true
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return false
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// Helpers: unwrap transform/sequence wrappers
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
/** Walk transform wrappers to find an inner literal's string value. */
|
|
376
|
+
export function getCoreLiteralValue(p: Combinator<unknown>): string | null {
|
|
377
|
+
const def = p._def
|
|
378
|
+
if (def.tag === 'literal' && !def.caseInsensitive) return def.value
|
|
379
|
+
if (def.tag === 'transform') return getCoreLiteralValue(def.parser)
|
|
380
|
+
return null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Walk transform wrappers to find an inner regex's source/flags. */
|
|
384
|
+
export function getCoreRegexDef(p: Combinator<unknown>): { source: string; flags: string } | null {
|
|
385
|
+
const def = p._def
|
|
386
|
+
if (def.tag === 'regex') return { source: def.source, flags: def.flags }
|
|
387
|
+
if (def.tag === 'transform') return getCoreRegexDef(def.parser)
|
|
388
|
+
if (def.tag === 'label') return getCoreRegexDef(def.parser)
|
|
389
|
+
return null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Apply a parser's transform chain to an already-known value, without re-parsing.
|
|
394
|
+
* Used by greedyClassify to avoid a second parse call for the winning literal arm.
|
|
395
|
+
*/
|
|
396
|
+
function applyTransforms(p: Combinator<unknown>, value: unknown, span: { start: number; end: number }): unknown {
|
|
397
|
+
const def = p._def
|
|
398
|
+
if (def.tag === 'transform') {
|
|
399
|
+
const inner = applyTransforms(def.parser, value, span)
|
|
400
|
+
return def.fn(inner, span)
|
|
401
|
+
}
|
|
402
|
+
return value
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Continuation first-set (for firstMatch auto-not analysis)
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
function continuationFirstSet(lit: string, source: string, flags: string): FirstSet | null {
|
|
410
|
+
const re = new RegExp(source, 'y' + flags.replace(/[gy]/g, ''))
|
|
411
|
+
re.lastIndex = 0
|
|
412
|
+
const base = re.exec(lit)
|
|
413
|
+
if (!base || base[0] !== lit) return null
|
|
414
|
+
|
|
415
|
+
const contCodes: number[] = []
|
|
416
|
+
for (let code = 1; code < 128; code++) {
|
|
417
|
+
re.lastIndex = 0
|
|
418
|
+
const m = re.exec(lit + String.fromCharCode(code))
|
|
419
|
+
if (m && m[0].length > lit.length) contCodes.push(code)
|
|
420
|
+
}
|
|
421
|
+
if (contCodes.length === 0) return null
|
|
422
|
+
return codesToFirstSet(contCodes)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function codesToFirstSet(codes: number[]): FirstSet {
|
|
426
|
+
codes.sort((a, b) => a - b)
|
|
427
|
+
const ranges: CharRange[] = []
|
|
428
|
+
let lo = codes[0]!, hi = codes[0]!
|
|
429
|
+
for (let i = 1; i < codes.length; i++) {
|
|
430
|
+
if (codes[i] === hi + 1) { hi = codes[i]! }
|
|
431
|
+
else { ranges.push({ lo, hi }); lo = hi = codes[i]! }
|
|
432
|
+
}
|
|
433
|
+
ranges.push({ lo, hi })
|
|
434
|
+
return { kind: 'ranges', ranges }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function inFirstSet(code: number, fs: FirstSet): boolean {
|
|
438
|
+
if (fs.kind === 'any') return true
|
|
439
|
+
if (fs.kind === 'empty') return false
|
|
440
|
+
for (const r of fs.ranges) if (code >= r.lo && code <= r.hi) return true
|
|
441
|
+
return false
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function areDisjoint(sets: FirstSet[]): boolean {
|
|
445
|
+
if (sets.some(s => s.kind === 'any')) return false
|
|
446
|
+
for (let i = 0; i < sets.length; i++)
|
|
447
|
+
for (let j = i + 1; j < sets.length; j++)
|
|
448
|
+
if (intersects(sets[i]!, sets[j]!)) return false
|
|
449
|
+
return true
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ASCII first-char → arm INDEX (not the parser) so a gated arm's gate can be
|
|
453
|
+
// looked up (gates[idx]) at dispatch time. -1 means "no arm keys off this char".
|
|
454
|
+
function buildAsciiDispatch(parsers: Combinator<unknown>[]): number[] {
|
|
455
|
+
const table = Array<number>(128).fill(-1)
|
|
456
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
457
|
+
const fs = parsers[i]!._meta.firstSet
|
|
458
|
+
if (fs.kind !== 'ranges') continue
|
|
459
|
+
for (const { lo, hi } of fs.ranges) {
|
|
460
|
+
for (let code = Math.max(0, lo); code <= Math.min(127, hi); code++) {
|
|
461
|
+
table[code] = i
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return table
|
|
466
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Combinator, ParseFail, ParseContext, ParseResult } from '../types.ts'
|
|
2
|
+
import { REC } from '../recovery/scan.ts'
|
|
3
|
+
import { createParseContext } from '../parse-context.ts'
|
|
4
|
+
|
|
5
|
+
/** Anything `completionsAt` can probe: an interpreter combinator OR a `compile()`d
|
|
6
|
+
* grammar. A grammar compiled with `{ recovery: true }` records the probe on its fast
|
|
7
|
+
* path, so completions work on the published compiled artifact — no interpreter needed. */
|
|
8
|
+
type CompletionsTarget =
|
|
9
|
+
| Combinator<unknown>
|
|
10
|
+
| { parseWithContext(input: string, ctx: ParseContext, pos?: number): ParseResult<unknown> }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the set of expected tokens at the given cursor offset.
|
|
14
|
+
* Useful for implementing IDE completions.
|
|
15
|
+
*
|
|
16
|
+
* Runs the parser on input truncated at `offset` with a probe that tracks the
|
|
17
|
+
* highest-position failure seen, even when sepBy/many backtrack past the cursor.
|
|
18
|
+
* Returns the expected tokens from that deepest failure.
|
|
19
|
+
*
|
|
20
|
+
* Works on the interpreter combinator OR a `compile(g, { recovery: true })` grammar
|
|
21
|
+
* (the probe is recorded on the compiled fast path). Returns an empty array when the
|
|
22
|
+
* input up to `offset` parses completely with no failures at or before the cursor.
|
|
23
|
+
*
|
|
24
|
+
* The returned strings use the same labels as ParseFail.expected:
|
|
25
|
+
* quoted literals like `"\"{\""` and regex patterns like `"/[0-9]+/"`.
|
|
26
|
+
*/
|
|
27
|
+
export function completionsAt(
|
|
28
|
+
target: CompletionsTarget,
|
|
29
|
+
input: string,
|
|
30
|
+
offset: number,
|
|
31
|
+
options: { tolerant?: boolean } = {},
|
|
32
|
+
): string[] {
|
|
33
|
+
const probe: { offset: number; best: ParseFail | null } = { offset, best: null }
|
|
34
|
+
// In tolerant mode, list recovery keeps the enclosing node parsing past a bad
|
|
35
|
+
// element so the failure at the cursor is actually recorded — the completion set
|
|
36
|
+
// is otherwise empty when a permissive top rule "succeeds" with an unconsumed tail.
|
|
37
|
+
const ctx = createParseContext()
|
|
38
|
+
ctx._probe = probe
|
|
39
|
+
if (options.tolerant) {
|
|
40
|
+
ctx._tolerant = true
|
|
41
|
+
ctx._rec = REC
|
|
42
|
+
}
|
|
43
|
+
const sliced = input.slice(0, offset)
|
|
44
|
+
const result = 'parseWithContext' in target
|
|
45
|
+
? target.parseWithContext(sliced, ctx, 0)
|
|
46
|
+
: target.parse(sliced, 0, ctx)
|
|
47
|
+
|
|
48
|
+
// If the parser consumed everything up to offset successfully, there is nothing
|
|
49
|
+
// to complete — the input is already valid at this position. In tolerant mode the
|
|
50
|
+
// parse "succeeds" (list recovery swallowed the incomplete element at the cursor),
|
|
51
|
+
// so the meaningful expectation lives in the deepest failure the probe recorded
|
|
52
|
+
// while recovering: return that instead of an empty set.
|
|
53
|
+
if (result.ok) return options.tolerant ? (probe.best?.expected ?? []) : []
|
|
54
|
+
|
|
55
|
+
// Use whichever failure (probe or top-level) sits at the deeper position.
|
|
56
|
+
const best = deeperFail(probe.best, result)
|
|
57
|
+
return best?.expected ?? []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function deeperFail(a: ParseFail | null, b: ParseFail | null): ParseFail | null {
|
|
61
|
+
if (!a) return b
|
|
62
|
+
if (!b) return a
|
|
63
|
+
return a.span.start >= b.span.start ? a : b
|
|
64
|
+
}
|