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,1672 @@
|
|
|
1
|
+
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
2
|
+
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
3
|
+
import { buildFieldMap } from '../compiler/fields.ts'
|
|
4
|
+
import { asciiFoldKey } from '../combinators/dispatch.ts'
|
|
5
|
+
import { projectChild, unwrapChild } from '../combinators/node.ts'
|
|
6
|
+
import { asciiFoldEq } from '../combinators/literal.ts'
|
|
7
|
+
import { cstOutputHost } from '../compiler/build-arity.ts'
|
|
8
|
+
import { consumeTrivia } from '../combinators/trivia-skip.ts'
|
|
9
|
+
import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, rollbackTriviaAt, saveTriviaMark, scanTrivia, skipTriviaScanned, type FastTriviaScanner } from '../combinators/trivia-skip.ts'
|
|
10
|
+
import {
|
|
11
|
+
beginCstNodeCapture, cstCaptureActive, cstLeavesLen, cstRawLen, cstTlLen,
|
|
12
|
+
demoteCapturedToRaw, endCstNodeCapture, pushCstChild, pushCstLeaf,
|
|
13
|
+
} from '../cst/capture-buffer.ts'
|
|
14
|
+
import {
|
|
15
|
+
OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
|
|
16
|
+
OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
|
|
17
|
+
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
18
|
+
OP_LIVE,
|
|
19
|
+
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
|
|
20
|
+
OP_COV,
|
|
21
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
|
|
22
|
+
} from './ops.ts'
|
|
23
|
+
import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
|
|
24
|
+
import { failAt } from '../combinators/probe.ts'
|
|
25
|
+
/**
|
|
26
|
+
* THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
|
|
27
|
+
*
|
|
28
|
+
* `failAt` (combinators/probe.ts) is called from exactly three places in the
|
|
29
|
+
* interpreter — `literal.ts`, `regex.ts`, `keywords.ts` — and codegen emits its
|
|
30
|
+
* mirror (`probeUpdate`) at the same leaf-fail sites. The table recorded
|
|
31
|
+
* NOTHING, so `completionsAt` on a table artifact saw only the top-level
|
|
32
|
+
* failure: a swallowed item failure inside a `sepBy` (`'{'` at offset 1 in a
|
|
33
|
+
* `{ decl; }` grammar) never reached the probe and the item's opener vanished
|
|
34
|
+
* from the completion set.
|
|
35
|
+
*
|
|
36
|
+
* Dormant unless `completionsAt` set `_probe`, so an ordinary parse pays one
|
|
37
|
+
* property read per terminal miss — the same price codegen pays.
|
|
38
|
+
*/
|
|
39
|
+
import { committed, spanLines, stampRuleMap } from './stamp.ts'
|
|
40
|
+
import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
|
|
41
|
+
import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
|
|
42
|
+
import {
|
|
43
|
+
decodeClassSpec, expandCompact, resolveTable,
|
|
44
|
+
type CompactProgram, type ResolvedClass, type ResolvedDispatch, type ResolvedDispatchSpec,
|
|
45
|
+
type SubtreeRef, type TableProgram, type TableRule,
|
|
46
|
+
} from './program.ts'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* THE SHARED DRIVER.
|
|
50
|
+
*
|
|
51
|
+
* One recognizer for every grammar, every rule and every variant — the thing
|
|
52
|
+
* codegen emits once PER RULE, and which therefore sets the per-rule byte floor.
|
|
53
|
+
* It uses the same zero-allocation protocol the emitted code uses (a sentinel
|
|
54
|
+
* for failure plus a shared end-position slot), so what it adds over open-coded
|
|
55
|
+
* recognition is the opcode read and the switch, not an allocation.
|
|
56
|
+
*
|
|
57
|
+
* The driver is instantiated ONCE PER RESOLVED TABLE, closing over that table's
|
|
58
|
+
* arrays. Instruction operands are then reads of context slots rather than
|
|
59
|
+
* property loads through an object, and — the point of G5 — an option can never
|
|
60
|
+
* be consulted here: two settings pairs are two tables, not two code paths.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Failure sentinel and end-position cell — SHARED with the other two engines,
|
|
65
|
+
* see `cell.ts`. Private copies were correct only while one engine ran a whole
|
|
66
|
+
* parse; a mixed assembly has two live at once and a private sentinel reads as
|
|
67
|
+
* a successful match to the other engine.
|
|
68
|
+
*/
|
|
69
|
+
import { FAIL, newEndCell, type EndCell } from './cell.ts'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* DIAGNOSTIC ROW COUNTER — off unless `PM_TABLE_COUNT=1` at process start.
|
|
73
|
+
*
|
|
74
|
+
* Read ONCE, at module load, into a module const, so this is not the per-parse
|
|
75
|
+
* option branch G5 forbids: with the variable false the whole thing folds away
|
|
76
|
+
* after tier-up. It exists because "too many rows" and "each row too slow" are
|
|
77
|
+
* different defects with the same symptom, and only a count separates them.
|
|
78
|
+
* No timing run in this repo may set it — see `bench/jess/table-rows.ts`.
|
|
79
|
+
*/
|
|
80
|
+
export const tableCounters: {
|
|
81
|
+
rows: number
|
|
82
|
+
byOp: Int32Array
|
|
83
|
+
/** Distinct reducer/host functions actually reaching each shared call site. */
|
|
84
|
+
sites: Map<string, Set<unknown>>
|
|
85
|
+
/**
|
|
86
|
+
* SPECULATION AT UNGATED CHOICES. `encode.ts:368-376` gives a choice its
|
|
87
|
+
* first-char dispatch only when NO arm is nullable, ALL arms are pairwise
|
|
88
|
+
* disjoint and ALL map to a char class; any one failure ungates every arm of
|
|
89
|
+
* the site, which then runs the linear loop below. Codegen has no such
|
|
90
|
+
* all-or-nothing rule — `emitFirstMatch` guards each arm on its OWN first set
|
|
91
|
+
* — so these counts are work the table does and the compiled engine does not.
|
|
92
|
+
*
|
|
93
|
+
* `ungatedEntries` is arms entered there; `ungatedFails` is those that then
|
|
94
|
+
* failed, which is pure waste by construction: a failing arm consumed nothing
|
|
95
|
+
* the winning arm will not scan again.
|
|
96
|
+
*/
|
|
97
|
+
ungatedEntries: number
|
|
98
|
+
ungatedFails: number
|
|
99
|
+
/** Rows executed INSIDE failed ungated arms — the subtree each wasted entry drags. */
|
|
100
|
+
ungatedFailRows: number
|
|
101
|
+
gatedEntries: number
|
|
102
|
+
/** Arm entries DECLINED by the per-arm class gate — the work this change removes. */
|
|
103
|
+
armGateSkips: number
|
|
104
|
+
} = { rows: 0, byOp: new Int32Array(64), sites: new Map(), ungatedEntries: 0, ungatedFails: 0, ungatedFailRows: 0, gatedEntries: 0, armGateSkips: 0 }
|
|
105
|
+
|
|
106
|
+
const COUNT = process.env.PM_TABLE_COUNT === '1'
|
|
107
|
+
|
|
108
|
+
function siteFn(site: string, fn: unknown): void {
|
|
109
|
+
let s = tableCounters.sites.get(site)
|
|
110
|
+
if (s === undefined) { s = new Set(); tableCounters.sites.set(site, s) }
|
|
111
|
+
s.add(fn)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resetTableCounters(): void {
|
|
115
|
+
tableCounters.rows = 0
|
|
116
|
+
tableCounters.byOp = new Int32Array(64)
|
|
117
|
+
tableCounters.sites = new Map()
|
|
118
|
+
tableCounters.ungatedEntries = 0
|
|
119
|
+
tableCounters.ungatedFails = 0
|
|
120
|
+
tableCounters.ungatedFailRows = 0
|
|
121
|
+
tableCounters.gatedEntries = 0
|
|
122
|
+
tableCounters.armGateSkips = 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const EMPTY_TL: readonly number[] = Object.freeze([])
|
|
126
|
+
const EMPTY_FX: string[] = []
|
|
127
|
+
const ROUTED_FX: string[] = ['routed()']
|
|
128
|
+
/**
|
|
129
|
+
* Does an encoded matcher arm claim this selector key?
|
|
130
|
+
*
|
|
131
|
+
* Reads the ENCODED operands directly rather than rebuilding a
|
|
132
|
+
* `DispatchMatcherCase` and calling `matchesDispatchMatcher`. That detour
|
|
133
|
+
* allocated a matcher shape per arm per parse AND hardcoded
|
|
134
|
+
* `caseInsensitive: false`, silently discarding `{ caseInsensitive: true }` on
|
|
135
|
+
* matcher arms. Case-insensitivity is folded into the operands by the encoder:
|
|
136
|
+
* kinds 3/4 are pre-folded startsWith/endsWith, and a case-insensitive
|
|
137
|
+
* `matches` already carries `i` in its flags.
|
|
138
|
+
*/
|
|
139
|
+
function matcherClaims(m: readonly [number, string, string, number], key: string): boolean {
|
|
140
|
+
switch (m[0]) {
|
|
141
|
+
case 0: return key.startsWith(m[1])
|
|
142
|
+
case 1: return key.endsWith(m[1])
|
|
143
|
+
case 3: return asciiFoldKey(key).startsWith(m[1])
|
|
144
|
+
case 4: return asciiFoldKey(key).endsWith(m[1])
|
|
145
|
+
// A fresh RegExp per test, as `matchesDispatchMatcher` builds — a cached one
|
|
146
|
+
// would carry `lastIndex` across parses whenever the author's pattern was
|
|
147
|
+
// sticky or global.
|
|
148
|
+
default: return new RegExp(m[1], m[2]).test(key)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type Leaf = { _tag: 'leaf'; value: string; span: { start: number; end: number } }
|
|
153
|
+
|
|
154
|
+
function rawEntry(v: unknown, input: string, s: number, e: number): unknown {
|
|
155
|
+
if (typeof v === 'object' && v !== null) {
|
|
156
|
+
const t = (v as { _tag?: string })._tag
|
|
157
|
+
if (t === 'node' || t === 'leaf' || t === 'parseError') return v
|
|
158
|
+
}
|
|
159
|
+
return { _tag: 'leaf', value: typeof v === 'string' ? v : (typeof v === 'object' && v !== null ? input.slice(s, e) : ''), span: { start: s, end: e } }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Same semantics as `trackLinesInto` in `src/table/assemble.ts` and `_trackLines`
|
|
163
|
+
* in `src/table/emit-assembly.ts` — three copies of one scan, and this is the one
|
|
164
|
+
* the emitted engine does not use. (Named for the source lowering before it, which
|
|
165
|
+
* spelled this `LINE_TRACK_DECL`; that module was deleted in `37c57b5`.) */
|
|
166
|
+
function trackLines(ctx: ParseContext, input: string, end: number): void {
|
|
167
|
+
const from = ctx._lineScannedTo ?? 0
|
|
168
|
+
if (end <= from) return
|
|
169
|
+
const starts = ctx._lineStarts
|
|
170
|
+
if (starts === undefined) return
|
|
171
|
+
for (let i = from; i < end; i++) if (input.charCodeAt(i) === 10) starts.push(i + 1)
|
|
172
|
+
ctx._lineScannedTo = end
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function classHas(cls: ResolvedClass, code: number): boolean {
|
|
176
|
+
if (code < 0) return false
|
|
177
|
+
if (code < 128) return cls.ascii[code] === 1
|
|
178
|
+
const hi = cls.hi
|
|
179
|
+
for (let i = 0; i < hi.length; i += 2) if (code >= hi[i]! && code <= hi[i + 1]!) return true
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type Driver = {
|
|
184
|
+
exec: (ip: number, input: string, pos: number, ctx: ParseContext) => unknown
|
|
185
|
+
end: () => number
|
|
186
|
+
/** Per-parse reset of the trivia leaf swap. See `begin` in `makeDriver`. */
|
|
187
|
+
begin: (ctx: ParseContext) => void
|
|
188
|
+
/** Restore a suspended outer entry after a re-entrant parse. */
|
|
189
|
+
finish: () => void
|
|
190
|
+
/** Ambient `scanSkip` sets, rebuilt from `prog.scanSkip`, indexed as encoded. */
|
|
191
|
+
scanSkip: readonly (readonly Combinator<unknown>[])[]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function makeDriver(
|
|
195
|
+
code: Int32Array,
|
|
196
|
+
k: readonly unknown[],
|
|
197
|
+
fns: readonly unknown[],
|
|
198
|
+
cc: readonly ResolvedClass[],
|
|
199
|
+
fx: readonly (readonly string[])[],
|
|
200
|
+
disp: readonly ResolvedDispatch[],
|
|
201
|
+
dsp: readonly ResolvedDispatchSpec[],
|
|
202
|
+
trivia: readonly unknown[],
|
|
203
|
+
triviaScan: readonly (FastTriviaScanner | null)[],
|
|
204
|
+
triviaLabelled: readonly boolean[],
|
|
205
|
+
prog: TableProgram,
|
|
206
|
+
/**
|
|
207
|
+
* The assembly's end-position cell (`_pfEnd` in emitted code). INJECTED
|
|
208
|
+
* rather than owned, so a mixed assembly's driver and its emitted pieces
|
|
209
|
+
* write one slot — see `cell.ts`.
|
|
210
|
+
*/
|
|
211
|
+
EC: EndCell,
|
|
212
|
+
): Driver {
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* THE INSTALLED TRIVIA LEAF — G5's *"some swaps on rules or sub-rules
|
|
216
|
+
* (leafs)"*, which is the half of that sentence the driver had not honoured.
|
|
217
|
+
*
|
|
218
|
+
* `SCAN` is the SPECIALISED scanner for the trivia currently in scope, chosen
|
|
219
|
+
* once at `OP_SCOPE` from `triviaScan` and null when no swap is legal. The
|
|
220
|
+
* generic path it replaces called `advanceTrivia` per sequence term, and that
|
|
221
|
+
* function re-derived the scanner through a WeakMap and re-tested the same
|
|
222
|
+
* options on every call — for json that was 22.0% of the table's time against
|
|
223
|
+
* codegen's 6.1% for the identical work.
|
|
224
|
+
*
|
|
225
|
+
* `FAST` is the one condition that is a property of the PARSE rather than of
|
|
226
|
+
* the scope (`ctx.trackLines`, fixed by `run()` at entry), so it is read once
|
|
227
|
+
* per parse in the entry wrapper and folded into `SCAN` at each scope. Nothing
|
|
228
|
+
* on the term path consults the table or an option.
|
|
229
|
+
*/
|
|
230
|
+
let SCAN: FastTriviaScanner | null = null
|
|
231
|
+
let FAST = false
|
|
232
|
+
/**
|
|
233
|
+
* IS THIS A RECOVERY TABLE? Table data, decided once when the driver is built,
|
|
234
|
+
* exactly as the assembler decides it — the two engines must agree here or a
|
|
235
|
+
* tolerant parse diverges on the one path no identity digest covers, since
|
|
236
|
+
* `errors` and `expected` are not in it.
|
|
237
|
+
*
|
|
238
|
+
* Recovery is DORMANT until a parse sets `ctx._tolerant`.
|
|
239
|
+
*/
|
|
240
|
+
const REC = prog.rec === 1
|
|
241
|
+
// Line tracking is table data, selected once at encode time. `OP_EXPECT` has
|
|
242
|
+
// no tracked opcode variant, so it reads this assembly-wide constant when it
|
|
243
|
+
// constructs the zero-width recovery diagnostic.
|
|
244
|
+
const LINES = prog.lines === 1
|
|
245
|
+
/**
|
|
246
|
+
* Sync sentinels by char-class index, built at most once each. The ranges are
|
|
247
|
+
* recoverable from the class spec, and `firstSetSentinel` is the interpreter's
|
|
248
|
+
* own constructor — the same object codegen builds per publish through
|
|
249
|
+
* `_ctx._rec.sentinel(...)`.
|
|
250
|
+
*/
|
|
251
|
+
const sentinels = new Map<number, Combinator<unknown> | undefined>()
|
|
252
|
+
function sentinelFor(cls: number): Combinator<unknown> | undefined {
|
|
253
|
+
if (cls < 0) return undefined
|
|
254
|
+
if (sentinels.has(cls)) return sentinels.get(cls)
|
|
255
|
+
const spec = prog.cc[cls]!
|
|
256
|
+
const ranges = decodeClassSpec(spec)
|
|
257
|
+
const made = firstSetSentinel({ kind: 'ranges', ranges }) ?? undefined
|
|
258
|
+
sentinels.set(cls, made)
|
|
259
|
+
return made
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Is this parse's host a CST-output host? `ctx.build` is fixed by `run()` before
|
|
263
|
+
* the entry is called, so this is a PER-PARSE constant that `OP_NODE` was
|
|
264
|
+
* re-deriving on every node. Decided once in `begin`.
|
|
265
|
+
*/
|
|
266
|
+
let HOSTCST = false
|
|
267
|
+
let HOSTREADSCHILDREN = true
|
|
268
|
+
let HOSTCAPTURETRIVIA: ((type: string) => boolean) | undefined
|
|
269
|
+
type DriverFrame = {
|
|
270
|
+
scan: FastTriviaScanner | null
|
|
271
|
+
fast: boolean
|
|
272
|
+
hostCst: boolean
|
|
273
|
+
hostReadsChildren: boolean
|
|
274
|
+
hostCaptureTrivia: ((type: string) => boolean) | undefined
|
|
275
|
+
end: number
|
|
276
|
+
}
|
|
277
|
+
// The ordinary path stays allocation-free. A frame exists only while a host
|
|
278
|
+
// callback re-enters this same reference driver before its outer parse ends.
|
|
279
|
+
const frames: DriverFrame[] = []
|
|
280
|
+
let depth = 0
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Capture goes through the runtime's own buffer (`src/cst/capture-buffer.ts`),
|
|
284
|
+
* not a hand-rolled copy: it is already the shared mechanism the interpreter
|
|
285
|
+
* uses, and a second implementation of it is precisely the duplication G5 is
|
|
286
|
+
* about removing.
|
|
287
|
+
*/
|
|
288
|
+
function pushLeaf(ctx: ParseContext, value: string, s: number, e: number): void {
|
|
289
|
+
const lf: Leaf = { _tag: 'leaf', value, span: { start: s, end: e } }
|
|
290
|
+
pushCstLeaf(ctx, lf)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Is there anything a failed branch could need to UNRECORD?
|
|
295
|
+
*
|
|
296
|
+
* `saveCstMark` allocates a 5-field object and `saveTriviaMark` allocates that
|
|
297
|
+
* plus a 7-field one; `rollbackTrivia` allocates a third to call through. In
|
|
298
|
+
* the driver those sat in the repetition and choice loops, so a parse
|
|
299
|
+
* allocated three-to-four objects PER ITEM that codegen never allocates (it
|
|
300
|
+
* emits scalar locals). That is a per-item cost, which is what the widening
|
|
301
|
+
* gap with input size was: +82% small, +275% large.
|
|
302
|
+
*
|
|
303
|
+
* When every sink either rollback touches is absent, nothing was recorded,
|
|
304
|
+
* so nothing needs unrecording and the mark need not exist. A grammar with no
|
|
305
|
+
* `node()` and no trivia log — json, csv — never allocates one.
|
|
306
|
+
*/
|
|
307
|
+
function rollbackNeeded(ctx: ParseContext): boolean {
|
|
308
|
+
return ctx._cstBuf !== undefined
|
|
309
|
+
|| ctx._cstLeaves !== undefined
|
|
310
|
+
|| ctx._cstRawChildren !== undefined
|
|
311
|
+
|| ctx._cstTriviaLog !== undefined
|
|
312
|
+
|| ctx._fields !== undefined
|
|
313
|
+
|| ctx._errors !== undefined
|
|
314
|
+
|| ctx._triviaLog !== undefined
|
|
315
|
+
|| ctx._rootTriviaLog !== undefined
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The lead character at `pos`, as the first-set tables index it.
|
|
320
|
+
*
|
|
321
|
+
* `charCodeAt` is the hot read; only a high surrogate needs the code-point
|
|
322
|
+
* decode, so that branch is paid on astral input and nowhere else.
|
|
323
|
+
*/
|
|
324
|
+
/**
|
|
325
|
+
* Skip trivia at `cur`, through the INSTALLED leaf when one is installed.
|
|
326
|
+
*
|
|
327
|
+
* The guard is the exact condition under which the generic functions take
|
|
328
|
+
* their own fast branch, restated over context fields so it costs loads rather
|
|
329
|
+
* than a call:
|
|
330
|
+
*
|
|
331
|
+
* - `advanceTrivia` (no deferred commit) runs `fast(input, cur)` whenever a
|
|
332
|
+
* scanner exists and `trackLines` is off — folded into `SCAN` already.
|
|
333
|
+
* - `scanTrivia` (deferred commit) runs it and returns a NO-OP commit when
|
|
334
|
+
* `_triviaLog` is unset and nothing is capturing trivia into a CST buffer.
|
|
335
|
+
* `_rootTriviaLog` is deliberately absent from this test: root rows are
|
|
336
|
+
* only ever written on the LABELLED scan path (see `OP_SCOPE`), and a
|
|
337
|
+
* labelled trivia never gets a `SCAN` installed.
|
|
338
|
+
*
|
|
339
|
+
* Anything outside that falls through to the shared implementations unchanged,
|
|
340
|
+
* so recording, labels and line tracking keep exactly one implementation.
|
|
341
|
+
*/
|
|
342
|
+
function skipTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
343
|
+
const s = SCAN
|
|
344
|
+
if (s !== null
|
|
345
|
+
&& ctx._triviaLog === undefined
|
|
346
|
+
&& !(ctx.captureTrivia === true && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined))) {
|
|
347
|
+
return s(input, cur)
|
|
348
|
+
}
|
|
349
|
+
// CAPTURE IS NOT A REASON TO LEAVE THE SCANNER. The recording an installed
|
|
350
|
+
// scanner's scope can owe is one `(cur, end)` row, so it is written here
|
|
351
|
+
// rather than through a deferred `{ end, commit }` pair per term. Appended
|
|
352
|
+
// rather than folded into the test above so the non-capturing path — every
|
|
353
|
+
// grammar that already had a scanner — keeps the exact branch it had.
|
|
354
|
+
if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
|
|
355
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
356
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
357
|
+
scan.commit()
|
|
358
|
+
return scan.end
|
|
359
|
+
}
|
|
360
|
+
return advanceTrivia(input, cur, ctx)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function lead(input: string, pos: number): number {
|
|
364
|
+
if (pos >= input.length) return -1
|
|
365
|
+
const c = input.charCodeAt(pos)
|
|
366
|
+
if (c < 0xd800 || c > 0xdbff) return c
|
|
367
|
+
return input.codePointAt(pos) ?? c
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function exec(ip: number, input: string, pos: number, ctx: ParseContext): unknown {
|
|
371
|
+
if (COUNT) { tableCounters.rows++; tableCounters.byOp[code[ip]!]!++ }
|
|
372
|
+
switch (code[ip]) {
|
|
373
|
+
case OP_LIT: {
|
|
374
|
+
const s = k[code[ip + 1]!] as string
|
|
375
|
+
if (input.startsWith(s, pos)) {
|
|
376
|
+
const e = pos + s.length
|
|
377
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
378
|
+
EC.e = e
|
|
379
|
+
return s
|
|
380
|
+
}
|
|
381
|
+
{
|
|
382
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
383
|
+
ctx._fe = pos; ctx._fx = xf
|
|
384
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
385
|
+
}
|
|
386
|
+
return FAIL
|
|
387
|
+
}
|
|
388
|
+
case OP_RX: {
|
|
389
|
+
const re = k[code[ip + 1]!] as RegExp
|
|
390
|
+
re.lastIndex = pos
|
|
391
|
+
const m = re.exec(input)
|
|
392
|
+
if (m !== null) {
|
|
393
|
+
const v = m[0]
|
|
394
|
+
const e = pos + v.length
|
|
395
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
|
|
396
|
+
EC.e = e
|
|
397
|
+
return v
|
|
398
|
+
}
|
|
399
|
+
{
|
|
400
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
401
|
+
ctx._fe = pos; ctx._fx = xf
|
|
402
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
403
|
+
}
|
|
404
|
+
return FAIL
|
|
405
|
+
}
|
|
406
|
+
case OP_LIT_TRACK: {
|
|
407
|
+
const s = k[code[ip + 1]!] as string
|
|
408
|
+
if (input.startsWith(s, pos)) {
|
|
409
|
+
const e = pos + s.length
|
|
410
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
411
|
+
trackLines(ctx, input, e)
|
|
412
|
+
EC.e = e
|
|
413
|
+
return s
|
|
414
|
+
}
|
|
415
|
+
{
|
|
416
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
417
|
+
ctx._fe = pos; ctx._fx = xf
|
|
418
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
419
|
+
}
|
|
420
|
+
return FAIL
|
|
421
|
+
}
|
|
422
|
+
case OP_RX_TRACK: {
|
|
423
|
+
const re = k[code[ip + 1]!] as RegExp
|
|
424
|
+
re.lastIndex = pos
|
|
425
|
+
const m = re.exec(input)
|
|
426
|
+
if (m !== null) {
|
|
427
|
+
const v = m[0]
|
|
428
|
+
const e = pos + v.length
|
|
429
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
|
|
430
|
+
trackLines(ctx, input, e)
|
|
431
|
+
EC.e = e
|
|
432
|
+
return v
|
|
433
|
+
}
|
|
434
|
+
{
|
|
435
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
436
|
+
ctx._fe = pos; ctx._fx = xf
|
|
437
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
438
|
+
}
|
|
439
|
+
return FAIL
|
|
440
|
+
}
|
|
441
|
+
case OP_LIT_CI:
|
|
442
|
+
case OP_LIT_CI_TRACK: {
|
|
443
|
+
const s = k[code[ip + 1]!] as string
|
|
444
|
+
const e = pos + s.length
|
|
445
|
+
// Yields the INPUT's casing (literal.ts:86), not the literal's — a node
|
|
446
|
+
// built from this carries the source text, and normalising it here would
|
|
447
|
+
// silently rewrite the user's CSS.
|
|
448
|
+
const matched = input.slice(pos, e)
|
|
449
|
+
if (asciiFoldEq(matched, s)) {
|
|
450
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, matched, pos, e)
|
|
451
|
+
if (code[ip] === OP_LIT_CI_TRACK) trackLines(ctx, input, e)
|
|
452
|
+
EC.e = e
|
|
453
|
+
return matched
|
|
454
|
+
}
|
|
455
|
+
{
|
|
456
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
457
|
+
ctx._fe = pos; ctx._fx = xf
|
|
458
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
459
|
+
}
|
|
460
|
+
return FAIL
|
|
461
|
+
}
|
|
462
|
+
case OP_EMPTY:
|
|
463
|
+
EC.e = pos
|
|
464
|
+
return ''
|
|
465
|
+
|
|
466
|
+
case OP_GUARD: {
|
|
467
|
+
if ((fns[code[ip + 1]!] as (s: unknown) => boolean)(ctx.state)) {
|
|
468
|
+
EC.e = pos
|
|
469
|
+
return null
|
|
470
|
+
}
|
|
471
|
+
ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]
|
|
472
|
+
return FAIL
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Reached only where there is NO boundary to test — a bare choice arm, a
|
|
476
|
+
// `node()`'s whole body, a repeat item. The SEQ cases above intercept the
|
|
477
|
+
// row wherever it CAN be answered, so arriving here means the same misuse
|
|
478
|
+
// the interpreter refuses (adjacency.ts), and it refuses identically.
|
|
479
|
+
case OP_ADJ:
|
|
480
|
+
throw adjacencyMisuse(code[ip + 1] === 1 ? 'notAdjacent' : 'adjacent')
|
|
481
|
+
|
|
482
|
+
case OP_GATE: {
|
|
483
|
+
// Skipped under a probe / tolerant recovery — see `assemble.ts`.
|
|
484
|
+
if (ctx._probe === undefined && !ctx._tolerant && !classHas(cc[code[ip + 1]!]!, lead(input, pos))) {
|
|
485
|
+
ctx._fe = pos; ctx._fx = fx[code[ip + 3]!] as string[]
|
|
486
|
+
return FAIL
|
|
487
|
+
}
|
|
488
|
+
return exec(code[ip + 2]!, input, pos, ctx)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
case OP_LABEL: {
|
|
492
|
+
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
493
|
+
// `_fe` untouched — `map.ts:84` keeps `result.span`.
|
|
494
|
+
if (v === FAIL) ctx._fx = fx[code[ip + 2]!] as string[]
|
|
495
|
+
return v
|
|
496
|
+
}
|
|
497
|
+
case OP_RULE:
|
|
498
|
+
return exec(code[ip + 1]!, input, pos, ctx)
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A COUNTER ROW IS TRANSPARENT HERE, and that is the whole of its handling.
|
|
502
|
+
*
|
|
503
|
+
* This module is the VALUE-IDENTITY reference the assembler is bisected
|
|
504
|
+
* against, and a coverage counter is not a value: it changes nothing about
|
|
505
|
+
* what is parsed, what is built, where a failure lands or what it expected.
|
|
506
|
+
* Passing the row through keeps a coverage-encoded table runnable under the
|
|
507
|
+
* reference — which is what makes the identity sweep able to run one at all
|
|
508
|
+
* — and keeps this file exactly as behaviourally identical as it was.
|
|
509
|
+
*
|
|
510
|
+
* It does NOT record hits. Counting is the assembler's, on the product path;
|
|
511
|
+
* a second implementation of it here would be a second place for the phase
|
|
512
|
+
* semantics (entry for a rule, success for an arm) to drift.
|
|
513
|
+
*/
|
|
514
|
+
case OP_COV:
|
|
515
|
+
return exec(code[ip + 1]!, input, pos, ctx)
|
|
516
|
+
|
|
517
|
+
case OP_LIVE: {
|
|
518
|
+
// A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
|
|
519
|
+
// codegen's `emitRuntimeFallback`. Its result IS the interpreter's, so
|
|
520
|
+
// `expected`/`span` propagate verbatim and `committed` is copied onto the
|
|
521
|
+
// ctx bit both drivers use for the cut.
|
|
522
|
+
const c = fns[code[ip + 1]!] as Combinator<unknown>
|
|
523
|
+
const r = c.parse(input, pos, ctx)
|
|
524
|
+
if (!r.ok) {
|
|
525
|
+
ctx._fe = r.span.start
|
|
526
|
+
ctx._fx = r.expected
|
|
527
|
+
ctx._fc = r.committed === true
|
|
528
|
+
return FAIL
|
|
529
|
+
}
|
|
530
|
+
EC.e = r.span.end
|
|
531
|
+
return r.value
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
case OP_EXPECT: {
|
|
535
|
+
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
536
|
+
if (v !== FAIL) return v
|
|
537
|
+
// Mirrors src/combinators/expect.ts:135-150 — succeed at zero width with
|
|
538
|
+
// a ParseError value, and record it in the flat sink when present.
|
|
539
|
+
// The table-wide tracking bit selects the same shared span helper used
|
|
540
|
+
// for tracked node and root spans. The failed child has already scanned
|
|
541
|
+
// through `pos`, so its terminal rows have populated `_lineStarts` before
|
|
542
|
+
// this zero-width diagnostic is constructed.
|
|
543
|
+
const span = LINES ? spanLines(ctx, pos, pos) : { start: pos, end: pos }
|
|
544
|
+
const err = { _tag: 'parseError' as const, span, expected: fx[code[ip + 2]!] as string[] }
|
|
545
|
+
ctx._errors?.push(err)
|
|
546
|
+
// A TOLERANT `expect()` also embeds the error in the TREE (expect.ts:150,
|
|
547
|
+
// codegen.ts:4470), so a tree walk finds every diagnostic and it survives
|
|
548
|
+
// incremental subtree reuse — the flat sink alone is not the contract.
|
|
549
|
+
if (REC && ctx._tolerant === true) captureError(ctx, err)
|
|
550
|
+
// See assemble.ts OP_EXPECT: the recovered failure is a SUCCESS, so the
|
|
551
|
+
// child's commit bit must not survive to cut an enclosing choice.
|
|
552
|
+
ctx._fc = false
|
|
553
|
+
EC.e = pos
|
|
554
|
+
return err
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
case OP_ROUTED: {
|
|
558
|
+
// Mirrors src/combinators/dispatch.ts `routed()`.
|
|
559
|
+
const item = ctx._routed
|
|
560
|
+
if (item === undefined || pos !== item.span.start) {
|
|
561
|
+
const fb = code[ip + 1]!
|
|
562
|
+
if (fb >= 0) return exec(fb, input, pos, ctx)
|
|
563
|
+
ctx._fe = pos; ctx._fx = ROUTED_FX
|
|
564
|
+
return FAIL
|
|
565
|
+
}
|
|
566
|
+
if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: item.value, span: item.span })
|
|
567
|
+
EC.e = item.span.end
|
|
568
|
+
return item.value
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
case OP_DISPATCH: {
|
|
572
|
+
// The selector runs ONCE; its value picks the arm. Resolution order is
|
|
573
|
+
// exact key -> ASCII-folded key -> matchers in declaration order ->
|
|
574
|
+
// otherwise, mirroring src/combinators/dispatch.ts:325.
|
|
575
|
+
const spec = dsp[code[ip + 2]!]!
|
|
576
|
+
const armBase = ip + 6
|
|
577
|
+
const selectorMark = saveTriviaMark(ctx)
|
|
578
|
+
const selVal = exec(code[ip + 1]!, input, pos, ctx)
|
|
579
|
+
if (selVal === FAIL) return FAIL
|
|
580
|
+
const selEnd = EC.e
|
|
581
|
+
const key = selVal as string
|
|
582
|
+
|
|
583
|
+
let arm = spec.byKey.get(key)
|
|
584
|
+
if (arm === undefined && spec.byFold.size > 0) arm = spec.byFold.get(asciiFoldKey(key))
|
|
585
|
+
if (arm === undefined) {
|
|
586
|
+
for (let i = 0; i < spec.match.length; i++) {
|
|
587
|
+
const m = spec.match[i]!
|
|
588
|
+
if (matcherClaims(m, key)) {
|
|
589
|
+
arm = m[3]
|
|
590
|
+
break
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
let target: number
|
|
596
|
+
let usesRouted: boolean
|
|
597
|
+
if (arm === undefined) {
|
|
598
|
+
const other = code[ip + 3]!
|
|
599
|
+
if (other < 0) {
|
|
600
|
+
// No branch and no fallback: fail AT THE SELECTOR'S END, not at pos.
|
|
601
|
+
ctx._fe = selEnd
|
|
602
|
+
ctx._fx = spec.expected as string[]
|
|
603
|
+
return FAIL
|
|
604
|
+
}
|
|
605
|
+
target = other
|
|
606
|
+
usesRouted = code[ip + 4]! === 1
|
|
607
|
+
} else {
|
|
608
|
+
target = code[armBase + arm]!
|
|
609
|
+
usesRouted = spec.routed[arm] === 1
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const savedRouted = ctx._routed
|
|
613
|
+
let mark = saveTriviaMark(ctx)
|
|
614
|
+
if (usesRouted) {
|
|
615
|
+
rollbackTrivia(ctx, selectorMark)
|
|
616
|
+
mark = saveTriviaMark(ctx)
|
|
617
|
+
ctx._routed = { value: key, span: { start: pos, end: selEnd } }
|
|
618
|
+
}
|
|
619
|
+
const v = exec(target, input, usesRouted ? pos : selEnd, ctx)
|
|
620
|
+
if (usesRouted) ctx._routed = savedRouted
|
|
621
|
+
if (v === FAIL) {
|
|
622
|
+
rollbackTrivia(ctx, mark)
|
|
623
|
+
// The interpreter marks a failed dispatch branch COMMITTED: the
|
|
624
|
+
// selector already matched, so an enclosing choice must not treat this
|
|
625
|
+
// as "try the next arm".
|
|
626
|
+
ctx._fc = true
|
|
627
|
+
return FAIL
|
|
628
|
+
}
|
|
629
|
+
// The end cell already holds the branch's end — dispatch's span runs from `pos`
|
|
630
|
+
// to there, which is what the caller reads.
|
|
631
|
+
return [key, v]
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
case OP_FIELD: {
|
|
635
|
+
const v = exec(code[ip + 2]!, input, pos, ctx)
|
|
636
|
+
if (v === FAIL) return FAIL
|
|
637
|
+
// Conditional on a live sink, exactly as src/combinators/map.ts has it:
|
|
638
|
+
// a `field()` outside any field-reading node costs nothing.
|
|
639
|
+
ctx._fields?.push({ name: k[code[ip + 1]!] as string, value: v, span: { start: pos, end: EC.e } })
|
|
640
|
+
return v
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
case OP_TOKEN: {
|
|
644
|
+
// Mirrors src/combinators/token.ts:21-65 exactly, including that
|
|
645
|
+
// `_triviaLog`/`_rootTriviaLog` are DELETED rather than set undefined
|
|
646
|
+
// and restored by presence, and that the leaf is pushed only when the
|
|
647
|
+
// caller was capturing BEFORE the sinks were cleared.
|
|
648
|
+
const sTrivia = ctx.trivia, sKinds = ctx.triviaKindLabels
|
|
649
|
+
const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
|
|
650
|
+
const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
|
|
651
|
+
const sOuterTl = ctx._triviaLog, sRootTl = ctx._rootTriviaLog
|
|
652
|
+
const wasCapturing = cstCaptureActive(ctx)
|
|
653
|
+
|
|
654
|
+
const sScan = SCAN
|
|
655
|
+
SCAN = null
|
|
656
|
+
ctx.trivia = undefined
|
|
657
|
+
ctx.triviaKindLabels = undefined
|
|
658
|
+
ctx._cstBuf = undefined
|
|
659
|
+
ctx._cstChildren = undefined
|
|
660
|
+
ctx._cstLeaves = undefined
|
|
661
|
+
ctx._cstRawChildren = undefined
|
|
662
|
+
ctx._cstTriviaLog = undefined
|
|
663
|
+
ctx._triviaLog = undefined
|
|
664
|
+
ctx._rootTriviaLog = undefined
|
|
665
|
+
|
|
666
|
+
let v: unknown
|
|
667
|
+
try {
|
|
668
|
+
v = exec(code[ip + 1]!, input, pos, ctx)
|
|
669
|
+
} finally {
|
|
670
|
+
SCAN = sScan
|
|
671
|
+
ctx.trivia = sTrivia
|
|
672
|
+
ctx.triviaKindLabels = sKinds
|
|
673
|
+
ctx._cstBuf = sBuf
|
|
674
|
+
ctx._cstChildren = sChildren
|
|
675
|
+
ctx._cstLeaves = sLeaves
|
|
676
|
+
ctx._cstRawChildren = sRaw
|
|
677
|
+
ctx._cstTriviaLog = sTl
|
|
678
|
+
ctx._triviaLog = sOuterTl
|
|
679
|
+
ctx._rootTriviaLog = sRootTl
|
|
680
|
+
}
|
|
681
|
+
if (v === FAIL) return FAIL
|
|
682
|
+
const end = EC.e
|
|
683
|
+
const value = input.slice(pos, end)
|
|
684
|
+
if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
|
|
685
|
+
EC.e = end
|
|
686
|
+
return value
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
case OP_SCAN: {
|
|
690
|
+
const c = scans[code[ip + 1]!]!
|
|
691
|
+
const r = c.parse(input, pos, ctx)
|
|
692
|
+
if (!r.ok) {
|
|
693
|
+
ctx._fe = r.span.start
|
|
694
|
+
ctx._fx = (r.expected ?? EMPTY_FX) as string[]
|
|
695
|
+
return FAIL
|
|
696
|
+
}
|
|
697
|
+
EC.e = r.span.end
|
|
698
|
+
return r.value
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
case OP_WITHCTX: {
|
|
702
|
+
const saved = ctx.state
|
|
703
|
+
ctx.state = k[code[ip + 1]!]
|
|
704
|
+
try { return exec(code[ip + 2]!, input, pos, ctx) }
|
|
705
|
+
finally { ctx.state = saved }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
case OP_SCOPE:
|
|
709
|
+
case OP_SCOPE_CAP: {
|
|
710
|
+
const ki = code[ip + 1]!
|
|
711
|
+
const saved = ctx.trivia
|
|
712
|
+
const savedLabels = ctx.triviaKindLabels
|
|
713
|
+
const scopeTrivia = ki < 0 ? undefined : (trivia[ki] as ParseContext['trivia'])
|
|
714
|
+
const savedScan = SCAN
|
|
715
|
+
// THE SWAP. Chosen from table data at scope entry, never per term. A
|
|
716
|
+
// labelled trivia is excluded here rather than tested later: `scanTrivia`
|
|
717
|
+
// suppresses its own fast path when labels are present, so a swap there
|
|
718
|
+
// would drop the labelled records silently.
|
|
719
|
+
SCAN = FAST && ki >= 0 && !triviaLabelled[ki]! ? triviaScan[ki]! : null
|
|
720
|
+
ctx.trivia = scopeTrivia
|
|
721
|
+
// A scope installs its trivia's KIND LABELS too. Root-trivia rows are
|
|
722
|
+
// only ever written on the labelled scan path (trivia-skip.ts:212) — the
|
|
723
|
+
// unlabelled fast scanner returns before any root logging and does not
|
|
724
|
+
// even test `_rootTriviaLog` — so a scope that sets trivia without its
|
|
725
|
+
// labels captures NOTHING at the root, silently.
|
|
726
|
+
ctx.triviaKindLabels = scopeTrivia?._meta.triviaKindLabels
|
|
727
|
+
// SCOPE_CAP additionally turns capture ON for the child. It is an OR with
|
|
728
|
+
// whatever the enclosing context already asked for (`grammar.ts:129`), so
|
|
729
|
+
// the RESTORE puts the saved value back rather than writing `false` — an
|
|
730
|
+
// inner scope must not switch an outer capture off.
|
|
731
|
+
const savedCap = ctx.captureTrivia
|
|
732
|
+
if (code[ip] === OP_SCOPE_CAP) ctx.captureTrivia = true
|
|
733
|
+
// ROOT-CAPTURE POLICY (ip + 3). Bit 1 = `rootCapture: 'opaque'`, which is
|
|
734
|
+
// NOT inert: it suppresses selected root rows for the region
|
|
735
|
+
// (`grammar.ts:141`), and the flag is restored here because the table
|
|
736
|
+
// shares one ctx where `parser()` copies it. Bit 2 = the unclassified-scope
|
|
737
|
+
// refusal `grammar.ts:98` raises.
|
|
738
|
+
const policy = code[ip + 3]!
|
|
739
|
+
if ((policy & 2) !== 0) refuseUnclassifiedRootScope(ctx._rootTriviaStrictScopes)
|
|
740
|
+
const savedRootCap = ctx._rootTriviaCapture
|
|
741
|
+
if ((policy & 1) !== 0) ctx._rootTriviaCapture = false
|
|
742
|
+
const v = exec(code[ip + 2]!, input, pos, ctx)
|
|
743
|
+
if ((policy & 1) !== 0) ctx._rootTriviaCapture = savedRootCap
|
|
744
|
+
ctx.captureTrivia = savedCap
|
|
745
|
+
ctx.trivia = saved
|
|
746
|
+
ctx.triviaKindLabels = savedLabels
|
|
747
|
+
SCAN = savedScan
|
|
748
|
+
return v
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
case OP_SEQ:
|
|
752
|
+
case OP_SEQV:
|
|
753
|
+
case OP_SEQX: {
|
|
754
|
+
const fused = code[ip] === OP_SEQX
|
|
755
|
+
const base = fused ? ip + 3 : ip + 2
|
|
756
|
+
const n = code[fused ? ip + 2 : ip + 1]!
|
|
757
|
+
const values: unknown[] | undefined = code[ip] === OP_SEQV ? undefined : []
|
|
758
|
+
let cur = pos
|
|
759
|
+
// RECOVERY SYNC PUBLISH. A recovery table carries one follow-set class per
|
|
760
|
+
// term after the child slots; before each term the sentinel for it becomes
|
|
761
|
+
// `ctx._sync`, so a list nested in that term resyncs to this sequence's
|
|
762
|
+
// enclosing delimiter with nothing annotated (combinators/sequence.ts:75).
|
|
763
|
+
// Restored on every exit, as `sequence()`'s
|
|
764
|
+
// `finally` does. Strict tables never enter any of this.
|
|
765
|
+
const inheritedSync = REC ? ctx._sync : undefined
|
|
766
|
+
for (let i = 0; i < n; i++) {
|
|
767
|
+
if (REC) ctx._sync = sentinelFor(code[base + n + i]!) ?? inheritedSync
|
|
768
|
+
const child = code[base + i]!
|
|
769
|
+
// ADJACENCY IS TESTED AT `cur`, BEFORE THE BOUNDARY'S TRIVIA SCAN, and
|
|
770
|
+
// moves nothing — the next term re-scans the same gap and keeps its own
|
|
771
|
+
// commit/rewind decision, so spans, tree and trivia log are identical to
|
|
772
|
+
// the same sequence without the marker (combinators/sequence.ts:124).
|
|
773
|
+
// Running it through `exec` at the POST-scan position would find the gap
|
|
774
|
+
// already consumed and answer "adjacent" unconditionally.
|
|
775
|
+
if (code[child] === OP_ADJ) {
|
|
776
|
+
if (COUNT) { tableCounters.rows++; tableCounters.byOp[OP_ADJ]!++ }
|
|
777
|
+
const ki = code[child + 2]!
|
|
778
|
+
if (!adjacencyHolds(input, cur, ctx, code[child + 1] === 1, ki < 0 ? undefined : k[ki] as readonly string[])) {
|
|
779
|
+
ctx._fe = cur; ctx._fx = fx[code[child + 3]!] as string[]
|
|
780
|
+
{ if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
781
|
+
}
|
|
782
|
+
if (values !== undefined) values.push(null)
|
|
783
|
+
continue
|
|
784
|
+
}
|
|
785
|
+
if (i > 0 && ctx.trivia !== undefined) {
|
|
786
|
+
// SCALAR MARKS — `saveTriviaMark` allocated TWICE per term (its own
|
|
787
|
+
// seven-field object plus the five-field CST mark it delegates to).
|
|
788
|
+
const need = rollbackNeeded(ctx)
|
|
789
|
+
const mRaw = need ? cstRawLen(ctx) : 0
|
|
790
|
+
const mTl = need ? cstTlLen(ctx) : 0
|
|
791
|
+
const mLv = need ? cstLeavesLen(ctx) : 0
|
|
792
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
793
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
794
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
795
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
796
|
+
const scanEnd = skipTrivia(input, cur, ctx)
|
|
797
|
+
const v = exec(child, input, scanEnd, ctx)
|
|
798
|
+
if (v === FAIL) { if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
799
|
+
if (EC.e > scanEnd) cur = EC.e
|
|
800
|
+
else if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
801
|
+
if (values !== undefined) values.push(v)
|
|
802
|
+
continue
|
|
803
|
+
}
|
|
804
|
+
// TERMINAL FAST PATH. Terminals are the majority of executed
|
|
805
|
+
// instructions, and reaching one through `exec` costs a JS call frame
|
|
806
|
+
// plus a switch dispatch that the emitted code does not pay. Running
|
|
807
|
+
// LIT/RX in place removes both. The duplication is in the DRIVER,
|
|
808
|
+
// which ships once for every grammar — the cost this design trades on.
|
|
809
|
+
const cop = code[child]
|
|
810
|
+
// The two inline terminals below execute a ROW without going through
|
|
811
|
+
// `exec`, so the counter has to see them or the row count understates
|
|
812
|
+
// exactly where the driver already won.
|
|
813
|
+
if (COUNT && (cop === OP_LIT || cop === OP_RX)) { tableCounters.rows++; tableCounters.byOp[cop]!++ }
|
|
814
|
+
if (cop === OP_LIT) {
|
|
815
|
+
const lit = k[code[child + 1]!] as string
|
|
816
|
+
if (!input.startsWith(lit, cur)) {
|
|
817
|
+
ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
|
|
818
|
+
{ if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
819
|
+
}
|
|
820
|
+
const e = cur + lit.length
|
|
821
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, lit, cur, e)
|
|
822
|
+
if (values !== undefined) values.push(lit)
|
|
823
|
+
cur = e
|
|
824
|
+
continue
|
|
825
|
+
}
|
|
826
|
+
if (cop === OP_RX) {
|
|
827
|
+
const re = k[code[child + 1]!] as RegExp
|
|
828
|
+
re.lastIndex = cur
|
|
829
|
+
const m = re.exec(input)
|
|
830
|
+
if (m === null) {
|
|
831
|
+
ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
|
|
832
|
+
{ if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
833
|
+
}
|
|
834
|
+
const mv = m[0]
|
|
835
|
+
const e = cur + mv.length
|
|
836
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, mv, cur, e)
|
|
837
|
+
if (values !== undefined) values.push(mv)
|
|
838
|
+
cur = e
|
|
839
|
+
continue
|
|
840
|
+
}
|
|
841
|
+
const v = exec(child, input, cur, ctx)
|
|
842
|
+
if (v === FAIL) { if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
843
|
+
if (values !== undefined) values.push(v)
|
|
844
|
+
cur = EC.e
|
|
845
|
+
}
|
|
846
|
+
if (REC) ctx._sync = inheritedSync
|
|
847
|
+
EC.e = cur
|
|
848
|
+
if (fused) {
|
|
849
|
+
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
850
|
+
if (COUNT) siteFn('SEQX fn()', fn)
|
|
851
|
+
return fn(values, { start: pos, end: cur })
|
|
852
|
+
}
|
|
853
|
+
return values
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
case OP_CHOICE: {
|
|
857
|
+
const d = code[ip + 1]!
|
|
858
|
+
const base = ip + 4
|
|
859
|
+
const choiceFx = fx[code[ip + 3]!] as string[]
|
|
860
|
+
// Report the union, at the choice's own position, on every exit that
|
|
861
|
+
// fails — matching both engines and, more importantly, never handing a
|
|
862
|
+
// user an empty expected set.
|
|
863
|
+
const failChoice = (): typeof FAIL => {
|
|
864
|
+
ctx._fe = pos
|
|
865
|
+
ctx._fx = choiceFx
|
|
866
|
+
return FAIL
|
|
867
|
+
}
|
|
868
|
+
const table = disp[d]!
|
|
869
|
+
const c = lead(input, pos)
|
|
870
|
+
if (table.exclusive) {
|
|
871
|
+
let arm = -1
|
|
872
|
+
if (c >= 0 && c < 128) {
|
|
873
|
+
const a = table.ascii[c]!
|
|
874
|
+
if (a !== 0) arm = a - 1
|
|
875
|
+
} else if (c >= 128) {
|
|
876
|
+
const hi = table.hi
|
|
877
|
+
for (let i = 0; i < hi.length; i += 3) {
|
|
878
|
+
if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (arm >= 0) {
|
|
882
|
+
ctx._fc = false
|
|
883
|
+
if (COUNT) tableCounters.gatedEntries++
|
|
884
|
+
const v = exec(code[base + arm]!, input, pos, ctx)
|
|
885
|
+
if (v !== FAIL) return v
|
|
886
|
+
// THE CUT. `dispatch()` is the library's one true cut: once its
|
|
887
|
+
// selector matched, a failing branch must fail the whole choice
|
|
888
|
+
// rather than let a later arm re-recognise the same text. The flag
|
|
889
|
+
// was being SET by OP_DISPATCH and read by nobody, so the table
|
|
890
|
+
// accepted input both shipped engines reject.
|
|
891
|
+
if (committed(ctx)) return FAIL
|
|
892
|
+
// THE DISPATCHED ARM'S OWN SET IS THE ANSWER, at the choice's own
|
|
893
|
+
// position. This is the rule BOTH shipped engines apply — see the
|
|
894
|
+
// block comment below `failChoice` — and `_fx` already holds it, so
|
|
895
|
+
// the whole of "report the arm" is declining to overwrite it.
|
|
896
|
+
const armFx = ctx._fx
|
|
897
|
+
if (armFx !== undefined && armFx.length > 0) { ctx._fe = pos; return FAIL }
|
|
898
|
+
}
|
|
899
|
+
// THERE ARE NO OPEN ARMS TO FALL BACK TO. An open arm is one whose class
|
|
900
|
+
// is −1, and `resolveDispatch` clears `exclusive` for exactly those arms
|
|
901
|
+
// (program.ts), so `table.open` is empty whenever `exclusive` holds — the
|
|
902
|
+
// fallback loop this branch used to carry was unreachable code.
|
|
903
|
+
//
|
|
904
|
+
// THE UNION IS REPORTED ONLY WHEN NO ARM WAS SELECTED — a dispatch miss,
|
|
905
|
+
// where the interpreter runs `parsers.flatMap(p => p.parse(...))`
|
|
906
|
+
// (choice.ts:110-118) and, every arm being non-nullable and excluded by
|
|
907
|
+
// the char at `pos`, gets back exactly each arm's own opener. The static
|
|
908
|
+
// union IS that flatMap's answer, without running seven parsers to
|
|
909
|
+
// rediscover it.
|
|
910
|
+
//
|
|
911
|
+
// NOT furthest-failure merging, which no engine on the `expected` path
|
|
912
|
+
// performs. `failAt` (combinators/probe.ts:13-24) and its compiled mirror
|
|
913
|
+
// `probeUpdate` (codegen.ts:692) are the only positional merges in the
|
|
914
|
+
// library, they are gated on `_ctx._probe` and they surface as
|
|
915
|
+
// `RunResult.furthestFail` under `{recover:true}` — a different field.
|
|
916
|
+
// Every `_fe`/`_fx` site in all three drivers is last-write-wins.
|
|
917
|
+
//
|
|
918
|
+
// A residual JSON divergence survives this and is NOT a failure-reporting
|
|
919
|
+
// bug: on `{"a":]` this driver reports `["}"]` (the Obj arm's own
|
|
920
|
+
// propagated set) and both engines report seven tokens, because
|
|
921
|
+
// `choice()` froze `disjoint = false` for `Value` at CONSTRUCTION, when
|
|
922
|
+
// its `g.X` arms were unresolved refs (choice.ts:35 — the same staleness
|
|
923
|
+
// `encode.ts` recomputes around), so they firstMatch all seven arms where
|
|
924
|
+
// this driver dispatches to one. The arm sets differ, not the rule.
|
|
925
|
+
return failChoice()
|
|
926
|
+
}
|
|
927
|
+
// THE PER-ARM GATE. Arms in source order, each skipped when its own
|
|
928
|
+
// class excludes the char at `pos`. Order is untouched, so this is not a
|
|
929
|
+
// reordering: it only declines to enter arms that provably cannot match.
|
|
930
|
+
// A `null` class means nullable or unmappable, and those are always
|
|
931
|
+
// entered.
|
|
932
|
+
const n = code[ip + 2]!
|
|
933
|
+
const armCls = table.armCls
|
|
934
|
+
const need = rollbackNeeded(ctx)
|
|
935
|
+
const mRaw = need ? cstRawLen(ctx) : 0
|
|
936
|
+
const mTl = need ? cstTlLen(ctx) : 0
|
|
937
|
+
const mLv = need ? cstLeavesLen(ctx) : 0
|
|
938
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
939
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
940
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
941
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
942
|
+
// THE ORDERED PATH REPORTS THE ARMS THAT ACTUALLY RAN, accumulated in
|
|
943
|
+
// order — `choice.ts:167`'s `expected.push(...result.expected)` per failed
|
|
944
|
+
// arm, reported at the choice's own position. The static `choiceFx` union
|
|
945
|
+
// is a set of OPENERS, so a left-factored or shared-prefix choice whose
|
|
946
|
+
// arms all failed PAST their common opener reported that opener instead of
|
|
947
|
+
// the tokens the arms were actually waiting for: `['/::?/']` where the
|
|
948
|
+
// interpreter says `['"before"', '"hover"']`.
|
|
949
|
+
//
|
|
950
|
+
// `choiceFx` is still the answer when nothing ran or every arm reported
|
|
951
|
+
// nothing — the char gate can skip arms the interpreter would have entered,
|
|
952
|
+
// and the union is what those arms' start-failures would have contributed.
|
|
953
|
+
let acc: string[] | undefined
|
|
954
|
+
const push = (ax: readonly string[] | undefined): void => {
|
|
955
|
+
if (ax === undefined || ax.length === 0) return
|
|
956
|
+
if (acc === undefined) acc = ax.slice()
|
|
957
|
+
else for (const s of ax) acc.push(s)
|
|
958
|
+
}
|
|
959
|
+
const armFx = base + n
|
|
960
|
+
for (let i = 0; i < n; i++) {
|
|
961
|
+
const cls = armCls[i]
|
|
962
|
+
if (cls !== undefined && cls !== null && !classHas(cls, c)) {
|
|
963
|
+
if (COUNT) tableCounters.armGateSkips++
|
|
964
|
+
// THE GATED ARM STILL REPORTS. The interpreter has no char gate: it
|
|
965
|
+
// enters the arm, the arm's opener fails, and its static set is what
|
|
966
|
+
// lands in the accumulation. Declining to enter must not also decline
|
|
967
|
+
// to say what the arm wanted.
|
|
968
|
+
push(fx[code[armFx + i]!] as string[])
|
|
969
|
+
continue
|
|
970
|
+
}
|
|
971
|
+
ctx._fc = false
|
|
972
|
+
if (COUNT) tableCounters.ungatedEntries++
|
|
973
|
+
const rows0 = COUNT ? tableCounters.rows : 0
|
|
974
|
+
const v = exec(code[base + i]!, input, pos, ctx)
|
|
975
|
+
if (v !== FAIL) return v
|
|
976
|
+
if (COUNT) { tableCounters.ungatedFails++; tableCounters.ungatedFailRows += tableCounters.rows - rows0 }
|
|
977
|
+
push(ctx._fx)
|
|
978
|
+
// A committed arm cuts, and the interpreter reports the accumulation at
|
|
979
|
+
// the arm's OWN span (`choice.ts:170`), not at the choice's position.
|
|
980
|
+
if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
|
|
981
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
982
|
+
}
|
|
983
|
+
if (acc !== undefined) { ctx._fe = pos; ctx._fx = acc; return FAIL }
|
|
984
|
+
return failChoice()
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
case OP_GREEDY: {
|
|
988
|
+
// The mark is taken BEFORE the super arm runs: the classified path
|
|
989
|
+
// unwinds the regex's leaf and lets the credited literal arm push its
|
|
990
|
+
// own, so exactly one leaf survives — same text, same span.
|
|
991
|
+
const need = rollbackNeeded(ctx)
|
|
992
|
+
const mRaw = need ? cstRawLen(ctx) : 0
|
|
993
|
+
const mTl = need ? cstTlLen(ctx) : 0
|
|
994
|
+
const mLv = need ? cstLeavesLen(ctx) : 0
|
|
995
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
996
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
997
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
998
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
999
|
+
const sup = exec(code[ip + 1]!, input, pos, ctx)
|
|
1000
|
+
// choice.ts:126 — the super arm's failure is returned VERBATIM, so `_fe`
|
|
1001
|
+
// and `_fx` are left exactly as it set them.
|
|
1002
|
+
if (sup === FAIL) return FAIL
|
|
1003
|
+
const end = EC.e
|
|
1004
|
+
const word = input.slice(pos, end)
|
|
1005
|
+
const n = code[ip + 2]!
|
|
1006
|
+
for (let i = 0; i < n; i++) {
|
|
1007
|
+
if (k[code[ip + 3 + 2 * i]!] !== word) continue
|
|
1008
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1009
|
+
// Cannot fail: `word` IS this arm's case-sensitive literal at `pos`.
|
|
1010
|
+
return exec(code[ip + 4 + 2 * i]!, input, pos, ctx)
|
|
1011
|
+
}
|
|
1012
|
+
EC.e = end
|
|
1013
|
+
return sup
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
case OP_ARMGATE: {
|
|
1017
|
+
if (!(fns[code[ip + 1]!] as (s: unknown) => boolean)(ctx.state)) {
|
|
1018
|
+
// choice.ts:150 is a `continue`, not a failure — the arm is treated as
|
|
1019
|
+
// never entered, so a cut it might have raised must not cut the choice.
|
|
1020
|
+
ctx._fc = false
|
|
1021
|
+
ctx._fe = pos
|
|
1022
|
+
ctx._fx = fx[code[ip + 3]!] as string[]
|
|
1023
|
+
return FAIL
|
|
1024
|
+
}
|
|
1025
|
+
return exec(code[ip + 2]!, input, pos, ctx)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
case OP_REJECT: {
|
|
1029
|
+
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
1030
|
+
if (v === FAIL) return FAIL
|
|
1031
|
+
const end = EC.e
|
|
1032
|
+
const n = code[ip + 2]!
|
|
1033
|
+
for (let i = 0; i < n; i++) {
|
|
1034
|
+
const o = code[ip + 4 + 2 * i]!
|
|
1035
|
+
const fires = code[ip + 3 + 2 * i]! === 0
|
|
1036
|
+
? input.startsWith(k[o] as string, end)
|
|
1037
|
+
: classHas(cc[o]!, lead(input, end))
|
|
1038
|
+
if (!fires) continue
|
|
1039
|
+
// choice.ts:161-164 is a `continue`, not a failure: the arm is treated
|
|
1040
|
+
// as never entered, so a cut it raised must not survive to cut the
|
|
1041
|
+
// choice, and it contributes NO expectation. The choice does the
|
|
1042
|
+
// capture-sink rollback, as for any arm.
|
|
1043
|
+
ctx._fc = false
|
|
1044
|
+
ctx._fx = undefined
|
|
1045
|
+
return FAIL
|
|
1046
|
+
}
|
|
1047
|
+
EC.e = end
|
|
1048
|
+
return v
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
case OP_OPT: {
|
|
1052
|
+
const need = rollbackNeeded(ctx)
|
|
1053
|
+
const mRaw = need ? cstRawLen(ctx) : 0
|
|
1054
|
+
const mTl = need ? cstTlLen(ctx) : 0
|
|
1055
|
+
const mLv = need ? cstLeavesLen(ctx) : 0
|
|
1056
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1057
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1058
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1059
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1060
|
+
ctx._fc = false
|
|
1061
|
+
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
1062
|
+
if (v === FAIL) {
|
|
1063
|
+
// repeat.ts:277 — `optional()` propagates a committed failure rather
|
|
1064
|
+
// than reporting "absent".
|
|
1065
|
+
if (committed(ctx)) return FAIL
|
|
1066
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1067
|
+
EC.e = pos
|
|
1068
|
+
// NULL, not undefined. `optional()` yields `null` on no-match
|
|
1069
|
+
// (src/combinators/repeat.ts:269,277) and grammars TEST for it:
|
|
1070
|
+
// examples/lang's `call` reducer is `if (args === null) return callee`,
|
|
1071
|
+
// so `undefined` there turned a bare identifier into a call node with
|
|
1072
|
+
// `args: undefined`. The parse succeeded and only the tree moved.
|
|
1073
|
+
return null
|
|
1074
|
+
}
|
|
1075
|
+
return v
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
case OP_REP:
|
|
1079
|
+
case OP_REPV: {
|
|
1080
|
+
const child = code[ip + 1]!
|
|
1081
|
+
const min = code[ip + 2]!
|
|
1082
|
+
const max = code[ip + 3]!
|
|
1083
|
+
const sep = code[ip + 4]!
|
|
1084
|
+
// bit 1 of the flags word: the author opted into keeping separators in
|
|
1085
|
+
// `children`. Absent, a list contributes its ITEMS and nothing else.
|
|
1086
|
+
const keepSeparators = (code[ip + 5]! & 2) !== 0
|
|
1087
|
+
// BIT 0 WAS WRITTEN AND NEVER READ. `sepBy({ trailing: 'allow' })` keeps
|
|
1088
|
+
// a separator that is not followed by an item, so `a,b,` consumes to 4;
|
|
1089
|
+
// ignoring the bit stopped at 3 while both shipped engines went to 4.
|
|
1090
|
+
const trailingAllowed = (code[ip + 5]! & 1) !== 0
|
|
1091
|
+
const out: unknown[] | undefined = code[ip] === OP_REP ? [] : undefined
|
|
1092
|
+
const hasTrivia = ctx.trivia !== undefined
|
|
1093
|
+
const needMark = rollbackNeeded(ctx)
|
|
1094
|
+
// WHO OWNS THE TRIVIA IN FRONT OF THE FIRST ITEM. Only `many()` — the
|
|
1095
|
+
// min-0, separator-less repeat — runs its first item through `repItem`
|
|
1096
|
+
// and therefore skips leading trivia (src/combinators/repeat.ts:130-137).
|
|
1097
|
+
// `oneOrMore`/`atLeast` (:203) and `sepBy` (:412) both parse the first
|
|
1098
|
+
// item AT `pos`, because leading trivia there is the ENCLOSING context's
|
|
1099
|
+
// responsibility. `many` is the only OP_REP with no separator and min 0,
|
|
1100
|
+
// so the shape identifies itself and no extra flag is needed.
|
|
1101
|
+
const skipBeforeFirst = sep < 0 && min === 0
|
|
1102
|
+
// TOLERANT RECOVERY, dormant unless `ctx._tolerant`. `mySync` is captured
|
|
1103
|
+
// at ENTRY because an element's own sequence publishes over `_sync` while
|
|
1104
|
+
// it runs — the interpreter restores in a `finally` (sequence.ts:105) and
|
|
1105
|
+
// codegen saves at entry (codegen.ts:3032); either way this is the list's
|
|
1106
|
+
// own sync. The item's expected set is at ip + 6 and the separator's
|
|
1107
|
+
// sentinel class at ip + 7, both laid down only for a recovery table.
|
|
1108
|
+
const mySync = REC ? ctx._sync : undefined
|
|
1109
|
+
const recFx = REC ? fx[code[ip + 6]!] as string[] : undefined
|
|
1110
|
+
const sepSent = REC ? sentinelFor(code[ip + 7]!) : undefined
|
|
1111
|
+
let cur = pos
|
|
1112
|
+
let count = 0
|
|
1113
|
+
for (;;) {
|
|
1114
|
+
if (max >= 0 && count >= max) break
|
|
1115
|
+
// A SEPARATED list is bounded by its SEPARATOR, so it stops at EOF at the
|
|
1116
|
+
// LOOP HEAD — `while (cur < input.length)` at repeat.ts's sepBy loop. The
|
|
1117
|
+
// `repItem` early-out below sits after the separator, a different
|
|
1118
|
+
// position, and standing in for this one dropped the item following a
|
|
1119
|
+
// final separator. Held to `count >= min`: a list still short of `min`
|
|
1120
|
+
// must attempt the separator so its failure sets the expected set, which
|
|
1121
|
+
// is the only thing an under-`min` list has to report.
|
|
1122
|
+
if (sep >= 0 && count > 0 && count >= min && cur >= input.length) break
|
|
1123
|
+
// One mark pair for the whole loop when a rollback is even possible,
|
|
1124
|
+
// refreshed per iteration rather than reallocated.
|
|
1125
|
+
// SCALAR MARKS. This loop took TWO allocations per item (a CST mark and
|
|
1126
|
+
// a trivia mark, the latter allocating a second one internally) — the
|
|
1127
|
+
// per-item allocation an earlier lane hunted on json and could not
|
|
1128
|
+
// replicate, because json builds almost no nodes and so never sets
|
|
1129
|
+
// `_cstBuf`, which is what makes `needMark` true for a whole parse.
|
|
1130
|
+
const mRaw = needMark ? cstRawLen(ctx) : 0
|
|
1131
|
+
const mTl = needMark ? cstTlLen(ctx) : 0
|
|
1132
|
+
const mLv = needMark ? cstLeavesLen(ctx) : 0
|
|
1133
|
+
const mFl = needMark ? ctx._fields?.length ?? 0 : 0
|
|
1134
|
+
const mEr = needMark ? ctx._errors?.length ?? 0 : 0
|
|
1135
|
+
const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
|
|
1136
|
+
const mRoot = needMark ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1137
|
+
let itemStart = cur
|
|
1138
|
+
let sepEnd = -1
|
|
1139
|
+
// WHICH ITEMS `repItem` ACTUALLY PARSES — the scope of every rule below
|
|
1140
|
+
// that was copied from it. `many` runs ALL its items through `repItem`;
|
|
1141
|
+
// `oneOrMore`/`atLeast` parse the mandatory first at `pos` themselves
|
|
1142
|
+
// (repeat.ts:203) and `sepBy` parses BOTH its first item (:412) and
|
|
1143
|
+
// every post-separator item (:481) itself, so a separated list never
|
|
1144
|
+
// reaches `repItem` at all. The trivia branch below already encodes this
|
|
1145
|
+
// (its `else if` is unreachable for `sep >= 0`); the two guards after it
|
|
1146
|
+
// did not, and applied `repItem`'s rules to items that never run it.
|
|
1147
|
+
//
|
|
1148
|
+
// `count >= min` extends that to ALL of `atLeast`'s mandatory items, not
|
|
1149
|
+
// just its first: items 2..min go through `repItem` (repeat.ts:213) but as
|
|
1150
|
+
// MANDATORY, which holds off both of its loop-termination stops. Without
|
|
1151
|
+
// it a `{ min: 2 }` repeat over a NULLABLE item stopped on the required
|
|
1152
|
+
// second item and then failed `count < min` — never satisfiable at all,
|
|
1153
|
+
// where the compiled engine yields the n-item derivation. `min === 0`
|
|
1154
|
+
// makes the clause vacuous, so `many()` is untouched.
|
|
1155
|
+
const viaRepItem = sep < 0 && count >= min && (count > 0 || skipBeforeFirst)
|
|
1156
|
+
if (sep >= 0 && count > 0) {
|
|
1157
|
+
// separator, with trivia on BOTH sides — mirrors repeat.ts's sepBy loop
|
|
1158
|
+
const leavesBefore = cstLeavesLen(ctx)
|
|
1159
|
+
let sp = cur
|
|
1160
|
+
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
1161
|
+
ctx._fc = false
|
|
1162
|
+
const sv = exec(sep, input, sp, ctx)
|
|
1163
|
+
if (sv === FAIL) {
|
|
1164
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1165
|
+
if (committed(ctx)) return FAIL
|
|
1166
|
+
break
|
|
1167
|
+
}
|
|
1168
|
+
// Demote the separator out of `children`, exactly where the
|
|
1169
|
+
// interpreter does it (src/combinators/repeat.ts, sepBy loop).
|
|
1170
|
+
if (!keepSeparators) demoteCapturedToRaw(ctx, leavesBefore)
|
|
1171
|
+
sepEnd = EC.e
|
|
1172
|
+
itemStart = hasTrivia ? skipTrivia(input, EC.e, ctx) : EC.e
|
|
1173
|
+
} else if (hasTrivia && (count > 0 || skipBeforeFirst)) {
|
|
1174
|
+
// Trivia precedes every item a `repItem` loop parses, the first of a
|
|
1175
|
+
// `many()` included — skipping it only for later items dropped
|
|
1176
|
+
// exactly one trivia-log entry per repetition, invisible in the
|
|
1177
|
+
// parse and visible in a node's `triviaLog`. It does NOT precede the
|
|
1178
|
+
// mandatory first item of `oneOrMore`/`sepBy`; see `skipBeforeFirst`.
|
|
1179
|
+
itemStart = skipTrivia(input, itemStart, ctx)
|
|
1180
|
+
}
|
|
1181
|
+
// Nothing but trivia left: don't speculatively parse an item at EOF.
|
|
1182
|
+
// This is `repItem`'s early-out, so it applies only where `repItem`
|
|
1183
|
+
// runs — never to a mandatory first item, which both other engines
|
|
1184
|
+
// attempt at `pos` whatever is there.
|
|
1185
|
+
if (itemStart >= input.length && viaRepItem) {
|
|
1186
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1187
|
+
// A trailing separator at EOF is the COMMON case for
|
|
1188
|
+
// `trailing: 'allow'` (`a,b,`), and this early-out ran before the
|
|
1189
|
+
// item was ever attempted — so handling it only on the item-failure
|
|
1190
|
+
// path left the separator unconsumed.
|
|
1191
|
+
if (trailingAllowed && sepEnd >= 0) cur = sepEnd
|
|
1192
|
+
break
|
|
1193
|
+
}
|
|
1194
|
+
ctx._fc = false
|
|
1195
|
+
const v = exec(child, input, itemStart, ctx)
|
|
1196
|
+
if (v === FAIL) {
|
|
1197
|
+
// repeat.ts:141/215/233 — a committed item failure fails the WHOLE
|
|
1198
|
+
// repetition. Breaking here is what let `many(dispatch(...))` return
|
|
1199
|
+
// ok:true with a silently truncated document.
|
|
1200
|
+
if (committed(ctx)) {
|
|
1201
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1202
|
+
return FAIL
|
|
1203
|
+
}
|
|
1204
|
+
// RESYNC. A mandatory item of a separator-less repeat does NOT recover
|
|
1205
|
+
// (`oneOrMore`'s first item propagates, repeat.ts:221); a `sepBy`'s
|
|
1206
|
+
// first element does. Sitting ON the sync token is a clean list end,
|
|
1207
|
+
// tested at `itemStart` so leading trivia is never swallowed into the
|
|
1208
|
+
// span. A separated list scans to its own separator OR the enclosing
|
|
1209
|
+
// delimiter, and does NOT roll back: the separator it consumed and the
|
|
1210
|
+
// error after it both belong to the list (repeat.ts:533).
|
|
1211
|
+
if (REC && ctx._tolerant === true
|
|
1212
|
+
&& mySync !== undefined
|
|
1213
|
+
&& (sep >= 0 || count >= min)
|
|
1214
|
+
&& !matchesAt(mySync, input, itemStart, ctx)) {
|
|
1215
|
+
if (sep < 0 && needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1216
|
+
const rr = recoverScan(
|
|
1217
|
+
input, itemStart, ctx,
|
|
1218
|
+
sep < 0 ? mySync : orSentinel(sepSent ?? mySync, sepSent === undefined ? undefined : mySync),
|
|
1219
|
+
recFx!,
|
|
1220
|
+
)
|
|
1221
|
+
if (out !== undefined) out.push(rr.error)
|
|
1222
|
+
captureError(ctx, rr.error)
|
|
1223
|
+
count++
|
|
1224
|
+
cur = rr.end
|
|
1225
|
+
continue
|
|
1226
|
+
}
|
|
1227
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1228
|
+
// `trailing: 'allow'` keeps a separator that no item followed.
|
|
1229
|
+
if (trailingAllowed && sepEnd >= 0) cur = sepEnd
|
|
1230
|
+
break
|
|
1231
|
+
}
|
|
1232
|
+
if (EC.e === itemStart && viaRepItem) {
|
|
1233
|
+
// Zero-width item: `repItem`'s stop, and a TERMINATION device, not a
|
|
1234
|
+
// semantic filter — a `many` loop whose only source of progress is the
|
|
1235
|
+
// item itself spins forever without it. That pressure does not exist
|
|
1236
|
+
// for a mandatory item (parsed once) or for a separated list (the
|
|
1237
|
+
// SEPARATOR advances the loop), and both shipped engines accordingly
|
|
1238
|
+
// take a zero-width item there: `sepBy(nullable, ',')` over `",a"` is
|
|
1239
|
+
// `["", "a"]`. Applying the stop to them made the table return `[]`
|
|
1240
|
+
// having consumed NOTHING, silently dropping real input.
|
|
1241
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1242
|
+
break
|
|
1243
|
+
}
|
|
1244
|
+
if (out !== undefined) out.push(v)
|
|
1245
|
+
cur = EC.e
|
|
1246
|
+
count++
|
|
1247
|
+
}
|
|
1248
|
+
if (count < min) {
|
|
1249
|
+
// A list that ends under `min` is stuck at `cur` wanting another ITEM,
|
|
1250
|
+
// and reports what would have let it CONTINUE there — `failAt` in
|
|
1251
|
+
// repeat.ts and `deriveExpectedArr([def.parser])` in codegen's
|
|
1252
|
+
// emitSepBy both say the ITEM. Left alone, `_fx` still holds whatever
|
|
1253
|
+
// sub-parse failed last, which for a separated list is the SEPARATOR.
|
|
1254
|
+
// The index is present only when the encoder could not rule that out
|
|
1255
|
+
// (flags bit 2, `min >= 2`); at `min === 1` being under `min` means the
|
|
1256
|
+
// FIRST item failed and set the item's own set already.
|
|
1257
|
+
if ((code[ip + 5]! & 4) !== 0) { ctx._fe = cur; ctx._fx = fx[code[ip + 6]!] as string[] }
|
|
1258
|
+
return FAIL
|
|
1259
|
+
}
|
|
1260
|
+
EC.e = cur
|
|
1261
|
+
return out
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
case OP_XFORM: {
|
|
1265
|
+
const v = exec(code[ip + 2]!, input, pos, ctx)
|
|
1266
|
+
if (v === FAIL) return FAIL
|
|
1267
|
+
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1268
|
+
if (COUNT) siteFn('XFORM fn()', fn)
|
|
1269
|
+
return fn(v, { start: pos, end: EC.e })
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
case OP_LEAF: {
|
|
1273
|
+
// Mirrors src/combinators/token.ts:89-127. `leaf()` is a CAPTURE
|
|
1274
|
+
// BOUNDARY: it suppresses the interior's own CST captures and exposes
|
|
1275
|
+
// exactly ONE leaf carrying the reducer's value. Running the interior
|
|
1276
|
+
// with the parent's sinks still live leaked every interior terminal
|
|
1277
|
+
// into the parent's `children`, which moved arity in the enclosing
|
|
1278
|
+
// reducer with no error of its own.
|
|
1279
|
+
//
|
|
1280
|
+
// It differs from `token()` deliberately: trivia POLICY is untouched
|
|
1281
|
+
// (`ctx.trivia`, `ctx.triviaKindLabels`), and `_rootTriviaLog` stays
|
|
1282
|
+
// live so root-source trivia inside the leaf remains visible.
|
|
1283
|
+
const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
|
|
1284
|
+
const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
|
|
1285
|
+
const sOuterTl = ctx._triviaLog
|
|
1286
|
+
const wasCapturing = cstCaptureActive(ctx)
|
|
1287
|
+
|
|
1288
|
+
ctx._cstBuf = undefined
|
|
1289
|
+
ctx._cstChildren = undefined
|
|
1290
|
+
ctx._cstLeaves = undefined
|
|
1291
|
+
ctx._cstRawChildren = undefined
|
|
1292
|
+
ctx._cstTriviaLog = undefined
|
|
1293
|
+
ctx._triviaLog = undefined
|
|
1294
|
+
|
|
1295
|
+
let v: unknown
|
|
1296
|
+
try {
|
|
1297
|
+
v = exec(code[ip + 2]!, input, pos, ctx)
|
|
1298
|
+
} finally {
|
|
1299
|
+
ctx._cstBuf = sBuf
|
|
1300
|
+
ctx._cstChildren = sChildren
|
|
1301
|
+
ctx._cstLeaves = sLeaves
|
|
1302
|
+
ctx._cstRawChildren = sRaw
|
|
1303
|
+
ctx._cstTriviaLog = sTl
|
|
1304
|
+
ctx._triviaLog = sOuterTl
|
|
1305
|
+
}
|
|
1306
|
+
if (v === FAIL) return FAIL
|
|
1307
|
+
const end = EC.e
|
|
1308
|
+
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1309
|
+
if (COUNT) siteFn('LEAF fn()', fn)
|
|
1310
|
+
const out = fn(v, { start: pos, end })
|
|
1311
|
+
if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value: out, span: { start: pos, end } })
|
|
1312
|
+
EC.e = end
|
|
1313
|
+
return out
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
case OP_NODE:
|
|
1317
|
+
case OP_NODE_TRACK: {
|
|
1318
|
+
const flags = code[ip + 3]!
|
|
1319
|
+
// `cstOutput` is a RUNTIME fact, not an encode-time one: it depends on
|
|
1320
|
+
// the host handed to this parse. Under a CST host the direct builder is
|
|
1321
|
+
// BYPASSED, so capture must widen regardless of what the reducer's arity
|
|
1322
|
+
// said — the encode-time flags under-approximate here by construction.
|
|
1323
|
+
const host = ctx.build
|
|
1324
|
+
const hostCst = HOSTCST
|
|
1325
|
+
const type = k[code[ip + 5]!] as string
|
|
1326
|
+
const structural = code[ip + 1]! < 0 && code[ip + 4]! < 0
|
|
1327
|
+
const grammarCapture = (flags & 1) !== 0 || (flags & 128) !== 0
|
|
1328
|
+
const hostCapturesThisType = structural && HOSTCAPTURETRIVIA !== undefined
|
|
1329
|
+
? HOSTCAPTURETRIVIA(type)
|
|
1330
|
+
: undefined
|
|
1331
|
+
const captureWide = (flags & 4) !== 0 || hostCst
|
|
1332
|
+
? !structural || grammarCapture || hostCapturesThisType !== false
|
|
1333
|
+
: hostCapturesThisType === true
|
|
1334
|
+
const keepChildren = !structural || HOSTREADSCHILDREN
|
|
1335
|
+
|| (flags & 32) !== 0 || (flags & 64) !== 0
|
|
1336
|
+
const rawOnly = !keepChildren
|
|
1337
|
+
const saved = beginCstNodeCapture(ctx)
|
|
1338
|
+
if (rawOnly) {
|
|
1339
|
+
ctx._cstBuf = { rawOnly: true }
|
|
1340
|
+
}
|
|
1341
|
+
const savedFields = ctx._fields
|
|
1342
|
+
ctx._fields = (flags & 16) !== 0 || hostCst ? [] : undefined
|
|
1343
|
+
ctx.captureTrivia = captureWide
|
|
1344
|
+
// Per-node-type trivia-kind mask — see `assemble.ts`. Structural nodes
|
|
1345
|
+
// only (no builder, no projection), matching `node.ts:260`.
|
|
1346
|
+
const savedMask = structural ? ctx._triviaCaptureMask : undefined
|
|
1347
|
+
if (structural && host?._parsemanTriviaKinds !== undefined) {
|
|
1348
|
+
ctx._triviaCaptureMask = host._parsemanTriviaKinds(type)
|
|
1349
|
+
}
|
|
1350
|
+
const v = exec(code[ip + 2]!, input, pos, ctx)
|
|
1351
|
+
if (v !== FAIL && (flags & 128) !== 0 && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
|
|
1352
|
+
const fieldMap: FieldMap | undefined = (flags & 16) !== 0 || hostCst ? buildFieldMap(ctx._fields) : undefined
|
|
1353
|
+
ctx._fields = savedFields
|
|
1354
|
+
if (structural) ctx._triviaCaptureMask = savedMask
|
|
1355
|
+
const cap = endCstNodeCapture(ctx, saved)
|
|
1356
|
+
if (v === FAIL) return FAIL
|
|
1357
|
+
const end = EC.e
|
|
1358
|
+
const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
|
|
1359
|
+
const st = (flags & 8) !== 0 && ctx.state !== undefined
|
|
1360
|
+
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
1361
|
+
: undefined
|
|
1362
|
+
const tagIdx = code[ip + 6]!
|
|
1363
|
+
const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
|
|
1364
|
+
|
|
1365
|
+
const kids = cap.children
|
|
1366
|
+
const proj = code[ip + 4]!
|
|
1367
|
+
const buildIdx = code[ip + 1]!
|
|
1368
|
+
// A direct builder that never declared `state` still owes the HOST its
|
|
1369
|
+
// snapshot — node.ts builds it here, on a branch the eval path never takes.
|
|
1370
|
+
//
|
|
1371
|
+
// It is built INLINE AT THE HOST CALL, not before the branch and not
|
|
1372
|
+
// through a helper closure — either would be an allocation per node.
|
|
1373
|
+
// Computed eagerly it was an `Object.assign` clone per node, on every AST
|
|
1374
|
+
// parse of a grammar that uses `ctx.state` at all, thrown away unread: the
|
|
1375
|
+
// two host branches below are the only readers and neither runs on the AST
|
|
1376
|
+
// path with a direct builder.
|
|
1377
|
+
let nd: unknown
|
|
1378
|
+
if ((flags & 64) !== 0 && kids.length === 1) {
|
|
1379
|
+
nd = unwrapChild(kids[0])
|
|
1380
|
+
} else if ((flags & 32) !== 0 && kids.length === 1) {
|
|
1381
|
+
nd = kids[0]
|
|
1382
|
+
} else if (
|
|
1383
|
+
// HOST COLLAPSE. Applies wherever the node's VALUE comes from the host,
|
|
1384
|
+
// which is any node under a CST host — NOT only builder-less ones.
|
|
1385
|
+
// Gating on `!build` alone made `cstBuildHost({ collapse })` a silent
|
|
1386
|
+
// no-op for every grammar whose rules carry reducers (node.ts says so
|
|
1387
|
+
// in as many words). jess turns this on for `NamedColor`.
|
|
1388
|
+
(hostCst || (buildIdx < 0 && proj < 0))
|
|
1389
|
+
&& keepChildren
|
|
1390
|
+
&& host?._parsemanCstCollapse !== undefined
|
|
1391
|
+
&& kids.length === 1
|
|
1392
|
+
&& cap.rawChildren.length === 1
|
|
1393
|
+
&& host._parsemanCstCollapse(type, kids[0], kids, cap.rawChildren)
|
|
1394
|
+
) {
|
|
1395
|
+
nd = kids[0]
|
|
1396
|
+
} else if (proj >= 0) {
|
|
1397
|
+
nd = hostCst && host !== undefined
|
|
1398
|
+
? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, (flags & 8) !== 0 ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
|
|
1399
|
+
: projectChild(kids, proj, type)
|
|
1400
|
+
} else if (buildIdx >= 0) {
|
|
1401
|
+
if (hostCst && host !== undefined) {
|
|
1402
|
+
// A direct builder is bypassed under a CST host: the host must never
|
|
1403
|
+
// receive an arbitrary AST object as a child of a CST node.
|
|
1404
|
+
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, (flags & 8) !== 0 ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
|
|
1405
|
+
} else {
|
|
1406
|
+
const build = fns[buildIdx] as (
|
|
1407
|
+
children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
|
|
1408
|
+
rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
|
|
1409
|
+
) => unknown
|
|
1410
|
+
if (COUNT) siteFn('NODE build()', build)
|
|
1411
|
+
nd = build(kids, fieldMap, span, cap.rawChildren, (flags & 4) !== 0 || hostCst ? cap.triviaLog : EMPTY_TL, st)
|
|
1412
|
+
}
|
|
1413
|
+
} else if (host !== undefined) {
|
|
1414
|
+
// Structural node: the host owns the value.
|
|
1415
|
+
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, st, tags)
|
|
1416
|
+
} else {
|
|
1417
|
+
nd = { _tag: 'node', type, span, state: st ?? null, children: kids }
|
|
1418
|
+
}
|
|
1419
|
+
// A ROOT NODE IS NOT A CHILD — see the same guard in `assemble.ts`.
|
|
1420
|
+
if (saved.buf !== undefined || saved.ch !== undefined) {
|
|
1421
|
+
pushCstChild(ctx, nd, rawEntry(nd, input, pos, end))
|
|
1422
|
+
}
|
|
1423
|
+
EC.e = end
|
|
1424
|
+
return nd
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
case OP_ATTEMPT: {
|
|
1428
|
+
// `attempt()` verbatim: mark, run, and on failure restore every capture
|
|
1429
|
+
// sink and re-anchor the report at the transaction's entry. A COMMITTED
|
|
1430
|
+
// failure is still rolled back and then propagated untouched — the
|
|
1431
|
+
// interpreter returns `result` itself on that branch.
|
|
1432
|
+
const need = rollbackNeeded(ctx)
|
|
1433
|
+
const mRaw = need ? cstRawLen(ctx) : 0
|
|
1434
|
+
const mTl = need ? cstTlLen(ctx) : 0
|
|
1435
|
+
const mLv = need ? cstLeavesLen(ctx) : 0
|
|
1436
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1437
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1438
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1439
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1440
|
+
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
1441
|
+
if (v !== FAIL) return v
|
|
1442
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1443
|
+
if (ctx._fc === true) return FAIL
|
|
1444
|
+
ctx._fe = pos
|
|
1445
|
+
return FAIL
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
case OP_NOT: {
|
|
1449
|
+
const need = rollbackNeeded(ctx)
|
|
1450
|
+
const mRaw = need ? cstRawLen(ctx) : 0
|
|
1451
|
+
const mTl = need ? cstTlLen(ctx) : 0
|
|
1452
|
+
const mLv = need ? cstLeavesLen(ctx) : 0
|
|
1453
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1454
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1455
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1456
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1457
|
+
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
1458
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1459
|
+
if (v === FAIL) { EC.e = pos; return null }
|
|
1460
|
+
// `not.ts:50` — the ASSERTION's own set, at the assertion's position.
|
|
1461
|
+
ctx._fe = pos
|
|
1462
|
+
ctx._fx = fx[code[ip + 2]!] as string[]
|
|
1463
|
+
return FAIL
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
case OP_PEEK: {
|
|
1467
|
+
const need = rollbackNeeded(ctx)
|
|
1468
|
+
const mRaw = need ? cstRawLen(ctx) : 0
|
|
1469
|
+
const mTl = need ? cstTlLen(ctx) : 0
|
|
1470
|
+
const mLv = need ? cstLeavesLen(ctx) : 0
|
|
1471
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1472
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1473
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1474
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1475
|
+
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
1476
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1477
|
+
// `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
|
|
1478
|
+
if (v === FAIL) { ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]; return FAIL }
|
|
1479
|
+
EC.e = pos
|
|
1480
|
+
return null
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
default:
|
|
1484
|
+
throw new Error(`table driver: unknown opcode ${String(code[ip])} at ${ip}`)
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* A table SUBTREE, presented as a combinator.
|
|
1490
|
+
*
|
|
1491
|
+
* `scanTo()` and `balanced()` are rebuilt from their specs with the SHARED
|
|
1492
|
+
* constructors rather than re-implemented here, and those constructors take
|
|
1493
|
+
* COMBINATORS — a sentinel, a skipper list. This is the adapter: it runs the
|
|
1494
|
+
* subtree through this same driver and returns the combinator protocol's
|
|
1495
|
+
* `ParseResult`. Nothing else in the table crosses that boundary, and the cost
|
|
1496
|
+
* is one result object per scan probe, which the interpreter pays too.
|
|
1497
|
+
*
|
|
1498
|
+
* `_def` is deliberately opaque (`unknown`), so `matchesEmpty` answers `true`
|
|
1499
|
+
* and `firstSetOf` returns the carried set — the SAFE directions: a nullable
|
|
1500
|
+
* answer only costs a `many`/`choice` optimization, it never changes what is
|
|
1501
|
+
* accepted. The sentinel is the one exception: a `literal()` sentinel keeps its
|
|
1502
|
+
* def, because `scanTo` derives its expected set from exactly that
|
|
1503
|
+
* (`src/combinators/scanTo.ts:168`) and reporting `"sentinel"` where the
|
|
1504
|
+
* interpreter reports `"{"` is a real divergence.
|
|
1505
|
+
*/
|
|
1506
|
+
function subtreeComb(r: SubtreeRef, def?: ParserDef): Combinator<unknown> {
|
|
1507
|
+
const ip = r[0]
|
|
1508
|
+
return {
|
|
1509
|
+
_tag: 'tableSubtree',
|
|
1510
|
+
_meta: { firstSet: refFirstSet(r[1]), canMatchNewline: true, isTrivia: false },
|
|
1511
|
+
_def: def ?? { tag: 'unknown' } as unknown as ParserDef,
|
|
1512
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown> {
|
|
1513
|
+
const v = exec(ip, input, pos, ctx)
|
|
1514
|
+
if (v === FAIL) {
|
|
1515
|
+
const fe = ctx._fe
|
|
1516
|
+
const at = fe === undefined || fe < 0 ? pos : fe
|
|
1517
|
+
return { ok: false, expected: (ctx._fx ?? EMPTY_FX) as string[], span: { start: at, end: at } }
|
|
1518
|
+
}
|
|
1519
|
+
return { ok: true, value: v, span: { start: pos, end: EC.e } }
|
|
1520
|
+
},
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/** The first set a `SubtreeRef` carries: −1 is `any`, −2 is `empty`. */
|
|
1525
|
+
function refFirstSet(cls: number): FirstSet {
|
|
1526
|
+
if (cls === -2) return { kind: 'empty' }
|
|
1527
|
+
if (cls < 0) return { kind: 'any' }
|
|
1528
|
+
const spec = prog.cc[cls] ?? ''
|
|
1529
|
+
const ranges = decodeClassSpec(spec)
|
|
1530
|
+
return { kind: 'ranges', ranges }
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* The scan pool: one real `scanTo()` / `balanced()` per spec.
|
|
1535
|
+
*
|
|
1536
|
+
* Built ONCE per resolved table, like the trivia pool, so a scan costs no
|
|
1537
|
+
* construction per parse. Rebuilding through the constructors is what keeps
|
|
1538
|
+
* `balanced`'s ambient re-resolution, its `expect()`-based recovery, its
|
|
1539
|
+
* `strict` failure and its one-leaf `token()` wrapper — none of which is
|
|
1540
|
+
* reconstructible from a driver-side re-implementation without a second copy to
|
|
1541
|
+
* drift.
|
|
1542
|
+
*/
|
|
1543
|
+
const scans: readonly Combinator<unknown>[] = (prog.scans ?? []).map(s => {
|
|
1544
|
+
const skip = s.skip.map(r => subtreeComb(r))
|
|
1545
|
+
const raw = (s.flags & 1) !== 0
|
|
1546
|
+
if (s.kind === 1) {
|
|
1547
|
+
return balanced(s.open!, s.close!, { skip, raw, strict: (s.flags & 4) !== 0 }) as Combinator<unknown>
|
|
1548
|
+
}
|
|
1549
|
+
const sentDef: ParserDef | undefined = typeof s.sent === 'string'
|
|
1550
|
+
? { tag: 'literal', value: s.sent, caseInsensitive: false } as unknown as ParserDef
|
|
1551
|
+
: undefined
|
|
1552
|
+
return scanTo(subtreeComb(s.sentinel!, sentDef), { skip, raw, orEOF: (s.flags & 2) !== 0 }) as Combinator<unknown>
|
|
1553
|
+
})
|
|
1554
|
+
|
|
1555
|
+
const scanSkip: readonly (readonly Combinator<unknown>[])[] =
|
|
1556
|
+
(prog.scanSkip ?? []).map(set => set.map(r => subtreeComb(r)))
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* ONCE PER PARSE. `trackLines` is fixed by `run()` before the entry is called,
|
|
1560
|
+
* and it is the only leg of the swap's legality that is a property of the
|
|
1561
|
+
* PARSE rather than of the scope — so it is decided here and never re-asked on
|
|
1562
|
+
* the term path. `SCAN` starts null: a rule reached before any scope has no
|
|
1563
|
+
* installed trivia, and a stale one from a previous parse on a reused `ctx`
|
|
1564
|
+
* would skip trivia this grammar never declared.
|
|
1565
|
+
*/
|
|
1566
|
+
function begin(ctx: ParseContext): void {
|
|
1567
|
+
const host = ctx.build
|
|
1568
|
+
// Read user-owned host properties before touching the active frame. A
|
|
1569
|
+
// throwing getter must not leave the driver one level deeper or half reset.
|
|
1570
|
+
const nextFast = ctx.trackLines !== true
|
|
1571
|
+
const nextHostCst = host !== undefined && cstOutputHost(host)
|
|
1572
|
+
const nextHostReadsChildren = host?._parsemanReadsChildren !== false
|
|
1573
|
+
|| host?._parsemanCstCollapse !== undefined
|
|
1574
|
+
const nextHostCaptureTrivia = host?._parsemanCaptureTrivia
|
|
1575
|
+
if (depth > 0) {
|
|
1576
|
+
frames.push({
|
|
1577
|
+
scan: SCAN, fast: FAST,
|
|
1578
|
+
hostCst: HOSTCST, hostReadsChildren: HOSTREADSCHILDREN,
|
|
1579
|
+
hostCaptureTrivia: HOSTCAPTURETRIVIA,
|
|
1580
|
+
end: EC.e,
|
|
1581
|
+
})
|
|
1582
|
+
}
|
|
1583
|
+
depth++
|
|
1584
|
+
FAST = nextFast
|
|
1585
|
+
SCAN = null
|
|
1586
|
+
HOSTCST = nextHostCst
|
|
1587
|
+
HOSTREADSCHILDREN = nextHostReadsChildren
|
|
1588
|
+
HOSTCAPTURETRIVIA = nextHostCaptureTrivia
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function finish(): void {
|
|
1592
|
+
if (depth <= 0) throw new Error('parseman table reference driver frame underflow')
|
|
1593
|
+
depth--
|
|
1594
|
+
if (depth === 0) return
|
|
1595
|
+
const prior = frames.pop()!
|
|
1596
|
+
SCAN = prior.scan
|
|
1597
|
+
FAST = prior.fast
|
|
1598
|
+
HOSTCST = prior.hostCst
|
|
1599
|
+
HOSTREADSCHILDREN = prior.hostReadsChildren
|
|
1600
|
+
HOSTCAPTURETRIVIA = prior.hostCaptureTrivia
|
|
1601
|
+
EC.e = prior.end
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
return { exec, end: () => EC.e, begin, finish, scanSkip }
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* THE REFERENCE DRIVER. Turn a program into a rule map, by INTERPRETING the
|
|
1609
|
+
* bytecode. Nothing ships on this — `tableRules` (`table/assemble.ts`) is
|
|
1610
|
+
* what `parseman/table` exports as `tableRules`, and what every artifact binds.
|
|
1611
|
+
*
|
|
1612
|
+
* IT IS CALLED `execRules` AND NOT `tableRules` FOR A MEASURED REASON. Until
|
|
1613
|
+
* this rename the two engines exported the SAME name with the SAME signature
|
|
1614
|
+
* across a module boundary, so `import { tableRules } from '…/table/exec.ts'`
|
|
1615
|
+
* and `import { tableRules } from 'parseman/table'` type-checked identically
|
|
1616
|
+
* and silently selected different engines. Three modules picked the wrong one:
|
|
1617
|
+
* `compiler/linker.ts` (the whole `compose()`/`fuse()` path), `table/fold.ts`
|
|
1618
|
+
* (every folded artifact's variant load), and `bench/jess/fixture.ts` — the
|
|
1619
|
+
* CANONICAL fixture harness, whose column printed as `table` was this function
|
|
1620
|
+
* for the entire cycle it was quoted in.
|
|
1621
|
+
*
|
|
1622
|
+
* A name that two engines can answer to is not a naming preference; it is a
|
|
1623
|
+
* defect with no diagnostic. Do not re-alias this to `tableRules` anywhere.
|
|
1624
|
+
*
|
|
1625
|
+
* The entries have the SAME signature as the assembler's, so `run()`, the
|
|
1626
|
+
* linker's public wrappers and every consumer are unchanged — which is exactly
|
|
1627
|
+
* why the substitution was invisible.
|
|
1628
|
+
*/
|
|
1629
|
+
export function execRules(
|
|
1630
|
+
source: TableProgram | CompactProgram,
|
|
1631
|
+
/**
|
|
1632
|
+
* MEASUREMENT CONTROL, not a feature. `leafSwap: false` hands the driver a
|
|
1633
|
+
* `triviaScan` of all nulls, so `SCAN` is never installed and every skip takes
|
|
1634
|
+
* the shared generic functions — the exact pre-swap behaviour, from the SAME
|
|
1635
|
+
* driver code, differing only in TABLE DATA. That is what makes an in-process
|
|
1636
|
+
* A/B of the swap possible on a machine where cross-run comparison is not, and
|
|
1637
|
+
* it is G5-legal for the same reason `lines` is: it is read once, at rule-map
|
|
1638
|
+
* construction, and the parse path never sees an option.
|
|
1639
|
+
*/
|
|
1640
|
+
opts: { leafSwap?: boolean } = {},
|
|
1641
|
+
artifactMetadata: Readonly<Record<symbol, unknown>> = {},
|
|
1642
|
+
): Record<string, TableRule> {
|
|
1643
|
+
const prog = expandCompact(source)
|
|
1644
|
+
const t = resolveTable(prog)
|
|
1645
|
+
const scan = opts.leafSwap === false ? t.triviaScan.map(() => null) : t.triviaScan
|
|
1646
|
+
const d = makeDriver(t.code, t.k, t.fns, t.cc, t.fx, t.disp, t.dsp, t.trivia, scan, t.triviaLabelled, prog, newEndCell())
|
|
1647
|
+
const names = Object.keys(prog.rules)
|
|
1648
|
+
const entries = names.map(n => prog.rules[n]!)
|
|
1649
|
+
let last: unknown
|
|
1650
|
+
return stampRuleMap(prog, {
|
|
1651
|
+
runRule: (ri, input, pos, ctx) => {
|
|
1652
|
+
d.begin(ctx)
|
|
1653
|
+
try {
|
|
1654
|
+
const v = d.exec(entries[ri]!, input, pos, ctx)
|
|
1655
|
+
if (v === FAIL) return -1
|
|
1656
|
+
last = v
|
|
1657
|
+
return d.end()
|
|
1658
|
+
} finally {
|
|
1659
|
+
// Host reducers are ordinary user code and may re-enter this exact rule
|
|
1660
|
+
// map (or throw). Restore its scan/host/end frame on every exit.
|
|
1661
|
+
d.finish()
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
lastValue: () => last,
|
|
1665
|
+
// Chosen PER RULE, from `scanSkipOf`: `run()` reads the ENTRY rule's own
|
|
1666
|
+
// `_meta.grammarScanSkip` (grammar.ts:203), and installing one program-wide
|
|
1667
|
+
// set instead gave a `composeLeaf` piece's rules a skip list the interpreter
|
|
1668
|
+
// never gives them — a divergence that is invisible, because the parse
|
|
1669
|
+
// succeeds having skipped over a delimiter it should have stopped at.
|
|
1670
|
+
scanSkipFor: ri => d.scanSkip[prog.scanSkipOf?.[ri] ?? -1],
|
|
1671
|
+
}, artifactMetadata, t)
|
|
1672
|
+
}
|