parseman 0.45.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-gate.d.ts +158 -0
- package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
- package/dist/analysis/choice-cost-render.d.ts +96 -0
- package/dist/analysis/choice-cost-render.d.ts.map +1 -0
- package/dist/analysis/choice-cost.d.ts +383 -0
- package/dist/analysis/choice-cost.d.ts.map +1 -0
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/corpus.d.ts +73 -0
- package/dist/analysis/corpus.d.ts.map +1 -0
- package/dist/analysis/diagnose-render.d.ts +65 -0
- package/dist/analysis/diagnose-render.d.ts.map +1 -0
- package/dist/analysis/diagnose.d.ts +15 -0
- package/dist/analysis/diagnose.d.ts.map +1 -1
- package/dist/analysis/diagnostics.cjs +13736 -0
- package/dist/analysis/diagnostics.cjs.map +6 -0
- package/dist/analysis/diagnostics.d.ts +27 -0
- package/dist/analysis/diagnostics.d.ts.map +1 -0
- package/dist/analysis/diagnostics.js +13713 -0
- package/dist/analysis/diagnostics.js.map +6 -0
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix-render.d.ts +43 -0
- package/dist/analysis/fix-render.d.ts.map +1 -0
- package/dist/analysis/fix.d.ts +172 -0
- package/dist/analysis/fix.d.ts.map +1 -0
- 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 +47 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts +61 -0
- package/dist/analysis/rebuild.d.ts.map +1 -0
- package/dist/analysis/terminal.d.ts +145 -0
- package/dist/analysis/terminal.d.ts.map +1 -0
- package/dist/cli/index.cjs +14837 -0
- package/dist/cli/index.cjs.map +6 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +14814 -0
- package/dist/cli/index.js.map +6 -0
- 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/build-arity.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 +79 -47
- 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 +10499 -7478
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +24 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10499 -7478
- 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 +41 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/free-identifiers.d.ts +23 -0
- package/dist/plugin/free-identifiers.d.ts.map +1 -0
- package/dist/plugin/index.cjs +11381 -9442
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11381 -9442
- package/dist/plugin/index.js.map +3 -4
- package/dist/plugin/reducer-resolver.d.ts +17 -1
- package/dist/plugin/reducer-resolver.d.ts.map +1 -1
- 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 -1622
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1622
- 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 +41 -3
- 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/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,393 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
import { literal } from './literal.ts'
|
|
3
|
+
import { regex } from './regex.ts'
|
|
4
|
+
import { sequence } from './sequence.ts'
|
|
5
|
+
import { choice } from './choice.ts'
|
|
6
|
+
import { many } from './repeat.ts'
|
|
7
|
+
import { transform } from './map.ts'
|
|
8
|
+
import { expect } from './expect.ts'
|
|
9
|
+
import { createDetachedParseContext } from '../parse-context.ts'
|
|
10
|
+
import { any } from './first-set.ts'
|
|
11
|
+
import { ref } from './ref.ts'
|
|
12
|
+
import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
|
|
13
|
+
import { token } from './token.ts'
|
|
14
|
+
import { recordLineRangeFromContext } from '../line-index.ts'
|
|
15
|
+
|
|
16
|
+
export type ScanToOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Per-call opaque-unit skippers (balanced parens/brackets, dialect
|
|
19
|
+
* interpolation, …). These EXTEND the grammar-level ambient default: ambient
|
|
20
|
+
* trivia (comments/ws) and ambient `scanSkip` (strings) are applied too, unless
|
|
21
|
+
* `raw`. So a site only needs to list the extra units its scan requires.
|
|
22
|
+
*/
|
|
23
|
+
skip?: Combinator<unknown>[]
|
|
24
|
+
/**
|
|
25
|
+
* Hard opt-out: skip NOTHING ambiently — no trivia, no scanSkip, and (with no
|
|
26
|
+
* per-call `skip`) a pure raw byte-walk. For the rare site that intends to scan
|
|
27
|
+
* literally through comments/strings.
|
|
28
|
+
*/
|
|
29
|
+
raw?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* If true, reaching EOF without finding the sentinel is a success — returns
|
|
32
|
+
* everything consumed so far. Default false (fail at EOF).
|
|
33
|
+
*/
|
|
34
|
+
orEOF?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* `balanced()` only. Make an unmatched close a genuine FAILURE instead of a
|
|
37
|
+
* recovered one.
|
|
38
|
+
*
|
|
39
|
+
* By default `balanced()` wraps its close in `expect()`, which never fails: on
|
|
40
|
+
* a miss it returns a ParseError, records it on `ctx._errors`, and reports a
|
|
41
|
+
* zero-width span. So the combinator is UNFAILABLE once its opener is consumed
|
|
42
|
+
* — the rejection is computed and recorded, but a caller cannot branch on it.
|
|
43
|
+
* `choice()` cannot fall through to another arm, `not()` cannot negate it, and
|
|
44
|
+
* a `sequence()` around it proceeds as if the group closed.
|
|
45
|
+
*
|
|
46
|
+
* With `strict: true` the close is required, so an unmatched or missing close
|
|
47
|
+
* fails the whole group and rolls back to the opener — the ordinary combinator
|
|
48
|
+
* contract. Nested groups inherit strictness (the interior recurses into the
|
|
49
|
+
* same combinator).
|
|
50
|
+
*
|
|
51
|
+
* Opt-in: the default is unchanged, because recovery is what a tolerant
|
|
52
|
+
* document parse wants, and existing grammars are built on it.
|
|
53
|
+
*/
|
|
54
|
+
strict?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the effective ordered skipper list for a scan, folding grammar-level
|
|
59
|
+
* ambient trivia + scanSkip in FRONT of the per-call `skip` (explicit skip
|
|
60
|
+
* EXTENDS the ambient default). Shared by the interpreter `scanTo`/`balanced`;
|
|
61
|
+
* the compiled path bakes the identical list in codegen.
|
|
62
|
+
*
|
|
63
|
+
* raw → [] (no trivia, no scanSkip, no skip)
|
|
64
|
+
* else → [ ...trivia?, ...scanSkip?, ...skip ]
|
|
65
|
+
*
|
|
66
|
+
* The ambient trivia (comments/ws) leads so a sentinel hidden in a comment is
|
|
67
|
+
* never matched, then ambient strings, then the site's extra units. The sentinel
|
|
68
|
+
* itself is still checked before any skipper, so a sentinel that also starts a
|
|
69
|
+
* skip region wins (unchanged priority).
|
|
70
|
+
*/
|
|
71
|
+
export function resolveScanSkip(
|
|
72
|
+
explicitSkip: Combinator<unknown>[],
|
|
73
|
+
raw: boolean,
|
|
74
|
+
ctx: ParseContext,
|
|
75
|
+
): Combinator<unknown>[] {
|
|
76
|
+
if (raw) return []
|
|
77
|
+
const trivia = ctx.trivia
|
|
78
|
+
const ambient = ctx.scanSkip
|
|
79
|
+
if (!trivia && !ambient) return explicitSkip
|
|
80
|
+
const out: Combinator<unknown>[] = []
|
|
81
|
+
if (trivia) out.push(trivia)
|
|
82
|
+
if (ambient) out.push(...ambient)
|
|
83
|
+
out.push(...explicitSkip)
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Consume input up to (but not including) the sentinel, skipping over any
|
|
89
|
+
* "hole" patterns in order so their contents are never mistaken for the sentinel.
|
|
90
|
+
*
|
|
91
|
+
* Returns the consumed text as a string. The sentinel is NOT consumed.
|
|
92
|
+
* Fails if the sentinel is never found (unless orEOF is true).
|
|
93
|
+
*
|
|
94
|
+
* const selector = scanTo(literal('{'), {
|
|
95
|
+
* skip: [cssComment, stringLit, balanced('(', ')'), balanced('[', ']')],
|
|
96
|
+
* })
|
|
97
|
+
*/
|
|
98
|
+
export function scanTo(
|
|
99
|
+
sentinel: Combinator<unknown>,
|
|
100
|
+
{ skip, raw = false, orEOF = false }: ScanToOptions = {},
|
|
101
|
+
): Combinator<string> {
|
|
102
|
+
const meta: ParserMeta = {
|
|
103
|
+
firstSet: any(),
|
|
104
|
+
canMatchNewline: true,
|
|
105
|
+
isTrivia: false,
|
|
106
|
+
}
|
|
107
|
+
const explicitSkip = skip ?? []
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
_tag: 'scanTo',
|
|
111
|
+
_meta: meta,
|
|
112
|
+
_def: { tag: 'scanTo', sentinel, skip: explicitSkip, raw, orEOF },
|
|
113
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
|
|
114
|
+
let cur = pos
|
|
115
|
+
// Fold grammar-level ambient trivia + scanSkip into the effective skippers.
|
|
116
|
+
const skip = resolveScanSkip(explicitSkip, raw, ctx)
|
|
117
|
+
|
|
118
|
+
// Sentinel checks and skip scans must not emit CST children of their own —
|
|
119
|
+
// scanTo represents the whole scanned span as one leaf. Probe them with a
|
|
120
|
+
// collector-free context so their internal literal()/regex() don't push.
|
|
121
|
+
// The error channel IS forwarded, so a committed skipper (e.g. balanced()
|
|
122
|
+
// whose open delimiter was consumed) can still report an unmatched close.
|
|
123
|
+
const probeCtx = createDetachedParseContext(false, ctx.state)
|
|
124
|
+
probeCtx._errors = ctx._errors
|
|
125
|
+
|
|
126
|
+
// Record the scanned text as a CSTLeaf so buildNode-driven grammars can
|
|
127
|
+
// see it in children/rawChildren (it would otherwise be lost — only the
|
|
128
|
+
// returned value carries it). Skipped when no collector is active.
|
|
129
|
+
const emit = (end: number) => {
|
|
130
|
+
if (end > pos && cstCaptureActive(ctx)) {
|
|
131
|
+
const leaf = { _tag: 'leaf', value: input.slice(pos, end), span: { start: pos, end } }
|
|
132
|
+
pushCstLeaf(ctx, leaf)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
while (cur < input.length) {
|
|
137
|
+
// Check sentinel — if it matches here, stop and return consumed text.
|
|
138
|
+
const s = sentinel.parse(input, cur, probeCtx)
|
|
139
|
+
if (s.ok) {
|
|
140
|
+
if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
|
|
141
|
+
emit(cur)
|
|
142
|
+
return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Try each skipper in order; take first that advances.
|
|
146
|
+
let advanced = false
|
|
147
|
+
for (const skipper of skip) {
|
|
148
|
+
const r = skipper.parse(input, cur, probeCtx)
|
|
149
|
+
if (r.ok && r.span.end > cur) {
|
|
150
|
+
cur = r.span.end
|
|
151
|
+
advanced = true
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Nothing matched — consume one character and continue.
|
|
157
|
+
if (!advanced) cur++
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reached EOF without finding sentinel.
|
|
161
|
+
if (orEOF) {
|
|
162
|
+
if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
|
|
163
|
+
emit(cur)
|
|
164
|
+
return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
|
|
165
|
+
}
|
|
166
|
+
const sentDef = sentinel._def
|
|
167
|
+
const expected = sentDef.tag === 'literal' ? [JSON.stringify(sentDef.value)] : ['sentinel']
|
|
168
|
+
return { ok: false, expected, span: { start: pos, end: cur } }
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Codegen marker: a balanced combinator that must re-resolve ambient scanSkip
|
|
174
|
+
* into its interior at emit time (the compiled mirror of the interpreter wrapper). */
|
|
175
|
+
export type BalancedAmbient = Combinator<string> & {
|
|
176
|
+
_balancedAmbient?: { open: string; close: string; ownSkip: Combinator<unknown>[]; strict?: boolean }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Marker: this combinator IS a balanced interior — its delimiters, and the fact
|
|
181
|
+
* that everything between them is a scan rather than authored structure.
|
|
182
|
+
*
|
|
183
|
+
* Set by `buildBalancedInterior`, so EVERY balanced carries it: the ambient one,
|
|
184
|
+
* the `raw` one, and each interior the ambient cache rebuilds. `_balancedAmbient`
|
|
185
|
+
* cannot serve this purpose — it is absent on `raw`, and it means "re-resolve
|
|
186
|
+
* ambient scanSkip", which is a different claim.
|
|
187
|
+
*
|
|
188
|
+
* Read by the spec/railroad emitter, which renders a balanced as its delimiters
|
|
189
|
+
* around an opaque interior. That is not a simplification: the delimiters are
|
|
190
|
+
* FIXED at construction and the interior genuinely is a delimiter scan, so the
|
|
191
|
+
* rendering states exactly what the construct is. Expanding the lowered shape
|
|
192
|
+
* instead would print the content-run regex and the `self` back-edge, which are
|
|
193
|
+
* emitter machinery, not language.
|
|
194
|
+
*/
|
|
195
|
+
export type BalancedMarked = Combinator<string> & {
|
|
196
|
+
_balanced?: { open: string; close: string }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The table lowering's marker, on the object `balanced()` RETURNS.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately not `_balancedAmbient`, and deliberately on the OUTER combinator.
|
|
203
|
+
* `_balancedAmbient` means "rebuild my interior with the ambient scanSkip", which
|
|
204
|
+
* is false for `raw`, and it sits on the INNER combinator because that is the one
|
|
205
|
+
* codegen's dedup and the interior `self` back-edge address. Reading `_def` (or
|
|
206
|
+
* the ambient marker) off the outer object gets the wrong thing — a structural
|
|
207
|
+
* encoder made exactly that mistake and lowered the wrong parser, silently.
|
|
208
|
+
*
|
|
209
|
+
* This marker instead records the CONSTRUCTOR ARGUMENTS, so an encoder can carry
|
|
210
|
+
* a `balanced()` as data and let `balanced()` itself rebuild it. It is present on
|
|
211
|
+
* every `balanced()`, `raw` included.
|
|
212
|
+
*/
|
|
213
|
+
export type BalancedSpec = Combinator<string> & {
|
|
214
|
+
_balancedSpec?: {
|
|
215
|
+
open: string
|
|
216
|
+
close: string
|
|
217
|
+
ownSkip: Combinator<unknown>[]
|
|
218
|
+
strict: boolean
|
|
219
|
+
raw: boolean
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function markSpec(
|
|
224
|
+
outer: Combinator<string>,
|
|
225
|
+
open: string, close: string, ownSkip: Combinator<unknown>[], strict: boolean, raw: boolean,
|
|
226
|
+
): Combinator<string> {
|
|
227
|
+
;(outer as BalancedSpec)._balancedSpec = { open, close, ownSkip, strict, raw }
|
|
228
|
+
return outer
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Match a balanced open/close pair, skipping over any holes inside.
|
|
233
|
+
* Returns the full matched text including delimiters.
|
|
234
|
+
*
|
|
235
|
+
* const parenGroup = balanced('(', ')', { skip: [comment, stringLit] })
|
|
236
|
+
*
|
|
237
|
+
* With no per-call `skip`, a balanced under a grammar that declares
|
|
238
|
+
* `rules({ scanSkip })` consults that ambient opaque-unit set in its INTERIOR too,
|
|
239
|
+
* so a delimiter hidden inside a string/bracket run never closes the balance
|
|
240
|
+
* early — the same footgun closure `scanTo` gets. `raw: true` opts out.
|
|
241
|
+
*
|
|
242
|
+
* By DEFAULT an unmatched close is RECOVERED, not failed: the close is wrapped in
|
|
243
|
+
* `expect()`, so the group always succeeds and records a ParseError instead. Pass
|
|
244
|
+
* `strict: true` to make it fail and roll back — see `ScanToOptions.strict`.
|
|
245
|
+
*/
|
|
246
|
+
export function balanced(
|
|
247
|
+
open: string,
|
|
248
|
+
close: string,
|
|
249
|
+
options: ScanToOptions = {},
|
|
250
|
+
): Combinator<string> {
|
|
251
|
+
const ownSkip = options.skip ?? []
|
|
252
|
+
const strict = options.strict ?? false
|
|
253
|
+
const combi = buildBalancedInterior(open, close, ownSkip, strict)
|
|
254
|
+
// `raw` keeps the pre-ambient behavior: the eager interior (per-call skip only).
|
|
255
|
+
if (options.raw) return markSpec(token(combi), open, close, ownSkip, strict, true)
|
|
256
|
+
|
|
257
|
+
// Ambient-aware in place — the returned combinator KEEPS its identity (its own
|
|
258
|
+
// interior `self` ref points back to it, and ir-serialize / codegen dedup rely
|
|
259
|
+
// on that), so we override `parse` rather than wrapping. `_def`/`_meta` are
|
|
260
|
+
// unchanged, so every static analysis sees the eager interior; only PARSING and
|
|
261
|
+
// codegen EMIT re-resolve `ctx.scanSkip`. Interiors are cached by the ambient
|
|
262
|
+
// array's identity (stable per grammar). Balanced consults ambient `scanSkip`
|
|
263
|
+
// (opaque units) but NOT trivia — its delimiters are structural and its content
|
|
264
|
+
// regex already spans whitespace, so adding trivia would perturb every existing
|
|
265
|
+
// balanced; the footgun is a delimiter hidden in opaque-unit content.
|
|
266
|
+
const eagerParse = combi.parse.bind(combi)
|
|
267
|
+
const cache = new Map<readonly Combinator<unknown>[], Combinator<string>>()
|
|
268
|
+
;(combi as BalancedAmbient)._balancedAmbient = { open, close, ownSkip, strict }
|
|
269
|
+
combi.parse = (input: string, pos: number, ctx: ParseContext): ParseResult<string> => {
|
|
270
|
+
const amb = ctx.scanSkip
|
|
271
|
+
if (!amb || amb.length === 0) return eagerParse(input, pos, ctx)
|
|
272
|
+
let interior = cache.get(amb)
|
|
273
|
+
if (!interior) {
|
|
274
|
+
interior = buildBalancedInterior(open, close, [...amb, ...ownSkip], strict)
|
|
275
|
+
cache.set(amb, interior)
|
|
276
|
+
}
|
|
277
|
+
return interior.parse(input, pos, ctx)
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* ONE LEAF, like `scanTo` — the sibling in this file that has always got this
|
|
281
|
+
* right.
|
|
282
|
+
*
|
|
283
|
+
* `balanced` is declared `Combinator<string>` and its interior callback
|
|
284
|
+
* reassembles the whole match into exactly that one string. But the interior is
|
|
285
|
+
* spelled `transform(sequence(literal(open), many(...), expect(literal(close))))`
|
|
286
|
+
* and `transform` is TRANSPARENT to CST capture, so the reassembled string never
|
|
287
|
+
* reached the parent's `children`: `balanced('(', ')')` over `"(a(b)c)"`
|
|
288
|
+
* contributed SEVEN children while `scanTo` in the same file contributed one.
|
|
289
|
+
* The declared type and the emitted arity disagreed, and nothing checked.
|
|
290
|
+
*
|
|
291
|
+
* `token()` is the fix rather than a bespoke wrapper because both engines
|
|
292
|
+
* already understand `tag: 'token'` — the interpreter suppresses the interior
|
|
293
|
+
* collectors and pushes one leaf, and codegen emits the mirror. Identity is
|
|
294
|
+
* preserved where it matters: `_balancedAmbient` and the interior `self`
|
|
295
|
+
* back-edge stay on the INNER combinator, which is what codegen's ambient
|
|
296
|
+
* `scanSkip` rebuild reads and what nested opens recurse into — so a nested
|
|
297
|
+
* balanced still contributes nothing of its own, and only the outermost match
|
|
298
|
+
* becomes a leaf.
|
|
299
|
+
*
|
|
300
|
+
* This is the same rule the separated-list change enforces: a combinator may
|
|
301
|
+
* collapse only what its construction makes recoverable. `balanced`'s
|
|
302
|
+
* delimiters are FIXED at construction, so collapsing to one string is
|
|
303
|
+
* legitimate here — which is exactly why it must actually collapse.
|
|
304
|
+
*/
|
|
305
|
+
return markSpec(token(combi), open, close, ownSkip, strict, false)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Build a balanced open/close interior for a FIXED skip set (no ambient). */
|
|
309
|
+
export function buildBalancedInterior(
|
|
310
|
+
open: string,
|
|
311
|
+
close: string,
|
|
312
|
+
skips: Combinator<unknown>[],
|
|
313
|
+
strict = false,
|
|
314
|
+
): Combinator<string> {
|
|
315
|
+
// The interior scan must skip NESTED same-delimiter pairs so depth is counted —
|
|
316
|
+
// otherwise `{{x}}` stops at the first `}`. `self` references this balanced
|
|
317
|
+
// combinator; added to the interior scan's skip list, a nested `open` is
|
|
318
|
+
// consumed intact (recursively) before the scan looks for the matching `close`.
|
|
319
|
+
const self = ref<string>()
|
|
320
|
+
// PREDICTIVE interior — no char-walk. The body is `many(choice(self, …skips,
|
|
321
|
+
// contentRun))`, where contentRun is a regex of chars that are NOT this pair's
|
|
322
|
+
// delimiters and NOT the start of any skip (so a string/comment arm still wins
|
|
323
|
+
// its position). At any other character — a stray close, or a *different* bracket
|
|
324
|
+
// type — no arm matches, `many` stops, and the required close (expect()) reports
|
|
325
|
+
// "expected <close>". So an unmatched open, a cross-type close `(a]`, and a stray
|
|
326
|
+
// close all surface as errors; nothing is silently swallowed. Well-formed input
|
|
327
|
+
// is consumed identically.
|
|
328
|
+
const stop = new Set<string>([open, close])
|
|
329
|
+
let bounded = true
|
|
330
|
+
for (const sk of skips) {
|
|
331
|
+
const cs = firstSetClassChars(sk)
|
|
332
|
+
if (cs === null) { bounded = false; break }
|
|
333
|
+
for (const ch of cs) stop.add(ch)
|
|
334
|
+
}
|
|
335
|
+
const cls = [...stop].map(escapeClassChar).join('')
|
|
336
|
+
// A run when every skip's start is bounded (fast); else one char at a time so a
|
|
337
|
+
// skip arm always gets the chance to match at its position.
|
|
338
|
+
const content = bounded
|
|
339
|
+
? regex(new RegExp(`[^${cls}]+`))
|
|
340
|
+
: regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`))
|
|
341
|
+
const inner = many(choice(self, ...skips, content))
|
|
342
|
+
// The close is the ONLY thing strict mode changes. `expect()` recovers — it
|
|
343
|
+
// returns a ParseError and lets the group succeed anyway — so the default
|
|
344
|
+
// balanced can never fail past its opener. A bare `literal(close)` fails the
|
|
345
|
+
// sequence instead, which rolls the whole group back to the opener and lets an
|
|
346
|
+
// enclosing choice()/not()/attempt() actually see the rejection. Everything
|
|
347
|
+
// else (first-set gating, trivia, the skip set, the content run, the interior
|
|
348
|
+
// recursion through `self`) is shared, so strict and tolerant differ in
|
|
349
|
+
// failure behaviour only, never in what they accept.
|
|
350
|
+
const closer = strict ? literal(close) : expect(literal(close))
|
|
351
|
+
const combi = transform(
|
|
352
|
+
sequence(literal(open), inner, closer),
|
|
353
|
+
// parts: strings (content/self) or arrays (a sequence-shaped skip) or a
|
|
354
|
+
// ParseError (recovered close). `c` is the close string or a ParseError.
|
|
355
|
+
([o, parts, c]) => o + (parts as unknown[]).map(p => typeof p === 'string' ? p : Array.isArray(p) ? p.join('') : '').join('') + (typeof c === 'string' ? c : ''),
|
|
356
|
+
)
|
|
357
|
+
// Provide the callback source so the macro can inline this library-internal
|
|
358
|
+
// transform (codegen derives map-fn sources from def.fnSrc).
|
|
359
|
+
if (combi._def.tag === 'transform') {
|
|
360
|
+
combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")'
|
|
361
|
+
// This is Parseman's structural delimiter reconstruction, not a grammar
|
|
362
|
+
// semantic reduction. It must remain eligible for recognition-only
|
|
363
|
+
// composeLeaf artifacts while user-authored transform() remains excluded.
|
|
364
|
+
combi._def.recognitionOnly = true
|
|
365
|
+
}
|
|
366
|
+
// Record what this interior IS, before the self-ref closes the cycle. Any walker
|
|
367
|
+
// that would otherwise descend through `self` forever can stop here and render
|
|
368
|
+
// the construct instead of its lowering.
|
|
369
|
+
;(combi as BalancedMarked)._balanced = { open, close }
|
|
370
|
+
self.define(combi as Combinator<string>)
|
|
371
|
+
return combi
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The characters a combinator can START with, as char-class members — or null if
|
|
376
|
+
* its first set is unbounded ('any') or too broad to be a delimiter. Used by
|
|
377
|
+
* balanced() to keep a content run from eating the start of a skip.
|
|
378
|
+
*/
|
|
379
|
+
function firstSetClassChars(c: Combinator<unknown>): string[] | null {
|
|
380
|
+
const fs = c._meta.firstSet
|
|
381
|
+
if (fs.kind === 'empty') return []
|
|
382
|
+
if (fs.kind !== 'ranges') return null
|
|
383
|
+
const out: string[] = []
|
|
384
|
+
for (const { lo, hi } of fs.ranges) {
|
|
385
|
+
if (hi - lo > 8) return null
|
|
386
|
+
for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp))
|
|
387
|
+
}
|
|
388
|
+
return out
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function escapeClassChar(ch: string): string {
|
|
392
|
+
return ch.replace(/[\\\]^-]/g, '\\$&')
|
|
393
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta, ParseFail } from '../types.ts'
|
|
2
|
+
import { sequenceFirstSet, firstSetOf, union } from './first-set.ts'
|
|
3
|
+
import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTrivia } from './trivia-skip.ts'
|
|
4
|
+
import { firstSetSentinel } from '../recovery/scan.ts'
|
|
5
|
+
import { adjacencyFail, adjacencyOf, holdsAdjacency, type AdjacencyDef } from './adjacency.ts'
|
|
6
|
+
|
|
7
|
+
type UnwrapParsers<T extends Combinator<unknown>[]> = {
|
|
8
|
+
[K in keyof T]: T[K] extends Combinator<infer U> ? U : never
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[]]>(
|
|
12
|
+
...parsers: T
|
|
13
|
+
): Combinator<UnwrapParsers<T>> {
|
|
14
|
+
// Adjacency markers (`adjacent()` / `notAdjacent()`) are BOUNDARY tests, lowered
|
|
15
|
+
// here rather than parsed as terms. At index 0 there is no preceding term in this
|
|
16
|
+
// sequence, and the gap before `pos` belongs to whoever called us — locally
|
|
17
|
+
// undecidable, so reject it at construction instead of answering it wrongly.
|
|
18
|
+
// Stays `null` for the overwhelming majority of sequences, which is the point:
|
|
19
|
+
// every grammar builds thousands of these, and a per-sequence array to say "no
|
|
20
|
+
// assertions here" would be a permanent retained allocation for a fact that is
|
|
21
|
+
// almost always no.
|
|
22
|
+
let adjacency: (AdjacencyDef | null)[] | null = null
|
|
23
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
24
|
+
const a = adjacencyOf(parsers[i]!)
|
|
25
|
+
if (a === null) continue
|
|
26
|
+
if (i === 0) {
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`sequence(): ${a.polarity}() cannot be the FIRST term — an adjacency assertion tests the `
|
|
29
|
+
+ 'gap after the PRECEDING term. Put a concrete term first.',
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
;(adjacency ??= Array.from({ length: parsers.length }, () => null))[i] = a
|
|
33
|
+
}
|
|
34
|
+
const hasAdjacency = adjacency !== null
|
|
35
|
+
|
|
36
|
+
const meta: ParserMeta = {
|
|
37
|
+
// Union through the nullable prefix — a leading optional()/many() lets a later
|
|
38
|
+
// term's first char start the sequence. Just `parsers[0]` under-approximates.
|
|
39
|
+
firstSet: sequenceFirstSet(parsers),
|
|
40
|
+
canMatchNewline: parsers.some(p => p._meta.canMatchNewline),
|
|
41
|
+
isTrivia: false,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const def: { tag: 'sequence'; parsers: Combinator<unknown>[]; valueUnused?: boolean } =
|
|
45
|
+
{ tag: 'sequence', parsers: parsers as Combinator<unknown>[] }
|
|
46
|
+
|
|
47
|
+
// Automatic recovery-sync inference, built lazily on the first tolerant parse and
|
|
48
|
+
// never touched on the strict path: followSentinels[i] matches (zero-width) when
|
|
49
|
+
// the input could start any term AFTER i, so a list nested in term i can resync
|
|
50
|
+
// to this sequence's enclosing delimiter with NO grammar annotation. This is the
|
|
51
|
+
// whole of "recovery config" — derived from structure, not authored.
|
|
52
|
+
let followSentinels: (Combinator<null> | null)[] | undefined
|
|
53
|
+
|
|
54
|
+
// Tolerant twin of the strict loop: identical term-parsing, but around each term
|
|
55
|
+
// it publishes the inferred follow sentinel into ctx._sync so a nested list can
|
|
56
|
+
// resync to this sequence's enclosing delimiter. Cold path (tolerant only).
|
|
57
|
+
function parseTolerant(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
|
|
58
|
+
followSentinels ??= parsers.map((_, i) => {
|
|
59
|
+
// A nested list can resync to the start of ANY term that follows it here, so
|
|
60
|
+
// union every following term's first set (not just up to the first
|
|
61
|
+
// non-nullable one — a mandatory middle term must not hide a later close).
|
|
62
|
+
const fs = parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
|
|
63
|
+
(acc, p) => union(acc, firstSetOf(p)),
|
|
64
|
+
{ kind: 'empty' },
|
|
65
|
+
)
|
|
66
|
+
return firstSetSentinel(fs)
|
|
67
|
+
})
|
|
68
|
+
const values: unknown[] | undefined = def.valueUnused ? undefined : []
|
|
69
|
+
let cur = pos
|
|
70
|
+
const inheritedSync = ctx._sync
|
|
71
|
+
try {
|
|
72
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
73
|
+
// Publish this term's follow set (or keep the inherited sync when the local
|
|
74
|
+
// follow isn't usable, e.g. the last term or an `any` first set).
|
|
75
|
+
ctx._sync = followSentinels[i] ?? inheritedSync
|
|
76
|
+
const adj = adjacency === null ? null : adjacency[i]
|
|
77
|
+
if (adj) {
|
|
78
|
+
if (!holdsAdjacency(input, cur, ctx, adj)) return adjacencyFail(cur, adj)
|
|
79
|
+
if (values !== undefined) values.push(null)
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (ctx.trivia && i > 0) {
|
|
83
|
+
const mark = saveTriviaMark(ctx)
|
|
84
|
+
let scanEnd: number
|
|
85
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
86
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
87
|
+
scan.commit()
|
|
88
|
+
scanEnd = scan.end
|
|
89
|
+
} else {
|
|
90
|
+
scanEnd = advanceTrivia(input, cur, ctx)
|
|
91
|
+
}
|
|
92
|
+
const result = parsers[i]!.parse(input, scanEnd, ctx)
|
|
93
|
+
if (!result.ok) return result as ParseFail
|
|
94
|
+
if (result.span.end > scanEnd) cur = result.span.end
|
|
95
|
+
else rollbackTrivia(ctx, mark)
|
|
96
|
+
if (values !== undefined) values.push(result.value)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
const result = parsers[i]!.parse(input, cur, ctx)
|
|
100
|
+
if (!result.ok) return result as ParseFail
|
|
101
|
+
if (values !== undefined) values.push(result.value)
|
|
102
|
+
cur = result.span.end
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
ctx._sync = inheritedSync
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
110
|
+
value: (values ?? undefined) as UnwrapParsers<T>,
|
|
111
|
+
span: { start: pos, end: cur },
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Strict twin of the loop below, for the (rare) sequence that carries an adjacency
|
|
116
|
+
// assertion. FORKED rather than branched inline so the ordinary strict loop — the
|
|
117
|
+
// hot path of every grammar — keeps exactly the instructions it had before.
|
|
118
|
+
function parseAdjacent(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
|
|
119
|
+
const values: unknown[] | undefined = def.valueUnused ? undefined : []
|
|
120
|
+
let cur = pos
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
123
|
+
const adj = adjacency![i]
|
|
124
|
+
if (adj) {
|
|
125
|
+
// Assert over the gap at `cur` and move nothing. The NEXT term re-scans the
|
|
126
|
+
// same gap and owns the commit/rewind decision, so the tree, the spans and
|
|
127
|
+
// the trivia log are byte-identical to the same sequence without the marker.
|
|
128
|
+
if (!holdsAdjacency(input, cur, ctx, adj)) return adjacencyFail(cur, adj)
|
|
129
|
+
if (values !== undefined) values.push(null)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (ctx.trivia && i > 0) {
|
|
133
|
+
let scanEnd: number
|
|
134
|
+
const mark = saveTriviaMark(ctx)
|
|
135
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
136
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
137
|
+
scan.commit()
|
|
138
|
+
scanEnd = scan.end
|
|
139
|
+
} else {
|
|
140
|
+
scanEnd = advanceTrivia(input, cur, ctx)
|
|
141
|
+
}
|
|
142
|
+
const result = parsers[i]!.parse(input, scanEnd, ctx)
|
|
143
|
+
if (!result.ok) return result as ParseFail
|
|
144
|
+
if (result.span.end > scanEnd) cur = result.span.end
|
|
145
|
+
else rollbackTrivia(ctx, mark)
|
|
146
|
+
if (values !== undefined) values.push(result.value)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
const result = parsers[i]!.parse(input, cur, ctx)
|
|
150
|
+
if (!result.ok) return result as ParseFail
|
|
151
|
+
if (values !== undefined) values.push(result.value)
|
|
152
|
+
cur = result.span.end
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
158
|
+
value: (values ?? undefined) as UnwrapParsers<T>,
|
|
159
|
+
span: { start: pos, end: cur },
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
_tag: 'sequence',
|
|
165
|
+
_meta: meta,
|
|
166
|
+
_def: def,
|
|
167
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
|
|
168
|
+
// One cold branch: in tolerant mode publish each term's follow-set as ctx._sync
|
|
169
|
+
// so a nested list resyncs to the enclosing delimiter (dynamic scoping through
|
|
170
|
+
// refs carries this across rule boundaries automatically). The strict loop
|
|
171
|
+
// below is byte-identical to a parser with no recovery.
|
|
172
|
+
if (ctx._tolerant) return parseTolerant(input, pos, ctx)
|
|
173
|
+
if (hasAdjacency) return parseAdjacent(input, pos, ctx)
|
|
174
|
+
|
|
175
|
+
// Skip the tuple when it's never observed (markUnusedValues): terms still
|
|
176
|
+
// parse (and self-capture) — only the array of their values is elided.
|
|
177
|
+
const values: unknown[] | undefined = def.valueUnused ? undefined : []
|
|
178
|
+
let cur = pos
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
181
|
+
if (ctx.trivia && i > 0) {
|
|
182
|
+
// Skip trivia between terms, but only *consume* it for span purposes if
|
|
183
|
+
// this term actually matches content past the trivia. A term that matches
|
|
184
|
+
// empty (optional/many/lookahead) leaves the trivia for the enclosing rule.
|
|
185
|
+
let scanEnd: number
|
|
186
|
+
let mark = saveTriviaMark(ctx)
|
|
187
|
+
|
|
188
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
189
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
190
|
+
scan.commit()
|
|
191
|
+
scanEnd = scan.end
|
|
192
|
+
} else {
|
|
193
|
+
scanEnd = advanceTrivia(input, cur, ctx)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = parsers[i]!.parse(input, scanEnd, ctx)
|
|
197
|
+
if (!result.ok) return result as ParseFail
|
|
198
|
+
if (result.span.end > scanEnd) {
|
|
199
|
+
cur = result.span.end
|
|
200
|
+
} else {
|
|
201
|
+
rollbackTrivia(ctx, mark)
|
|
202
|
+
}
|
|
203
|
+
if (values !== undefined) values.push(result.value)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const result = parsers[i]!.parse(input, cur, ctx)
|
|
208
|
+
if (!result.ok) return result as ParseFail
|
|
209
|
+
if (values !== undefined) values.push(result.value)
|
|
210
|
+
cur = result.span.end
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
ok: true,
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
216
|
+
value: (values ?? undefined) as UnwrapParsers<T>,
|
|
217
|
+
span: { start: pos, end: cur },
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
}
|