parseman 0.46.0 → 0.47.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 +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 +9929 -7465
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +9929 -7465
- 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 +10865 -8535
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10865 -8535
- 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 +40 -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 +9224 -7203
- 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 +9219 -7198
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +311 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +311 -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 +11146 -9593
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11146 -9593
- 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 +2898 -1623
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1623
- 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 +11375 -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 +11352 -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 +446 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +493 -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 +25 -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 +322 -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 +221 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +821 -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 +2994 -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 +1766 -0
- package/src/table/emit.ts +418 -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 +1672 -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 +459 -0
- package/src/table/program.ts +855 -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,322 @@
|
|
|
1
|
+
import type { CharRange, FirstSet, Combinator, ParserDef } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a NAMED cross-artifact rule reference (`g.Foo`) whose own thunk does not
|
|
5
|
+
* resolve — the shared-shape hole, bound by name at fuse time.
|
|
6
|
+
*
|
|
7
|
+
* A shape module compiles `Ratio: sequence(g.Value, …)` without defining `Value`, so
|
|
8
|
+
* that `lazy`'s thunk throws and every first-set through it degrades to `any`. Once
|
|
9
|
+
* the shape is FUSED with a dialect that defines `Value`, the hole is bound — pass a
|
|
10
|
+
* resolver over the fused winner map and the first-set is the real one.
|
|
11
|
+
*
|
|
12
|
+
* Only NAMED refs are resolvable: an unnamed `ref()` that was never `.define()`d
|
|
13
|
+
* carries no name for anyone to bind, so it stays `any`.
|
|
14
|
+
*/
|
|
15
|
+
export type RefResolver = (name: string) => Combinator<unknown> | undefined
|
|
16
|
+
|
|
17
|
+
/** The `resolve`-supplied target for an unresolvable NAMED lazy, if any. */
|
|
18
|
+
function resolveNamedRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
|
|
19
|
+
if (resolve === undefined) return undefined
|
|
20
|
+
const name = (p as unknown as { _ruleName?: string })._ruleName
|
|
21
|
+
return name === undefined ? undefined : resolve(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function union(a: FirstSet, b: FirstSet): FirstSet {
|
|
25
|
+
if (a.kind === 'any' || b.kind === 'any') return { kind: 'any' }
|
|
26
|
+
if (a.kind === 'empty') return b
|
|
27
|
+
if (b.kind === 'empty') return a
|
|
28
|
+
return { kind: 'ranges', ranges: mergeRanges([...a.ranges, ...b.ranges]) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function intersects(a: FirstSet, b: FirstSet): boolean {
|
|
32
|
+
if (a.kind === 'any' || b.kind === 'any') return true
|
|
33
|
+
if (a.kind === 'empty' || b.kind === 'empty') return false
|
|
34
|
+
for (const ra of a.ranges) {
|
|
35
|
+
for (const rb of b.ranges) {
|
|
36
|
+
if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function fromChar(code: number): FirstSet {
|
|
43
|
+
return { kind: 'ranges', ranges: [{ lo: code, hi: code }] }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* True when `combinator`'s first set admits the code point at `input[pos]` (or its
|
|
48
|
+
* first set is `any`). The runtime counterpart of codegen's `firstSetCond` guard —
|
|
49
|
+
* used by the interpreter's first-set fail-fast in `optional`/`many`/`attempt`/
|
|
50
|
+
* `node` to reject a doomed sub-parse before doing any setup. Returns `false` at EOF.
|
|
51
|
+
*/
|
|
52
|
+
export function startsFirstSet(combinator: Combinator<unknown>, input: string, pos: number): boolean {
|
|
53
|
+
const fs = combinator._meta.firstSet
|
|
54
|
+
if (fs.kind === 'any') return true
|
|
55
|
+
if (fs.kind === 'empty') return false
|
|
56
|
+
const code = input.codePointAt(pos)
|
|
57
|
+
if (code === undefined) return false
|
|
58
|
+
for (const r of fs.ranges) if (code >= r.lo && code <= r.hi) return true
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function fromRange(lo: number, hi: number): FirstSet {
|
|
63
|
+
return { kind: 'ranges', ranges: [{ lo, hi }] }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function any(): FirstSet {
|
|
67
|
+
return { kind: 'any' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function empty(): FirstSet {
|
|
71
|
+
return { kind: 'empty' }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Can this parser SUCCEED consuming zero characters (nullable / matches-empty)?
|
|
76
|
+
* Used to compute a sound sequence first-set: a nullable leading term lets the
|
|
77
|
+
* NEXT term's first chars start the whole sequence. MUST err toward `true` when
|
|
78
|
+
* unsure — over-estimating nullability only widens the (over-approximated) first
|
|
79
|
+
* set, which stays sound; under-estimating would drop valid start chars and make
|
|
80
|
+
* first-char dispatch skip a matching arm.
|
|
81
|
+
*/
|
|
82
|
+
export function matchesEmpty(
|
|
83
|
+
p: Combinator<unknown>,
|
|
84
|
+
seen: Set<Combinator<unknown>> = new Set(),
|
|
85
|
+
resolve?: RefResolver,
|
|
86
|
+
): boolean {
|
|
87
|
+
// Cycle guard: a mutually-nullable ref cycle (e.g. `A = oneOrMore(B); B = oneOrMore(A)`)
|
|
88
|
+
// would recurse forever. Treat a re-entered node as nullable — the safe (`true`)
|
|
89
|
+
// default, consistent with the err-toward-true contract below.
|
|
90
|
+
if (seen.has(p)) return true
|
|
91
|
+
seen.add(p)
|
|
92
|
+
const me = (c: Combinator<unknown>): boolean => matchesEmpty(c, seen, resolve)
|
|
93
|
+
const d = p._def as ParserDef
|
|
94
|
+
switch (d.tag) {
|
|
95
|
+
case 'literal': return d.value.length === 0
|
|
96
|
+
case 'keywords': return false
|
|
97
|
+
// The routed token itself is the selector's match, never empty; a fallback can be.
|
|
98
|
+
case 'routed': return d.fallback === undefined ? false : me(d.fallback)
|
|
99
|
+
case 'regex':
|
|
100
|
+
// Precise: does the pattern admit a zero-length match? (`a*`, `a?`, `a|`, …)
|
|
101
|
+
try { const m = new RegExp(d.source).exec(''); return m != null && m[0] === '' }
|
|
102
|
+
catch { return true }
|
|
103
|
+
case 'many':
|
|
104
|
+
case 'optional':
|
|
105
|
+
case 'not':
|
|
106
|
+
case 'peek': return true // zero repetitions / absent / lookahead
|
|
107
|
+
// Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
|
|
108
|
+
// Any `min >= 1` requires that many ITEMS, so it is nullable only when the
|
|
109
|
+
// item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
|
|
110
|
+
// nullable — safe, but wrong, and it put a bogus `nullable-prefix` note on
|
|
111
|
+
// the gating diagnostic for a list that can never match empty.)
|
|
112
|
+
case 'sepBy': return d.min >= 1 ? me(d.parser) : true
|
|
113
|
+
case 'oneOrMore': return me(d.parser)
|
|
114
|
+
case 'sequence': return d.parsers.every(me)
|
|
115
|
+
case 'choice':
|
|
116
|
+
return d.parsers.some(me)
|
|
117
|
+
case 'dispatch':
|
|
118
|
+
return me(d.selector)
|
|
119
|
+
case 'transform':
|
|
120
|
+
case 'label':
|
|
121
|
+
case 'trivia':
|
|
122
|
+
case 'token':
|
|
123
|
+
case 'leaf':
|
|
124
|
+
case 'expect':
|
|
125
|
+
case 'withCtx':
|
|
126
|
+
case 'node':
|
|
127
|
+
case 'grammar':
|
|
128
|
+
case 'recover': return me(d.parser)
|
|
129
|
+
case 'lazy':
|
|
130
|
+
try { return me(d.thunk()) }
|
|
131
|
+
catch { const t = resolveNamedRef(p, resolve); return t ? me(t) : true }
|
|
132
|
+
default: return true // scanTo / guard / unknown → assume nullable (safe)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A ZERO-WIDTH ASSERTION never consumes input, so it contributes NOTHING to a
|
|
138
|
+
* sequence's first-set — the first consumed char comes from the following
|
|
139
|
+
* non-nullable term. `not(X)` reports `firstSet: any()` (it cannot know what it
|
|
140
|
+
* forbids), which would otherwise poison a sequence's first-set to `any` and kill
|
|
141
|
+
* first-char dispatch of the whole arm. Skipping its contribution is SOUND: a
|
|
142
|
+
* first-set used for dispatch gating must stay a correct SUPERSET of the rule's
|
|
143
|
+
* true first chars, and `not(X) Y` can only start with a char in firstSet(Y) — the
|
|
144
|
+
* assertion only NARROWS the language (it forbids a full match ahead), it never
|
|
145
|
+
* widens the set of possible first chars beyond Y. So firstSet(Y) is a sound (and
|
|
146
|
+
* tighter) superset.
|
|
147
|
+
*
|
|
148
|
+
* The POSITIVE lookahead `peek(X)` is zero-width too, but it is NOT in this
|
|
149
|
+
* predicate: unlike `not`, it knows what it requires, so its first-set is a real
|
|
150
|
+
* constraint that must be INTERSECTED into the sequence's set rather than dropped
|
|
151
|
+
* (see `isPositiveLookahead` and `sequenceFirstSet`).
|
|
152
|
+
*/
|
|
153
|
+
export function isZeroWidthAssertion(p: Combinator<unknown>): boolean {
|
|
154
|
+
const tag = (p._def as ParserDef).tag
|
|
155
|
+
// `adjacency` joins `not` for the same reason: `adjacent()`/`notAdjacent()` are
|
|
156
|
+
// zero-width tests of the gap BEHIND the cursor, so they constrain nothing about
|
|
157
|
+
// the first char ahead and must not contribute their `any` to the sequence.
|
|
158
|
+
return tag === 'not' || tag === 'adjacency'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A POSITIVE zero-width assertion (`peek(X)`). It consumes nothing, so like
|
|
163
|
+
* `not(X)` it does not contribute a first char of its own — but it does REQUIRE
|
|
164
|
+
* that X match here, so `peek(X) Y` can only start with a char in
|
|
165
|
+
* firstSet(X) ∩ firstSet(Y). That intersection is what makes a leading `peek()`
|
|
166
|
+
* gate its choice arm; `not(not(X))`, the only previous spelling, reports `any()`
|
|
167
|
+
* and poisons the dispatch instead.
|
|
168
|
+
*
|
|
169
|
+
* Soundness: first-sets are SUPERSETS of the true first chars, and
|
|
170
|
+
* (A ⊇ a) ∧ (B ⊇ b) ⇒ A ∩ B ⊇ a ∩ b — so intersecting stays a superset and can
|
|
171
|
+
* never skip a real match. A NULLABLE body succeeds on the empty string and
|
|
172
|
+
* therefore constrains nothing; `peek()` reports `any()` in that case, which the
|
|
173
|
+
* intersection treats as "no constraint".
|
|
174
|
+
*/
|
|
175
|
+
export function isPositiveLookahead(p: Combinator<unknown>): boolean {
|
|
176
|
+
return (p._def as ParserDef).tag === 'peek'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Intersect a lookahead constraint into an accumulator; `any` = no constraint. */
|
|
180
|
+
function narrowBy(acc: FirstSet | null, constraint: FirstSet): FirstSet | null {
|
|
181
|
+
if (constraint.kind === 'any') return acc
|
|
182
|
+
if (acc === null) return constraint
|
|
183
|
+
if (acc.kind === 'any') return constraint
|
|
184
|
+
if (acc.kind === 'empty' || constraint.kind === 'empty') return { kind: 'empty' }
|
|
185
|
+
const ranges: CharRange[] = []
|
|
186
|
+
for (const a of acc.ranges) for (const b of constraint.ranges) {
|
|
187
|
+
const lo = Math.max(a.lo, b.lo)
|
|
188
|
+
const hi = Math.min(a.hi, b.hi)
|
|
189
|
+
if (lo <= hi) ranges.push({ lo, hi })
|
|
190
|
+
}
|
|
191
|
+
return ranges.length === 0 ? { kind: 'empty' } : { kind: 'ranges', ranges }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Apply the accumulated `peek()` constraints to a sequence's first-set. When the
|
|
196
|
+
* consuming terms contributed NOTHING (the sequence is all zero-width/nullable,
|
|
197
|
+
* e.g. a bare `peek(X)` arm), the assertion IS the first-set: the sequence can
|
|
198
|
+
* only succeed — even zero-width — where X matches.
|
|
199
|
+
*/
|
|
200
|
+
function applyAssertion(fs: FirstSet, assertion: FirstSet | null): FirstSet {
|
|
201
|
+
if (assertion === null) return fs
|
|
202
|
+
if (fs.kind === 'empty') return assertion
|
|
203
|
+
return narrowBy(fs, assertion) ?? fs
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* First-set of a sequence: union each term's first-set through the NULLABLE
|
|
208
|
+
* PREFIX — a leading `optional(…)` / `many(…)` / nullable term can be skipped, so
|
|
209
|
+
* the sequence can begin with a LATER term's first char. Stop at (and include)
|
|
210
|
+
* the first non-nullable term. (`parsers[0].firstSet` alone under-approximates
|
|
211
|
+
* and silently breaks first-char dispatch — see the InterpolatedSelector bug.)
|
|
212
|
+
* A leading zero-width assertion (`not(…)`) is nullable but contributes NOTHING to
|
|
213
|
+
* the first-set (see `isZeroWidthAssertion`) — its `any` must not poison the union.
|
|
214
|
+
*/
|
|
215
|
+
export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): FirstSet {
|
|
216
|
+
let fs: FirstSet = empty()
|
|
217
|
+
let assertion: FirstSet | null = null
|
|
218
|
+
for (const p of parsers) {
|
|
219
|
+
if (isPositiveLookahead(p)) {
|
|
220
|
+
// Zero-width but CONSTRAINING: intersect, keep scanning (it consumes nothing).
|
|
221
|
+
assertion = narrowBy(assertion, p._meta.firstSet)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet)
|
|
225
|
+
if (!matchesEmpty(p)) return applyAssertion(fs, assertion)
|
|
226
|
+
}
|
|
227
|
+
return applyAssertion(fs, assertion)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Deep first-set that RESOLVES `lazy`/`ref` combinators to their targets. The
|
|
232
|
+
* combinators bake `_meta.firstSet` at CONSTRUCTION, when a `ref()` still reads
|
|
233
|
+
* `any()` (define() never updates it) — so a `choice`/`sequence` built over refs
|
|
234
|
+
* caches a spuriously-`any` first-set and loses first-char dispatch. Recomputing
|
|
235
|
+
* here, following refs, recovers the real set. Over-approximates on cycles /
|
|
236
|
+
* unknown constructs (returns `any`) — always sound: a wider set only means "try
|
|
237
|
+
* this arm for more first chars", never skips a real match.
|
|
238
|
+
*
|
|
239
|
+
* SOUND ONLY where refs are FINAL (monolithic compile). Under compose OVERRIDE a
|
|
240
|
+
* referenced rule can be replaced with a WIDER first-set, so a baked deep set
|
|
241
|
+
* would wrongly skip valid input — the compose path defers dispatch to fuse time.
|
|
242
|
+
*
|
|
243
|
+
* `resolve` binds NAMED cross-artifact holes (`g.Foo`) against a fused winner map —
|
|
244
|
+
* see `RefResolver`. Diagnostic-only today: it is what lets the gating analysis ask
|
|
245
|
+
* the question at the site where the hole actually HAS an answer.
|
|
246
|
+
*/
|
|
247
|
+
export function firstSetOf(
|
|
248
|
+
p: Combinator<unknown>,
|
|
249
|
+
seen: Set<Combinator<unknown>> = new Set(),
|
|
250
|
+
resolve?: RefResolver,
|
|
251
|
+
): FirstSet {
|
|
252
|
+
if (seen.has(p)) return any() // cycle → any (safe over-approximation)
|
|
253
|
+
seen.add(p)
|
|
254
|
+
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve)
|
|
255
|
+
const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
|
|
256
|
+
const d = p._def as ParserDef
|
|
257
|
+
switch (d.tag) {
|
|
258
|
+
case 'literal':
|
|
259
|
+
case 'regex':
|
|
260
|
+
case 'keywords': return p._meta.firstSet // terminals: no refs, cached set is exact
|
|
261
|
+
case 'dispatch': return fs(d.selector)
|
|
262
|
+
case 'lazy':
|
|
263
|
+
try { return fs(d.thunk()) }
|
|
264
|
+
catch { const t = resolveNamedRef(p, resolve); return t ? fs(t) : any() }
|
|
265
|
+
case 'choice': {
|
|
266
|
+
let out: FirstSet = empty()
|
|
267
|
+
for (const arm of d.parsers) out = union(out, fs(arm))
|
|
268
|
+
return out
|
|
269
|
+
}
|
|
270
|
+
case 'sequence': {
|
|
271
|
+
// Union through the nullable prefix (a leading nullable term lets a later
|
|
272
|
+
// term's first chars start the sequence) — ref-resolving `sequenceFirstSet`.
|
|
273
|
+
// A leading zero-width assertion (`not`) contributes nothing (its `any` would
|
|
274
|
+
// poison the union) but is still nullable, so keep scanning past it. A
|
|
275
|
+
// positive `peek()` is zero-width AND constraining → intersect.
|
|
276
|
+
let out: FirstSet = empty()
|
|
277
|
+
let assertion: FirstSet | null = null
|
|
278
|
+
for (const term of d.parsers) {
|
|
279
|
+
if (isPositiveLookahead(term)) { assertion = narrowBy(assertion, fs(term)); continue }
|
|
280
|
+
if (!isZeroWidthAssertion(term)) out = union(out, fs(term))
|
|
281
|
+
if (!empties(term)) return applyAssertion(out, assertion)
|
|
282
|
+
}
|
|
283
|
+
return applyAssertion(out, assertion)
|
|
284
|
+
}
|
|
285
|
+
case 'peek': {
|
|
286
|
+
// Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
|
|
287
|
+
// shallow `_meta.firstSet` baked into the assertion would lose the gate.
|
|
288
|
+
const inner = d.parser
|
|
289
|
+
return empties(inner) ? any() : fs(inner)
|
|
290
|
+
}
|
|
291
|
+
case 'oneOrMore':
|
|
292
|
+
case 'many':
|
|
293
|
+
case 'optional':
|
|
294
|
+
case 'transform':
|
|
295
|
+
case 'label':
|
|
296
|
+
case 'trivia':
|
|
297
|
+
case 'token':
|
|
298
|
+
case 'leaf':
|
|
299
|
+
case 'node':
|
|
300
|
+
case 'grammar':
|
|
301
|
+
case 'expect': return fs(d.parser)
|
|
302
|
+
case 'sepBy': return fs(d.parser) // both min 0 and min 1 start with the item
|
|
303
|
+
default: return p._meta.firstSet // not / scanTo / guard / withCtx / recover / unknown
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mergeRanges(ranges: CharRange[]): CharRange[] {
|
|
308
|
+
if (ranges.length === 0) return []
|
|
309
|
+
const sorted = [...ranges].sort((a, b) => a.lo - b.lo)
|
|
310
|
+
// Always copy — never alias input objects
|
|
311
|
+
const out: CharRange[] = [{ lo: sorted[0]!.lo, hi: sorted[0]!.hi }]
|
|
312
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
313
|
+
const top = out[out.length - 1]!
|
|
314
|
+
const cur = sorted[i]!
|
|
315
|
+
if (cur.lo <= top.hi + 1) {
|
|
316
|
+
if (cur.hi > top.hi) top.hi = cur.hi
|
|
317
|
+
} else {
|
|
318
|
+
out.push({ lo: cur.lo, hi: cur.hi })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out
|
|
322
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zero-width state ASSERTION: succeeds (consuming nothing) only when `predicate`
|
|
5
|
+
* returns true for `ctx.state`. Fails otherwise.
|
|
6
|
+
*
|
|
7
|
+
* Intended for use inside sequence() to gate subsequent parsing on runtime
|
|
8
|
+
* context set with withCtx():
|
|
9
|
+
*
|
|
10
|
+
* const returnStmt = sequence(
|
|
11
|
+
* gate(ctx => (ctx as { inFn: boolean }).inFn),
|
|
12
|
+
* literal('return'), optional(expr)
|
|
13
|
+
* )
|
|
14
|
+
*
|
|
15
|
+
* Naming: this matches the `gate:` field on a gated CHOICE arm
|
|
16
|
+
* (`choice({ gate, combinator }, other)`). Use the arm FIELD to SELECT a branch by
|
|
17
|
+
* a cheap state predicate; use this COMBINATOR to ASSERT a state predicate
|
|
18
|
+
* mid-sequence. Formerly named `guard()` — see the deprecated alias in `guard.ts`.
|
|
19
|
+
*
|
|
20
|
+
* Note: like all state predicates its first-set is `any`, so a `gate(...)` as the
|
|
21
|
+
* LEADING term of a choice arm poisons that choice's first-char dispatch. Keep it
|
|
22
|
+
* after a concrete leading terminal.
|
|
23
|
+
*/
|
|
24
|
+
export function gate(predicate: (state: unknown) => boolean): Combinator<null> {
|
|
25
|
+
const meta: ParserMeta = {
|
|
26
|
+
firstSet: { kind: 'any' },
|
|
27
|
+
canMatchNewline: false,
|
|
28
|
+
isTrivia: false,
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
_tag: 'guard',
|
|
32
|
+
_meta: meta,
|
|
33
|
+
_def: { tag: 'guard', predicate },
|
|
34
|
+
parse(_input: string, pos: number, ctx: ParseContext): ParseResult<null> {
|
|
35
|
+
if (predicate(ctx.state))
|
|
36
|
+
return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
37
|
+
// The label is the PUBLIC name. It used to be `'guard'`, justified in a
|
|
38
|
+
// comment here as "parity with the compiled path" — which had not been true
|
|
39
|
+
// since the rename: codegen emitted `'gate'` and this emitted `'guard'`, so
|
|
40
|
+
// the same failing input reported a different expected set depending on
|
|
41
|
+
// which engine ran it. Nothing caught it because no test compared the two
|
|
42
|
+
// engines' expected sets on a state-scoped failure.
|
|
43
|
+
//
|
|
44
|
+
// `guard` was removed as a public name in 0.44.0, so it also pointed users
|
|
45
|
+
// at something they could no longer find. The def TAG stays `'guard'` —
|
|
46
|
+
// that is internal and the encoder switches on it.
|
|
47
|
+
return { ok: false, expected: ['gate'], span: { start: pos, end: pos } }
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParseFail, ParseError } from '../types.ts'
|
|
2
|
+
import { createLineIndex, recordLineRange, normalizeLineIndex, annotateSpan, type LineIndex } from '../line-index.ts'
|
|
3
|
+
import { markUnusedValues } from '../compiler/value-usage.ts'
|
|
4
|
+
import { triviaKindMask } from '../cst/trivia-kinds.ts'
|
|
5
|
+
import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
|
|
6
|
+
import { createParseContext } from '../parse-context.ts'
|
|
7
|
+
|
|
8
|
+
export type ParseOptions = {
|
|
9
|
+
trackLines?: boolean
|
|
10
|
+
/**
|
|
11
|
+
* Enable the error-collection channel. When true, recovery points (expect(), and
|
|
12
|
+
* tolerant list recovery) collect their ParseErrors into a side-channel array
|
|
13
|
+
* rather than only embedding them in the value tree. The returned ParseOk will
|
|
14
|
+
* have an `errors` field listing all recovered errors. Top-level parse failures
|
|
15
|
+
* (where nothing recovered) still return ParseFail as usual.
|
|
16
|
+
*/
|
|
17
|
+
recover?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ParserOptions = ParseOptions & {
|
|
21
|
+
/**
|
|
22
|
+
* Trivia parser for this scope. Three states:
|
|
23
|
+
* - a Combinator → skip this trivia between sequence/repeat terms
|
|
24
|
+
* - `undefined` → inherit the enclosing scope's trivia (no change)
|
|
25
|
+
* - `null` → CLEAR trivia: no trivia skipped, so terms must be contiguous.
|
|
26
|
+
* Re-enable inside a nested region with another parser({ trivia }).
|
|
27
|
+
*/
|
|
28
|
+
trivia?: Combinator<unknown> | null
|
|
29
|
+
/**
|
|
30
|
+
* Declare that this local trivia scope intentionally does not preserve
|
|
31
|
+
* selected root categories. Required only by
|
|
32
|
+
* `rootTrivia: { select }` when the scope does not use
|
|
33
|
+
* `classifiedTrivia()`.
|
|
34
|
+
*/
|
|
35
|
+
rootCapture?: 'opaque'
|
|
36
|
+
/** Record consumed trivia as CSTTrivia tokens in rawChildren. Default: skip. */
|
|
37
|
+
captureTrivia?: boolean
|
|
38
|
+
/**
|
|
39
|
+
* Restrict PER-NODE CST trivia capture to these trivia kinds (label names from
|
|
40
|
+
* the trivia's `label()` arms). Whitespace and any other unlisted kind is still
|
|
41
|
+
* skipped over but NOT recorded into a node's `triviaLog` — so a host that only
|
|
42
|
+
* consumes (say) comments pays nothing to log every whitespace run. The global
|
|
43
|
+
* trivia log is unaffected. Requires labeled trivia; ignored otherwise.
|
|
44
|
+
*/
|
|
45
|
+
captureTriviaKinds?: readonly string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ParsemanParser<T> extends Combinator<T> {
|
|
49
|
+
parse(input: string): ParseResult<T>
|
|
50
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function annotateResultLines<T, R extends ParseResult<T>>(result: R, index: LineIndex): R {
|
|
54
|
+
const annotated = { ...result, span: annotateSpan(result.span, index) } as R
|
|
55
|
+
const errors = (annotated as { errors?: ParseError[] }).errors
|
|
56
|
+
if (errors) {
|
|
57
|
+
;(annotated as { errors: ParseError[] }).errors = errors.map(error => ({
|
|
58
|
+
...error,
|
|
59
|
+
span: annotateSpan(error.span, index),
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
return annotated
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createParseLineContext(input: string, pos: number): { lineIndex: LineIndex; lineScannedTo: number } {
|
|
66
|
+
const lineIndex = createLineIndex()
|
|
67
|
+
if (pos > 0) recordLineRange(lineIndex, input, 0, pos)
|
|
68
|
+
return { lineIndex, lineScannedTo: pos }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The three pre-written `trackLines` resolvers. One is SELECTED at link time. */
|
|
72
|
+
const TRACK_LINES_ON = (): boolean => true
|
|
73
|
+
const TRACK_LINES_OFF = (): boolean => false
|
|
74
|
+
const TRACK_LINES_INHERIT = (ctx: ParseContext | undefined): boolean => ctx?.trackLines === true
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A trivia scope.
|
|
78
|
+
*
|
|
79
|
+
* ── NO RULE MAY CONSULT AN OPTION WHILE IT PARSES ───────────────────────────
|
|
80
|
+
*
|
|
81
|
+
* `opts` is fixed when `parser(opts, root)` is called. That is LINK TIME — the
|
|
82
|
+
* same moment `assemble.ts` resolves `RunCfg` into a choice of piece, and the
|
|
83
|
+
* same rule (`RunCfg`'s header, `scripts/check-invariants.mjs` INV-6/INV-7).
|
|
84
|
+
* Everything below used to be read INSIDE `parse`, which a nested scope enters
|
|
85
|
+
* on every visit, so a grammar with a `parser({ trivia })` region paid nine
|
|
86
|
+
* option reads per entry for answers that could not change.
|
|
87
|
+
*
|
|
88
|
+
* They are now resolved once, here, and the parse path reads only the resolved
|
|
89
|
+
* values. Where the answer genuinely depends on the CALLER's context — an
|
|
90
|
+
* inherited `trackLines`, inherited trivia labels — a pre-written variant is
|
|
91
|
+
* SELECTED at link time rather than branched on per entry. Selecting costs one
|
|
92
|
+
* closure per scope in the grammar; branching cost one test per entry per parse.
|
|
93
|
+
*
|
|
94
|
+
* `_ctx.trivia`, `_ctx.captureTrivia` and the CST sinks stay where they are:
|
|
95
|
+
* those are per-scope RUNTIME state that `node()` opens and closes mid-parse,
|
|
96
|
+
* not configuration. Resolving them early would be wrong, not fast.
|
|
97
|
+
*/
|
|
98
|
+
export function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanParser<T> {
|
|
99
|
+
const clearTrivia = opts.trivia === null
|
|
100
|
+
const opaqueRootCapture = opts.rootCapture === 'opaque'
|
|
101
|
+
if (opaqueRootCapture && opts.trivia === undefined) {
|
|
102
|
+
throw new TypeError('parser({ rootCapture: \'opaque\' }) requires an explicit trivia scope.')
|
|
103
|
+
}
|
|
104
|
+
/** This scope's own trivia, or `undefined` for cleared/inherited. */
|
|
105
|
+
const scopeTrivia: Combinator<unknown> | undefined = clearTrivia ? undefined : (opts.trivia ?? undefined)
|
|
106
|
+
/**
|
|
107
|
+
* `?._meta?.` and not `?._meta.`: the macro's own evaluator builds a
|
|
108
|
+
* `parser({ trivia: /re/ })` from source text, so a raw RegExp reaches here.
|
|
109
|
+
* The reads used to happen inside `parse`, where such a scope threw only if it
|
|
110
|
+
* ever ran; hoisting them to construction must not turn that into a build-time
|
|
111
|
+
* throw. (`test/unit/plugin-coverage.test.ts`, "anyValue edge forms".)
|
|
112
|
+
*/
|
|
113
|
+
const scopeLabels = scopeTrivia?._meta?.triviaKindLabels
|
|
114
|
+
/** Does this scope have to refuse an unclassified root scope? Link-time fact. */
|
|
115
|
+
const refuseUnclassified = scopeTrivia !== undefined
|
|
116
|
+
&& !scopeTrivia._meta?.rootTriviaClassified && !opaqueRootCapture
|
|
117
|
+
const forceCaptureTrivia = opts.captureTrivia === true
|
|
118
|
+
const trackLinesOf = opts.trackLines === true ? TRACK_LINES_ON
|
|
119
|
+
: opts.trackLines === false ? TRACK_LINES_OFF
|
|
120
|
+
: TRACK_LINES_INHERIT
|
|
121
|
+
/**
|
|
122
|
+
* The per-node capture mask, resolved as far as this scope can resolve it.
|
|
123
|
+
*
|
|
124
|
+
* With its own labelled trivia the mask is a constant and is computed once.
|
|
125
|
+
* With INHERITED labels it depends on the caller, so the link step selects the
|
|
126
|
+
* deriving variant instead — `undefined` here means "no mask to install".
|
|
127
|
+
*/
|
|
128
|
+
const captureKinds = clearTrivia ? undefined : opts.captureTriviaKinds
|
|
129
|
+
const ownCaptureMask = captureKinds === undefined ? undefined
|
|
130
|
+
: scopeLabels !== undefined ? triviaKindMask(scopeLabels, captureKinds)
|
|
131
|
+
: undefined
|
|
132
|
+
const inheritCaptureMask = captureKinds !== undefined && scopeLabels === undefined
|
|
133
|
+
return {
|
|
134
|
+
_tag: 'grammar',
|
|
135
|
+
_meta: {
|
|
136
|
+
...root._meta,
|
|
137
|
+
// Trivia classification describes this grammar's own active trivia
|
|
138
|
+
// scope. A directly nested grammar may have classified trivia of its
|
|
139
|
+
// own, but that must not advertise classification for this wrapper.
|
|
140
|
+
triviaKindLabels: undefined,
|
|
141
|
+
rootTriviaClassified: undefined,
|
|
142
|
+
...(opts.trivia?._meta?.triviaKindLabels ? { triviaKindLabels: opts.trivia._meta.triviaKindLabels } : {}),
|
|
143
|
+
...(opts.trivia?._meta?.rootTriviaClassified ? { rootTriviaClassified: true as const } : {}),
|
|
144
|
+
},
|
|
145
|
+
_def: {
|
|
146
|
+
tag: 'grammar',
|
|
147
|
+
parser: root as Combinator<unknown>,
|
|
148
|
+
triviaParser: scopeTrivia,
|
|
149
|
+
clearTrivia,
|
|
150
|
+
...(opaqueRootCapture ? { rootCapture: 'opaque' as const } : {}),
|
|
151
|
+
...(opts.captureTrivia ? { captureTrivia: true } : {}),
|
|
152
|
+
trackLines: opts.trackLines ?? false,
|
|
153
|
+
},
|
|
154
|
+
parse(input: string, pos?: number, _ctx?: ParseContext): ParseResult<T> {
|
|
155
|
+
if (refuseUnclassified) refuseUnclassifiedRootScope(_ctx?._rootTriviaStrictScopes)
|
|
156
|
+
const trackLines = trackLinesOf(_ctx)
|
|
157
|
+
const lineContext = trackLines && _ctx?._lineIndex === undefined && _ctx?._lineStarts === undefined
|
|
158
|
+
? createParseLineContext(input, pos ?? 0)
|
|
159
|
+
: undefined
|
|
160
|
+
const lineIndex = trackLines
|
|
161
|
+
? (_ctx?._lineIndex ?? (_ctx?._lineStarts ? { lineStarts: _ctx._lineStarts } : lineContext!.lineIndex))
|
|
162
|
+
: undefined
|
|
163
|
+
// Preserve any CST collectors / capture flag from the caller (e.g. an
|
|
164
|
+
// enclosing node()), layering this grammar's trivia on top. Without this,
|
|
165
|
+
// a parser() nested inside a node() would drop the node's child collectors.
|
|
166
|
+
// Inheriting copy, then STORES — see `createParseContext`. `{ ..._ctx }`
|
|
167
|
+
// preserves the canonical shape because `_ctx` already has it, and the
|
|
168
|
+
// overrides below only touch slots that already exist.
|
|
169
|
+
const ctx: ParseContext = _ctx === undefined ? createParseContext() : { ..._ctx }
|
|
170
|
+
ctx.trackLines = trackLines
|
|
171
|
+
if (lineIndex) ctx._lineIndex = lineIndex
|
|
172
|
+
if (lineContext) ctx._lineScannedTo = lineContext.lineScannedTo
|
|
173
|
+
// trivia: null clears (contiguous terms); a Combinator sets; undefined inherits.
|
|
174
|
+
if (clearTrivia) {
|
|
175
|
+
ctx.trivia = undefined
|
|
176
|
+
ctx.triviaKindLabels = undefined
|
|
177
|
+
} else if (scopeTrivia !== undefined) {
|
|
178
|
+
ctx.trivia = scopeTrivia
|
|
179
|
+
if (scopeLabels) ctx.triviaKindLabels = scopeLabels
|
|
180
|
+
}
|
|
181
|
+
if (forceCaptureTrivia || _ctx?.captureTrivia) ctx.captureTrivia = true
|
|
182
|
+
// Kind-filter for per-node capture. Resolve against this scope's trivia
|
|
183
|
+
// labels — this parser's own trivia if it declares one, else the INHERITED
|
|
184
|
+
// labels (`_ctx.triviaKindLabels`), so captureTriviaKinds still applies when
|
|
185
|
+
// trivia is inherited rather than re-declared here. No labels → undefined
|
|
186
|
+
// (capture all).
|
|
187
|
+
if (ownCaptureMask !== undefined) ctx._triviaCaptureMask = ownCaptureMask
|
|
188
|
+
else if (inheritCaptureMask) ctx._triviaCaptureMask = triviaKindMask(_ctx?.triviaKindLabels, captureKinds)
|
|
189
|
+
// This is a property of the local trivia scope, not a post-parse filter:
|
|
190
|
+
// nested recognition can still skip its trivia, but selected root rows
|
|
191
|
+
// must never be written for an explicitly opaque region.
|
|
192
|
+
if (opaqueRootCapture) ctx._rootTriviaCapture = false
|
|
193
|
+
const result = root.parse(input, pos ?? 0, ctx)
|
|
194
|
+
if (trackLines && _ctx && ctx._lineScannedTo !== undefined) {
|
|
195
|
+
_ctx._lineScannedTo = Math.max(_ctx._lineScannedTo ?? 0, ctx._lineScannedTo)
|
|
196
|
+
}
|
|
197
|
+
return lineIndex ? annotateResultLines(result, normalizeLineIndex(lineIndex)) : result
|
|
198
|
+
},
|
|
199
|
+
} as ParsemanParser<T>
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Run `root` with the active trivia cleared — no trivia is skipped between its
|
|
204
|
+
* sequence/repeat terms, so they must be contiguous in the input. (Trivia is
|
|
205
|
+
* whatever parser({ trivia }) installed — often whitespace/comments, but it is
|
|
206
|
+
* grammar-defined.) The inverse of parser({ trivia }); re-enable trivia in a
|
|
207
|
+
* nested region with another parser({ trivia }).
|
|
208
|
+
*
|
|
209
|
+
* Wrap the WHOLE contiguous run: an enclosing sequence skips trivia *before* a
|
|
210
|
+
* term runs, so wrapping just the inner part would let leading trivia through.
|
|
211
|
+
*
|
|
212
|
+
* For a static glued token just use one literal/regex; reach for noTrivia when a
|
|
213
|
+
* glued part is a structured sub-rule — e.g. a head glued to a `[subscript]`
|
|
214
|
+
* whose interior still allows trivia. Turn trivia back on for a region by nesting
|
|
215
|
+
* another parser({ trivia }) (innermost wins, reverts on exit); put the WHOLE
|
|
216
|
+
* spaced region — including its leading `[` — inside it, since sequence skips
|
|
217
|
+
* trivia only between terms, not before its first:
|
|
218
|
+
*
|
|
219
|
+
* // `arr[i + 1]` — `arr` touches `[`, but the subscript is a spaced expr:
|
|
220
|
+
* noTrivia(sequence(name,
|
|
221
|
+
* parser({ trivia: ws }, sequence(literal('['), expr, literal(']')))))
|
|
222
|
+
*/
|
|
223
|
+
export function noTrivia<T>(root: Combinator<T>): ParsemanParser<T> {
|
|
224
|
+
return parser({ trivia: null }, root)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Roots already run through dead-value analysis, so a hot parse loop doesn't
|
|
228
|
+
// re-walk the tree. (A `rules()` grammar was analyzed at build; a bare combinator
|
|
229
|
+
// passed straight to parse() is analyzed once here.)
|
|
230
|
+
const _analyzed = new WeakSet<Combinator<unknown>>()
|
|
231
|
+
|
|
232
|
+
export function parse<T>(
|
|
233
|
+
combinator: Combinator<T>,
|
|
234
|
+
input: string,
|
|
235
|
+
opts: ParseOptions = {}
|
|
236
|
+
): ParseResult<T> {
|
|
237
|
+
if (!_analyzed.has(combinator)) {
|
|
238
|
+
_analyzed.add(combinator)
|
|
239
|
+
markUnusedValues(combinator)
|
|
240
|
+
}
|
|
241
|
+
const trackLines = opts.trackLines ?? combinator._meta.grammarTrackLines ?? false
|
|
242
|
+
const lineContext = trackLines ? createParseLineContext(input, 0) : undefined
|
|
243
|
+
const lineIndex = lineContext?.lineIndex
|
|
244
|
+
const _errors = opts.recover ? [] : undefined
|
|
245
|
+
// In recovery mode also track the furthest-position failure, so the caller can
|
|
246
|
+
// report "where it got stuck + what was expected" even when a permissive top
|
|
247
|
+
// rule succeeds with unconsumed trailing input. Off by default (it adds
|
|
248
|
+
// bookkeeping to every failed alternative).
|
|
249
|
+
const _probe = opts.recover ? { offset: input.length, best: null as ParseFail | null } : undefined
|
|
250
|
+
// Grammar-level ambient trivia declared via rules({ trivia }, factory): install
|
|
251
|
+
// it as ctx.trivia so it's ambient (interpreter). parser/noTrivia override locally.
|
|
252
|
+
const grammarTrivia = combinator._meta.grammarTrivia
|
|
253
|
+
const grammarScanSkip = combinator._meta.grammarScanSkip
|
|
254
|
+
// ONE shape — see `createParseContext`. Stores, not conditional spreads.
|
|
255
|
+
const ctx = createParseContext()
|
|
256
|
+
ctx.trackLines = trackLines
|
|
257
|
+
if (lineIndex) ctx._lineIndex = lineIndex
|
|
258
|
+
if (lineContext) ctx._lineScannedTo = lineContext.lineScannedTo
|
|
259
|
+
if (grammarTrivia !== undefined) {
|
|
260
|
+
ctx.trivia = grammarTrivia
|
|
261
|
+
if (grammarTrivia._meta.triviaKindLabels) ctx.triviaKindLabels = grammarTrivia._meta.triviaKindLabels
|
|
262
|
+
}
|
|
263
|
+
if (grammarScanSkip !== undefined) ctx.scanSkip = grammarScanSkip
|
|
264
|
+
if (_errors !== undefined) ctx._errors = _errors
|
|
265
|
+
if (_probe !== undefined) ctx._probe = _probe
|
|
266
|
+
const result = combinator.parse(input, 0, ctx)
|
|
267
|
+
if (!result.ok) return lineIndex ? annotateResultLines(result, normalizeLineIndex(lineIndex)) : result
|
|
268
|
+
const withErrors = _errors !== undefined
|
|
269
|
+
? { ...result, errors: _errors, furthestFail: _probe?.best ?? null }
|
|
270
|
+
: result
|
|
271
|
+
return lineIndex ? annotateResultLines(withErrors, normalizeLineIndex(lineIndex)) : withErrors
|
|
272
|
+
}
|