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,561 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTrivia } from './trivia-skip.ts'
|
|
3
|
+
import { matchesEmpty, startsFirstSet } from './first-set.ts'
|
|
4
|
+
import { deriveExpected } from './expect.ts'
|
|
5
|
+
import { matchesAt, orSentinel, recoverScan, captureError } from '../recovery/scan.ts'
|
|
6
|
+
import { demoteCapturedToRaw } from '../cst/capture-buffer.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse one repetition item at `cur`, first skipping (and, in capture mode,
|
|
10
|
+
* recording) any leading trivia — so a repeating combinator consumes the trivia
|
|
11
|
+
* between items uniformly, the way advancing the index always should. Trivia is
|
|
12
|
+
* committed *before* the item so rawChildren order stays [item, trivia, item];
|
|
13
|
+
* if the item then fails or makes no progress the trivia is rolled back and the
|
|
14
|
+
* loop stops (the trivia is trailing and belongs to the enclosing context).
|
|
15
|
+
*
|
|
16
|
+
* Returns the item value + end position, the underlying failure (so oneOrMore
|
|
17
|
+
* can propagate a first-item failure), or 'stop'.
|
|
18
|
+
*
|
|
19
|
+
* `mandatory` marks an item `min` REQUIRES rather than one the greedy loop is
|
|
20
|
+
* merely trying. Both stops below — the EOF early-out and the zero-width stop —
|
|
21
|
+
* are TERMINATION DEVICES for an unbounded loop whose only source of progress is
|
|
22
|
+
* the item; that pressure does not exist for a prefix of exactly `min` items,
|
|
23
|
+
* which is finite by construction. A required item is therefore attempted at its
|
|
24
|
+
* position whatever is there, exactly as the first one already is (:203) — the
|
|
25
|
+
* trivia skip, which is real structure and not a stop, still applies to both.
|
|
26
|
+
*/
|
|
27
|
+
function repItem<T>(
|
|
28
|
+
combinator: Combinator<T>,
|
|
29
|
+
input: string,
|
|
30
|
+
cur: number,
|
|
31
|
+
ctx: ParseContext,
|
|
32
|
+
guardable: boolean,
|
|
33
|
+
mandatory: boolean,
|
|
34
|
+
): { value: T; end: number } | { fail: ParseResult<T>; failPos: number } | 'stop' {
|
|
35
|
+
const mark = saveTriviaMark(ctx)
|
|
36
|
+
let pos = cur
|
|
37
|
+
if (ctx.trivia) {
|
|
38
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
39
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
40
|
+
scan.commit()
|
|
41
|
+
pos = scan.end
|
|
42
|
+
} else {
|
|
43
|
+
pos = advanceTrivia(input, cur, ctx)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Nothing but trivia left: don't speculatively parse an item at EOF (it would
|
|
47
|
+
// fail and could trigger an item's expect()/error side-effects). The trivia
|
|
48
|
+
// is trailing — roll it back for the enclosing context and stop. SPECULATIVE
|
|
49
|
+
// is the operative word: a `mandatory` item is not speculative, so it is
|
|
50
|
+
// attempted at EOF like the first item is, and a NULLABLE one matches there.
|
|
51
|
+
if (pos >= input.length && !mandatory) {
|
|
52
|
+
rollbackTrivia(ctx, mark)
|
|
53
|
+
return 'stop'
|
|
54
|
+
}
|
|
55
|
+
// First-set fast-path (mirrors emitMany's codegen guard): a body that can't match
|
|
56
|
+
// empty and whose first set can't start at `pos` can ONLY fail this iteration, so
|
|
57
|
+
// stop before the (composite) body's setup-then-fail. This is behaviour-identical
|
|
58
|
+
// in strict mode — a swallowed body failure is discarded either way, and a
|
|
59
|
+
// zero-width/leaf miss reaches the same loop stop. Skipped under a completions
|
|
60
|
+
// probe or tolerant recovery, where a swallowed failure still feeds the probe /
|
|
61
|
+
// triggers resync (matching the codegen guard's `!ctx.recovery` gate).
|
|
62
|
+
if (guardable && ctx._probe === undefined && !ctx._tolerant && !startsFirstSet(combinator, input, pos)) {
|
|
63
|
+
rollbackTrivia(ctx, mark)
|
|
64
|
+
return 'stop'
|
|
65
|
+
}
|
|
66
|
+
const result = combinator.parse(input, pos, ctx)
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
rollbackTrivia(ctx, mark)
|
|
69
|
+
if (result.committed) return { fail: result, failPos: pos }
|
|
70
|
+
// Surface the POST-trivia position where the element actually failed. The
|
|
71
|
+
// tolerant recovery guard must check the sync token there — not at `cur`,
|
|
72
|
+
// which sits before any leading trivia — so trailing trivia before the sync
|
|
73
|
+
// isn't mistaken for junk and swallowed into a spurious ParseError.
|
|
74
|
+
return { fail: result, failPos: pos }
|
|
75
|
+
}
|
|
76
|
+
// Zero-width item: it cannot make progress, so the greedy loop stops without
|
|
77
|
+
// taking it — a TERMINATION device, not a semantic filter (the same rule the
|
|
78
|
+
// table driver holds to `viaRepItem`). A `mandatory` item is counted whatever
|
|
79
|
+
// its width: `min` counts ITEMS, and `x*` over a nullable `x` derives the empty
|
|
80
|
+
// string with any number of them, so a `{ min: n }` prefix takes the n-item
|
|
81
|
+
// derivation. It cannot spin — the prefix is exactly `n` long.
|
|
82
|
+
if (result.span.end === pos && !mandatory) {
|
|
83
|
+
rollbackTrivia(ctx, mark)
|
|
84
|
+
return 'stop'
|
|
85
|
+
}
|
|
86
|
+
return { value: result.value, end: result.span.end }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type RepeatOptions = {
|
|
90
|
+
/**
|
|
91
|
+
* Minimum number of ITEMS. Default `0`.
|
|
92
|
+
*
|
|
93
|
+
* `min >= 1` is not a validation nicety — it is what makes the combinator
|
|
94
|
+
* NON-NULLABLE. A nullable arm matches at every position, which disables its
|
|
95
|
+
* `choice`'s first-char dispatch by parseman's own first-set rule; a `min >= 1`
|
|
96
|
+
* repeat keeps the item's first-set, so an arm led by it still gates.
|
|
97
|
+
*/
|
|
98
|
+
min?: number
|
|
99
|
+
/** Maximum number of ITEMS. Default: unbounded. Never affects nullability. */
|
|
100
|
+
max?: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Shared `min`/`max` validation — a bad bound is an authoring error, not a parse
|
|
104
|
+
* outcome, so it throws at CONSTRUCTION where the stack points at the grammar. */
|
|
105
|
+
function resolveBounds(what: string, opts: { min?: number; max?: number }): { min: number; max: number } {
|
|
106
|
+
const min = opts.min ?? 0
|
|
107
|
+
const max = opts.max ?? Infinity
|
|
108
|
+
const bad = (msg: string): never => { throw new RangeError(`parseman: ${what} ${msg}`) }
|
|
109
|
+
if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`)
|
|
110
|
+
if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`)
|
|
111
|
+
if (max < min) bad(`max (${max}) is less than min (${min}) — the combinator could never succeed`)
|
|
112
|
+
return { min, max }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Repetition: `item*` by default, bounded by `{ min, max }` (both count ITEMS).
|
|
117
|
+
*
|
|
118
|
+
* many(g.Decl) // zero or more — NULLABLE
|
|
119
|
+
* many(g.Decl, { min: 1 }) // one or more — same as oneOrMore(g.Decl)
|
|
120
|
+
* many(g.HexDigit, { min: 3, max: 8 })
|
|
121
|
+
*
|
|
122
|
+
* `oneOrMore(x)` is kept as the sugar for the common `{ min: 1 }` case.
|
|
123
|
+
*/
|
|
124
|
+
export function many<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Combinator<T[]> {
|
|
125
|
+
const { min, max } = resolveBounds('many()', opts)
|
|
126
|
+
// `min >= 1` routes to the SAME implementation `oneOrMore` has always used, so
|
|
127
|
+
// `many(x, { min: 1 })` and `oneOrMore(x)` are the identical combinator — not
|
|
128
|
+
// merely equivalent-looking. (They differ from the min-0 loop in one real way:
|
|
129
|
+
// the mandatory items parse at `pos` with no leading-trivia skip, which is the
|
|
130
|
+
// enclosing context's job.)
|
|
131
|
+
if (min >= 1) return atLeast(combinator, min, max)
|
|
132
|
+
const meta: ParserMeta = {
|
|
133
|
+
firstSet: combinator._meta.firstSet,
|
|
134
|
+
canMatchNewline: combinator._meta.canMatchNewline,
|
|
135
|
+
isTrivia: false,
|
|
136
|
+
}
|
|
137
|
+
const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean } =
|
|
138
|
+
{ tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }) }
|
|
139
|
+
let expected: string[] | undefined
|
|
140
|
+
// A non-nullable body can be first-set-gated per loop iteration (see repItem).
|
|
141
|
+
const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
_tag: 'many',
|
|
145
|
+
_meta: meta,
|
|
146
|
+
_def: def,
|
|
147
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
|
|
148
|
+
// When the aggregate is never observed (markUnusedValues), skip the array:
|
|
149
|
+
// items still parse and self-capture into the enclosing node's children.
|
|
150
|
+
const values: T[] | undefined = def.valueUnused ? undefined : []
|
|
151
|
+
let cur = pos
|
|
152
|
+
let count = 0
|
|
153
|
+
while (cur < input.length) {
|
|
154
|
+
if (count >= max) break
|
|
155
|
+
const item = repItem(combinator, input, cur, ctx, guardable, false)
|
|
156
|
+
if (item === 'stop') break
|
|
157
|
+
if ('fail' in item) {
|
|
158
|
+
if (!item.fail.ok && item.fail.committed) return item.fail
|
|
159
|
+
// Cold path: only reached on an element failure. Strict mode ⇒ `break`.
|
|
160
|
+
// Tolerant ⇒ resync to the sync sentinel the enclosing sequence inferred
|
|
161
|
+
// and published as ctx._sync (the grammar carries no recovery config). No
|
|
162
|
+
// sync available ⇒ nothing to skip to → break.
|
|
163
|
+
const sync = ctx._tolerant ? ctx._sync : undefined
|
|
164
|
+
if (sync === undefined) break
|
|
165
|
+
// Sync token at the POST-trivia failure position ⇒ clean list end (the
|
|
166
|
+
// trailing trivia belongs to the enclosing context), not junk. Checking
|
|
167
|
+
// `item.failPos` (past leading trivia), not `cur`, keeps trivia out of
|
|
168
|
+
// both the break decision and the recovered error span.
|
|
169
|
+
if (matchesAt(sync, input, item.failPos, ctx)) break
|
|
170
|
+
expected ??= deriveExpected(combinator)
|
|
171
|
+
const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected)
|
|
172
|
+
if (values !== undefined) values.push(error as unknown as T)
|
|
173
|
+
captureError(ctx, error)
|
|
174
|
+
count++
|
|
175
|
+
cur = end
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
if (values !== undefined) values.push(item.value)
|
|
179
|
+
count++
|
|
180
|
+
cur = item.end
|
|
181
|
+
}
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
183
|
+
return { ok: true, value: (values ?? undefined) as T[], span: { start: pos, end: cur } }
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Sugar for the commonest bound: `many(combinator, { min: 1 })` — the identical
|
|
190
|
+
* combinator, not merely an equivalent one.
|
|
191
|
+
*/
|
|
192
|
+
export function oneOrMore<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Combinator<T[]> {
|
|
193
|
+
return many(combinator, { ...opts, min: opts.min ?? 1 })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `min >= 1` repetition: `min` MANDATORY items (whose failure propagates) then a
|
|
198
|
+
* greedy loop up to `max`. The tag stays `oneOrMore` — every downstream switch
|
|
199
|
+
* keys on it for "non-nullable repeat" — and `min` carries the real count.
|
|
200
|
+
*/
|
|
201
|
+
function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combinator<T[]> {
|
|
202
|
+
const meta: ParserMeta = {
|
|
203
|
+
firstSet: combinator._meta.firstSet,
|
|
204
|
+
canMatchNewline: combinator._meta.canMatchNewline,
|
|
205
|
+
isTrivia: false,
|
|
206
|
+
}
|
|
207
|
+
const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean } =
|
|
208
|
+
{ tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }) }
|
|
209
|
+
let expected: string[] | undefined
|
|
210
|
+
// A non-nullable body can be first-set-gated per loop iteration (see repItem).
|
|
211
|
+
const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
_tag: 'oneOrMore',
|
|
215
|
+
_meta: meta,
|
|
216
|
+
_def: def,
|
|
217
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
|
|
218
|
+
// First item is mandatory (parsed at pos directly — leading trivia is the
|
|
219
|
+
// enclosing context's responsibility); subsequent items skip leading trivia.
|
|
220
|
+
const first = combinator.parse(input, pos, ctx)
|
|
221
|
+
if (!first.ok) return first
|
|
222
|
+
// Aggregate skipped when never observed (see `many`).
|
|
223
|
+
const values: T[] | undefined = def.valueUnused ? undefined : [first.value]
|
|
224
|
+
let cur = first.span.end
|
|
225
|
+
let count = 1
|
|
226
|
+
// Mandatory items 2..min (only entered for min > 1) — each failure propagates,
|
|
227
|
+
// exactly like the first. They go through repItem so the trivia BETWEEN items
|
|
228
|
+
// is consumed the same way the loop consumes it, but `mandatory` holds off its
|
|
229
|
+
// two loop-termination stops: a required item is attempted at its position
|
|
230
|
+
// whatever is there, and counts whatever its width.
|
|
231
|
+
while (count < min) {
|
|
232
|
+
const item = repItem(combinator, input, cur, ctx, guardable, true)
|
|
233
|
+
if (item === 'stop' || 'fail' in item) {
|
|
234
|
+
if (item !== 'stop' && !item.fail.ok && item.fail.committed) return item.fail
|
|
235
|
+
// Anchored at `cur` — the furthest position the repeat reached — not at
|
|
236
|
+
// its start. `many(regex(/x/), { min: 3 })` over "xx" consumed both x's
|
|
237
|
+
// and got stuck at 2 wanting a third; reporting offset 0 points the
|
|
238
|
+
// caret at input that matched. Same rule as `sepBy`'s `failAt`, and the
|
|
239
|
+
// compiled emitter already anchored here — this side was the outlier.
|
|
240
|
+
expected ??= deriveExpected(combinator)
|
|
241
|
+
return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } }
|
|
242
|
+
}
|
|
243
|
+
if (values !== undefined) values.push(item.value)
|
|
244
|
+
cur = item.end
|
|
245
|
+
count++
|
|
246
|
+
}
|
|
247
|
+
while (cur < input.length) {
|
|
248
|
+
if (count >= max) break
|
|
249
|
+
const item = repItem(combinator, input, cur, ctx, guardable, false)
|
|
250
|
+
if (item === 'stop') break
|
|
251
|
+
if ('fail' in item) {
|
|
252
|
+
if (!item.fail.ok && item.fail.committed) return item.fail
|
|
253
|
+
// Cold path (element failure). Strict: break. Tolerant: resync — see many().
|
|
254
|
+
const sync = ctx._tolerant ? ctx._sync : undefined
|
|
255
|
+
if (sync === undefined) break
|
|
256
|
+
if (matchesAt(sync, input, item.failPos, ctx)) break
|
|
257
|
+
expected ??= deriveExpected(combinator)
|
|
258
|
+
const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected)
|
|
259
|
+
if (values !== undefined) values.push(error as unknown as T)
|
|
260
|
+
captureError(ctx, error)
|
|
261
|
+
count++
|
|
262
|
+
cur = end
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (values !== undefined) values.push(item.value)
|
|
266
|
+
count++
|
|
267
|
+
cur = item.end
|
|
268
|
+
}
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
270
|
+
return { ok: true, value: (values ?? undefined) as T[], span: { start: pos, end: cur } }
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function optional<T>(combinator: Combinator<T>): Combinator<T | null> {
|
|
276
|
+
const meta: ParserMeta = {
|
|
277
|
+
firstSet: combinator._meta.firstSet,
|
|
278
|
+
canMatchNewline: combinator._meta.canMatchNewline,
|
|
279
|
+
isTrivia: false,
|
|
280
|
+
}
|
|
281
|
+
const firstSetSkippable = !matchesEmpty(combinator)
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
_tag: 'optional',
|
|
285
|
+
_meta: meta,
|
|
286
|
+
_def: { tag: 'optional', parser: combinator as Combinator<unknown> },
|
|
287
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T | null> {
|
|
288
|
+
if (firstSetSkippable && ctx._probe === undefined && !startsFirstSet(combinator, input, pos)) {
|
|
289
|
+
return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
290
|
+
}
|
|
291
|
+
const mark = saveTriviaMark(ctx)
|
|
292
|
+
const result = combinator.parse(input, pos, ctx)
|
|
293
|
+
if (result.ok) return result as ParseResult<T>
|
|
294
|
+
// Inner failed → roll back any CST leaves/trivia it captured before giving up.
|
|
295
|
+
rollbackTrivia(ctx, mark)
|
|
296
|
+
if (result.committed) return result
|
|
297
|
+
return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Non-empty separated list — `oneOrMore`'s relationship to `many`, for separated
|
|
304
|
+
* lists. `oneOrMoreSep(item, sep)` is exactly `sepBy(item, sep, { min: 1 })`.
|
|
305
|
+
*
|
|
306
|
+
* oneOrMoreSep(g.Selector, literal(',')) // a selector list is never empty
|
|
307
|
+
*
|
|
308
|
+
* REACH FOR THIS, NOT `sepBy`, for any list that cannot actually be empty —
|
|
309
|
+
* selector lists, value lists, media-query preludes, keyframe selectors. `sepBy`'s
|
|
310
|
+
* min-0 default matches the EMPTY STRING, which makes it nullable, and a nullable
|
|
311
|
+
* arm disables its `choice`'s first-char dispatch by parseman's own first-set
|
|
312
|
+
* rule. This form is non-nullable and keeps the item's first-set, so an arm led by
|
|
313
|
+
* it still gates.
|
|
314
|
+
*/
|
|
315
|
+
export function oneOrMoreSep<T, S>(combinator: Combinator<T>, separator: Combinator<S> | KeptSeparator<S>, opts: SepByOptions = {}): Combinator<T[]> {
|
|
316
|
+
return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** How a separator with NO item after it is treated. */
|
|
320
|
+
export type TrailingSeparator =
|
|
321
|
+
/**
|
|
322
|
+
* DEFAULT, and what `sepBy` has always done: the trailing separator is NOT
|
|
323
|
+
* consumed — the list ends before it and the enclosing grammar sees it. (It is
|
|
324
|
+
* not an error here; "forbid" means the list refuses to own it.)
|
|
325
|
+
*/
|
|
326
|
+
| 'forbid'
|
|
327
|
+
/** Consume a trailing separator when present (`a, b,` → 2 items, comma eaten). */
|
|
328
|
+
| 'allow'
|
|
329
|
+
|
|
330
|
+
export type SepByOptions = RepeatOptions & {
|
|
331
|
+
/** What to do with a separator that has no item after it. Default `'forbid'`. */
|
|
332
|
+
trailing?: TrailingSeparator
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A separator the author asked to keep in `children`. Produced by `keepSeparator`. */
|
|
336
|
+
export type KeptSeparator<S> = { readonly _keepSeparator: Combinator<S> }
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Keep this list's separators in `children`, interleaved with the items.
|
|
340
|
+
*
|
|
341
|
+
* A list contributes its ITEMS and nothing else — that is the default and it is
|
|
342
|
+
* not negotiable, because a `children` array whose arity depends on a detail of
|
|
343
|
+
* the separator is the thing that made this defect invisible. But a combinator
|
|
344
|
+
* may collapse only what its CONSTRUCTION makes recoverable, and that is exactly
|
|
345
|
+
* the line this helper draws:
|
|
346
|
+
*
|
|
347
|
+
* sepBy(g.Value, literal(',')) // ',' is fixed here — recoverable, drop it
|
|
348
|
+
* sepBy(g.Track, keepSeparator(SLASH_OR_COMMA)) // could be '/' OR ',' — NOT recoverable, keep it
|
|
349
|
+
*
|
|
350
|
+
* Wrap the separator when it could have matched more than one thing: a `choice`,
|
|
351
|
+
* a regex with alternation or a quantifier, a rule reference. In CSS the
|
|
352
|
+
* separator carries meaning — `grid-area: 1 / 2` and `font: 12px/1.5` do not mean
|
|
353
|
+
* what `1, 2` means — and dropping it destroys information that exists nowhere
|
|
354
|
+
* else in the tree.
|
|
355
|
+
*
|
|
356
|
+
* The wrap is read at CONSTRUCTION, not per parse, and it is deliberately applied
|
|
357
|
+
* to the separator rather than passed as an option: the call site then STATES its
|
|
358
|
+
* own children arity, which is the failure being fixed. `keepSeparator` in the
|
|
359
|
+
* source is the only documentation that reaches an author who never reads docs.
|
|
360
|
+
*/
|
|
361
|
+
export function keepSeparator<S>(separator: Combinator<S>): KeptSeparator<S> {
|
|
362
|
+
return { _keepSeparator: separator }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function unwrapSeparator<S>(separator: Combinator<S> | KeptSeparator<S>): { sep: Combinator<S>; keep: boolean } {
|
|
366
|
+
return '_keepSeparator' in separator
|
|
367
|
+
? { sep: separator._keepSeparator, keep: true }
|
|
368
|
+
: { sep: separator, keep: false }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Separated list: `(item (sep item)*)?` by default — note that it MATCHES THE
|
|
373
|
+
* EMPTY STRING, which makes it nullable and therefore un-gateable as a choice arm.
|
|
374
|
+
* For a list that cannot be empty reach for `oneOrMoreSep`, or pass `{ min: 1 }`.
|
|
375
|
+
*
|
|
376
|
+
* sepBy(g.Value, literal(',')) // may be empty — NULLABLE
|
|
377
|
+
* oneOrMoreSep(g.Selector, literal(',')) // non-empty — gates as a choice arm
|
|
378
|
+
* sepBy(g.Decl, literal(';'), { trailing: 'allow' })
|
|
379
|
+
*
|
|
380
|
+
* `min`/`max` count ITEMS, not separators.
|
|
381
|
+
*/
|
|
382
|
+
export function sepBy<T, S>(
|
|
383
|
+
combinator: Combinator<T>,
|
|
384
|
+
separatorArg: Combinator<S> | KeptSeparator<S>,
|
|
385
|
+
opts: SepByOptions = {},
|
|
386
|
+
): Combinator<T[]> {
|
|
387
|
+
const { sep: separator, keep: keepSeparators } = unwrapSeparator(separatorArg)
|
|
388
|
+
const { min, max } = resolveBounds('sepBy()', opts)
|
|
389
|
+
const trailing = opts.trailing ?? 'forbid'
|
|
390
|
+
const meta: ParserMeta = {
|
|
391
|
+
firstSet: combinator._meta.firstSet,
|
|
392
|
+
canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
|
|
393
|
+
isTrivia: false,
|
|
394
|
+
}
|
|
395
|
+
let expected: string[] | undefined
|
|
396
|
+
/**
|
|
397
|
+
* A list-level failure is anchored at the FURTHEST position the list reached,
|
|
398
|
+
* and reports what would have allowed it to CONTINUE there — never at the
|
|
399
|
+
* list's own start.
|
|
400
|
+
*
|
|
401
|
+
* Anchoring at the start throws away everything the parse learned: for
|
|
402
|
+
* `sepBy(item, ',', { min: 3 })` over `"a,b"` the list consumed `a,b` perfectly
|
|
403
|
+
* and got stuck at offset 3 wanting a third item, so a failure reported at 0
|
|
404
|
+
* points the caret at a token that parsed fine. It also mislocates
|
|
405
|
+
* `completionsAt`, which asks what may appear AT a cursor — the answer belongs
|
|
406
|
+
* at the stuck position, not at the list's start.
|
|
407
|
+
*
|
|
408
|
+
* BOTH engines must follow this rule; it drifted three separate ways across
|
|
409
|
+
* this option surface, and `test/parity/repeat-options-parity.test.ts` is what
|
|
410
|
+
* now holds them together. The empty-set fallback mirrors codegen's
|
|
411
|
+
* `deriveExpectedArr`, so the two payloads are identical.
|
|
412
|
+
*/
|
|
413
|
+
const failAt = (at: number): ParseResult<T[]> => {
|
|
414
|
+
expected ??= deriveExpected(combinator)
|
|
415
|
+
return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
_tag: 'sepBy',
|
|
420
|
+
_meta: meta,
|
|
421
|
+
_def: {
|
|
422
|
+
tag: 'sepBy', parser: combinator as Combinator<unknown>, separator: separator as Combinator<unknown>, min,
|
|
423
|
+
...(max === Infinity ? {} : { max }),
|
|
424
|
+
...(trailing === 'forbid' ? {} : { trailing }),
|
|
425
|
+
// Carried into the IR so the COMPILED engine demotes exactly where the
|
|
426
|
+
// interpreter does. The two engines have drifted three separate ways across
|
|
427
|
+
// this option surface already; parity is held by test/parity, not by hope.
|
|
428
|
+
...(keepSeparators ? { keepSeparators: true } : {}),
|
|
429
|
+
},
|
|
430
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
|
|
431
|
+
const first = combinator.parse(input, pos, ctx)
|
|
432
|
+
const values: T[] = []
|
|
433
|
+
let cur: number
|
|
434
|
+
if (first.ok) {
|
|
435
|
+
values.push(first.value)
|
|
436
|
+
cur = first.span.end
|
|
437
|
+
} else {
|
|
438
|
+
if (first.committed) return first
|
|
439
|
+
// Cold path. Strict: an empty/absent first element is a legal empty list
|
|
440
|
+
// for `sepBy`, and a FAILURE for `sepBy1` (min 1 — that is the whole point).
|
|
441
|
+
// Tolerant: if the first element is JUNK (a terminator is inferable and we
|
|
442
|
+
// are not already sitting on it) recover it and enter the loop; otherwise
|
|
443
|
+
// it is a genuine empty list.
|
|
444
|
+
const term = ctx._tolerant ? ctx._sync : undefined
|
|
445
|
+
if (term === undefined || matchesAt(term, input, pos, ctx)) {
|
|
446
|
+
// `min >= 1` — nothing was consumed, so the furthest position IS `pos`.
|
|
447
|
+
// The compiled form swallows the first element's sub-parse (it is
|
|
448
|
+
// discarded on the min-0 path), so it cannot reproduce the inner
|
|
449
|
+
// failure's exact payload; both sides report this same derived set.
|
|
450
|
+
if (min >= 1) return failAt(pos)
|
|
451
|
+
return { ok: true, value: [], span: { start: pos, end: pos } }
|
|
452
|
+
}
|
|
453
|
+
expected ??= deriveExpected(combinator)
|
|
454
|
+
const rec = recoverScan(input, pos, ctx, orSentinel(separator, term), expected)
|
|
455
|
+
values.push(rec.error as unknown as T)
|
|
456
|
+
captureError(ctx, rec.error)
|
|
457
|
+
cur = rec.end
|
|
458
|
+
}
|
|
459
|
+
// `|| values.length < min` — the separated twin of `repItem`'s `mandatory`
|
|
460
|
+
// (1c5b2e8). `cur < input.length` is a TERMINATION device for the greedy
|
|
461
|
+
// tail, and that pressure does not exist for a prefix of exactly `min`
|
|
462
|
+
// items, which is finite by construction: each iteration that succeeds
|
|
463
|
+
// pushes one, so the disjunct can hold at most `min` times. Stopping on it
|
|
464
|
+
// made a REQUIRED item unreachable at EOF, so `sepBy(x, s, { min: n })` over
|
|
465
|
+
// a nullable `x` AND a nullable `s` — where the n-item derivation of the
|
|
466
|
+
// empty string genuinely exists — failed instead of taking it.
|
|
467
|
+
//
|
|
468
|
+
// A non-nullable separator still fails the list here rather than padding it:
|
|
469
|
+
// n items need n-1 separators, so there is no derivation to take, and the
|
|
470
|
+
// iteration below breaks on the separator exactly as it always did.
|
|
471
|
+
// Vacuous for `min <= 1`, which the first element already satisfies.
|
|
472
|
+
while (cur < input.length || values.length < min) {
|
|
473
|
+
if (values.length >= max) break
|
|
474
|
+
// One mark for the whole iteration (separator + following item): if the
|
|
475
|
+
// item fails, the trailing separator must be rolled back with it, or its
|
|
476
|
+
// captured leaves leak past the end of the list.
|
|
477
|
+
const loopMark = saveTriviaMark(ctx)
|
|
478
|
+
let sepPos = cur
|
|
479
|
+
if (ctx.trivia) {
|
|
480
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
481
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
482
|
+
scan.commit()
|
|
483
|
+
sepPos = scan.end
|
|
484
|
+
} else {
|
|
485
|
+
sepPos = advanceTrivia(input, cur, ctx)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const sep = separator.parse(input, sepPos, ctx)
|
|
489
|
+
if (!sep.ok) {
|
|
490
|
+
rollbackTrivia(ctx, loopMark)
|
|
491
|
+
if (sep.committed) return sep
|
|
492
|
+
break
|
|
493
|
+
}
|
|
494
|
+
// A LIST CONTRIBUTES ITS ITEMS AND NOTHING ELSE. The separator matched, so
|
|
495
|
+
// it is consumed and it is recorded in `rawChildren` — but it is not an
|
|
496
|
+
// item, so it does not belong in the structural `children` array. Demote it
|
|
497
|
+
// using the mark this iteration already took; no extra allocation, one
|
|
498
|
+
// guarded truncation, and only when CST capture is live at all.
|
|
499
|
+
if (!keepSeparators) demoteCapturedToRaw(ctx, loopMark.leaves)
|
|
500
|
+
// Mark taken AFTER the separator: `trailing` keeps the separator but must
|
|
501
|
+
// still unwind the trivia + leaves captured past it.
|
|
502
|
+
const sepMark = trailing === 'forbid' ? undefined : saveTriviaMark(ctx)
|
|
503
|
+
let nextPos = sep.span.end
|
|
504
|
+
if (ctx.trivia) {
|
|
505
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
506
|
+
const scan = scanTrivia(input, sep.span.end, ctx)
|
|
507
|
+
scan.commit()
|
|
508
|
+
nextPos = scan.end
|
|
509
|
+
} else {
|
|
510
|
+
nextPos = advanceTrivia(input, sep.span.end, ctx)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const next = combinator.parse(input, nextPos, ctx)
|
|
514
|
+
if (!next.ok) {
|
|
515
|
+
if (next.committed) return next
|
|
516
|
+
// Cold path. Strict: roll back the trailing separator + break. Tolerant:
|
|
517
|
+
// the separator we just consumed is real, so resync the bad element after
|
|
518
|
+
// it. If a terminator is inferable and already present at nextPos, the
|
|
519
|
+
// separator was a trailing one (e.g. `a;}`) → roll it back and stop.
|
|
520
|
+
//
|
|
521
|
+
// RECOVERY IS TESTED FIRST, and is ORTHOGONAL to `trailing`. This block
|
|
522
|
+
// used to lead with the `sepMark !== undefined` branch — but sepMark is
|
|
523
|
+
// non-undefined for EVERY 'allow' list, so that branch always won and the
|
|
524
|
+
// resync below became unreachable the moment a grammar opted into a
|
|
525
|
+
// trailing separator. `{a,,b}` recovered under the default 'forbid' and
|
|
526
|
+
// hard-failed under 'allow', which is not a policy anyone would choose:
|
|
527
|
+
// permitting a trailing comma has nothing to do with whether a tolerant
|
|
528
|
+
// parse may resynchronize. (The compiled `failItem` already ordered it
|
|
529
|
+
// this way — the engines disagreed because of this.)
|
|
530
|
+
//
|
|
531
|
+
// Junk after a REAL separator is recovered whatever `trailing` says, and
|
|
532
|
+
// no rollback happens on that path: both the separator and the recovered
|
|
533
|
+
// error element belong to the list.
|
|
534
|
+
const term = ctx._tolerant ? ctx._sync : undefined
|
|
535
|
+
if (term !== undefined && !matchesAt(term, input, nextPos, ctx)) {
|
|
536
|
+
expected ??= deriveExpected(combinator)
|
|
537
|
+
const rec = recoverScan(input, nextPos, ctx, orSentinel(separator, term), expected)
|
|
538
|
+
values.push(rec.error as unknown as T)
|
|
539
|
+
captureError(ctx, rec.error)
|
|
540
|
+
cur = rec.end
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
if (sepMark !== undefined) {
|
|
544
|
+
// 'allow': the separator we just consumed IS part of the list — a
|
|
545
|
+
// genuine trailing one, since no resync applied above.
|
|
546
|
+
rollbackTrivia(ctx, sepMark)
|
|
547
|
+
cur = sep.span.end
|
|
548
|
+
break
|
|
549
|
+
}
|
|
550
|
+
rollbackTrivia(ctx, loopMark)
|
|
551
|
+
break
|
|
552
|
+
}
|
|
553
|
+
values.push(next.value)
|
|
554
|
+
cur = next.span.end
|
|
555
|
+
}
|
|
556
|
+
// Too few items: the list is stuck at `cur` wanting another ITEM.
|
|
557
|
+
if (values.length < min) return failAt(cur)
|
|
558
|
+
return { ok: true, value: values, span: { start: pos, end: cur } }
|
|
559
|
+
},
|
|
560
|
+
}
|
|
561
|
+
}
|