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,2994 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ASSEMBLER — the table, LINKED into closures instead of interpreted.
|
|
3
|
+
*
|
|
4
|
+
* This is ledger row G5 as stated: *"build the grammar reference at run start,
|
|
5
|
+
* making the swaps on rules / sub-rules (leaves) at that point, then run with NO
|
|
6
|
+
* logic branching for that option input"*. `exec.ts` builds the same table and
|
|
7
|
+
* then interprets it: one `switch (code[ip])` over 29 opcodes, executed ONCE PER
|
|
8
|
+
* ROW — 497,360 rows for `benchmark.less` (`bench/jess/g5-rows.ts`) — each
|
|
9
|
+
* re-reading its opcode and re-decoding its operands from the `Int32Array`, and
|
|
10
|
+
* re-testing the same per-parse options. That is the per-node branching the
|
|
11
|
+
* design exists to remove.
|
|
12
|
+
*
|
|
13
|
+
* ## The shape
|
|
14
|
+
*
|
|
15
|
+
* Assembly walks the reachable table ONCE and lowers each site to a PIECE: a
|
|
16
|
+
* closure with its operands already captured as `const`s and its children bound
|
|
17
|
+
* as DIRECT references to their own pieces. At parse time there is no opcode
|
|
18
|
+
* read, no operand decode and no switch — a piece is called, and it calls the
|
|
19
|
+
* pieces it holds.
|
|
20
|
+
*
|
|
21
|
+
* The numbers that make this obviously the right trade, measured not assumed:
|
|
22
|
+
*
|
|
23
|
+
* 2,241 distinct reachable sites in the less table (`bench/jess/g5-sites.ts`)
|
|
24
|
+
* 497,360 rows executed for one parse of `benchmark.less`
|
|
25
|
+
*
|
|
26
|
+
* Pieces are GRAMMAR-sized; rows are INPUT-sized. Assembly allocates ~2.2k
|
|
27
|
+
* closures once per process and removes a dispatch plus an operand decode from
|
|
28
|
+
* ~497k executions — a 222x ratio. Assembly cost is paid once and is not the
|
|
29
|
+
* metric; it is measured anyway (`bench/jess/g5-ms.ts`, `bench/jess/g5-profile.ts`).
|
|
30
|
+
*
|
|
31
|
+
* ## Why this is not "just tuning the switch"
|
|
32
|
+
*
|
|
33
|
+
* A 29-case switch on an `Int32Array` load is a jump table whose successor V8
|
|
34
|
+
* cannot know. The interpreter loop is ONE basic block with 29 merge edges, so
|
|
35
|
+
* TurboFan cannot specialise any arm against its caller and every operand stays
|
|
36
|
+
* an untyped load. Measured on this branch: `exec` reaches TURBOFAN and is
|
|
37
|
+
* deoptimised back to MAGLEV repeatedly — 100 deopt events in a 20-parse run.
|
|
38
|
+
* The design was not paying dispatch overhead, it was opting its whole hot path
|
|
39
|
+
* out of the optimising compiler.
|
|
40
|
+
*
|
|
41
|
+
* A piece is created once and its call sites see one shape, so they stay
|
|
42
|
+
* monomorphic and TurboFan can inline them. That is the structural win, and it is
|
|
43
|
+
* why the target is codegen's number rather than a fraction of the gap.
|
|
44
|
+
*
|
|
45
|
+
* ## Why this keeps the artifact small
|
|
46
|
+
*
|
|
47
|
+
* The pieces ship ONCE, here, shared by every grammar and every variant. What a
|
|
48
|
+
* bundle carries is still the table — DATA. Codegen's 2.10 MB is recognition
|
|
49
|
+
* machinery inlined bespoke per rule; this stays at the table's 0.56 MB because
|
|
50
|
+
* the machinery is one copy in the runtime and the variation lives in the
|
|
51
|
+
* ASSEMBLY, not in duplicated piece bodies.
|
|
52
|
+
*
|
|
53
|
+
* ## Options are consumed by SELECTION, not by testing
|
|
54
|
+
*
|
|
55
|
+
* The piece set is a SUPERSET; an option set reaches a subset of it. Assembly
|
|
56
|
+
* walks from the entry rule and instantiates only what it touches, so a piece an
|
|
57
|
+
* option excludes is never allocated, never linked and costs zero at run — not a
|
|
58
|
+
* cheap branch, zero. Where a decision is knowable from the option set
|
|
59
|
+
* (`hostCst` below), assembly picks the piece rather than emitting one that
|
|
60
|
+
* tests. `scripts/check-invariants.mjs` enforces that no piece body reads a
|
|
61
|
+
* config field; see `CONFIG_FIELDS` there.
|
|
62
|
+
*
|
|
63
|
+
* Semantics are `exec.ts`'s, case for case. That file remains the reference and
|
|
64
|
+
* the three-way identity sweep gates this against it.
|
|
65
|
+
*/
|
|
66
|
+
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
67
|
+
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
68
|
+
import { buildFieldMap } from '../compiler/fields.ts'
|
|
69
|
+
import { asciiFoldKey } from '../combinators/dispatch.ts'
|
|
70
|
+
import { projectChild, unwrapChild } from '../combinators/node.ts'
|
|
71
|
+
import { cstOutputHost } from '../compiler/build-arity.ts'
|
|
72
|
+
import { consumeTrivia } from '../combinators/trivia-skip.ts'
|
|
73
|
+
import {
|
|
74
|
+
advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, rollbackTriviaAt,
|
|
75
|
+
saveTriviaMark, scanTrivia, skipTriviaScanned, type FastTriviaScanner,
|
|
76
|
+
} from '../combinators/trivia-skip.ts'
|
|
77
|
+
import {
|
|
78
|
+
cstCaptureActive, cstLeavesLen,
|
|
79
|
+
demoteCapturedToRaw, pushCstChild, pushCstLeaf,
|
|
80
|
+
type CstCaptureBuf,
|
|
81
|
+
} from '../cst/capture-buffer.ts'
|
|
82
|
+
import {
|
|
83
|
+
OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
|
|
84
|
+
OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
|
|
85
|
+
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
86
|
+
OP_LIVE,
|
|
87
|
+
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
|
|
88
|
+
OP_COV,
|
|
89
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
|
|
90
|
+
} from './ops.ts'
|
|
91
|
+
import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
|
|
92
|
+
import { failAt } from '../combinators/probe.ts'
|
|
93
|
+
/**
|
|
94
|
+
* U4 — the emitted engine, and the three pure helpers both engines share.
|
|
95
|
+
*
|
|
96
|
+
* `spanLines`/`rawEntry`/`lead` moved out of this function's closure so the
|
|
97
|
+
* emitted assembly can call the SAME definitions. A second copy of `rawEntry`
|
|
98
|
+
* is exactly how a CST leaf's span drifts between two engines that exist to be
|
|
99
|
+
* gated against each other.
|
|
100
|
+
*/
|
|
101
|
+
import {
|
|
102
|
+
EMITTED_PARAMS, Unemittable, emitAssemblySource, rebuildPools,
|
|
103
|
+
type EmittedAssembly, type EmittedFactory,
|
|
104
|
+
} from './emit-assembly.ts'
|
|
105
|
+
import { lead, rawEntry, spanLines } from './run-support.ts'
|
|
106
|
+
/**
|
|
107
|
+
* THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
|
|
108
|
+
*
|
|
109
|
+
* `failAt` (combinators/probe.ts) is called from exactly three places in the
|
|
110
|
+
* interpreter — `literal.ts`, `regex.ts`, `keywords.ts` — and codegen emits its
|
|
111
|
+
* mirror (`probeUpdate`) at the same leaf-fail sites. The table recorded
|
|
112
|
+
* NOTHING, so `completionsAt` on a table artifact saw only the top-level
|
|
113
|
+
* failure: a swallowed item failure inside a `sepBy` (`'{'` at offset 1 in a
|
|
114
|
+
* `{ decl; }` grammar) never reached the probe and the item's opener vanished
|
|
115
|
+
* from the completion set.
|
|
116
|
+
*
|
|
117
|
+
* Dormant unless `completionsAt` set `_probe`, so an ordinary parse pays one
|
|
118
|
+
* property read per terminal miss — the same price codegen pays.
|
|
119
|
+
*/
|
|
120
|
+
import {
|
|
121
|
+
classHas, decodeClassSpec, expandCompact, resolveTable,
|
|
122
|
+
type CompactProgram, type ResolvedClass, type ResolvedTable,
|
|
123
|
+
type SubtreeRef, type TableProgram, type TableRule,
|
|
124
|
+
} from './program.ts'
|
|
125
|
+
import { stampRuleMap } from './stamp.ts'
|
|
126
|
+
import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
|
|
127
|
+
import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Is the EMITTED engine (`emit-assembly.ts`) enabled for this process?
|
|
131
|
+
*
|
|
132
|
+
* Read ONCE, here, at module load — never on a parse path and never per
|
|
133
|
+
* assembly. It exists so the two engines can be A/B'd in ONE checkout: the
|
|
134
|
+
* bench guidance is explicit that a cross-worktree comparison carries a bias
|
|
135
|
+
* repetition does not remove.
|
|
136
|
+
*/
|
|
137
|
+
const EMIT_ENABLED = (globalThis as { process?: { env?: Record<string, string | undefined> } })
|
|
138
|
+
.process?.env?.PM_TABLE_EMIT !== '0'
|
|
139
|
+
|
|
140
|
+
/** Failure sentinel — SHARED with the other two engines, see `cell.ts`. */
|
|
141
|
+
import { FAIL, newEndCell, type EndCell } from './cell.ts'
|
|
142
|
+
|
|
143
|
+
const EMPTY_TL: readonly number[] = Object.freeze([])
|
|
144
|
+
/** `finishCstBuf`'s two empty sentinels, held here so the node piece can inline it. */
|
|
145
|
+
const EMPTY_CH: unknown[] = []
|
|
146
|
+
const EMPTY_TLOG: number[] = []
|
|
147
|
+
const EMPTY_FX: string[] = []
|
|
148
|
+
const ROUTED_FX: string[] = ['routed()']
|
|
149
|
+
/** The sentinel pool a STRICT emitted assembly binds — it has no reader. */
|
|
150
|
+
const EMPTY_SENTS: readonly (Combinator<null> | undefined)[] = Object.freeze([])
|
|
151
|
+
/**
|
|
152
|
+
* One encoded matcher arm, LINKED into a predicate at assembly.
|
|
153
|
+
*
|
|
154
|
+
* The previous form rebuilt a `DispatchMatcherCase` per arm PER PARSE and handed
|
|
155
|
+
* it to `matchesDispatchMatcher` — an operand decode and an allocation in a piece
|
|
156
|
+
* body, and it passed a hardcoded `caseInsensitive: false`, which silently
|
|
157
|
+
* dropped `{ caseInsensitive: true }` on matcher arms and selected the wrong arm.
|
|
158
|
+
* The encoder now folds the case-insensitivity into the operands (kinds 3/4 are
|
|
159
|
+
* pre-folded startsWith/endsWith; a case-insensitive `matches` carries `i` in its
|
|
160
|
+
* flags), so the whole decision is a `const` closure chosen once.
|
|
161
|
+
*/
|
|
162
|
+
function linkMatcher(m: readonly [number, string, string, number]): (key: string) => boolean {
|
|
163
|
+
const value = m[1]
|
|
164
|
+
switch (m[0]) {
|
|
165
|
+
case 0: return key => key.startsWith(value)
|
|
166
|
+
case 1: return key => key.endsWith(value)
|
|
167
|
+
case 3: return key => asciiFoldKey(key).startsWith(value)
|
|
168
|
+
case 4: return key => asciiFoldKey(key).endsWith(value)
|
|
169
|
+
// A fresh RegExp per test, as `matchesDispatchMatcher` builds — a cached one
|
|
170
|
+
// would carry `lastIndex` across parses whenever the author's pattern was
|
|
171
|
+
// sticky or global.
|
|
172
|
+
default: { const flags = m[2]; return key => new RegExp(value, flags).test(key) }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type Leaf = { _tag: 'leaf'; value: string; span: { start: number; end: number } }
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The `COV` slot's value when a parse installs no collector. ONE shared function
|
|
180
|
+
* across every assembly, so the slot's type never widens to `| undefined` and no
|
|
181
|
+
* counter piece has to test it.
|
|
182
|
+
*/
|
|
183
|
+
const NO_COVERAGE = (_id: string): void => {}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* THE PIECE SIGNATURE — uniform, narrow, and the same for every one of the 29
|
|
187
|
+
* lowerings.
|
|
188
|
+
*
|
|
189
|
+
* Three arguments and one return, so every call site in the assembled graph sees
|
|
190
|
+
* one shape and stays monomorphic. A wider or varying signature (an out-param
|
|
191
|
+
* object, a per-op return shape) reintroduces the polymorphism this design
|
|
192
|
+
* exists to remove, and it would show up as exactly the megamorphic call sites
|
|
193
|
+
* `exec`'s switch already was.
|
|
194
|
+
*
|
|
195
|
+
* The end position travels in the assembly-scope `EC.e` slot, as it does in
|
|
196
|
+
* `exec.ts` and in codegen's `_pfEnd`, rather than in the return value — a
|
|
197
|
+
* `{ value, end }` pair would be an allocation per row.
|
|
198
|
+
*/
|
|
199
|
+
type Piece = (input: string, pos: number, ctx: ParseContext) => unknown
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* ONE NON-FIRST SEQUENCE TERM, in a sequence that carries an adjacency
|
|
203
|
+
* assertion. Returns the new cursor or −1, with the value in `TERMV` —
|
|
204
|
+
* `nextTerm`'s protocol, so a bound term and a bound assertion are
|
|
205
|
+
* interchangeable in the loop and the loop tests neither.
|
|
206
|
+
*/
|
|
207
|
+
type TermRunner = (input: string, cur: number, ctx: ParseContext) => number
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The option set an assembly is specialised for.
|
|
211
|
+
*
|
|
212
|
+
* These are the facts `run()` fixes BEFORE the entry is called and that never
|
|
213
|
+
* change during a parse, so they are resolved by choosing a piece rather than by
|
|
214
|
+
* a test inside one. Anything that varies DURING a parse (position, the capture
|
|
215
|
+
* sinks a `node()` opens and closes, the error sink's contents) is runtime and
|
|
216
|
+
* stays where it is.
|
|
217
|
+
*/
|
|
218
|
+
export type RunCfg = {
|
|
219
|
+
/** Is this parse's host a CST-output host? Fixes `OP_NODE`'s whole shape. */
|
|
220
|
+
readonly hostCst: boolean
|
|
221
|
+
/**
|
|
222
|
+
* Does the structural host consume the semantic `children` array? `false`
|
|
223
|
+
* selects raw-only node collectors. Collapse forces this back to true at the
|
|
224
|
+
* run boundary because its predicate consumes both child views.
|
|
225
|
+
*/
|
|
226
|
+
readonly hostReadsChildren?: boolean
|
|
227
|
+
/**
|
|
228
|
+
* Structural node types whose host consumes trivia. Evaluated while an
|
|
229
|
+
* assembly is built, once per node site, never from the node execution path.
|
|
230
|
+
* Absent means the backwards-compatible default: capture every structural
|
|
231
|
+
* node's trivia.
|
|
232
|
+
*/
|
|
233
|
+
readonly hostCaptureTrivia?: ((type: string) => boolean) | undefined
|
|
234
|
+
/** `ctx.trackLines` — decides whether the trivia leaf swap is legal at all. */
|
|
235
|
+
readonly trackLines: boolean
|
|
236
|
+
/**
|
|
237
|
+
* `ctx._tolerant` — is THIS parse allowed to recover?
|
|
238
|
+
*
|
|
239
|
+
* FIXED FOR THE LIFETIME OF A PARSE, which is what makes it legal here rather
|
|
240
|
+
* than a per-piece test. Every writer of `ctx._tolerant` sets it BEFORE the
|
|
241
|
+
* parse begins and never during — `compile.ts:119` (`parseWithErrors`),
|
|
242
|
+
* `functional/run.ts:399`, `functional/doc.ts:29`, `combinators/completions.ts:40`.
|
|
243
|
+
* The single mid-parse mutation is `recovery/scan.ts:36`, which CLEARS it for
|
|
244
|
+
* the duration of a sentinel probe and restores it in `finally`; a sentinel is
|
|
245
|
+
* a `firstSetSentinel` char-class combinator (or an `orSentinel` of two), never
|
|
246
|
+
* an assembled rule, so no piece runs under the cleared value. Even if one did,
|
|
247
|
+
* the selection would be the RIGHT one: a probe must not recover, and it would
|
|
248
|
+
* get the strict assembly.
|
|
249
|
+
*
|
|
250
|
+
* Contrast `cstCaptureActive`, which a previous lane proposed for the same
|
|
251
|
+
* treatment and which is per-NODE state — keying on that would have been
|
|
252
|
+
* incorrect, not merely redundant.
|
|
253
|
+
*/
|
|
254
|
+
readonly tolerant: boolean
|
|
255
|
+
/**
|
|
256
|
+
* `ctx._grammarCoverage` — is THIS parse counting grammar coverage?
|
|
257
|
+
*
|
|
258
|
+
* FIXED FOR THE LIFETIME OF A PARSE, checked the same way `tolerant` was. There
|
|
259
|
+
* are exactly two writers — `createGrammarInstrumentationContext` (coverage.ts),
|
|
260
|
+
* which builds the field into a FRESH context object before any parse, and
|
|
261
|
+
* `functional/run.ts:403`, which installs it on the context it is about to run
|
|
262
|
+
* — and no reader anywhere mutates or clears it, mid-parse or otherwise.
|
|
263
|
+
* `createParseContext` initialises it to `undefined` once, at construction.
|
|
264
|
+
*
|
|
265
|
+
* So it is per-PARSE, unlike `ctx._cstBuf`, which a previous lane proposed
|
|
266
|
+
* keying on and which `beginCstNodeCapture`/`endCstNodeCapture` replace per
|
|
267
|
+
* NODE — that selection would have been incorrect, not merely redundant.
|
|
268
|
+
*
|
|
269
|
+
* An ordinary table has no `OP_COV` rows at all, so this bit selects a DIFFERENT
|
|
270
|
+
* assembly only for a table encoded with a coverage plan. For every other table
|
|
271
|
+
* the two assemblies are identical work and the extra bit costs one cache slot.
|
|
272
|
+
*/
|
|
273
|
+
readonly coverage: boolean
|
|
274
|
+
/**
|
|
275
|
+
* `ctx._probe !== undefined` — is THIS parse feeding a completions probe?
|
|
276
|
+
*
|
|
277
|
+
* FIXED FOR THE LIFETIME OF A PARSE, on exactly the evidence `tolerant` is
|
|
278
|
+
* held to. The two writers both install it BEFORE the parse begins and never
|
|
279
|
+
* during — `combinators/grammar.ts:213` (built in `run()`'s prologue when
|
|
280
|
+
* `recover` is set) and `combinators/completions.ts:38`. The single mid-parse
|
|
281
|
+
* mutation is `recovery/scan.ts:29`, which CLEARS it for the duration of a
|
|
282
|
+
* sentinel probe and restores it in `finally`; a sentinel is a
|
|
283
|
+
* `firstSetSentinel` char-class combinator, never an assembled rule, so no
|
|
284
|
+
* piece runs under the cleared value. Even if one did the selection would be
|
|
285
|
+
* the RIGHT one — a sentinel probe must fail fast, and it would get the
|
|
286
|
+
* strict assembly.
|
|
287
|
+
*
|
|
288
|
+
* It is here for `OP_GATE`, which is the only piece that reads it on a
|
|
289
|
+
* SUCCESS path. The six leaf sites read it after their own `return`, on the
|
|
290
|
+
* failure path only, and are deliberately left alone: selecting a second body
|
|
291
|
+
* per literal length to remove a failure-path test would double the runtime's
|
|
292
|
+
* literal bodies to buy nothing measurable.
|
|
293
|
+
*/
|
|
294
|
+
readonly probe: boolean
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** THE bit packing for an assembly cache key. Six bits, so at most sixty-four
|
|
298
|
+
* ordinary assemblies per table. Host trivia predicates use a host-identity
|
|
299
|
+
* cache in addition to this key because their per-type answer is not one bit.
|
|
300
|
+
*
|
|
301
|
+
* Taken as six booleans rather than as a `RunCfg` because `AssemblyCache.forCtx`
|
|
302
|
+
* runs once per entry invocation and must allocate nothing — it derives these bits
|
|
303
|
+
* from the `ctx` and builds a `RunCfg` only on the miss. That constraint is why the
|
|
304
|
+
* packing was written twice, 2500 lines apart in this file, and a bit added to one
|
|
305
|
+
* copy but not the other is a cache COLLISION: a parse served the wrong assembly.
|
|
306
|
+
* Splitting the packing from the object read satisfies both callers from one place. */
|
|
307
|
+
function cfgKeyOf(hostCst: boolean, trackLines: boolean, tolerant: boolean, coverage: boolean, probe: boolean, hostReadsChildren = true): number {
|
|
308
|
+
return (hostCst ? 1 : 0) | (trackLines ? 2 : 0) | (tolerant ? 4 : 0)
|
|
309
|
+
| (coverage ? 8 : 0) | (probe ? 16 : 0) | (hostReadsChildren ? 0 : 32)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The cfg key an assembly is cached under. */
|
|
313
|
+
export function cfgKey(c: RunCfg): number {
|
|
314
|
+
return cfgKeyOf(c.hostCst, c.trackLines, c.tolerant, c.coverage, c.probe, c.hostReadsChildren)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export type Assembly = {
|
|
318
|
+
/** One entry piece per rule name, already linked. */
|
|
319
|
+
readonly pieces: Readonly<Record<string, Piece>>
|
|
320
|
+
readonly end: () => number
|
|
321
|
+
/**
|
|
322
|
+
* Per-parse reset. What `exec.ts`'s `begin` DECIDED here (`trackLines`, the
|
|
323
|
+
* host mode) is exactly what assembly resolved, so all that is left is
|
|
324
|
+
* clearing the installed scanner and latching the host value.
|
|
325
|
+
*/
|
|
326
|
+
readonly begin: (ctx: ParseContext) => void
|
|
327
|
+
/** Close an invocation and restore a suspended re-entrant frame, if any. */
|
|
328
|
+
readonly finish: () => void
|
|
329
|
+
readonly scanSkip: readonly (readonly Combinator<unknown>[])[]
|
|
330
|
+
/**
|
|
331
|
+
* The sites this option set actually REACHED. A strict subset of the table's
|
|
332
|
+
* reachable set whenever an option excludes anything, and the assertion
|
|
333
|
+
* `test/unit/table-assemble.test.ts` makes on that.
|
|
334
|
+
*/
|
|
335
|
+
readonly reached: ReadonlySet<number>
|
|
336
|
+
/**
|
|
337
|
+
* WHY THIS ASSEMBLY IS RUNNING CLOSURES, when it is.
|
|
338
|
+
*
|
|
339
|
+
* `undefined` means the emitted engine (`emit-assembly.ts`) built this
|
|
340
|
+
* assembly. A string names the construct it refused. It is a field rather
|
|
341
|
+
* than a log line because a grammar that quietly drops to the closure path
|
|
342
|
+
* is a permanently slow path nobody would ever find — the same failure
|
|
343
|
+
* `encode.ts:1208-1213` refuses to allow for `OP_LIVE`.
|
|
344
|
+
*/
|
|
345
|
+
readonly emitRefusal: string | undefined
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Link one resolved table, for one option set, into a graph of closures.
|
|
350
|
+
*
|
|
351
|
+
* ONE walk. Each site is lowered at most once and memoised by its code offset,
|
|
352
|
+
* so a subtree shared by two parents is one piece with two references to it.
|
|
353
|
+
*/
|
|
354
|
+
export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Assembly {
|
|
355
|
+
const { code, k, fns, cc, fx, disp, dsp, trivia, triviaLabelled } = t
|
|
356
|
+
// The leaf swap is only legal when line tracking is off, and that is a property
|
|
357
|
+
// of the OPTION SET — so it is decided HERE, once, and the scope pieces below
|
|
358
|
+
// are chosen accordingly. `exec.ts` re-tested `FAST` at every scope entry.
|
|
359
|
+
const triviaScan = t.triviaScan
|
|
360
|
+
const swapLegal = !cfg.trackLines
|
|
361
|
+
const hostCst = cfg.hostCst
|
|
362
|
+
/**
|
|
363
|
+
* DOES THIS ASSEMBLY LOWER RECOVERY? Two facts, both known here.
|
|
364
|
+
*
|
|
365
|
+
* `prog.rec === 1` is TABLE DATA — `encodeTable({ recovery })` lays down the
|
|
366
|
+
* inferred sync operands, and their presence is what makes the recovery pieces
|
|
367
|
+
* lowerable at all. It is now always 1, since recovery is always lowered.
|
|
368
|
+
*
|
|
369
|
+
* `cfg.tolerant` is the OPTION half. Recovery being lowered into the table must
|
|
370
|
+
* not make a strict parse pay for it, and the way this design resolves an
|
|
371
|
+
* option is by CHOOSING A PIECE, not by testing a flag inside one. So the
|
|
372
|
+
* tolerant assembly holds the recovery pieces and the strict assembly holds the
|
|
373
|
+
* arity-specialised ones it always held — `RunCfg.tolerant` documents why that
|
|
374
|
+
* is sound. `parseWithErrors` and `completionsAt` set `_tolerant` before the
|
|
375
|
+
* entry runs, so they land on the tolerant assembly; every other parse gets a
|
|
376
|
+
* graph with no recovery in it anywhere.
|
|
377
|
+
*
|
|
378
|
+
* Dormancy is unchanged for the OTHER two engines: the source lowering
|
|
379
|
+
* (`codegen.ts:3153`) and the interpreter (`repeat.ts:163`) still gate on
|
|
380
|
+
* `ctx._tolerant` at runtime, and the recovery pieces below keep their own
|
|
381
|
+
* `_tolerant === true` failure-path gates so `exec.ts` stays the identity
|
|
382
|
+
* reference.
|
|
383
|
+
*/
|
|
384
|
+
const REC = prog.rec === 1 && cfg.tolerant
|
|
385
|
+
/**
|
|
386
|
+
* The sync sentinel for a char-class index, built ONCE at assembly.
|
|
387
|
+
*
|
|
388
|
+
* `firstSetSentinel` is the interpreter's own constructor and codegen calls it
|
|
389
|
+
* per publish through `_ctx._rec.sentinel(...)`; the ranges are recoverable from
|
|
390
|
+
* the class spec, so the table builds the identical combinator here and the
|
|
391
|
+
* publish is a slot read. −1 means "no usable sentinel", which is the same
|
|
392
|
+
* answer `firstSetSentinel` gives for an `any`/`empty` first set.
|
|
393
|
+
*/
|
|
394
|
+
const sentinels = new Map<number, Combinator<null> | undefined>()
|
|
395
|
+
function sentinelFor(cls: number): Combinator<null> | undefined {
|
|
396
|
+
if (cls < 0) return undefined
|
|
397
|
+
const hit = sentinels.get(cls)
|
|
398
|
+
if (hit !== undefined || sentinels.has(cls)) return hit
|
|
399
|
+
const spec = prog.cc[cls]!
|
|
400
|
+
const ranges = decodeClassSpec(spec)
|
|
401
|
+
const made = firstSetSentinel({ kind: 'ranges', ranges }) ?? undefined
|
|
402
|
+
sentinels.set(cls, made)
|
|
403
|
+
return made
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* THE ONE END-POSITION CELL FOR THIS ASSEMBLY (`_pfEnd` in emitted code).
|
|
408
|
+
*
|
|
409
|
+
* Whichever engines this assembly builds — the closure pieces below, the
|
|
410
|
+
* emitted pieces from `emit-assembly.ts`, an `exec.ts` driver — all write
|
|
411
|
+
* this slot, so a piece from one may be called by a piece from another. Per
|
|
412
|
+
* ASSEMBLY rather than per module: two grammars live in one process must not
|
|
413
|
+
* share it. See `cell.ts`.
|
|
414
|
+
*/
|
|
415
|
+
const EC = newEndCell()
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The INSTALLED trivia scanner for the scope currently running.
|
|
419
|
+
*
|
|
420
|
+
* Still a slot rather than a capture because `OP_SCOPE` and `OP_TOKEN` change
|
|
421
|
+
* it DURING a parse — it is runtime state, not configuration. What assembly
|
|
422
|
+
* removed is the per-scope re-test of `FAST` and `triviaLabelled`: which
|
|
423
|
+
* scanner a scope installs is now a `const` in that scope's piece.
|
|
424
|
+
*/
|
|
425
|
+
let SCAN: FastTriviaScanner | null = null
|
|
426
|
+
|
|
427
|
+
const memo = new Map<number, Piece>()
|
|
428
|
+
const reached = new Set<number>()
|
|
429
|
+
|
|
430
|
+
function pushLeaf(ctx: ParseContext, value: string, s: number, e: number): void {
|
|
431
|
+
const lf: Leaf = { _tag: 'leaf', value, span: { start: s, end: e } }
|
|
432
|
+
pushCstLeaf(ctx, lf)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Mirrors `exec.ts`'s `rollbackNeeded` — a runtime question about live sinks. */
|
|
436
|
+
function rollbackNeeded(ctx: ParseContext): boolean {
|
|
437
|
+
return ctx._cstBuf !== undefined
|
|
438
|
+
|| ctx._cstLeaves !== undefined
|
|
439
|
+
|| ctx._cstRawChildren !== undefined
|
|
440
|
+
|| ctx._cstTriviaLog !== undefined
|
|
441
|
+
|| ctx._fields !== undefined
|
|
442
|
+
|| ctx._errors !== undefined
|
|
443
|
+
|| ctx._triviaLog !== undefined
|
|
444
|
+
|| ctx._rootTriviaLog !== undefined
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The three CST lengths of a rollback mark, as out-slots — see `markCst`.
|
|
449
|
+
*
|
|
450
|
+
* Slots for the same reason `EC.e` and `TERMV` are slots: a `{ raw, tl, lv }`
|
|
451
|
+
* return would be an allocation per mark, and marks are the single most
|
|
452
|
+
* executed thing in the driver.
|
|
453
|
+
*
|
|
454
|
+
* READ THEM IMMEDIATELY. They are only valid until the next `markCst`, which
|
|
455
|
+
* in practice means the three lines under the call.
|
|
456
|
+
*/
|
|
457
|
+
let MRAW = 0
|
|
458
|
+
let MTL = 0
|
|
459
|
+
let MLV = 0
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Take a rollback mark's three CST lengths with ONE `ctx._cstBuf` load, and
|
|
463
|
+
* answer `rollbackNeeded` on the way past.
|
|
464
|
+
*
|
|
465
|
+
* Every backtrackable construct marks before trying something it may roll
|
|
466
|
+
* back, and `nextTerm` alone reaches here >200,000 times per parse of
|
|
467
|
+
* `benchmark.less`. The previous shape called `rollbackNeeded` and then
|
|
468
|
+
* `cstRawLen`/`cstTlLen`/`cstLeavesLen` — four calls, three of them
|
|
469
|
+
* CROSS-MODULE into `../cst/capture-buffer.ts` and each independently
|
|
470
|
+
* re-loading `ctx._cstBuf`. V8 inlined the local `rollbackNeeded` and did NOT
|
|
471
|
+
* inline the other three: they cost 1.0% + 0.7% + 0.9% of the assembled
|
|
472
|
+
* parse in SELF time. Source lowering emits the same mark as inline direct
|
|
473
|
+
* loads, which is why it never paid this.
|
|
474
|
+
*
|
|
475
|
+
* The hoist is valid at EXACTLY this granularity and no wider. `_cstBuf` is
|
|
476
|
+
* per-NODE state, not per-parse config: `beginCstNodeCapture` sets it and
|
|
477
|
+
* `endCstNodeCapture` restores it, so it changes DURING a parse. The three
|
|
478
|
+
* lengths are read here at one instant with nothing running between them, so
|
|
479
|
+
* one load serves all three. It must NOT be cached in a piece or held across
|
|
480
|
+
* the child call — between the mark and the rollback the child RUNS and may
|
|
481
|
+
* replace or clear the buffer, so `rollbackCstCaptureAt` re-loads. A buffer
|
|
482
|
+
* held across that would roll back against a stale object, which does not
|
|
483
|
+
* throw: it silently keeps or drops CST children.
|
|
484
|
+
*
|
|
485
|
+
* `capture-buffer.ts`'s helpers stay exported and are still what `exec.ts`
|
|
486
|
+
* calls. `exec.ts` is the identity reference a divergence is bisected
|
|
487
|
+
* against; it is not inlined there.
|
|
488
|
+
*/
|
|
489
|
+
function markCst(ctx: ParseContext): boolean {
|
|
490
|
+
const b = ctx._cstBuf
|
|
491
|
+
if (b !== undefined) {
|
|
492
|
+
// `cstRawLen`/`cstLeavesLen` collapse the lazy single-entry form; `cstTlLen`
|
|
493
|
+
// has no single form. `rollbackNeeded` short-circuits true on a live buffer,
|
|
494
|
+
// so the other seven sinks are not consulted — as before.
|
|
495
|
+
const raw = b.raw
|
|
496
|
+
MRAW = raw !== undefined ? raw.length : b.rawSingle !== undefined ? 1 : 0
|
|
497
|
+
const ch = b.ch
|
|
498
|
+
MLV = ch !== undefined ? ch.length : b.single !== undefined ? 1 : 0
|
|
499
|
+
const tl = b.tl
|
|
500
|
+
MTL = tl !== undefined ? tl.length : 0
|
|
501
|
+
return true
|
|
502
|
+
}
|
|
503
|
+
if (ctx._cstLeaves !== undefined
|
|
504
|
+
|| ctx._cstRawChildren !== undefined
|
|
505
|
+
|| ctx._cstTriviaLog !== undefined
|
|
506
|
+
|| ctx._fields !== undefined
|
|
507
|
+
|| ctx._errors !== undefined
|
|
508
|
+
|| ctx._triviaLog !== undefined
|
|
509
|
+
|| ctx._rootTriviaLog !== undefined) {
|
|
510
|
+
MRAW = ctx._cstRawChildren?.length ?? 0
|
|
511
|
+
MTL = ctx._cstTriviaLog?.length ?? 0
|
|
512
|
+
MLV = ctx._cstLeaves?.length ?? 0
|
|
513
|
+
return true
|
|
514
|
+
}
|
|
515
|
+
MRAW = 0
|
|
516
|
+
MTL = 0
|
|
517
|
+
MLV = 0
|
|
518
|
+
return false
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Wrap a scope piece in its ROOT-CAPTURE POLICY, when it has one.
|
|
523
|
+
*
|
|
524
|
+
* Both bits are TABLE DATA, so the wrapping happens here and the common scope
|
|
525
|
+
* — every scope in every grammar that never asked for either — gets the bare
|
|
526
|
+
* piece back and pays nothing. `parser()` answers the same two questions in
|
|
527
|
+
* `combinators/grammar.ts`; the table answered neither.
|
|
528
|
+
*/
|
|
529
|
+
function scopeRootPolicy(piece: Piece, flags: number): Piece {
|
|
530
|
+
let out = piece
|
|
531
|
+
if ((flags & 1) !== 0) {
|
|
532
|
+
// `grammar.ts:141` on a per-scope ctx COPY. One shared ctx here, so restore.
|
|
533
|
+
const inner = out
|
|
534
|
+
out = (input, pos, ctx) => {
|
|
535
|
+
const saved = ctx._rootTriviaCapture
|
|
536
|
+
ctx._rootTriviaCapture = false
|
|
537
|
+
const v = inner(input, pos, ctx)
|
|
538
|
+
ctx._rootTriviaCapture = saved
|
|
539
|
+
return v
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if ((flags & 2) !== 0) {
|
|
543
|
+
const inner = out
|
|
544
|
+
out = (input, pos, ctx) => {
|
|
545
|
+
refuseUnclassifiedRootScope(ctx._rootTriviaStrictScopes)
|
|
546
|
+
return inner(input, pos, ctx)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return out
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function skipTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
553
|
+
const s = SCAN
|
|
554
|
+
if (s !== null
|
|
555
|
+
&& ctx._triviaLog === undefined
|
|
556
|
+
&& !(ctx.captureTrivia === true && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined))) {
|
|
557
|
+
return s(input, cur)
|
|
558
|
+
}
|
|
559
|
+
// CAPTURE IS NOT A REASON TO LEAVE THE SCANNER — see `skipTriviaScanned`.
|
|
560
|
+
// Appended rather than folded into the test above so the non-capturing path
|
|
561
|
+
// keeps the exact branch it had.
|
|
562
|
+
if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
|
|
563
|
+
if (needsDeferredTriviaCommit(ctx)) {
|
|
564
|
+
const scan = scanTrivia(input, cur, ctx)
|
|
565
|
+
scan.commit()
|
|
566
|
+
return scan.end
|
|
567
|
+
}
|
|
568
|
+
return advanceTrivia(input, cur, ctx)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* The value the last `nextTerm` produced.
|
|
573
|
+
*
|
|
574
|
+
* A second out-parameter slot beside `EC.e`, for the same reason `EC.e` is one:
|
|
575
|
+
* a `{ value, end }` pair would be an allocation per sequence TERM, which is
|
|
576
|
+
* the single most executed thing in any grammar here.
|
|
577
|
+
*/
|
|
578
|
+
let TERMV: unknown
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* THE HOST, read ONCE PER PARSE.
|
|
582
|
+
*
|
|
583
|
+
* `ctx.build` is fixed by `run()` before the entry is called and cannot change
|
|
584
|
+
* during a parse, so it is configuration — but unlike `hostCst` it is a VALUE
|
|
585
|
+
* assembly cannot bake in, because two parses with the same option set can
|
|
586
|
+
* carry two different host functions. So it is hoisted to the boundary instead:
|
|
587
|
+
* `begin` reads it once, and the node piece reads this slot rather than going
|
|
588
|
+
* back to `ctx` on each of the 145,512 nodes `benchmark.less` builds.
|
|
589
|
+
*
|
|
590
|
+
* INV-6 in `scripts/check-invariants.mjs` is what caught this: the first
|
|
591
|
+
* version of the node piece opened with `const host = ctx.build`, which is a
|
|
592
|
+
* per-parse config read on the hottest non-terminal in the grammar.
|
|
593
|
+
*/
|
|
594
|
+
let HOST: ParseContext['build']
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* THE COLLECTOR THIS PARSE IS COUNTING INTO, latched at the boundary exactly as
|
|
598
|
+
* `HOST` is and for the same reason: the assembly is SELECTED by whether one is
|
|
599
|
+
* present (`RunCfg.coverage`), but WHICH one it is is a per-parse VALUE, and two
|
|
600
|
+
* parses with the same option set can carry two different collectors.
|
|
601
|
+
*
|
|
602
|
+
* So no `OP_COV` piece reads `ctx` at all — it calls this slot. The initial
|
|
603
|
+
* no-op exists because `begin` is what installs the real function; a counter
|
|
604
|
+
* piece is only ever reachable from the coverage assembly, whose `begin` always
|
|
605
|
+
* finds one on the context that selected it.
|
|
606
|
+
*/
|
|
607
|
+
let COV: (id: string) => void = NO_COVERAGE
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Assembly-local slots are scalar on the ordinary path. User host/reducer
|
|
611
|
+
* code can synchronously invoke this same table map, though, so suspend them
|
|
612
|
+
* only for that cold nested case rather than letting the inner parse poison
|
|
613
|
+
* its caller's scanner, host, coverage sink, or end cell.
|
|
614
|
+
*/
|
|
615
|
+
const frames: Array<{
|
|
616
|
+
scan: FastTriviaScanner | null
|
|
617
|
+
host: ParseContext['build']
|
|
618
|
+
coverage: (id: string) => void
|
|
619
|
+
end: number
|
|
620
|
+
}> = []
|
|
621
|
+
let depth = 0
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* A NON-FIRST sequence term: skip the installed trivia, run the child, and
|
|
625
|
+
* unrecord that trivia if the child matched nothing.
|
|
626
|
+
*
|
|
627
|
+
* Returns the new cursor, or −1 on failure; the term's value lands in `TERMV`.
|
|
628
|
+
* Split out of the loop because the FIRST term of a sequence never has trivia
|
|
629
|
+
* before it — that is a property of the POSITION, known at assembly, so the
|
|
630
|
+
* unrolled pieces call the first child directly and only reach here from term
|
|
631
|
+
* two onward.
|
|
632
|
+
*
|
|
633
|
+
* Small and monomorphic on purpose: every unrolled sequence piece calls this
|
|
634
|
+
* one function, so its call sites stay single-shape and TurboFan can inline it
|
|
635
|
+
* into them.
|
|
636
|
+
*/
|
|
637
|
+
function nextTerm(child: Piece, input: string, cur: number, ctx: ParseContext): number {
|
|
638
|
+
if (ctx.trivia === undefined) {
|
|
639
|
+
const v = child(input, cur, ctx)
|
|
640
|
+
if (v === FAIL) return -1
|
|
641
|
+
TERMV = v
|
|
642
|
+
return EC.e
|
|
643
|
+
}
|
|
644
|
+
// The overwhelmingly common AST/value parse has no live rollback sink. Do
|
|
645
|
+
// not manufacture zero marks and then copy seven zero lengths for it: with
|
|
646
|
+
// no buffer, leaves, fields, diagnostics, or trivia logs, `skipTrivia()`
|
|
647
|
+
// cannot create anything that a failed following term would need to undo.
|
|
648
|
+
// This is exactly `markCst(ctx) === false`, spelled here so that the hot
|
|
649
|
+
// success path has neither its scalar writes nor its failure-only locals.
|
|
650
|
+
if (ctx._cstBuf === undefined
|
|
651
|
+
&& ctx._cstLeaves === undefined
|
|
652
|
+
&& ctx._cstRawChildren === undefined
|
|
653
|
+
&& ctx._cstTriviaLog === undefined
|
|
654
|
+
&& ctx._fields === undefined
|
|
655
|
+
&& ctx._errors === undefined
|
|
656
|
+
&& ctx._triviaLog === undefined
|
|
657
|
+
&& ctx._rootTriviaLog === undefined) {
|
|
658
|
+
const scanEnd = skipTrivia(input, cur, ctx)
|
|
659
|
+
const v = child(input, scanEnd, ctx)
|
|
660
|
+
if (v === FAIL) return -1
|
|
661
|
+
TERMV = v
|
|
662
|
+
return EC.e > scanEnd ? EC.e : cur
|
|
663
|
+
}
|
|
664
|
+
// SCALAR MARKS — no per-term mark object, as `exec.ts` established.
|
|
665
|
+
const need = markCst(ctx)
|
|
666
|
+
const mRaw = MRAW
|
|
667
|
+
const mTl = MTL
|
|
668
|
+
const mLv = MLV
|
|
669
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
670
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
671
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
672
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
673
|
+
const scanEnd = skipTrivia(input, cur, ctx)
|
|
674
|
+
const v = child(input, scanEnd, ctx)
|
|
675
|
+
if (v === FAIL) return -1
|
|
676
|
+
TERMV = v
|
|
677
|
+
if (EC.e > scanEnd) return EC.e
|
|
678
|
+
// The term matched nothing, so the trivia in front of it was never consumed
|
|
679
|
+
// by anything — unrecord it and leave the cursor where it was.
|
|
680
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
681
|
+
return cur
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function committed(c: ParseContext): boolean {
|
|
685
|
+
return c._fc === true
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Accumulate one failed choice arm's expected set, in arm order.
|
|
690
|
+
*
|
|
691
|
+
* `choice.ts:167` is `expected.push(...result.expected)` per failed arm, and the
|
|
692
|
+
* accumulation — not the static union of the arms' OPENERS — is what a failing
|
|
693
|
+
* ordered choice reports. A left-factored or shared-prefix choice whose arms all
|
|
694
|
+
* failed PAST their common opener reported that opener alone: `['/::?/']` where
|
|
695
|
+
* the interpreter says `['"before"', '"hover"']`.
|
|
696
|
+
*
|
|
697
|
+
* Returns the accumulator so the common one-failing-arm case borrows the arm's
|
|
698
|
+
* own array (`slice` once) instead of building one per choice execution the way
|
|
699
|
+
* the interpreter's eager `const expected: string[] = []` does.
|
|
700
|
+
*/
|
|
701
|
+
function accSet(ax: readonly string[] | undefined, acc: string[] | undefined): string[] | undefined {
|
|
702
|
+
if (ax === undefined || ax.length === 0) return acc
|
|
703
|
+
if (acc === undefined) return ax.slice()
|
|
704
|
+
for (const s of ax) acc.push(s)
|
|
705
|
+
return acc
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
function trackLinesInto(ctx: ParseContext, input: string, end: number): void {
|
|
711
|
+
const from = ctx._lineScannedTo ?? 0
|
|
712
|
+
if (end <= from) return
|
|
713
|
+
const starts = ctx._lineStarts
|
|
714
|
+
if (starts === undefined) return
|
|
715
|
+
for (let i = from; i < end; i++) if (input.charCodeAt(i) === 10) starts.push(i + 1)
|
|
716
|
+
ctx._lineScannedTo = end
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* ── the link step ──────────────────────────────────────────────────────── */
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Lower one site, memoised.
|
|
723
|
+
*
|
|
724
|
+
* CYCLES. The encoder emits children before parents, so the only back-edges are
|
|
725
|
+
* the `OP_RULE` trampolines it patches for recursion (`encode.ts:228-241`). A
|
|
726
|
+
* site already in flight gets a STUB that forwards through a slot patched when
|
|
727
|
+
* the real piece exists, so recursive rules still hold direct references and
|
|
728
|
+
* nothing falls back to an index lookup. The stub costs one call, only on a
|
|
729
|
+
* genuine back-edge — 4 reachable `OP_RULE` sites in the less table.
|
|
730
|
+
*/
|
|
731
|
+
const inFlight = new Map<number, { fwd: Piece; set: (p: Piece) => void }>()
|
|
732
|
+
|
|
733
|
+
function link(ip: number): Piece {
|
|
734
|
+
const done = memo.get(ip)
|
|
735
|
+
if (done !== undefined) return done
|
|
736
|
+
const flight = inFlight.get(ip)
|
|
737
|
+
if (flight !== undefined) return flight.fwd
|
|
738
|
+
|
|
739
|
+
let target: Piece | undefined
|
|
740
|
+
const fwd: Piece = (input, pos, ctx) => target!(input, pos, ctx)
|
|
741
|
+
const holder = { fwd, set: (p: Piece) => { target = p } }
|
|
742
|
+
inFlight.set(ip, holder)
|
|
743
|
+
|
|
744
|
+
const piece = lower(ip)
|
|
745
|
+
|
|
746
|
+
inFlight.delete(ip)
|
|
747
|
+
holder.set(piece)
|
|
748
|
+
memo.set(ip, piece)
|
|
749
|
+
reached.add(ip)
|
|
750
|
+
return piece
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function lower(ip: number): Piece {
|
|
754
|
+
const op = code[ip]
|
|
755
|
+
switch (op) {
|
|
756
|
+
/* ── terminals ───────────────────────────────────────────────────────── */
|
|
757
|
+
|
|
758
|
+
case OP_LIT: {
|
|
759
|
+
const s = k[code[ip + 1]!] as string
|
|
760
|
+
const len = s.length
|
|
761
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
762
|
+
// LENGTH IS TABLE DATA, so the COMPARE is chosen here rather than
|
|
763
|
+
// delegated to a builtin that has to rediscover it. See `litBodyNote`.
|
|
764
|
+
if (len === 1) {
|
|
765
|
+
const c0 = s.charCodeAt(0)
|
|
766
|
+
return (input, pos, ctx) => {
|
|
767
|
+
if (input.charCodeAt(pos) === c0) {
|
|
768
|
+
const e = pos + 1
|
|
769
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
770
|
+
EC.e = e
|
|
771
|
+
return s
|
|
772
|
+
}
|
|
773
|
+
ctx._fe = pos; ctx._fx = xf
|
|
774
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
775
|
+
return FAIL
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (len === 2) {
|
|
779
|
+
const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1)
|
|
780
|
+
return (input, pos, ctx) => {
|
|
781
|
+
if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1) {
|
|
782
|
+
const e = pos + 2
|
|
783
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
784
|
+
EC.e = e
|
|
785
|
+
return s
|
|
786
|
+
}
|
|
787
|
+
ctx._fe = pos; ctx._fx = xf
|
|
788
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
789
|
+
return FAIL
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (len === 3) {
|
|
793
|
+
const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1), c2 = s.charCodeAt(2)
|
|
794
|
+
return (input, pos, ctx) => {
|
|
795
|
+
if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1
|
|
796
|
+
&& input.charCodeAt(pos + 2) === c2) {
|
|
797
|
+
const e = pos + 3
|
|
798
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
799
|
+
EC.e = e
|
|
800
|
+
return s
|
|
801
|
+
}
|
|
802
|
+
ctx._fe = pos; ctx._fx = xf
|
|
803
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
804
|
+
return FAIL
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return (input, pos, ctx) => {
|
|
808
|
+
if (input.startsWith(s, pos)) {
|
|
809
|
+
const e = pos + len
|
|
810
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
811
|
+
EC.e = e
|
|
812
|
+
return s
|
|
813
|
+
}
|
|
814
|
+
ctx._fe = pos; ctx._fx = xf
|
|
815
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
816
|
+
return FAIL
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
case OP_LIT_TRACK: {
|
|
821
|
+
const s = k[code[ip + 1]!] as string
|
|
822
|
+
const len = s.length
|
|
823
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
824
|
+
if (len === 1) {
|
|
825
|
+
const c0 = s.charCodeAt(0)
|
|
826
|
+
return (input, pos, ctx) => {
|
|
827
|
+
if (input.charCodeAt(pos) === c0) {
|
|
828
|
+
const e = pos + 1
|
|
829
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
830
|
+
trackLinesInto(ctx, input, e)
|
|
831
|
+
EC.e = e
|
|
832
|
+
return s
|
|
833
|
+
}
|
|
834
|
+
ctx._fe = pos; ctx._fx = xf
|
|
835
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
836
|
+
return FAIL
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (len === 2) {
|
|
840
|
+
const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1)
|
|
841
|
+
return (input, pos, ctx) => {
|
|
842
|
+
if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1) {
|
|
843
|
+
const e = pos + 2
|
|
844
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
845
|
+
trackLinesInto(ctx, input, e)
|
|
846
|
+
EC.e = e
|
|
847
|
+
return s
|
|
848
|
+
}
|
|
849
|
+
ctx._fe = pos; ctx._fx = xf
|
|
850
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
851
|
+
return FAIL
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (len === 3) {
|
|
855
|
+
const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1), c2 = s.charCodeAt(2)
|
|
856
|
+
return (input, pos, ctx) => {
|
|
857
|
+
if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1
|
|
858
|
+
&& input.charCodeAt(pos + 2) === c2) {
|
|
859
|
+
const e = pos + 3
|
|
860
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
861
|
+
trackLinesInto(ctx, input, e)
|
|
862
|
+
EC.e = e
|
|
863
|
+
return s
|
|
864
|
+
}
|
|
865
|
+
ctx._fe = pos; ctx._fx = xf
|
|
866
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
867
|
+
return FAIL
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return (input, pos, ctx) => {
|
|
871
|
+
if (input.startsWith(s, pos)) {
|
|
872
|
+
const e = pos + len
|
|
873
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
|
|
874
|
+
trackLinesInto(ctx, input, e)
|
|
875
|
+
EC.e = e
|
|
876
|
+
return s
|
|
877
|
+
}
|
|
878
|
+
ctx._fe = pos; ctx._fx = xf
|
|
879
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
880
|
+
return FAIL
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
case OP_RX: {
|
|
885
|
+
const re = k[code[ip + 1]!] as RegExp
|
|
886
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
887
|
+
return (input, pos, ctx) => {
|
|
888
|
+
re.lastIndex = pos
|
|
889
|
+
// `regex()` rows are sticky and expose only their whole match. `exec()`
|
|
890
|
+
// materialises a RegExpResult array just to read m[0]; `test()` performs
|
|
891
|
+
// the identical sticky match and leaves its end in `lastIndex`. This is
|
|
892
|
+
// the exact primitive the emitted table body uses, so the compact closure
|
|
893
|
+
// path must not retain an allocation on every regex token.
|
|
894
|
+
if (re.test(input)) {
|
|
895
|
+
const e = re.lastIndex
|
|
896
|
+
const v = input.slice(pos, e)
|
|
897
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
|
|
898
|
+
EC.e = e
|
|
899
|
+
return v
|
|
900
|
+
}
|
|
901
|
+
ctx._fe = pos; ctx._fx = xf
|
|
902
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
903
|
+
return FAIL
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
case OP_RX_TRACK: {
|
|
908
|
+
const re = k[code[ip + 1]!] as RegExp
|
|
909
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
910
|
+
return (input, pos, ctx) => {
|
|
911
|
+
re.lastIndex = pos
|
|
912
|
+
if (re.test(input)) {
|
|
913
|
+
const e = re.lastIndex
|
|
914
|
+
const v = input.slice(pos, e)
|
|
915
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
|
|
916
|
+
trackLinesInto(ctx, input, e)
|
|
917
|
+
EC.e = e
|
|
918
|
+
return v
|
|
919
|
+
}
|
|
920
|
+
ctx._fe = pos; ctx._fx = xf
|
|
921
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
922
|
+
return FAIL
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
case OP_LIT_CI:
|
|
927
|
+
case OP_LIT_CI_TRACK: {
|
|
928
|
+
const s = k[code[ip + 1]!] as string
|
|
929
|
+
const len = s.length
|
|
930
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
931
|
+
// The track/no-track choice was a per-row `code[ip] === OP_LIT_CI_TRACK`
|
|
932
|
+
// re-read in `exec.ts`. It is a property of the ROW, so it is resolved here.
|
|
933
|
+
// ASCII FOLD, one char at a time, WITHOUT the slice.
|
|
934
|
+
//
|
|
935
|
+
// `asciiFoldEq(input.slice(pos, e), s)` allocated a string on every
|
|
936
|
+
// ATTEMPT — including the failures, which is where a case-insensitive
|
|
937
|
+
// literal spends most of its executions (it is nearly always an arm of a
|
|
938
|
+
// choice). The chain compares in place and allocates only the value it
|
|
939
|
+
// is about to hand back, so the failure path allocates nothing at all.
|
|
940
|
+
//
|
|
941
|
+
// The semantics are `asciiFoldEq`'s, exactly: fold only A-Z (65-90) on
|
|
942
|
+
// BOTH sides, compare code unit by code unit. Past end-of-input
|
|
943
|
+
// `charCodeAt` is NaN, which folds to NaN and compares unequal — the
|
|
944
|
+
// same answer `asciiFoldEq` gave via its short-slice length test.
|
|
945
|
+
const fold = (c: number): number => (c >= 65 && c <= 90 ? c + 32 : c)
|
|
946
|
+
const foldedLit: number[] = []
|
|
947
|
+
for (let i = 0; i < len; i++) foldedLit.push(fold(s.charCodeAt(i)))
|
|
948
|
+
const matchesFolded = (input: string, pos: number): boolean => {
|
|
949
|
+
for (let i = 0; i < len; i++) {
|
|
950
|
+
if (fold(input.charCodeAt(pos + i)) !== foldedLit[i]!) return false
|
|
951
|
+
}
|
|
952
|
+
return true
|
|
953
|
+
}
|
|
954
|
+
if (op === OP_LIT_CI_TRACK) {
|
|
955
|
+
return (input, pos, ctx) => {
|
|
956
|
+
const e = pos + len
|
|
957
|
+
if (matchesFolded(input, pos)) {
|
|
958
|
+
// Yields the INPUT's casing (`literal.ts:86`), not the literal's.
|
|
959
|
+
const matched = input.slice(pos, e)
|
|
960
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, matched, pos, e)
|
|
961
|
+
trackLinesInto(ctx, input, e)
|
|
962
|
+
EC.e = e
|
|
963
|
+
return matched
|
|
964
|
+
}
|
|
965
|
+
ctx._fe = pos; ctx._fx = xf
|
|
966
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
967
|
+
return FAIL
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return (input, pos, ctx) => {
|
|
971
|
+
const e = pos + len
|
|
972
|
+
if (matchesFolded(input, pos)) {
|
|
973
|
+
// Yields the INPUT's casing (`literal.ts:86`), not the literal's.
|
|
974
|
+
const matched = input.slice(pos, e)
|
|
975
|
+
if (cstCaptureActive(ctx)) pushLeaf(ctx, matched, pos, e)
|
|
976
|
+
EC.e = e
|
|
977
|
+
return matched
|
|
978
|
+
}
|
|
979
|
+
ctx._fe = pos; ctx._fx = xf
|
|
980
|
+
if (ctx._probe !== undefined) failAt(ctx, xf, pos)
|
|
981
|
+
return FAIL
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
case OP_EMPTY:
|
|
986
|
+
return (_input, pos) => { EC.e = pos; return '' }
|
|
987
|
+
|
|
988
|
+
/* ── transparent / structural ────────────────────────────────────────── */
|
|
989
|
+
|
|
990
|
+
case OP_GUARD: {
|
|
991
|
+
const pred = fns[code[ip + 1]!] as (s: unknown) => boolean
|
|
992
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
993
|
+
return (_input, pos, ctx) => {
|
|
994
|
+
if (pred(ctx.state)) { EC.e = pos; return null }
|
|
995
|
+
ctx._fe = pos; ctx._fx = xf
|
|
996
|
+
return FAIL
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
case OP_ADJ: {
|
|
1001
|
+
// A row reached in its own right has NO boundary to test — the SEQ
|
|
1002
|
+
// pieces intercept the assertion wherever the question is answerable.
|
|
1003
|
+
// So this is the misuse the interpreter refuses (a bare choice arm, a
|
|
1004
|
+
// `node()` body, a repeat item), refused with the same sentence. Built
|
|
1005
|
+
// The POLARITY is bound here; the error itself is built where it is
|
|
1006
|
+
// thrown, so its stack points at the parse and not at assembly.
|
|
1007
|
+
const polarity = code[ip + 1] === 1 ? 'notAdjacent' : 'adjacent'
|
|
1008
|
+
return () => { throw adjacencyMisuse(polarity) }
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
case OP_GATE: {
|
|
1012
|
+
const cls = cc[code[ip + 1]!]!
|
|
1013
|
+
const xf = fx[code[ip + 3]!] as string[]
|
|
1014
|
+
const child = link(code[ip + 2]!)
|
|
1015
|
+
// SKIPPED ENTIRELY UNDER A PROBE / TOLERANT RECOVERY, exactly as the
|
|
1016
|
+
// interpreter skips it (`node.ts:239`, `attempt.ts:22`): the swallowed
|
|
1017
|
+
// inner failure is what feeds a completions probe, so a fail-fast that
|
|
1018
|
+
// never enters the body silently narrows `completionsAt` to the openers
|
|
1019
|
+
// it could see.
|
|
1020
|
+
//
|
|
1021
|
+
// Both halves of that condition are per-PARSE options, so the gate is
|
|
1022
|
+
// resolved HERE. Under either the gate is a no-op that forwards to the
|
|
1023
|
+
// child, and an assembly that forwards is better expressed as the child
|
|
1024
|
+
// itself: those assemblies drop the closure AND its call frame. The
|
|
1025
|
+
// strict assembly gets a body whose only test is the `classHas` the gate
|
|
1026
|
+
// exists for — the success path no longer reads `_probe` or `_tolerant`
|
|
1027
|
+
// at all, which is two context loads per gate execution removed (css has
|
|
1028
|
+
// 13 `GATE` rows, less 22, json none).
|
|
1029
|
+
if (cfg.tolerant || cfg.probe) return child
|
|
1030
|
+
return (input, pos, ctx) => {
|
|
1031
|
+
if (!classHas(cls, lead(input, pos))) {
|
|
1032
|
+
ctx._fe = pos; ctx._fx = xf
|
|
1033
|
+
return FAIL
|
|
1034
|
+
}
|
|
1035
|
+
return child(input, pos, ctx)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
case OP_LABEL: {
|
|
1040
|
+
const child = link(code[ip + 1]!)
|
|
1041
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
1042
|
+
return (input, pos, ctx) => {
|
|
1043
|
+
const v = child(input, pos, ctx)
|
|
1044
|
+
// `_fe` UNTOUCHED — `map.ts:84` keeps `result.span`.
|
|
1045
|
+
if (v === FAIL) ctx._fx = xf
|
|
1046
|
+
return v
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
case OP_COV: {
|
|
1051
|
+
const child = link(code[ip + 1]!)
|
|
1052
|
+
// NOT AN OPTION TEST IN A PIECE BODY — the option is resolved HERE, by
|
|
1053
|
+
// choosing which of three pieces to return. `cfg.coverage` false hands
|
|
1054
|
+
// back the child itself, so a coverage-encoded table run without a
|
|
1055
|
+
// collector is the same graph, with the same call depth, as an ordinary
|
|
1056
|
+
// one: the counter rows vanish at link time rather than short-circuiting
|
|
1057
|
+
// at parse time.
|
|
1058
|
+
if (!cfg.coverage) return child
|
|
1059
|
+
const def = prog.cov?.[code[ip + 2]!]
|
|
1060
|
+
// An `OP_COV` row with no pool behind it is a table whose `cov` field was
|
|
1061
|
+
// dropped somewhere between encode and load — the emitter, the compact
|
|
1062
|
+
// wire form, or a fold. Say so, rather than counting `undefined` as an id
|
|
1063
|
+
// and reporting a full denominator with no hits in it.
|
|
1064
|
+
if (def === undefined) {
|
|
1065
|
+
throw new TypeError(
|
|
1066
|
+
`assemble: OP_COV at ${ip} indexes coverage definition ${code[ip + 2]}, but this program `
|
|
1067
|
+
+ 'carries no `cov` pool. The counter rows and the definition pool are one artifact.',
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
1070
|
+
const id = def[0]
|
|
1071
|
+
// ENTRY vs SUCCESS, decided once. `EC.e` is deliberately untouched on both
|
|
1072
|
+
// paths: the counter runs before the child (entry) or after the child's
|
|
1073
|
+
// value is already in hand (success), so nothing observes a stale end.
|
|
1074
|
+
if (code[ip + 3] === 0) {
|
|
1075
|
+
return (input, pos, ctx) => {
|
|
1076
|
+
COV(id)
|
|
1077
|
+
return child(input, pos, ctx)
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return (input, pos, ctx) => {
|
|
1081
|
+
const v = child(input, pos, ctx)
|
|
1082
|
+
if (v !== FAIL) COV(id)
|
|
1083
|
+
return v
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
case OP_RULE:
|
|
1088
|
+
// Pure indirection in the tape; in the assembled graph the parent simply
|
|
1089
|
+
// holds the target's piece. The trampoline row disappears entirely — this
|
|
1090
|
+
// is the only place a back-edge can occur, and `link` handles it.
|
|
1091
|
+
return link(code[ip + 1]!)
|
|
1092
|
+
|
|
1093
|
+
case OP_LIVE: {
|
|
1094
|
+
// A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
|
|
1095
|
+
// codegen's `emitRuntimeFallback`. Its result IS the interpreter's, so
|
|
1096
|
+
// `expected`/`span` propagate verbatim and `committed` is copied onto the
|
|
1097
|
+
// ctx bit both drivers use for the cut.
|
|
1098
|
+
const c = fns[code[ip + 1]!] as Combinator<unknown>
|
|
1099
|
+
return (input, pos, ctx) => {
|
|
1100
|
+
const r = c.parse(input, pos, ctx)
|
|
1101
|
+
if (!r.ok) {
|
|
1102
|
+
ctx._fe = r.span.start
|
|
1103
|
+
ctx._fx = r.expected
|
|
1104
|
+
ctx._fc = r.committed === true
|
|
1105
|
+
return FAIL
|
|
1106
|
+
}
|
|
1107
|
+
EC.e = r.span.end
|
|
1108
|
+
return r.value
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
case OP_EXPECT: {
|
|
1113
|
+
const child = link(code[ip + 1]!)
|
|
1114
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
1115
|
+
// Line tracking is table data (`OP_EXPECT` has no tracked opcode variant).
|
|
1116
|
+
// Use the same helper as tracked node/root spans so an expect() recovery
|
|
1117
|
+
// diagnostic carries the interpreter's line/column contract in both
|
|
1118
|
+
// table engines. Do not use the incoming ctx bit: a grammar scope can
|
|
1119
|
+
// declare tracking even when the outer run context begins untracked.
|
|
1120
|
+
const expectSpan = (ctx: ParseContext, pos: number) =>
|
|
1121
|
+
prog.lines === 1 ? spanLines(ctx, pos, pos) : { start: pos, end: pos }
|
|
1122
|
+
// TOLERANT `expect()` EMBEDS ITS ERROR IN THE TREE, not only in the flat
|
|
1123
|
+
// `_errors` side-channel (`combinators/expect.ts:150`, and codegen's
|
|
1124
|
+
// `_ctx._rec.capture` at codegen.ts:4470) — so a tree walk finds every
|
|
1125
|
+
// diagnostic and the node survives incremental subtree reuse. The table
|
|
1126
|
+
// pushed to `_errors` and stopped, so a tolerant CST parse was missing the
|
|
1127
|
+
// `parseError` child the other two engines both produce.
|
|
1128
|
+
if (REC) {
|
|
1129
|
+
return (input, pos, ctx) => {
|
|
1130
|
+
const v = child(input, pos, ctx)
|
|
1131
|
+
if (v !== FAIL) return v
|
|
1132
|
+
const span = expectSpan(ctx, pos)
|
|
1133
|
+
const err = { _tag: 'parseError' as const, span, expected: xf }
|
|
1134
|
+
ctx._errors?.push(err)
|
|
1135
|
+
if (ctx._tolerant === true) captureError(ctx, err)
|
|
1136
|
+
// A RECOVERED FAILURE IS NO LONGER A FAILURE, so the commit bit the
|
|
1137
|
+
// child raised must not survive it. `_fc` is ctx-global for both table
|
|
1138
|
+
// drivers; the interpreter carries commitment on the RESULT object, so
|
|
1139
|
+
// returning a success drops it structurally and it has no bit to clear.
|
|
1140
|
+
// Leaving it set let a committed `dispatch`-tail failure that expect()
|
|
1141
|
+
// had already recovered CUT an enclosing choice, so the table rejected
|
|
1142
|
+
// input the interpreter accepts. Codegen has always cleared it
|
|
1143
|
+
// (`emitExpect`, `_ctx._fc = false`).
|
|
1144
|
+
ctx._fc = false
|
|
1145
|
+
EC.e = pos
|
|
1146
|
+
return err
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return (input, pos, ctx) => {
|
|
1150
|
+
const v = child(input, pos, ctx)
|
|
1151
|
+
if (v !== FAIL) return v
|
|
1152
|
+
const span = expectSpan(ctx, pos)
|
|
1153
|
+
const err = { _tag: 'parseError' as const, span, expected: xf }
|
|
1154
|
+
ctx._errors?.push(err)
|
|
1155
|
+
ctx._fc = false
|
|
1156
|
+
EC.e = pos
|
|
1157
|
+
return err
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
case OP_FIELD: {
|
|
1162
|
+
const name = k[code[ip + 1]!] as string
|
|
1163
|
+
const child = link(code[ip + 2]!)
|
|
1164
|
+
return (input, pos, ctx) => {
|
|
1165
|
+
const v = child(input, pos, ctx)
|
|
1166
|
+
if (v === FAIL) return FAIL
|
|
1167
|
+
ctx._fields?.push({ name, value: v, span: { start: pos, end: EC.e } })
|
|
1168
|
+
return v
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
case OP_XFORM: {
|
|
1173
|
+
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1174
|
+
const child = link(code[ip + 2]!)
|
|
1175
|
+
return (input, pos, ctx) => {
|
|
1176
|
+
const v = child(input, pos, ctx)
|
|
1177
|
+
if (v === FAIL) return FAIL
|
|
1178
|
+
return fn(v, { start: pos, end: EC.e })
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
case OP_SCAN: {
|
|
1183
|
+
// The scan POOL is built from subtrees, so it is linked after the graph
|
|
1184
|
+
// it references — a scan piece cannot capture its combinator at lowering
|
|
1185
|
+
// time without a cycle. It binds on first execution instead: one null
|
|
1186
|
+
// check, on 3 sites reached 6 times per parse of `benchmark.less`.
|
|
1187
|
+
const si = code[ip + 1]!
|
|
1188
|
+
let bound: Combinator<unknown> | undefined
|
|
1189
|
+
return (input, pos, ctx) => {
|
|
1190
|
+
const c = bound ?? (bound = scans[si]!)
|
|
1191
|
+
const r = c.parse(input, pos, ctx)
|
|
1192
|
+
if (!r.ok) {
|
|
1193
|
+
ctx._fe = r.span.start
|
|
1194
|
+
ctx._fx = (r.expected ?? EMPTY_FX) as string[]
|
|
1195
|
+
return FAIL
|
|
1196
|
+
}
|
|
1197
|
+
EC.e = r.span.end
|
|
1198
|
+
return r.value
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
case OP_WITHCTX: {
|
|
1203
|
+
// `extra` and the child are both bound HERE; the piece only swaps a field.
|
|
1204
|
+
const extra = k[code[ip + 1]!]
|
|
1205
|
+
const child = link(code[ip + 2]!)
|
|
1206
|
+
return (input, pos, ctx) => {
|
|
1207
|
+
const saved = ctx.state
|
|
1208
|
+
ctx.state = extra
|
|
1209
|
+
try { return child(input, pos, ctx) }
|
|
1210
|
+
finally { ctx.state = saved }
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
case OP_SCOPE:
|
|
1215
|
+
case OP_SCOPE_CAP: {
|
|
1216
|
+
const ki = code[ip + 1]!
|
|
1217
|
+
const scopeTrivia = ki < 0 ? undefined : (trivia[ki] as ParseContext['trivia'])
|
|
1218
|
+
const scopeLabels = scopeTrivia?._meta.triviaKindLabels
|
|
1219
|
+
// THE SWAP, RESOLVED AT ASSEMBLY. `exec.ts` computed
|
|
1220
|
+
// `FAST && ki >= 0 && !triviaLabelled[ki]` on every scope entry; `FAST` is
|
|
1221
|
+
// `!trackLines`, an option, and the other two are table data. All three are
|
|
1222
|
+
// known here, so the scope piece holds the scanner it installs as a `const`.
|
|
1223
|
+
const scanFor: FastTriviaScanner | null =
|
|
1224
|
+
swapLegal && ki >= 0 && !triviaLabelled[ki]! ? triviaScan[ki]! : null
|
|
1225
|
+
const child = link(code[ip + 2]!)
|
|
1226
|
+
// TWO PIECES, SELECTED HERE — not one piece that tests the opcode. Capture
|
|
1227
|
+
// is fixed for the whole parse, so `code[ip] === OP_SCOPE_CAP` is an
|
|
1228
|
+
// assembly-time fact and reading it per scope entry would be exactly the
|
|
1229
|
+
// config test INV-6 forbids.
|
|
1230
|
+
//
|
|
1231
|
+
// Both RESTORE rather than clear: capture is an OR with the enclosing
|
|
1232
|
+
// context (`grammar.ts:129`), so an inner scope must not switch an outer
|
|
1233
|
+
// capture off.
|
|
1234
|
+
const scopePiece: Piece = code[ip] === OP_SCOPE_CAP
|
|
1235
|
+
? (input, pos, ctx) => {
|
|
1236
|
+
const saved = ctx.trivia
|
|
1237
|
+
const savedLabels = ctx.triviaKindLabels
|
|
1238
|
+
const savedScan = SCAN
|
|
1239
|
+
const savedCap = ctx.captureTrivia
|
|
1240
|
+
SCAN = scanFor
|
|
1241
|
+
ctx.trivia = scopeTrivia
|
|
1242
|
+
ctx.triviaKindLabels = scopeLabels
|
|
1243
|
+
ctx.captureTrivia = true
|
|
1244
|
+
const v = child(input, pos, ctx)
|
|
1245
|
+
ctx.captureTrivia = savedCap
|
|
1246
|
+
ctx.trivia = saved
|
|
1247
|
+
ctx.triviaKindLabels = savedLabels
|
|
1248
|
+
SCAN = savedScan
|
|
1249
|
+
return v
|
|
1250
|
+
}
|
|
1251
|
+
: (input, pos, ctx) => {
|
|
1252
|
+
const saved = ctx.trivia
|
|
1253
|
+
const savedLabels = ctx.triviaKindLabels
|
|
1254
|
+
const savedScan = SCAN
|
|
1255
|
+
SCAN = scanFor
|
|
1256
|
+
ctx.trivia = scopeTrivia
|
|
1257
|
+
ctx.triviaKindLabels = scopeLabels
|
|
1258
|
+
const v = child(input, pos, ctx)
|
|
1259
|
+
ctx.trivia = saved
|
|
1260
|
+
ctx.triviaKindLabels = savedLabels
|
|
1261
|
+
SCAN = savedScan
|
|
1262
|
+
return v
|
|
1263
|
+
}
|
|
1264
|
+
return scopeRootPolicy(scopePiece, code[ip + 3]!)
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
case OP_ROUTED: {
|
|
1268
|
+
const fb = code[ip + 1]!
|
|
1269
|
+
const fallback = fb >= 0 ? link(fb) : undefined
|
|
1270
|
+
return (input, pos, ctx) => {
|
|
1271
|
+
const item = ctx._routed
|
|
1272
|
+
if (item === undefined || pos !== item.span.start) {
|
|
1273
|
+
if (fallback !== undefined) return fallback(input, pos, ctx)
|
|
1274
|
+
ctx._fe = pos; ctx._fx = ROUTED_FX
|
|
1275
|
+
return FAIL
|
|
1276
|
+
}
|
|
1277
|
+
if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: item.value, span: item.span })
|
|
1278
|
+
EC.e = item.span.end
|
|
1279
|
+
return item.value
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/* ── transaction ─────────────────────────────────────────────────────── */
|
|
1284
|
+
|
|
1285
|
+
case OP_ATTEMPT: {
|
|
1286
|
+
const child = link(code[ip + 1]!)
|
|
1287
|
+
return (input, pos, ctx) => {
|
|
1288
|
+
const need = markCst(ctx)
|
|
1289
|
+
const mRaw = MRAW
|
|
1290
|
+
const mTl = MTL
|
|
1291
|
+
const mLv = MLV
|
|
1292
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1293
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1294
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1295
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1296
|
+
const v = child(input, pos, ctx)
|
|
1297
|
+
if (v !== FAIL) return v
|
|
1298
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1299
|
+
// A committed failure propagates VERBATIM — rolled back, not re-anchored.
|
|
1300
|
+
if (committed(ctx)) return FAIL
|
|
1301
|
+
ctx._fe = pos
|
|
1302
|
+
return FAIL
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/* ── zero-width ──────────────────────────────────────────────────────── */
|
|
1307
|
+
|
|
1308
|
+
case OP_NOT: {
|
|
1309
|
+
const child = link(code[ip + 1]!)
|
|
1310
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
1311
|
+
return (input, pos, ctx) => {
|
|
1312
|
+
const need = markCst(ctx)
|
|
1313
|
+
const mRaw = MRAW
|
|
1314
|
+
const mTl = MTL
|
|
1315
|
+
const mLv = MLV
|
|
1316
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1317
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1318
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1319
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1320
|
+
const v = child(input, pos, ctx)
|
|
1321
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1322
|
+
if (v === FAIL) { EC.e = pos; return null }
|
|
1323
|
+
// `not.ts:50` — the ASSERTION's own set, at the assertion's position.
|
|
1324
|
+
ctx._fe = pos
|
|
1325
|
+
ctx._fx = xf
|
|
1326
|
+
return FAIL
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
case OP_PEEK: {
|
|
1331
|
+
const child = link(code[ip + 1]!)
|
|
1332
|
+
const xf = fx[code[ip + 2]!] as string[]
|
|
1333
|
+
return (input, pos, ctx) => {
|
|
1334
|
+
const need = markCst(ctx)
|
|
1335
|
+
const mRaw = MRAW
|
|
1336
|
+
const mTl = MTL
|
|
1337
|
+
const mLv = MLV
|
|
1338
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1339
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1340
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1341
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1342
|
+
const v = child(input, pos, ctx)
|
|
1343
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1344
|
+
// `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
|
|
1345
|
+
if (v === FAIL) { ctx._fe = pos; ctx._fx = xf; return FAIL }
|
|
1346
|
+
EC.e = pos
|
|
1347
|
+
return null
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
case OP_OPT: {
|
|
1352
|
+
const child = link(code[ip + 1]!)
|
|
1353
|
+
return (input, pos, ctx) => {
|
|
1354
|
+
const need = markCst(ctx)
|
|
1355
|
+
const mRaw = MRAW
|
|
1356
|
+
const mTl = MTL
|
|
1357
|
+
const mLv = MLV
|
|
1358
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1359
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1360
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1361
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1362
|
+
ctx._fc = false
|
|
1363
|
+
const v = child(input, pos, ctx)
|
|
1364
|
+
if (v === FAIL) {
|
|
1365
|
+
if (committed(ctx)) return FAIL
|
|
1366
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1367
|
+
EC.e = pos
|
|
1368
|
+
// NULL, not undefined — `repeat.ts:269,277`, and grammars TEST for it.
|
|
1369
|
+
return null
|
|
1370
|
+
}
|
|
1371
|
+
return v
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* ── boundaries ──────────────────────────────────────────────────────── */
|
|
1376
|
+
|
|
1377
|
+
case OP_TOKEN: {
|
|
1378
|
+
const child = link(code[ip + 1]!)
|
|
1379
|
+
return (input, pos, ctx) => {
|
|
1380
|
+
const sTrivia = ctx.trivia, sKinds = ctx.triviaKindLabels
|
|
1381
|
+
const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
|
|
1382
|
+
const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
|
|
1383
|
+
const sOuterTl = ctx._triviaLog, sRootTl = ctx._rootTriviaLog
|
|
1384
|
+
const wasCapturing = cstCaptureActive(ctx)
|
|
1385
|
+
|
|
1386
|
+
const sScan = SCAN
|
|
1387
|
+
SCAN = null
|
|
1388
|
+
ctx.trivia = undefined
|
|
1389
|
+
ctx.triviaKindLabels = undefined
|
|
1390
|
+
ctx._cstBuf = undefined
|
|
1391
|
+
ctx._cstChildren = undefined
|
|
1392
|
+
ctx._cstLeaves = undefined
|
|
1393
|
+
ctx._cstRawChildren = undefined
|
|
1394
|
+
ctx._cstTriviaLog = undefined
|
|
1395
|
+
ctx._triviaLog = undefined
|
|
1396
|
+
ctx._rootTriviaLog = undefined
|
|
1397
|
+
|
|
1398
|
+
let v: unknown
|
|
1399
|
+
try {
|
|
1400
|
+
v = child(input, pos, ctx)
|
|
1401
|
+
} finally {
|
|
1402
|
+
SCAN = sScan
|
|
1403
|
+
ctx.trivia = sTrivia
|
|
1404
|
+
ctx.triviaKindLabels = sKinds
|
|
1405
|
+
ctx._cstBuf = sBuf
|
|
1406
|
+
ctx._cstChildren = sChildren
|
|
1407
|
+
ctx._cstLeaves = sLeaves
|
|
1408
|
+
ctx._cstRawChildren = sRaw
|
|
1409
|
+
ctx._cstTriviaLog = sTl
|
|
1410
|
+
ctx._triviaLog = sOuterTl
|
|
1411
|
+
ctx._rootTriviaLog = sRootTl
|
|
1412
|
+
}
|
|
1413
|
+
if (v === FAIL) return FAIL
|
|
1414
|
+
const end = EC.e
|
|
1415
|
+
const value = input.slice(pos, end)
|
|
1416
|
+
if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
|
|
1417
|
+
EC.e = end
|
|
1418
|
+
return value
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
case OP_LEAF: {
|
|
1423
|
+
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1424
|
+
const child = link(code[ip + 2]!)
|
|
1425
|
+
return (input, pos, ctx) => {
|
|
1426
|
+
const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
|
|
1427
|
+
const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
|
|
1428
|
+
const sOuterTl = ctx._triviaLog
|
|
1429
|
+
const wasCapturing = cstCaptureActive(ctx)
|
|
1430
|
+
|
|
1431
|
+
ctx._cstBuf = undefined
|
|
1432
|
+
ctx._cstChildren = undefined
|
|
1433
|
+
ctx._cstLeaves = undefined
|
|
1434
|
+
ctx._cstRawChildren = undefined
|
|
1435
|
+
ctx._cstTriviaLog = undefined
|
|
1436
|
+
ctx._triviaLog = undefined
|
|
1437
|
+
|
|
1438
|
+
let v: unknown
|
|
1439
|
+
try {
|
|
1440
|
+
v = child(input, pos, ctx)
|
|
1441
|
+
} finally {
|
|
1442
|
+
ctx._cstBuf = sBuf
|
|
1443
|
+
ctx._cstChildren = sChildren
|
|
1444
|
+
ctx._cstLeaves = sLeaves
|
|
1445
|
+
ctx._cstRawChildren = sRaw
|
|
1446
|
+
ctx._cstTriviaLog = sTl
|
|
1447
|
+
ctx._triviaLog = sOuterTl
|
|
1448
|
+
}
|
|
1449
|
+
if (v === FAIL) return FAIL
|
|
1450
|
+
const end = EC.e
|
|
1451
|
+
const out = fn(v, { start: pos, end })
|
|
1452
|
+
if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value: out, span: { start: pos, end } })
|
|
1453
|
+
EC.e = end
|
|
1454
|
+
return out
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/* ── sequences ───────────────────────────────────────────────────────── */
|
|
1459
|
+
|
|
1460
|
+
case OP_SEQ:
|
|
1461
|
+
case OP_SEQV:
|
|
1462
|
+
case OP_SEQX: {
|
|
1463
|
+
const fused = op === OP_SEQX
|
|
1464
|
+
const base = fused ? ip + 3 : ip + 2
|
|
1465
|
+
const n = code[fused ? ip + 2 : ip + 1]!
|
|
1466
|
+
// Children bound as a plain array of DIRECT references, laid out once.
|
|
1467
|
+
const kids: Piece[] = new Array<Piece>(n)
|
|
1468
|
+
for (let i = 0; i < n; i++) kids[i] = link(code[base + i]!)
|
|
1469
|
+
const wantValues = op !== OP_SEQV
|
|
1470
|
+
const fn = fused
|
|
1471
|
+
? fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1472
|
+
: undefined
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* The generic term loop, for arities the unrolled pieces below do not
|
|
1476
|
+
* cover.
|
|
1477
|
+
*
|
|
1478
|
+
* `exec.ts` open-coded a LIT/RX fast path here to dodge "a JS call frame
|
|
1479
|
+
* plus a switch dispatch". Half of that cost is gone by construction — a
|
|
1480
|
+
* piece has no switch — and the other half is what TurboFan removes by
|
|
1481
|
+
* INLINING a small monomorphic child. Re-emitting the terminal bodies
|
|
1482
|
+
* would instead grow this function past the inlining budget, which is the
|
|
1483
|
+
* defect being fixed, so they are not re-emitted.
|
|
1484
|
+
*/
|
|
1485
|
+
const runTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
|
|
1486
|
+
const v0 = kids[0]!(input, pos, ctx)
|
|
1487
|
+
if (v0 === FAIL) return -1
|
|
1488
|
+
if (values !== undefined) values.push(v0)
|
|
1489
|
+
let cur = EC.e
|
|
1490
|
+
for (let i = 1; i < n; i++) {
|
|
1491
|
+
cur = nextTerm(kids[i]!, input, cur, ctx)
|
|
1492
|
+
if (cur < 0) return -1
|
|
1493
|
+
if (values !== undefined) values.push(TERMV)
|
|
1494
|
+
}
|
|
1495
|
+
return cur
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* SEQUENCES CARRYING AN ADJACENCY ASSERTION — the assembly-time twin of
|
|
1500
|
+
* `sequence()`'s `parseAdjacent` fork (combinators/sequence.ts:118).
|
|
1501
|
+
*
|
|
1502
|
+
* `adjacent()` / `notAdjacent()` are BOUNDARY tests, not terms: each asks
|
|
1503
|
+
* whether trivia sat between the previous term and here, so it has to be
|
|
1504
|
+
* answered at the cursor, BEFORE the ambient scan `nextTerm` performs.
|
|
1505
|
+
* Handed the post-scan position it would find the gap already consumed
|
|
1506
|
+
* and answer "adjacent" every time — `adjacent()` a no-op,
|
|
1507
|
+
* `notAdjacent()` a guaranteed failure, both behind a parse that still
|
|
1508
|
+
* succeeds. Which term holds an assertion is TABLE DATA, so the decision
|
|
1509
|
+
* is made here and the parse path holds a bound runner per term rather
|
|
1510
|
+
* than an opcode test per term.
|
|
1511
|
+
*
|
|
1512
|
+
* A runner returns the new cursor or −1, with the value in `TERMV` —
|
|
1513
|
+
* `nextTerm`'s protocol, so the two kinds are interchangeable in the
|
|
1514
|
+
* loop. The assertion's runner returns `cur` UNMOVED: the following term
|
|
1515
|
+
* re-scans the same gap and keeps its own commit/rewind decision, which
|
|
1516
|
+
* is what makes the tree, the spans and the trivia log identical to the
|
|
1517
|
+
* same sequence written without the marker.
|
|
1518
|
+
*
|
|
1519
|
+
* Index 0 is never an assertion — `sequence()` rejects it at
|
|
1520
|
+
* construction, since the gap before the first term belongs to the
|
|
1521
|
+
* caller — so the first child is still called directly.
|
|
1522
|
+
*/
|
|
1523
|
+
/**
|
|
1524
|
+
* RECOVERY SEQUENCES PUBLISH `ctx._sync`, and that is the whole of
|
|
1525
|
+
* "recovery config" — no grammar carries any.
|
|
1526
|
+
*
|
|
1527
|
+
* Before term `i`, the sentinel for the union of every LATER term's first
|
|
1528
|
+
* set becomes the sync point a list nested in that term resyncs to, so a
|
|
1529
|
+
* `sepBy` inside `sequence('{', list, '}')` finds `}` with nothing
|
|
1530
|
+
* annotated. Where there is no usable local follow (the last term, an
|
|
1531
|
+
* `any` first set) the INHERITED sync stays published, which is how the
|
|
1532
|
+
* enclosing delimiter reaches across a rule boundary.
|
|
1533
|
+
*
|
|
1534
|
+
* ONE generic loop rather than the arity-specialised pieces: this is only
|
|
1535
|
+
* reached in a recovery table, where the arity unroll buys a fraction of a
|
|
1536
|
+
* cold-path parse and costs four more copies of the publish protocol to
|
|
1537
|
+
* keep in step with the other two engines.
|
|
1538
|
+
*
|
|
1539
|
+
* The restore is `sequence()`'s `finally` (combinators/sequence.ts:105).
|
|
1540
|
+
* Codegen does not restore and compensates by having every list capture
|
|
1541
|
+
* `_sync` at ENTRY (codegen.ts:3032); the table captures at entry too, so
|
|
1542
|
+
* this is unobservable either way — it is here because leaving a stale
|
|
1543
|
+
* sentinel published is the kind of state a later reader will trust.
|
|
1544
|
+
*/
|
|
1545
|
+
if (REC) {
|
|
1546
|
+
const syncs: (Combinator<unknown> | undefined)[] = new Array<Combinator<unknown> | undefined>(n)
|
|
1547
|
+
for (let i = 0; i < n; i++) syncs[i] = sentinelFor(code[base + n + i]!)
|
|
1548
|
+
const runners: TermRunner[] = new Array<TermRunner>(n)
|
|
1549
|
+
for (let i = 1; i < n; i++) {
|
|
1550
|
+
const kidIp = code[base + i]!
|
|
1551
|
+
if (code[kidIp] !== OP_ADJ) {
|
|
1552
|
+
const kid = kids[i]!
|
|
1553
|
+
runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
|
|
1554
|
+
continue
|
|
1555
|
+
}
|
|
1556
|
+
const negated = code[kidIp + 1] === 1
|
|
1557
|
+
const ki = code[kidIp + 2]!
|
|
1558
|
+
const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
|
|
1559
|
+
const xf = fx[code[kidIp + 3]!] as string[]
|
|
1560
|
+
runners[i] = (input, cur, ctx) => {
|
|
1561
|
+
if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
|
|
1562
|
+
ctx._fe = cur; ctx._fx = xf
|
|
1563
|
+
return -1
|
|
1564
|
+
}
|
|
1565
|
+
TERMV = null
|
|
1566
|
+
return cur
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const runSyncTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
|
|
1570
|
+
const inherited = ctx._sync
|
|
1571
|
+
ctx._sync = syncs[0] ?? inherited
|
|
1572
|
+
const v0 = kids[0]!(input, pos, ctx)
|
|
1573
|
+
if (v0 === FAIL) { ctx._sync = inherited; return -1 }
|
|
1574
|
+
if (values !== undefined) values.push(v0)
|
|
1575
|
+
let cur = EC.e
|
|
1576
|
+
for (let i = 1; i < n; i++) {
|
|
1577
|
+
ctx._sync = syncs[i] ?? inherited
|
|
1578
|
+
cur = runners[i]!(input, cur, ctx)
|
|
1579
|
+
if (cur < 0) { ctx._sync = inherited; return -1 }
|
|
1580
|
+
if (values !== undefined) values.push(TERMV)
|
|
1581
|
+
}
|
|
1582
|
+
ctx._sync = inherited
|
|
1583
|
+
return cur
|
|
1584
|
+
}
|
|
1585
|
+
if (fused) {
|
|
1586
|
+
return (input, pos, ctx) => {
|
|
1587
|
+
const values: unknown[] = []
|
|
1588
|
+
const cur = runSyncTerms(input, pos, ctx, values)
|
|
1589
|
+
if (cur < 0) return FAIL
|
|
1590
|
+
EC.e = cur
|
|
1591
|
+
return fn!(values, { start: pos, end: cur })
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (wantValues) {
|
|
1595
|
+
return (input, pos, ctx) => {
|
|
1596
|
+
const values: unknown[] = []
|
|
1597
|
+
const cur = runSyncTerms(input, pos, ctx, values)
|
|
1598
|
+
if (cur < 0) return FAIL
|
|
1599
|
+
EC.e = cur
|
|
1600
|
+
return values
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return (input, pos, ctx) => {
|
|
1604
|
+
const cur = runSyncTerms(input, pos, ctx, undefined)
|
|
1605
|
+
if (cur < 0) return FAIL
|
|
1606
|
+
EC.e = cur
|
|
1607
|
+
return undefined
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
let hasAdj = false
|
|
1612
|
+
for (let i = 1; i < n; i++) if (code[code[base + i]!] === OP_ADJ) { hasAdj = true; break }
|
|
1613
|
+
if (hasAdj) {
|
|
1614
|
+
const runners: TermRunner[] = new Array<TermRunner>(n)
|
|
1615
|
+
for (let i = 1; i < n; i++) {
|
|
1616
|
+
const kidIp = code[base + i]!
|
|
1617
|
+
if (code[kidIp] !== OP_ADJ) {
|
|
1618
|
+
const kid = kids[i]!
|
|
1619
|
+
runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
|
|
1620
|
+
continue
|
|
1621
|
+
}
|
|
1622
|
+
const negated = code[kidIp + 1] === 1
|
|
1623
|
+
const ki = code[kidIp + 2]!
|
|
1624
|
+
// The FILTER is bound; the MASK is not. A scope can swap the trivia
|
|
1625
|
+
// table, so which bit a category name means is a parse-time fact —
|
|
1626
|
+
// `adjacencyHolds` resolves it against the active table, exactly as
|
|
1627
|
+
// the interpreter does.
|
|
1628
|
+
const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
|
|
1629
|
+
const xf = fx[code[kidIp + 3]!] as string[]
|
|
1630
|
+
runners[i] = (input, cur, ctx) => {
|
|
1631
|
+
if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
|
|
1632
|
+
ctx._fe = cur; ctx._fx = xf
|
|
1633
|
+
return -1
|
|
1634
|
+
}
|
|
1635
|
+
TERMV = null
|
|
1636
|
+
return cur
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const runAdjTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
|
|
1640
|
+
const v0 = kids[0]!(input, pos, ctx)
|
|
1641
|
+
if (v0 === FAIL) return -1
|
|
1642
|
+
if (values !== undefined) values.push(v0)
|
|
1643
|
+
let cur = EC.e
|
|
1644
|
+
for (let i = 1; i < n; i++) {
|
|
1645
|
+
cur = runners[i]!(input, cur, ctx)
|
|
1646
|
+
if (cur < 0) return -1
|
|
1647
|
+
if (values !== undefined) values.push(TERMV)
|
|
1648
|
+
}
|
|
1649
|
+
return cur
|
|
1650
|
+
}
|
|
1651
|
+
if (fused) {
|
|
1652
|
+
return (input, pos, ctx) => {
|
|
1653
|
+
const values: unknown[] = []
|
|
1654
|
+
const cur = runAdjTerms(input, pos, ctx, values)
|
|
1655
|
+
if (cur < 0) return FAIL
|
|
1656
|
+
EC.e = cur
|
|
1657
|
+
return fn!(values, { start: pos, end: cur })
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
if (wantValues) {
|
|
1661
|
+
return (input, pos, ctx) => {
|
|
1662
|
+
const values: unknown[] = []
|
|
1663
|
+
const cur = runAdjTerms(input, pos, ctx, values)
|
|
1664
|
+
if (cur < 0) return FAIL
|
|
1665
|
+
EC.e = cur
|
|
1666
|
+
return values
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return (input, pos, ctx) => {
|
|
1670
|
+
const cur = runAdjTerms(input, pos, ctx, undefined)
|
|
1671
|
+
if (cur < 0) return FAIL
|
|
1672
|
+
EC.e = cur
|
|
1673
|
+
return undefined
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* ARITY-SPECIALISED PIECES — the "pre-made, fully-formed" half of the
|
|
1679
|
+
* design, at the shape that dominates every grammar here.
|
|
1680
|
+
*
|
|
1681
|
+
* A sequence's ARITY is table data, so the loop over it is work assembly
|
|
1682
|
+
* can finish. Unrolling removes, per term: the counter, the bounds-checked
|
|
1683
|
+
* `kids[i]` load, and the `i > 0` test that decides whether trivia
|
|
1684
|
+
* precedes the term — the FIRST term never has trivia before it, and that
|
|
1685
|
+
* was being re-decided 166,842 times per parse for `SEQV` alone.
|
|
1686
|
+
*
|
|
1687
|
+
* The value array is built as a LITERAL of the right size rather than
|
|
1688
|
+
* grown by `push`, so a 2- or 3-term sequence allocates once at its final
|
|
1689
|
+
* capacity instead of reallocating as it fills.
|
|
1690
|
+
*
|
|
1691
|
+
* Arity 1-3 covers the overwhelming majority; above that the general loop
|
|
1692
|
+
* runs, so there is no arity this fails to lower.
|
|
1693
|
+
*/
|
|
1694
|
+
if (n === 1) {
|
|
1695
|
+
const k0 = kids[0]!
|
|
1696
|
+
if (fused) {
|
|
1697
|
+
return (input, pos, ctx) => {
|
|
1698
|
+
const v = k0(input, pos, ctx)
|
|
1699
|
+
if (v === FAIL) return FAIL
|
|
1700
|
+
return fn!([v], { start: pos, end: EC.e })
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (wantValues) {
|
|
1704
|
+
return (input, pos, ctx) => {
|
|
1705
|
+
const v = k0(input, pos, ctx)
|
|
1706
|
+
if (v === FAIL) return FAIL
|
|
1707
|
+
return [v]
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return (input, pos, ctx) => {
|
|
1711
|
+
const v = k0(input, pos, ctx)
|
|
1712
|
+
if (v === FAIL) return FAIL
|
|
1713
|
+
return undefined
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (n === 2) {
|
|
1718
|
+
const k0 = kids[0]!, k1 = kids[1]!
|
|
1719
|
+
if (fused) {
|
|
1720
|
+
return (input, pos, ctx) => {
|
|
1721
|
+
const v0 = k0(input, pos, ctx)
|
|
1722
|
+
if (v0 === FAIL) return FAIL
|
|
1723
|
+
const cur = nextTerm(k1, input, EC.e, ctx)
|
|
1724
|
+
if (cur < 0) return FAIL
|
|
1725
|
+
EC.e = cur
|
|
1726
|
+
return fn!([v0, TERMV], { start: pos, end: cur })
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (wantValues) {
|
|
1730
|
+
return (input, pos, ctx) => {
|
|
1731
|
+
const v0 = k0(input, pos, ctx)
|
|
1732
|
+
if (v0 === FAIL) return FAIL
|
|
1733
|
+
const cur = nextTerm(k1, input, EC.e, ctx)
|
|
1734
|
+
if (cur < 0) return FAIL
|
|
1735
|
+
EC.e = cur
|
|
1736
|
+
return [v0, TERMV]
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return (input, pos, ctx) => {
|
|
1740
|
+
const v0 = k0(input, pos, ctx)
|
|
1741
|
+
if (v0 === FAIL) return FAIL
|
|
1742
|
+
const cur = nextTerm(k1, input, EC.e, ctx)
|
|
1743
|
+
if (cur < 0) return FAIL
|
|
1744
|
+
EC.e = cur
|
|
1745
|
+
return undefined
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (n === 3) {
|
|
1750
|
+
const k0 = kids[0]!, k1 = kids[1]!, k2 = kids[2]!
|
|
1751
|
+
if (fused) {
|
|
1752
|
+
return (input, pos, ctx) => {
|
|
1753
|
+
const v0 = k0(input, pos, ctx)
|
|
1754
|
+
if (v0 === FAIL) return FAIL
|
|
1755
|
+
let cur = nextTerm(k1, input, EC.e, ctx)
|
|
1756
|
+
if (cur < 0) return FAIL
|
|
1757
|
+
const v1 = TERMV
|
|
1758
|
+
cur = nextTerm(k2, input, cur, ctx)
|
|
1759
|
+
if (cur < 0) return FAIL
|
|
1760
|
+
EC.e = cur
|
|
1761
|
+
return fn!([v0, v1, TERMV], { start: pos, end: cur })
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (wantValues) {
|
|
1765
|
+
return (input, pos, ctx) => {
|
|
1766
|
+
const v0 = k0(input, pos, ctx)
|
|
1767
|
+
if (v0 === FAIL) return FAIL
|
|
1768
|
+
let cur = nextTerm(k1, input, EC.e, ctx)
|
|
1769
|
+
if (cur < 0) return FAIL
|
|
1770
|
+
const v1 = TERMV
|
|
1771
|
+
cur = nextTerm(k2, input, cur, ctx)
|
|
1772
|
+
if (cur < 0) return FAIL
|
|
1773
|
+
EC.e = cur
|
|
1774
|
+
return [v0, v1, TERMV]
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return (input, pos, ctx) => {
|
|
1778
|
+
const v0 = k0(input, pos, ctx)
|
|
1779
|
+
if (v0 === FAIL) return FAIL
|
|
1780
|
+
let cur = nextTerm(k1, input, EC.e, ctx)
|
|
1781
|
+
if (cur < 0) return FAIL
|
|
1782
|
+
cur = nextTerm(k2, input, cur, ctx)
|
|
1783
|
+
if (cur < 0) return FAIL
|
|
1784
|
+
EC.e = cur
|
|
1785
|
+
return undefined
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (fused) {
|
|
1790
|
+
return (input, pos, ctx) => {
|
|
1791
|
+
const values: unknown[] = []
|
|
1792
|
+
const cur = runTerms(input, pos, ctx, values)
|
|
1793
|
+
if (cur < 0) return FAIL
|
|
1794
|
+
EC.e = cur
|
|
1795
|
+
return fn!(values, { start: pos, end: cur })
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (wantValues) {
|
|
1799
|
+
return (input, pos, ctx) => {
|
|
1800
|
+
const values: unknown[] = []
|
|
1801
|
+
const cur = runTerms(input, pos, ctx, values)
|
|
1802
|
+
if (cur < 0) return FAIL
|
|
1803
|
+
EC.e = cur
|
|
1804
|
+
return values
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return (input, pos, ctx) => {
|
|
1808
|
+
const cur = runTerms(input, pos, ctx, undefined)
|
|
1809
|
+
if (cur < 0) return FAIL
|
|
1810
|
+
EC.e = cur
|
|
1811
|
+
return undefined
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/* ── choice ──────────────────────────────────────────────────────────── */
|
|
1816
|
+
|
|
1817
|
+
case OP_CHOICE: {
|
|
1818
|
+
const table = disp[code[ip + 1]!]!
|
|
1819
|
+
const n = code[ip + 2]!
|
|
1820
|
+
const choiceFx = fx[code[ip + 3]!] as string[]
|
|
1821
|
+
const base = ip + 4
|
|
1822
|
+
const arms: Piece[] = new Array<Piece>(n)
|
|
1823
|
+
for (let i = 0; i < n; i++) arms[i] = link(code[base + i]!)
|
|
1824
|
+
// PER-ARM STATIC SETS, resolved once at assembly. An arm the char gate
|
|
1825
|
+
// declines to enter still owes its expectation: the interpreter has no
|
|
1826
|
+
// such gate, enters the arm, and its opener's failure is what lands in
|
|
1827
|
+
// the accumulation.
|
|
1828
|
+
const armFx: (string[])[] = new Array<string[]>(n)
|
|
1829
|
+
for (let i = 0; i < n; i++) armFx[i] = fx[code[base + n + i]!] as string[]
|
|
1830
|
+
|
|
1831
|
+
// THE TWO CHOICE SHAPES ARE TWO PIECES. `exec.ts` tested `table.exclusive`
|
|
1832
|
+
// on every choice execution; it is table data, so it selects here.
|
|
1833
|
+
if (table.exclusive) {
|
|
1834
|
+
const ascii = table.ascii
|
|
1835
|
+
const hi = table.hi
|
|
1836
|
+
// THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
|
|
1837
|
+
// ungated arm is one whose class is −1, and `resolveDispatch` clears
|
|
1838
|
+
// `exclusive` for exactly those arms (program.ts) — so `table.open` is
|
|
1839
|
+
// empty whenever `exclusive` holds, and the open-arm fallback loop this
|
|
1840
|
+
// piece used to carry was unreachable code plus a per-parse length test.
|
|
1841
|
+
return (input, pos, ctx) => {
|
|
1842
|
+
const c = lead(input, pos)
|
|
1843
|
+
let arm = -1
|
|
1844
|
+
if (c >= 0 && c < 128) {
|
|
1845
|
+
const a = ascii[c]!
|
|
1846
|
+
if (a !== 0) arm = a - 1
|
|
1847
|
+
} else if (c >= 128) {
|
|
1848
|
+
for (let i = 0; i < hi.length; i += 3) {
|
|
1849
|
+
if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (arm >= 0) {
|
|
1853
|
+
ctx._fc = false
|
|
1854
|
+
const v = arms[arm]!(input, pos, ctx)
|
|
1855
|
+
if (v !== FAIL) return v
|
|
1856
|
+
// THE CUT — a committed failure fails the whole choice.
|
|
1857
|
+
if (committed(ctx)) return FAIL
|
|
1858
|
+
// THE DISPATCHED ARM'S OWN SET IS THE ANSWER, at the choice's own
|
|
1859
|
+
// position — the rule both shipped engines apply (choice.ts:105,
|
|
1860
|
+
// codegen.ts:1985). `_fx` already holds it; reporting the arm is
|
|
1861
|
+
// declining to overwrite it. See `exec.ts` for why this is NOT
|
|
1862
|
+
// furthest-failure merging and what the residual JSON gap is.
|
|
1863
|
+
const armFx = ctx._fx
|
|
1864
|
+
if (armFx !== undefined && armFx.length > 0) { ctx._fe = pos; return FAIL }
|
|
1865
|
+
}
|
|
1866
|
+
// THE UNION IS REPORTED ONLY ON A DISPATCH MISS, where the interpreter's
|
|
1867
|
+
// `parsers.flatMap` over non-nullable, char-excluded arms yields exactly
|
|
1868
|
+
// each arm's own opener — this set, without running the arms.
|
|
1869
|
+
ctx._fe = pos; ctx._fx = choiceFx
|
|
1870
|
+
return FAIL
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// THE PER-ARM GATE. Arms in source order, each skipped when its own class
|
|
1875
|
+
// excludes the char at `pos`. `null` means nullable or unmappable, and
|
|
1876
|
+
// those arms are always entered.
|
|
1877
|
+
const armCls = table.armCls
|
|
1878
|
+
const gates: (ResolvedClass | null)[] = new Array<ResolvedClass | null>(n)
|
|
1879
|
+
for (let i = 0; i < n; i++) gates[i] = armCls[i] ?? null
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* THE GATE, PRECOMPUTED. One `Uint32Array` load replaces `n` class tests.
|
|
1883
|
+
*
|
|
1884
|
+
* `exec.ts` asked, per arm per execution, "does this arm's class hold the
|
|
1885
|
+
* char at `pos`" — `n` calls into `classHas`, each a bounds check plus a
|
|
1886
|
+
* `Uint8Array` load, on 136,760 choice executions per parse of
|
|
1887
|
+
* `benchmark.less`. Every one of those answers is a function of the
|
|
1888
|
+
* CHARACTER and the TABLE, both of which are known here, so the whole
|
|
1889
|
+
* question is answered once at assembly: `cand[c]` is the bitmask of arms
|
|
1890
|
+
* that could match `c`, and the loop visits only set bits, IN ORDER, so
|
|
1891
|
+
* the source ordering the gate exists to preserve is preserved exactly.
|
|
1892
|
+
*
|
|
1893
|
+
* Slot 128 is EOF (`lead` returns −1): `classHas` rejects a negative code,
|
|
1894
|
+
* so only the always-enter arms are candidates there. Code points ≥ 128
|
|
1895
|
+
* keep the per-arm loop — the ranges make a table the wrong shape and
|
|
1896
|
+
* astral input is not the hot path.
|
|
1897
|
+
*
|
|
1898
|
+
* Bounded at 32 arms because the mask is one word. Above that the general
|
|
1899
|
+
* loop runs, which is the same code the ≥ 128 path takes, so there is no
|
|
1900
|
+
* second implementation to drift.
|
|
1901
|
+
*/
|
|
1902
|
+
const maskable = n <= 32
|
|
1903
|
+
let cand: Uint32Array | undefined
|
|
1904
|
+
if (maskable) {
|
|
1905
|
+
const m = new Uint32Array(129)
|
|
1906
|
+
for (let i = 0; i < n; i++) {
|
|
1907
|
+
const cls = gates[i]!
|
|
1908
|
+
const bit = 1 << i
|
|
1909
|
+
if (cls === null) {
|
|
1910
|
+
for (let c = 0; c < 129; c++) m[c]! |= bit
|
|
1911
|
+
continue
|
|
1912
|
+
}
|
|
1913
|
+
for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) m[c]! |= bit
|
|
1914
|
+
}
|
|
1915
|
+
cand = m
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (cand !== undefined) {
|
|
1919
|
+
const mask = cand
|
|
1920
|
+
return (input, pos, ctx) => {
|
|
1921
|
+
const c = lead(input, pos)
|
|
1922
|
+
const need = markCst(ctx)
|
|
1923
|
+
const mRaw = MRAW
|
|
1924
|
+
const mTl = MTL
|
|
1925
|
+
const mLv = MLV
|
|
1926
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1927
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1928
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1929
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1930
|
+
let acc: string[] | undefined
|
|
1931
|
+
if (c < 128) {
|
|
1932
|
+
// `c` is −1 at EOF, which indexes slot 128 — the always-enter arms.
|
|
1933
|
+
const bits0 = mask[c < 0 ? 128 : c]!
|
|
1934
|
+
let bits = bits0
|
|
1935
|
+
// Arms below `prev` have already contributed. Only the FAILURE path
|
|
1936
|
+
// advances it, so a matching arm returns without paying for any of
|
|
1937
|
+
// this — the skipped arms' sets are read on no other exit.
|
|
1938
|
+
let prev = 0
|
|
1939
|
+
while (bits !== 0) {
|
|
1940
|
+
const i = 31 - Math.clz32(bits & -bits)
|
|
1941
|
+
bits &= bits - 1
|
|
1942
|
+
ctx._fc = false
|
|
1943
|
+
const v = arms[i]!(input, pos, ctx)
|
|
1944
|
+
if (v !== FAIL) return v
|
|
1945
|
+
for (let j = prev; j < i; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
|
|
1946
|
+
prev = i + 1
|
|
1947
|
+
acc = accSet(ctx._fx, acc)
|
|
1948
|
+
// A committed arm cuts, and the interpreter reports the
|
|
1949
|
+
// accumulation at the ARM's span (choice.ts:170), not at `pos`.
|
|
1950
|
+
if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
|
|
1951
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1952
|
+
}
|
|
1953
|
+
for (let j = prev; j < n; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
|
|
1954
|
+
ctx._fe = pos; ctx._fx = acc ?? choiceFx
|
|
1955
|
+
return FAIL
|
|
1956
|
+
}
|
|
1957
|
+
for (let i = 0; i < n; i++) {
|
|
1958
|
+
const cls = gates[i]!
|
|
1959
|
+
if (cls !== null && !classHas(cls, c)) { acc = accSet(armFx[i]!, acc); continue }
|
|
1960
|
+
ctx._fc = false
|
|
1961
|
+
const v = arms[i]!(input, pos, ctx)
|
|
1962
|
+
if (v !== FAIL) return v
|
|
1963
|
+
acc = accSet(ctx._fx, acc)
|
|
1964
|
+
if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
|
|
1965
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1966
|
+
}
|
|
1967
|
+
ctx._fe = pos; ctx._fx = acc ?? choiceFx
|
|
1968
|
+
return FAIL
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
return (input, pos, ctx) => {
|
|
1973
|
+
const c = lead(input, pos)
|
|
1974
|
+
const need = markCst(ctx)
|
|
1975
|
+
const mRaw = MRAW
|
|
1976
|
+
const mTl = MTL
|
|
1977
|
+
const mLv = MLV
|
|
1978
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
1979
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
1980
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
1981
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1982
|
+
let acc: string[] | undefined
|
|
1983
|
+
for (let i = 0; i < n; i++) {
|
|
1984
|
+
const cls = gates[i]!
|
|
1985
|
+
if (cls !== null && !classHas(cls, c)) { acc = accSet(armFx[i]!, acc); continue }
|
|
1986
|
+
ctx._fc = false
|
|
1987
|
+
const v = arms[i]!(input, pos, ctx)
|
|
1988
|
+
if (v !== FAIL) return v
|
|
1989
|
+
acc = accSet(ctx._fx, acc)
|
|
1990
|
+
if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
|
|
1991
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1992
|
+
}
|
|
1993
|
+
ctx._fe = pos; ctx._fx = acc ?? choiceFx
|
|
1994
|
+
return FAIL
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
/* ── greedyClassify / autoNot ────────────────────────────────────────── */
|
|
1999
|
+
|
|
2000
|
+
case OP_GREEDY: {
|
|
2001
|
+
const sup = link(code[ip + 1]!)
|
|
2002
|
+
const n = code[ip + 2]!
|
|
2003
|
+
// THE CLASSIFICATION MAP, BUILT ONCE. `choice()` builds the same map once
|
|
2004
|
+
// per construction (choice.ts:64-70); nothing about it is per-parse.
|
|
2005
|
+
const byWord = new Map<string, Piece>()
|
|
2006
|
+
for (let i = 0; i < n; i++) byWord.set(k[code[ip + 3 + 2 * i]!] as string, link(code[ip + 4 + 2 * i]!))
|
|
2007
|
+
return (input, pos, ctx) => {
|
|
2008
|
+
const need = markCst(ctx)
|
|
2009
|
+
const mRaw = MRAW
|
|
2010
|
+
const mTl = MTL
|
|
2011
|
+
const mLv = MLV
|
|
2012
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
2013
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
2014
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
2015
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
2016
|
+
const v = sup(input, pos, ctx)
|
|
2017
|
+
// The super arm's failure propagates verbatim — its `_fe`/`_fx`, not
|
|
2018
|
+
// the union of the arms (choice.ts:126).
|
|
2019
|
+
if (v === FAIL) return FAIL
|
|
2020
|
+
const end = EC.e
|
|
2021
|
+
const lit = byWord.get(input.slice(pos, end))
|
|
2022
|
+
if (lit === undefined) { EC.e = end; return v }
|
|
2023
|
+
// Unwind the regex's leaf so the credited arm's own leaf is the only
|
|
2024
|
+
// one. Re-running the arm cannot fail: the word IS its literal.
|
|
2025
|
+
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2026
|
+
return lit(input, pos, ctx)
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
case OP_ARMGATE: {
|
|
2031
|
+
// Predicate, arm and expected set all bound HERE — the body reads no
|
|
2032
|
+
// opcode, decodes no operand and tests no setting.
|
|
2033
|
+
const pred = fns[code[ip + 1]!] as (s: unknown) => boolean
|
|
2034
|
+
const child = link(code[ip + 2]!)
|
|
2035
|
+
const xf = fx[code[ip + 3]!] as string[]
|
|
2036
|
+
return (input, pos, ctx) => {
|
|
2037
|
+
if (!pred(ctx.state)) {
|
|
2038
|
+
// A `continue` in the interpreter (choice.ts:150), not a failure —
|
|
2039
|
+
// so no cut survives to the enclosing choice.
|
|
2040
|
+
ctx._fc = false
|
|
2041
|
+
ctx._fe = pos
|
|
2042
|
+
ctx._fx = xf
|
|
2043
|
+
return FAIL
|
|
2044
|
+
}
|
|
2045
|
+
return child(input, pos, ctx)
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
case OP_REJECT: {
|
|
2050
|
+
const child = link(code[ip + 1]!)
|
|
2051
|
+
const n = code[ip + 2]!
|
|
2052
|
+
// The checks SPLIT BY KIND here, so neither loop below tests which kind
|
|
2053
|
+
// it is holding. `autoNotFires` is a pure OR over independent lookahead
|
|
2054
|
+
// predicates, so grouping them changes nothing but the branch.
|
|
2055
|
+
const strs: string[] = []
|
|
2056
|
+
const clss: ResolvedClass[] = []
|
|
2057
|
+
for (let i = 0; i < n; i++) {
|
|
2058
|
+
const o = code[ip + 4 + 2 * i]!
|
|
2059
|
+
if (code[ip + 3 + 2 * i]! === 0) strs.push(k[o] as string)
|
|
2060
|
+
else clss.push(cc[o]!)
|
|
2061
|
+
}
|
|
2062
|
+
const ns = strs.length
|
|
2063
|
+
const nc = clss.length
|
|
2064
|
+
return (input, pos, ctx) => {
|
|
2065
|
+
const v = child(input, pos, ctx)
|
|
2066
|
+
if (v === FAIL) return FAIL
|
|
2067
|
+
const end = EC.e
|
|
2068
|
+
for (let i = 0; i < ns; i++) {
|
|
2069
|
+
if (!input.startsWith(strs[i]!, end)) continue
|
|
2070
|
+
// A `continue` in the interpreter, not a failure — so no cut survives
|
|
2071
|
+
// AND no expectation is contributed: `choice.ts:174` rolls back and
|
|
2072
|
+
// moves on without touching `expected`. `_fx` still holds whatever the
|
|
2073
|
+
// arm's last inner failure left, so it is cleared rather than left to
|
|
2074
|
+
// be accumulated as if this arm had failed wanting something.
|
|
2075
|
+
ctx._fc = false
|
|
2076
|
+
ctx._fx = undefined
|
|
2077
|
+
return FAIL
|
|
2078
|
+
}
|
|
2079
|
+
if (nc !== 0) {
|
|
2080
|
+
const c = lead(input, end)
|
|
2081
|
+
for (let i = 0; i < nc; i++) {
|
|
2082
|
+
if (!classHas(clss[i]!, c)) continue
|
|
2083
|
+
ctx._fc = false
|
|
2084
|
+
ctx._fx = undefined
|
|
2085
|
+
return FAIL
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
EC.e = end
|
|
2089
|
+
return v
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
/* ── repetition ──────────────────────────────────────────────────────── */
|
|
2094
|
+
|
|
2095
|
+
case OP_REP:
|
|
2096
|
+
case OP_REPV: {
|
|
2097
|
+
const child = link(code[ip + 1]!)
|
|
2098
|
+
const min = code[ip + 2]!
|
|
2099
|
+
const max = code[ip + 3]!
|
|
2100
|
+
const sepIp = code[ip + 4]!
|
|
2101
|
+
const sep = sepIp >= 0 ? link(sepIp) : undefined
|
|
2102
|
+
const flags = code[ip + 5]!
|
|
2103
|
+
const keepSeparators = (flags & 2) !== 0
|
|
2104
|
+
const trailingAllowed = (flags & 1) !== 0
|
|
2105
|
+
const reportItem = (flags & 4) !== 0
|
|
2106
|
+
const itemFx = reportItem ? fx[code[ip + 6]!] as string[] : EMPTY_FX
|
|
2107
|
+
const collect = op === OP_REP
|
|
2108
|
+
// `many()` — the min-0, separator-less repeat — is the only shape that runs
|
|
2109
|
+
// its FIRST item through `repItem` and so skips leading trivia. The shape
|
|
2110
|
+
// identifies itself, and it is table data, so it is decided here.
|
|
2111
|
+
const skipBeforeFirst = sepIp < 0 && min === 0
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* TOLERANT RECOVERY — the third implementation of `recoverScan`'s
|
|
2115
|
+
* protocol, and it calls the SAME functions the other two do
|
|
2116
|
+
* (`recovery/scan.ts`), so an error's span, its expected set and its CST
|
|
2117
|
+
* embedding cannot drift between engines.
|
|
2118
|
+
*
|
|
2119
|
+
* Reached only in a recovery table, and inside it only on the FAILURE of
|
|
2120
|
+
* an element, so a matching item pays a `_sync` read at entry and nothing
|
|
2121
|
+
* else. The `ctx._tolerant` gate is the same dormancy the source lowering
|
|
2122
|
+
* and the interpreter have.
|
|
2123
|
+
*
|
|
2124
|
+
* WHAT RECOVERS, AND WHERE THE THREE ENGINES AGREE:
|
|
2125
|
+
* - a MANDATORY item (`count < min`) of a separator-less repeat does NOT
|
|
2126
|
+
* recover: `oneOrMore`'s first item propagates its failure
|
|
2127
|
+
* (repeat.ts:221), and codegen inlines the `min` items ahead of the
|
|
2128
|
+
* loop with an early return. A `sepBy`'s first element DOES recover,
|
|
2129
|
+
* which is why the test is `sep !== undefined || count >= min`.
|
|
2130
|
+
* - the check is `at(mySync, itemStart)`: sitting ON the sync token is a
|
|
2131
|
+
* clean list end, not junk, and `itemStart` is past any leading trivia
|
|
2132
|
+
* so that trivia is never swallowed into the error span
|
|
2133
|
+
* (repeat.ts:167).
|
|
2134
|
+
* - a separated list scans to its OWN separator or the enclosing
|
|
2135
|
+
* delimiter (`orSentinel`, repeat.ts:454).
|
|
2136
|
+
* - `mySync` is captured at ENTRY. An element's own sequence publishes
|
|
2137
|
+
* over `_sync` while it runs; the interpreter restores in a `finally`
|
|
2138
|
+
* and codegen saves at entry (codegen.ts:3032) — same value, both ways.
|
|
2139
|
+
* - the separator-less path rolls the leading trivia back BEFORE
|
|
2140
|
+
* recovering (repeat.ts's `repItem`), and the separated path does NOT:
|
|
2141
|
+
* a consumed separator and the error after it both belong to the list
|
|
2142
|
+
* (repeat.ts:533).
|
|
2143
|
+
*/
|
|
2144
|
+
if (REC) {
|
|
2145
|
+
const itemFx = fx[code[ip + 6]!] as string[]
|
|
2146
|
+
const sepSent = sentinelFor(code[ip + 7]!)
|
|
2147
|
+
return (input, pos, ctx) => {
|
|
2148
|
+
const out: unknown[] | undefined = collect ? [] : undefined
|
|
2149
|
+
const hasTrivia = ctx.trivia !== undefined
|
|
2150
|
+
const needMark = rollbackNeeded(ctx)
|
|
2151
|
+
const mySync = ctx._sync
|
|
2152
|
+
let cur = pos
|
|
2153
|
+
let count = 0
|
|
2154
|
+
for (;;) {
|
|
2155
|
+
if (max >= 0 && count >= max) break
|
|
2156
|
+
if (sep !== undefined && count > 0 && count >= min && cur >= input.length) break
|
|
2157
|
+
if (needMark) markCst(ctx)
|
|
2158
|
+
const mRaw = needMark ? MRAW : 0
|
|
2159
|
+
const mTl = needMark ? MTL : 0
|
|
2160
|
+
const mLv = needMark ? MLV : 0
|
|
2161
|
+
const mFl = needMark ? ctx._fields?.length ?? 0 : 0
|
|
2162
|
+
const mEr = needMark ? ctx._errors?.length ?? 0 : 0
|
|
2163
|
+
const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
|
|
2164
|
+
const mRoot = needMark ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
2165
|
+
let itemStart = cur
|
|
2166
|
+
let sepEnd = -1
|
|
2167
|
+
const viaRepItem = sep === undefined && count >= min && (count > 0 || skipBeforeFirst)
|
|
2168
|
+
if (sep !== undefined && count > 0) {
|
|
2169
|
+
const leavesBefore = cstLeavesLen(ctx)
|
|
2170
|
+
let sp = cur
|
|
2171
|
+
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
2172
|
+
ctx._fc = false
|
|
2173
|
+
const sv = sep(input, sp, ctx)
|
|
2174
|
+
if (sv === FAIL) {
|
|
2175
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2176
|
+
if (committed(ctx)) return FAIL
|
|
2177
|
+
break
|
|
2178
|
+
}
|
|
2179
|
+
if (!keepSeparators) demoteCapturedToRaw(ctx, leavesBefore)
|
|
2180
|
+
sepEnd = EC.e
|
|
2181
|
+
itemStart = hasTrivia ? skipTrivia(input, EC.e, ctx) : EC.e
|
|
2182
|
+
} else if (hasTrivia && (count > 0 || skipBeforeFirst)) {
|
|
2183
|
+
itemStart = skipTrivia(input, itemStart, ctx)
|
|
2184
|
+
}
|
|
2185
|
+
if (itemStart >= input.length && viaRepItem) {
|
|
2186
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2187
|
+
if (trailingAllowed && sepEnd >= 0) cur = sepEnd
|
|
2188
|
+
break
|
|
2189
|
+
}
|
|
2190
|
+
ctx._fc = false
|
|
2191
|
+
const v = child(input, itemStart, ctx)
|
|
2192
|
+
if (v === FAIL) {
|
|
2193
|
+
if (committed(ctx)) {
|
|
2194
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2195
|
+
return FAIL
|
|
2196
|
+
}
|
|
2197
|
+
if (ctx._tolerant === true
|
|
2198
|
+
&& mySync !== undefined
|
|
2199
|
+
&& (sep !== undefined || count >= min)
|
|
2200
|
+
&& !matchesAt(mySync, input, itemStart, ctx)) {
|
|
2201
|
+
if (sep === undefined && needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2202
|
+
const rr = recoverScan(
|
|
2203
|
+
input, itemStart, ctx,
|
|
2204
|
+
sep === undefined ? mySync : orSentinel(sepSent ?? mySync, sepSent === undefined ? undefined : mySync),
|
|
2205
|
+
itemFx,
|
|
2206
|
+
)
|
|
2207
|
+
if (out !== undefined) out.push(rr.error)
|
|
2208
|
+
captureError(ctx, rr.error)
|
|
2209
|
+
count++
|
|
2210
|
+
cur = rr.end
|
|
2211
|
+
continue
|
|
2212
|
+
}
|
|
2213
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2214
|
+
if (trailingAllowed && sepEnd >= 0) cur = sepEnd
|
|
2215
|
+
break
|
|
2216
|
+
}
|
|
2217
|
+
if (EC.e === itemStart && viaRepItem) {
|
|
2218
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2219
|
+
break
|
|
2220
|
+
}
|
|
2221
|
+
if (out !== undefined) out.push(v)
|
|
2222
|
+
cur = EC.e
|
|
2223
|
+
count++
|
|
2224
|
+
}
|
|
2225
|
+
if (count < min) {
|
|
2226
|
+
if (reportItem) { ctx._fe = cur; ctx._fx = itemFx }
|
|
2227
|
+
return FAIL
|
|
2228
|
+
}
|
|
2229
|
+
EC.e = cur
|
|
2230
|
+
return out
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
return (input, pos, ctx) => {
|
|
2235
|
+
const out: unknown[] | undefined = collect ? [] : undefined
|
|
2236
|
+
const hasTrivia = ctx.trivia !== undefined
|
|
2237
|
+
const needMark = rollbackNeeded(ctx)
|
|
2238
|
+
let cur = pos
|
|
2239
|
+
let count = 0
|
|
2240
|
+
for (;;) {
|
|
2241
|
+
if (max >= 0 && count >= max) break
|
|
2242
|
+
// A separated list is bounded by its SEPARATOR, so it stops at EOF at
|
|
2243
|
+
// the LOOP HEAD. Held to `count >= min` so an under-`min` list still
|
|
2244
|
+
// attempts the separator and reports its expected set.
|
|
2245
|
+
if (sep !== undefined && count > 0 && count >= min && cur >= input.length) break
|
|
2246
|
+
// Per ITERATION, never hoisted: a preceding item's `node()` opened and
|
|
2247
|
+
// closed a capture buffer, so `ctx._cstBuf` is not the object it was
|
|
2248
|
+
// at the loop head. `needMark` is the pre-existing loop-invariant.
|
|
2249
|
+
if (needMark) markCst(ctx)
|
|
2250
|
+
const mRaw = needMark ? MRAW : 0
|
|
2251
|
+
const mTl = needMark ? MTL : 0
|
|
2252
|
+
const mLv = needMark ? MLV : 0
|
|
2253
|
+
const mFl = needMark ? ctx._fields?.length ?? 0 : 0
|
|
2254
|
+
const mEr = needMark ? ctx._errors?.length ?? 0 : 0
|
|
2255
|
+
const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
|
|
2256
|
+
const mRoot = needMark ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
2257
|
+
let itemStart = cur
|
|
2258
|
+
let sepEnd = -1
|
|
2259
|
+
const viaRepItem = sep === undefined && count >= min && (count > 0 || skipBeforeFirst)
|
|
2260
|
+
if (sep !== undefined && count > 0) {
|
|
2261
|
+
const leavesBefore = cstLeavesLen(ctx)
|
|
2262
|
+
let sp = cur
|
|
2263
|
+
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
2264
|
+
ctx._fc = false
|
|
2265
|
+
const sv = sep(input, sp, ctx)
|
|
2266
|
+
if (sv === FAIL) {
|
|
2267
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2268
|
+
if (committed(ctx)) return FAIL
|
|
2269
|
+
break
|
|
2270
|
+
}
|
|
2271
|
+
if (!keepSeparators) demoteCapturedToRaw(ctx, leavesBefore)
|
|
2272
|
+
sepEnd = EC.e
|
|
2273
|
+
itemStart = hasTrivia ? skipTrivia(input, EC.e, ctx) : EC.e
|
|
2274
|
+
} else if (hasTrivia && (count > 0 || skipBeforeFirst)) {
|
|
2275
|
+
itemStart = skipTrivia(input, itemStart, ctx)
|
|
2276
|
+
}
|
|
2277
|
+
if (itemStart >= input.length && viaRepItem) {
|
|
2278
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2279
|
+
if (trailingAllowed && sepEnd >= 0) cur = sepEnd
|
|
2280
|
+
break
|
|
2281
|
+
}
|
|
2282
|
+
ctx._fc = false
|
|
2283
|
+
const v = child(input, itemStart, ctx)
|
|
2284
|
+
if (v === FAIL) {
|
|
2285
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2286
|
+
if (committed(ctx)) return FAIL
|
|
2287
|
+
if (trailingAllowed && sepEnd >= 0) cur = sepEnd
|
|
2288
|
+
break
|
|
2289
|
+
}
|
|
2290
|
+
if (EC.e === itemStart && viaRepItem) {
|
|
2291
|
+
// Zero-width item: `repItem`'s TERMINATION device, not a semantic
|
|
2292
|
+
// filter — and it applies only where `repItem` runs.
|
|
2293
|
+
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
2294
|
+
break
|
|
2295
|
+
}
|
|
2296
|
+
if (out !== undefined) out.push(v)
|
|
2297
|
+
cur = EC.e
|
|
2298
|
+
count++
|
|
2299
|
+
}
|
|
2300
|
+
if (count < min) {
|
|
2301
|
+
if (reportItem) { ctx._fe = cur; ctx._fx = itemFx }
|
|
2302
|
+
return FAIL
|
|
2303
|
+
}
|
|
2304
|
+
EC.e = cur
|
|
2305
|
+
return out
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
/* ── dispatch ────────────────────────────────────────────────────────── */
|
|
2310
|
+
|
|
2311
|
+
case OP_DISPATCH: {
|
|
2312
|
+
const spec = dsp[code[ip + 2]!]!
|
|
2313
|
+
const selector = link(code[ip + 1]!)
|
|
2314
|
+
const otherIp = code[ip + 3]!
|
|
2315
|
+
const other = otherIp >= 0 ? link(otherIp) : undefined
|
|
2316
|
+
const otherRouted = code[ip + 4]! === 1
|
|
2317
|
+
const n = code[ip + 5]!
|
|
2318
|
+
const armBase = ip + 6
|
|
2319
|
+
const arms: Piece[] = new Array<Piece>(n)
|
|
2320
|
+
for (let i = 0; i < n; i++) arms[i] = link(code[armBase + i]!)
|
|
2321
|
+
const byKey = spec.byKey
|
|
2322
|
+
const byFold = spec.byFold
|
|
2323
|
+
const hasFold = spec.byFold.size > 0
|
|
2324
|
+
const match = spec.match
|
|
2325
|
+
const matchN = match.length
|
|
2326
|
+
const matchFn: Array<(key: string) => boolean> = new Array<(key: string) => boolean>(matchN)
|
|
2327
|
+
const matchArm: number[] = new Array<number>(matchN)
|
|
2328
|
+
for (let i = 0; i < matchN; i++) { matchFn[i] = linkMatcher(match[i]!); matchArm[i] = match[i]![3] }
|
|
2329
|
+
const routed = spec.routed
|
|
2330
|
+
const expected = spec.expected as string[]
|
|
2331
|
+
|
|
2332
|
+
return (input, pos, ctx) => {
|
|
2333
|
+
const selectorMark = saveTriviaMark(ctx)
|
|
2334
|
+
const selVal = selector(input, pos, ctx)
|
|
2335
|
+
if (selVal === FAIL) return FAIL
|
|
2336
|
+
const selEnd = EC.e
|
|
2337
|
+
const key = selVal as string
|
|
2338
|
+
|
|
2339
|
+
let arm = byKey.get(key)
|
|
2340
|
+
if (arm === undefined && hasFold) arm = byFold.get(asciiFoldKey(key))
|
|
2341
|
+
if (arm === undefined) {
|
|
2342
|
+
for (let i = 0; i < matchN; i++) {
|
|
2343
|
+
if (matchFn[i]!(key)) {
|
|
2344
|
+
arm = matchArm[i]!
|
|
2345
|
+
break
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
let target: Piece
|
|
2351
|
+
let usesRouted: boolean
|
|
2352
|
+
if (arm === undefined) {
|
|
2353
|
+
if (other === undefined) {
|
|
2354
|
+
// No branch and no fallback: fail AT THE SELECTOR'S END.
|
|
2355
|
+
ctx._fe = selEnd
|
|
2356
|
+
ctx._fx = expected
|
|
2357
|
+
return FAIL
|
|
2358
|
+
}
|
|
2359
|
+
target = other
|
|
2360
|
+
usesRouted = otherRouted
|
|
2361
|
+
} else {
|
|
2362
|
+
target = arms[arm]!
|
|
2363
|
+
usesRouted = routed[arm] === 1
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const savedRouted = ctx._routed
|
|
2367
|
+
let mark = saveTriviaMark(ctx)
|
|
2368
|
+
if (usesRouted) {
|
|
2369
|
+
rollbackTrivia(ctx, selectorMark)
|
|
2370
|
+
mark = saveTriviaMark(ctx)
|
|
2371
|
+
ctx._routed = { value: key, span: { start: pos, end: selEnd } }
|
|
2372
|
+
}
|
|
2373
|
+
const v = target(input, usesRouted ? pos : selEnd, ctx)
|
|
2374
|
+
if (usesRouted) ctx._routed = savedRouted
|
|
2375
|
+
if (v === FAIL) {
|
|
2376
|
+
rollbackTrivia(ctx, mark)
|
|
2377
|
+
// A failed dispatch branch is COMMITTED: the selector already matched.
|
|
2378
|
+
ctx._fc = true
|
|
2379
|
+
return FAIL
|
|
2380
|
+
}
|
|
2381
|
+
return [key, v]
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/* ── node ────────────────────────────────────────────────────────────── */
|
|
2386
|
+
|
|
2387
|
+
case OP_NODE:
|
|
2388
|
+
case OP_NODE_TRACK: {
|
|
2389
|
+
const flags = code[ip + 3]!
|
|
2390
|
+
const child = link(code[ip + 2]!)
|
|
2391
|
+
const proj = code[ip + 4]!
|
|
2392
|
+
const buildIdx = code[ip + 1]!
|
|
2393
|
+
const type = k[code[ip + 5]!] as string
|
|
2394
|
+
const tagIdx = code[ip + 6]!
|
|
2395
|
+
const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
|
|
2396
|
+
const tracked = op === OP_NODE_TRACK
|
|
2397
|
+
const readsTrivia = (flags & 4) !== 0
|
|
2398
|
+
const readsState = (flags & 8) !== 0
|
|
2399
|
+
const hasFields = (flags & 16) !== 0
|
|
2400
|
+
const collapse = (flags & 32) !== 0
|
|
2401
|
+
const unwrap = (flags & 64) !== 0
|
|
2402
|
+
const trailingTrivia = (flags & 128) !== 0
|
|
2403
|
+
// HOST MODE IS AN OPTION, and it decided five separate runtime ternaries
|
|
2404
|
+
// in `exec.ts`'s node case — the single most-executed non-terminal at
|
|
2405
|
+
// 145,512 executions per parse of `benchmark.less`. `cstOutputHost(ctx.build)`
|
|
2406
|
+
// is fixed by `run()` before the entry is called, so it selects the piece.
|
|
2407
|
+
const build = buildIdx >= 0
|
|
2408
|
+
? fns[buildIdx] as (
|
|
2409
|
+
children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
|
|
2410
|
+
rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
|
|
2411
|
+
) => unknown
|
|
2412
|
+
: undefined
|
|
2413
|
+
|
|
2414
|
+
// A STRUCTURAL node — no builder, no projection — is the only shape that
|
|
2415
|
+
// takes a per-node-type trivia-kind mask off the host (`node.ts:260`).
|
|
2416
|
+
// Assembly-time, so a node with a reducer pays nothing for the question.
|
|
2417
|
+
const structural = build === undefined && proj < 0
|
|
2418
|
+
const grammarCapture = (flags & 1) !== 0 || trailingTrivia
|
|
2419
|
+
const hostCapturesThisType = structural && cfg.hostCaptureTrivia !== undefined
|
|
2420
|
+
? cfg.hostCaptureTrivia(type)
|
|
2421
|
+
: undefined
|
|
2422
|
+
const wantFields = hasFields || hostCst
|
|
2423
|
+
const captureWide = readsTrivia || hostCst
|
|
2424
|
+
? !structural || grammarCapture || hostCapturesThisType !== false
|
|
2425
|
+
: hostCapturesThisType === true
|
|
2426
|
+
// Only a plain structural host call can omit semantic children. Every
|
|
2427
|
+
// operation that consumes them keeps the lazy buffer, including either
|
|
2428
|
+
// grammar collapse form and the host collapse predicate.
|
|
2429
|
+
const keepChildren = !structural || cfg.hostReadsChildren !== false || collapse || unwrap
|
|
2430
|
+
const rawOnly = !keepChildren
|
|
2431
|
+
return (input, pos, ctx) => {
|
|
2432
|
+
const host = HOST
|
|
2433
|
+
// `beginCstNodeCapture`/`endCstNodeCapture` INLINED, and the two objects
|
|
2434
|
+
// they exchange — the six-field `saved` and the three-field
|
|
2435
|
+
// `{children, rawChildren, triviaLog}` — held in locals instead.
|
|
2436
|
+
//
|
|
2437
|
+
// This is the same scalarisation `rollbackCstCaptureAt` already documents
|
|
2438
|
+
// for the five-field mark, applied to the node piece: `node()` runs
|
|
2439
|
+
// 145,512 times per parse of `benchmark.less`, so those were ~291k
|
|
2440
|
+
// allocations a parse that the compiled engine (which inlines its own
|
|
2441
|
+
// capture prologue and epilogue) never makes.
|
|
2442
|
+
const sCh = ctx._cstChildren
|
|
2443
|
+
const sLv = ctx._cstLeaves
|
|
2444
|
+
const sRaw = ctx._cstRawChildren
|
|
2445
|
+
const sTl = ctx._cstTriviaLog
|
|
2446
|
+
const sCap = ctx.captureTrivia
|
|
2447
|
+
const sBuf = ctx._cstBuf
|
|
2448
|
+
const buf: CstCaptureBuf = rawOnly ? { rawOnly: true } : {}
|
|
2449
|
+
ctx._cstBuf = buf
|
|
2450
|
+
ctx._cstChildren = undefined
|
|
2451
|
+
ctx._cstLeaves = undefined
|
|
2452
|
+
ctx._cstRawChildren = undefined
|
|
2453
|
+
ctx._cstTriviaLog = undefined
|
|
2454
|
+
// `begin` sets this true and the caller immediately cleared it when the
|
|
2455
|
+
// node does not capture wide. Net: the flag IS `captureWide`, a const.
|
|
2456
|
+
ctx.captureTrivia = captureWide
|
|
2457
|
+
const savedFields = ctx._fields
|
|
2458
|
+
ctx._fields = wantFields ? [] : undefined
|
|
2459
|
+
// PER-NODE-TYPE TRIVIA-KIND MASK, scoped to the body and restored —
|
|
2460
|
+
// `node.ts:259-273`, and codegen's `_triviaCaptureMask` save/install/
|
|
2461
|
+
// restore around the same scope (`codegen.ts:4130-4136`). The table
|
|
2462
|
+
// installed nothing, so a host that asked `Outer` for comments only got
|
|
2463
|
+
// every kind the scanner saw.
|
|
2464
|
+
const savedMask = structural ? ctx._triviaCaptureMask : undefined
|
|
2465
|
+
if (structural && host?._parsemanTriviaKinds !== undefined) {
|
|
2466
|
+
ctx._triviaCaptureMask = host._parsemanTriviaKinds(type)
|
|
2467
|
+
}
|
|
2468
|
+
const v = child(input, pos, ctx)
|
|
2469
|
+
if (v !== FAIL && trailingTrivia && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
|
|
2470
|
+
const fieldMap: FieldMap | undefined = wantFields ? buildFieldMap(ctx._fields) : undefined
|
|
2471
|
+
ctx._fields = savedFields
|
|
2472
|
+
if (structural) ctx._triviaCaptureMask = savedMask
|
|
2473
|
+
const kids = rawOnly ? EMPTY_CH : buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY_CH)
|
|
2474
|
+
const hostKids = kids
|
|
2475
|
+
const rawKids = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY_CH)
|
|
2476
|
+
const tlog = buf.tl ?? EMPTY_TLOG
|
|
2477
|
+
ctx._cstBuf = sBuf
|
|
2478
|
+
ctx._cstChildren = sCh
|
|
2479
|
+
ctx._cstLeaves = sLv
|
|
2480
|
+
ctx._cstRawChildren = sRaw
|
|
2481
|
+
ctx._cstTriviaLog = sTl
|
|
2482
|
+
ctx.captureTrivia = sCap
|
|
2483
|
+
if (v === FAIL) return FAIL
|
|
2484
|
+
const end = EC.e
|
|
2485
|
+
const span = tracked ? spanLines(ctx, pos, end) : { start: pos, end }
|
|
2486
|
+
const st = readsState && ctx.state !== undefined
|
|
2487
|
+
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
2488
|
+
: undefined
|
|
2489
|
+
|
|
2490
|
+
let nd: unknown
|
|
2491
|
+
if (unwrap && kids.length === 1) {
|
|
2492
|
+
nd = unwrapChild(kids[0])
|
|
2493
|
+
} else if (collapse && kids.length === 1) {
|
|
2494
|
+
nd = kids[0]
|
|
2495
|
+
} else if (
|
|
2496
|
+
// HOST COLLAPSE — applies wherever the node's VALUE comes from the
|
|
2497
|
+
// host, which is any node under a CST host, not only builder-less ones.
|
|
2498
|
+
(hostCst || (build === undefined && proj < 0))
|
|
2499
|
+
&& keepChildren
|
|
2500
|
+
&& host?._parsemanCstCollapse !== undefined
|
|
2501
|
+
&& kids.length === 1
|
|
2502
|
+
&& rawKids.length === 1
|
|
2503
|
+
&& host._parsemanCstCollapse(type, kids[0], kids, rawKids)
|
|
2504
|
+
) {
|
|
2505
|
+
nd = kids[0]
|
|
2506
|
+
} else if (proj >= 0) {
|
|
2507
|
+
nd = hostCst && host !== undefined
|
|
2508
|
+
? host(type, kids, fieldMap, span, rawKids, tlog, readsState ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
|
|
2509
|
+
: projectChild(kids, proj, type)
|
|
2510
|
+
} else if (build !== undefined) {
|
|
2511
|
+
if (hostCst && host !== undefined) {
|
|
2512
|
+
// A direct builder is bypassed under a CST host.
|
|
2513
|
+
nd = host(type, kids, fieldMap, span, rawKids, tlog, readsState ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
|
|
2514
|
+
} else {
|
|
2515
|
+
nd = build(kids, fieldMap, span, rawKids, captureWide ? tlog : EMPTY_TL, st)
|
|
2516
|
+
}
|
|
2517
|
+
} else if (host !== undefined) {
|
|
2518
|
+
nd = host(type, hostKids, fieldMap, span, rawKids, tlog, st, tags)
|
|
2519
|
+
} else {
|
|
2520
|
+
nd = { _tag: 'node', type, span, state: st ?? null, children: kids }
|
|
2521
|
+
}
|
|
2522
|
+
// A ROOT NODE IS NOT A CHILD. `node()` pushes only when it was entered
|
|
2523
|
+
// from an enclosing collector — `saved.buf !== undefined || saved.ch !==
|
|
2524
|
+
// undefined` (node.ts:326). An outer `_cstLeaves` alone is a caller's
|
|
2525
|
+
// own sink, not a parent node, and pushing into it published the root
|
|
2526
|
+
// node as a leaf of itself.
|
|
2527
|
+
if (sBuf !== undefined || sCh !== undefined) {
|
|
2528
|
+
pushCstChild(ctx, nd, rawEntry(nd, input, pos, end))
|
|
2529
|
+
}
|
|
2530
|
+
EC.e = end
|
|
2531
|
+
return nd
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
default:
|
|
2536
|
+
throw new Error(`table assembler: unknown opcode ${String(op)} at ${ip}`)
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
/* ── scans, built once per assembly exactly as `exec.ts` builds them ─────── */
|
|
2541
|
+
|
|
2542
|
+
function subtreeComb(r: SubtreeRef, def?: ParserDef): Combinator<unknown> {
|
|
2543
|
+
const piece = pieceAt(r[0])
|
|
2544
|
+
// THE END SLOT IS ONE CELL FOR THE WHOLE ASSEMBLY (`cell.ts`), so this
|
|
2545
|
+
// reads the end of whichever engine ran the piece without asking which.
|
|
2546
|
+
// It used to select `emitted.end` or this file's own slot, which was
|
|
2547
|
+
// correct only because exactly one engine was ever live: an emitted piece
|
|
2548
|
+
// writing its own private slot left this file's at zero, and a scan
|
|
2549
|
+
// sentinel read that as an empty span.
|
|
2550
|
+
return {
|
|
2551
|
+
_tag: 'tableSubtree',
|
|
2552
|
+
_meta: { firstSet: refFirstSet(r[1]), canMatchNewline: true, isTrivia: false },
|
|
2553
|
+
_def: def ?? { tag: 'unknown' } as unknown as ParserDef,
|
|
2554
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown> {
|
|
2555
|
+
const v = piece(input, pos, ctx)
|
|
2556
|
+
if (v === FAIL) {
|
|
2557
|
+
const fe = ctx._fe
|
|
2558
|
+
const at = fe === undefined || fe < 0 ? pos : fe
|
|
2559
|
+
return { ok: false, expected: (ctx._fx ?? EMPTY_FX) as string[], span: { start: at, end: at } }
|
|
2560
|
+
}
|
|
2561
|
+
return { ok: true, value: v, span: { start: pos, end: EC.e } }
|
|
2562
|
+
},
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
function refFirstSet(cls: number): FirstSet {
|
|
2567
|
+
if (cls === -2) return { kind: 'empty' }
|
|
2568
|
+
if (cls < 0) return { kind: 'any' }
|
|
2569
|
+
const spec = prog.cc[cls] ?? ''
|
|
2570
|
+
const ranges = decodeClassSpec(spec)
|
|
2571
|
+
return { kind: 'ranges', ranges }
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/* ── U4: the EMITTED assembly, tried before the closure one ─────────────── */
|
|
2575
|
+
|
|
2576
|
+
/**
|
|
2577
|
+
* Emitted source is attempted FIRST, and the closure walk above runs only for
|
|
2578
|
+
* what it refuses.
|
|
2579
|
+
*
|
|
2580
|
+
* The reason it has to be source at all is in `emit-assembly.ts`'s header:
|
|
2581
|
+
* V8's inline-cache feedback is per FUNCTION LITERAL, so every piece minted
|
|
2582
|
+
* from one literal here shares one feedback vector and every call site inside
|
|
2583
|
+
* it is megamorphic. That is not a property this file can rearrange away.
|
|
2584
|
+
*
|
|
2585
|
+
* The refusal is RECORDED, not swallowed. A grammar that silently drops to
|
|
2586
|
+
* the closure path is a permanently slow path nobody would ever find, which
|
|
2587
|
+
* is the same failure `encode.ts:1208-1213` refuses for `OP_LIVE`.
|
|
2588
|
+
*/
|
|
2589
|
+
const scansArr: Combinator<unknown>[] = []
|
|
2590
|
+
let emitted: EmittedAssembly | undefined
|
|
2591
|
+
/** "The build already did this" — not a refusal, and never a `emitRefusal`. */
|
|
2592
|
+
class ServedByBuild extends Error {}
|
|
2593
|
+
let emitRefusal: string | undefined
|
|
2594
|
+
let emitReached: ReadonlySet<number> | undefined
|
|
2595
|
+
{
|
|
2596
|
+
// The scan pool and the scan-skip sets are built FROM subtrees, so their
|
|
2597
|
+
// sites need emitted names too or `subtreeComb` would have to fall back to
|
|
2598
|
+
// a closure for them and the two engines would both be live in one parse.
|
|
2599
|
+
const extraIps: number[] = []
|
|
2600
|
+
for (const s of prog.scans ?? []) {
|
|
2601
|
+
for (const r of s.skip) extraIps.push(r[0])
|
|
2602
|
+
if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
|
|
2603
|
+
}
|
|
2604
|
+
for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
|
|
2605
|
+
/**
|
|
2606
|
+
* THE BUILD'S OWN ANSWER, TRIED BEFORE THE CONSTRUCTOR.
|
|
2607
|
+
*
|
|
2608
|
+
* `prog.asm` carries assemblies a build-time emitter already compiled, one
|
|
2609
|
+
* per option set. When this parse's option set is among them there is
|
|
2610
|
+
* nothing to compile: the factory is an ordinary function literal in the
|
|
2611
|
+
* emitted module, and the only work left is rebuilding the three data pools
|
|
2612
|
+
* from `plan` (see `PoolPlan` — allocation, no source, no eval).
|
|
2613
|
+
*
|
|
2614
|
+
* This is what makes `docs/guide/modes.md` and `docs/reference/api.md` TRUE.
|
|
2615
|
+
* Both said a macro build is the answer for a CSP environment without
|
|
2616
|
+
* `unsafe-eval`; until this existed, the first `parse()` of a macro-built
|
|
2617
|
+
* table called `new Function` like every other path and the promise was
|
|
2618
|
+
* decided by nothing. `test/unit/no-function-constructor.test.ts` decides it
|
|
2619
|
+
* now, by counting constructor calls across a real parse.
|
|
2620
|
+
*/
|
|
2621
|
+
// A build-time assembly cannot know a runtime host's per-type trivia
|
|
2622
|
+
// predicate. Never serve the scalar-keyed default factory for a
|
|
2623
|
+
// predicate-specialised parse; lower a fresh host-owned assembly instead.
|
|
2624
|
+
const pre = cfg.hostCaptureTrivia === undefined
|
|
2625
|
+
? prog.asm?.find(a => a.key === cfgKey(cfg))
|
|
2626
|
+
: undefined
|
|
2627
|
+
if (pre !== undefined) {
|
|
2628
|
+
const pools = rebuildPools(t.cc, t.fx, pre.plan)
|
|
2629
|
+
emitted = pre.factory(
|
|
2630
|
+
EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
|
|
2631
|
+
trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
|
|
2632
|
+
scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
|
|
2633
|
+
cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, failAt,
|
|
2634
|
+
classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
|
|
2635
|
+
demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
|
|
2636
|
+
scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
|
|
2637
|
+
asciiFoldKey, ROUTED_FX,
|
|
2638
|
+
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
2639
|
+
matchesAt, recoverScan, orSentinel, captureError,
|
|
2640
|
+
)
|
|
2641
|
+
emitReached = new Set(pre.reached)
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* ONCE A PROGRAM CARRIES ASSEMBLIES, THE CONSTRUCTOR IS OFF FOR IT — for
|
|
2645
|
+
* EVERY option set, not just the ones that were pre-compiled.
|
|
2646
|
+
*
|
|
2647
|
+
* "Prefer the pre-compiled factory, fall back to `new Function`" is how this
|
|
2648
|
+
* defect survives forever: the fallback becomes load-bearing, production
|
|
2649
|
+
* takes it, and nobody can tell which path ran. So a macro-built artifact
|
|
2650
|
+
* whose option set was not pre-compiled runs the CLOSURE engine, and the
|
|
2651
|
+
* reason is on `Assembly.emitRefusal` where `test/unit/table-assemble.test.ts`
|
|
2652
|
+
* and anyone debugging can read it. Slower, correct, and observable — the
|
|
2653
|
+
* three properties a silent eval had none of.
|
|
2654
|
+
*
|
|
2655
|
+
* A program with NO `asm` at all is an explicit low-level hand-built
|
|
2656
|
+
* `tableRules(prog)` input. Compiler-created and macro-created artifacts
|
|
2657
|
+
* always carry the field, including the canonical empty inventory.
|
|
2658
|
+
*/
|
|
2659
|
+
try {
|
|
2660
|
+
// Served above by a pre-compiled factory. Nothing to compile, nothing to
|
|
2661
|
+
// refuse — and in particular no constructor call.
|
|
2662
|
+
if (emitted !== undefined) throw new ServedByBuild()
|
|
2663
|
+
if (prog.asm !== undefined) {
|
|
2664
|
+
throw new Unemittable(
|
|
2665
|
+
`option set ${cfgKey(cfg)} (hostCst=${cfg.hostCst} trackLines=${cfg.trackLines} `
|
|
2666
|
+
+ `tolerant=${cfg.tolerant} coverage=${cfg.coverage} probe=${cfg.probe}), which this build `
|
|
2667
|
+
+ 'did not pre-compile. The Function constructor is NOT used for an artifact that carries '
|
|
2668
|
+
+ 'assemblies — see `EmitOptions.assemblies` to add this option set to the build',
|
|
2669
|
+
)
|
|
2670
|
+
}
|
|
2671
|
+
// THE A/B TOGGLE, read ONCE at module load and never on a parse path.
|
|
2672
|
+
// Two engines that can only be compared across two checkouts cannot be
|
|
2673
|
+
// compared at all: the bench harness's own guidance is that a
|
|
2674
|
+
// cross-worktree measurement carries a bias no repetition removes. Same
|
|
2675
|
+
// spelling as `PM_TABLE_COUNT` (`bench/table-opcode-gaps.ts`).
|
|
2676
|
+
if (!EMIT_ENABLED) throw new Unemittable('PM_TABLE_EMIT=0 (measurement toggle)')
|
|
2677
|
+
const em = emitAssemblySource(t, prog, cfg, extraIps)
|
|
2678
|
+
/**
|
|
2679
|
+
* COMPILING THE TEXT — and the two ways it can fail are NOT the same.
|
|
2680
|
+
*
|
|
2681
|
+
* An `EvalError` is the ENVIRONMENT refusing: a Content-Security-Policy
|
|
2682
|
+
* without `unsafe-eval` forbids the `Function` constructor. That is a
|
|
2683
|
+
* legitimate refusal and the closure engine is the correct answer.
|
|
2684
|
+
*
|
|
2685
|
+
* A `SyntaxError` is THIS EMITTER having generated invalid JavaScript.
|
|
2686
|
+
* That is a bug in `emit-assembly.ts`, and catching it would ship a
|
|
2687
|
+
* permanently slow path that is also permanently undiagnosed — every
|
|
2688
|
+
* grammar would quietly run closures because of one bad template. It
|
|
2689
|
+
* propagates, with the offending source attached.
|
|
2690
|
+
*/
|
|
2691
|
+
let factory: EmittedFactory
|
|
2692
|
+
try {
|
|
2693
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
2694
|
+
factory = new Function(...EMITTED_PARAMS, em.source) as EmittedFactory
|
|
2695
|
+
} catch (e) {
|
|
2696
|
+
if (e instanceof SyntaxError) {
|
|
2697
|
+
throw new Error(
|
|
2698
|
+
`table emitter: generated invalid JavaScript (${e.message}). This is a defect in `
|
|
2699
|
+
+ 'emit-assembly.ts, not a property of the grammar.\n' + em.source,
|
|
2700
|
+
)
|
|
2701
|
+
}
|
|
2702
|
+
throw new Unemittable(`the Function constructor (${String(e)})`)
|
|
2703
|
+
}
|
|
2704
|
+
emitted = factory(
|
|
2705
|
+
EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia,
|
|
2706
|
+
trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
|
|
2707
|
+
scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
|
|
2708
|
+
cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, failAt,
|
|
2709
|
+
classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
|
|
2710
|
+
demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
|
|
2711
|
+
scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
|
|
2712
|
+
asciiFoldKey, ROUTED_FX,
|
|
2713
|
+
// THE SENTINEL POOL, dense over char-class indices so the emitted text can
|
|
2714
|
+
// index it the way it indexes `SCANS`. Built only for a recovery assembly
|
|
2715
|
+
// — a strict one emits no reader — and through the SAME `sentinelFor` memo
|
|
2716
|
+
// the closure pieces use, so both engines resync to the same combinator
|
|
2717
|
+
// object rather than to two separately constructed ones.
|
|
2718
|
+
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
2719
|
+
matchesAt, recoverScan, orSentinel, captureError,
|
|
2720
|
+
)
|
|
2721
|
+
emitReached = em.reached
|
|
2722
|
+
} catch (e) {
|
|
2723
|
+
if (e instanceof ServedByBuild) { /* already emitted; no refusal to record */ }
|
|
2724
|
+
else if (e instanceof Unemittable) emitRefusal = e.construct
|
|
2725
|
+
else throw e
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/** One piece for a site, from whichever engine this assembly is running. */
|
|
2730
|
+
function pieceAt(ip: number): Piece {
|
|
2731
|
+
const em = emitted
|
|
2732
|
+
if (em !== undefined) {
|
|
2733
|
+
const p = em.byIp[ip]
|
|
2734
|
+
if (p === undefined) throw new Error(`table emitter: no emitted body for site ${ip}`)
|
|
2735
|
+
return p as Piece
|
|
2736
|
+
}
|
|
2737
|
+
return link(ip)
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
for (const s of prog.scans ?? []) {
|
|
2741
|
+
const skip = s.skip.map(r => subtreeComb(r))
|
|
2742
|
+
const raw = (s.flags & 1) !== 0
|
|
2743
|
+
if (s.kind === 1) {
|
|
2744
|
+
scansArr.push(balanced(s.open!, s.close!, { skip, raw, strict: (s.flags & 4) !== 0 }) as Combinator<unknown>)
|
|
2745
|
+
continue
|
|
2746
|
+
}
|
|
2747
|
+
const sentDef: ParserDef | undefined = typeof s.sent === 'string'
|
|
2748
|
+
? { tag: 'literal', value: s.sent, caseInsensitive: false } as unknown as ParserDef
|
|
2749
|
+
: undefined
|
|
2750
|
+
scansArr.push(scanTo(subtreeComb(s.sentinel!, sentDef), { skip, raw, orEOF: (s.flags & 2) !== 0 }) as Combinator<unknown>)
|
|
2751
|
+
}
|
|
2752
|
+
const scans: readonly Combinator<unknown>[] = scansArr
|
|
2753
|
+
|
|
2754
|
+
const scanSkip: readonly (readonly Combinator<unknown>[])[] =
|
|
2755
|
+
(prog.scanSkip ?? []).map(set => set.map(r => subtreeComb(r)))
|
|
2756
|
+
|
|
2757
|
+
/* ── link the rules ──────────────────────────────────────────────────────── */
|
|
2758
|
+
|
|
2759
|
+
if (emitted !== undefined) {
|
|
2760
|
+
const em = emitted
|
|
2761
|
+
return {
|
|
2762
|
+
pieces: em.pieces as Record<string, Piece>,
|
|
2763
|
+
end: em.end,
|
|
2764
|
+
begin: em.begin,
|
|
2765
|
+
finish: em.finish,
|
|
2766
|
+
scanSkip,
|
|
2767
|
+
reached: emitReached!,
|
|
2768
|
+
emitRefusal: undefined,
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const pieces: Record<string, Piece> = {}
|
|
2773
|
+
for (const [name, entryIp] of Object.entries(prog.rules)) pieces[name] = link(entryIp)
|
|
2774
|
+
|
|
2775
|
+
/**
|
|
2776
|
+
* PER PARSE. `SCAN` starts null — a rule reached before any scope has no
|
|
2777
|
+
* installed trivia, and a stale one from a previous parse on a reused `ctx`
|
|
2778
|
+
* would skip trivia this grammar never declared.
|
|
2779
|
+
*
|
|
2780
|
+
* `FAST` and `HOSTCST` are gone: both were options, and both are now baked into
|
|
2781
|
+
* which pieces this assembly holds. What is left is the one piece of
|
|
2782
|
+
* configuration that is a per-parse VALUE rather than a per-parse FACT — the
|
|
2783
|
+
* host itself — and it is read here, once, instead of per node.
|
|
2784
|
+
*/
|
|
2785
|
+
function begin(ctx: ParseContext): void {
|
|
2786
|
+
// Read user-owned values before touching the active frame. A getter that
|
|
2787
|
+
// throws must not leave an outer parse suspended.
|
|
2788
|
+
const nextHost = ctx.build
|
|
2789
|
+
const nextCoverage = ctx._grammarCoverage ?? NO_COVERAGE
|
|
2790
|
+
if (depth > 0) frames.push({ scan: SCAN, host: HOST, coverage: COV, end: EC.e })
|
|
2791
|
+
depth++
|
|
2792
|
+
SCAN = null
|
|
2793
|
+
HOST = nextHost
|
|
2794
|
+
COV = nextCoverage
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
function finish(): void {
|
|
2798
|
+
if (depth <= 0) throw new Error('parseman table assembly frame underflow')
|
|
2799
|
+
depth--
|
|
2800
|
+
if (depth === 0) return
|
|
2801
|
+
const prior = frames.pop()!
|
|
2802
|
+
SCAN = prior.scan
|
|
2803
|
+
HOST = prior.host
|
|
2804
|
+
COV = prior.coverage
|
|
2805
|
+
EC.e = prior.end
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
return { pieces, end: () => EC.e, begin, finish, scanSkip, reached, emitRefusal }
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
/**
|
|
2812
|
+
* Assemblies for one resolved table, one per option set, built on demand.
|
|
2813
|
+
*
|
|
2814
|
+
* The option set is not known when the rule map is created — `ctx.build`,
|
|
2815
|
+
* `ctx.trackLines` and `ctx._tolerant` arrive with the parse — so the entry
|
|
2816
|
+
* computes the scalar key and takes the assembly for it, building it the first
|
|
2817
|
+
* time that combination is seen. That is the "assembled at run start" half of G5:
|
|
2818
|
+
* only the option combinations a process actually uses, each holding the pieces
|
|
2819
|
+
* its options reach. A process that never parses tolerantly never builds, and
|
|
2820
|
+
* never runs, a single recovery piece.
|
|
2821
|
+
*/
|
|
2822
|
+
export class AssemblyCache {
|
|
2823
|
+
private readonly t: ResolvedTable
|
|
2824
|
+
private readonly prog: TableProgram
|
|
2825
|
+
private readonly byCfg: Array<Assembly | undefined> = Array.from({ length: 64 })
|
|
2826
|
+
/**
|
|
2827
|
+
* Monomorphic predicate inline cache. An assembly depends on the predicate
|
|
2828
|
+
* FUNCTION and the scalar cfg key, not on the host object that carries it, so
|
|
2829
|
+
* retaining the host would add neither correctness nor reuse. Stable parsing
|
|
2830
|
+
* is one identity comparison plus an array index; replacing the predicate
|
|
2831
|
+
* replaces this specialisation without retaining either host.
|
|
2832
|
+
*/
|
|
2833
|
+
private hostPredicate: ((type: string) => boolean) | undefined
|
|
2834
|
+
private hostAssemblies: Array<Assembly | undefined> | undefined
|
|
2835
|
+
|
|
2836
|
+
constructor(prog: TableProgram, resolved: ResolvedTable = resolveTable(prog)) {
|
|
2837
|
+
this.prog = prog
|
|
2838
|
+
this.t = resolved
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
for(cfg: RunCfg): Assembly {
|
|
2842
|
+
const key = cfgKey(cfg)
|
|
2843
|
+
// No host identity is available to own this predicate specialisation.
|
|
2844
|
+
if (cfg.hostCaptureTrivia !== undefined) return assemble(this.t, this.prog, cfg)
|
|
2845
|
+
const hit = this.byCfg[key]
|
|
2846
|
+
if (hit !== undefined) return hit
|
|
2847
|
+
const made = assemble(this.t, this.prog, cfg)
|
|
2848
|
+
this.byCfg[key] = made
|
|
2849
|
+
return made
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
/**
|
|
2853
|
+
* The assembly for the option set this `ctx` implies.
|
|
2854
|
+
*
|
|
2855
|
+
* ─── WHY THIS IS NOT A VIOLATION OF "NO OPTION READS AT PARSE TIME" ─────────
|
|
2856
|
+
*
|
|
2857
|
+
* The rule that shaped 0.47 is that consulting an option PER RULE or PER
|
|
2858
|
+
* COMBINATOR is a fail, and the reason it is a fail is that such a consult
|
|
2859
|
+
* scales with the input. This one does not, and the previous defence of it —
|
|
2860
|
+
* "cheap, allocation-free, only once" — was the wrong argument, because
|
|
2861
|
+
* cheapness is not the criterion. The right argument is that the consult is
|
|
2862
|
+
* IRREDUCIBLE, and it is worth stating precisely so nobody re-opens it:
|
|
2863
|
+
*
|
|
2864
|
+
* A table entry has the artifact signature `(input, pos, ctx)`, shared with
|
|
2865
|
+
* codegen. All scalar option bits live on that `ctx`, and every one of them is
|
|
2866
|
+
* supplied PER CALL by the caller — `run()` takes `tolerant`, `build` and
|
|
2867
|
+
* `instrumentation` as options on an artifact it was handed;
|
|
2868
|
+
* `combinators/grammar.ts` sets `trackLines` on scope entry;
|
|
2869
|
+
* `completionsAt` installs `_probe`. So the option set is not knowable
|
|
2870
|
+
* before the call, and a selection that cannot happen before the call must
|
|
2871
|
+
* happen at it.
|
|
2872
|
+
*
|
|
2873
|
+
* That is not a hole in G5, it is G5's own first clause: "quickly building the
|
|
2874
|
+
* grammar reference ON RUN START, making some swaps on rules or sub-rules, and
|
|
2875
|
+
* then the run actually runs with no logic branching for that option input"
|
|
2876
|
+
* (`notes/TABLE-DRIVER.md`). This IS the run-start step. What the criterion
|
|
2877
|
+
* forbids is the second sentence, and past this call there is no option read
|
|
2878
|
+
* anywhere — `scripts/check-invariants.mjs` INV-6 decides that mechanically.
|
|
2879
|
+
*
|
|
2880
|
+
* MEASURED, not asserted: exactly ONE call per entry invocation, including
|
|
2881
|
+
* `benchmark.less` at 106,802 bytes. Eliminating it would require binding the
|
|
2882
|
+
* option set to the ARTIFACT rather than to the CALL, which is a change to the
|
|
2883
|
+
* public run API (the map would have to hand back a cfg-keyed family and
|
|
2884
|
+
* `run()` index it), not a change to this file.
|
|
2885
|
+
*
|
|
2886
|
+
* It allocates nothing: the key is packed from the ctx's own bits by `cfgKeyOf`,
|
|
2887
|
+
* which takes them as arguments precisely so no `RunCfg` need exist here — that
|
|
2888
|
+
* object is built only on the miss that builds an assembly. A host trivia
|
|
2889
|
+
* predicate additionally selects its identity-specialised inline cache
|
|
2890
|
+
* because a function cannot be represented by a scalar bit.
|
|
2891
|
+
*
|
|
2892
|
+
* DO NOT CACHE THE RESULT ACROSS CALLS. Keying it on anything but the `ctx`'s
|
|
2893
|
+
* own option bits is how `tableRules` handed a strict parse the PREVIOUS
|
|
2894
|
+
* parse's tolerant assembly (`test/unit/table-assemble.test.ts`).
|
|
2895
|
+
*/
|
|
2896
|
+
forCtx(ctx: ParseContext): Assembly {
|
|
2897
|
+
const host = ctx.build
|
|
2898
|
+
const hostCst = host !== undefined && cstOutputHost(host)
|
|
2899
|
+
const hostReadsChildren = host?._parsemanReadsChildren !== false
|
|
2900
|
+
|| host?._parsemanCstCollapse !== undefined
|
|
2901
|
+
const hostCaptureTrivia = host?._parsemanCaptureTrivia
|
|
2902
|
+
const trackLines = ctx.trackLines === true
|
|
2903
|
+
const tolerant = ctx._tolerant === true
|
|
2904
|
+
const coverage = ctx._grammarCoverage !== undefined
|
|
2905
|
+
const probe = ctx._probe !== undefined
|
|
2906
|
+
const key = cfgKeyOf(hostCst, trackLines, tolerant, coverage, probe, hostReadsChildren)
|
|
2907
|
+
let cache = this.byCfg
|
|
2908
|
+
if (hostCaptureTrivia !== undefined) {
|
|
2909
|
+
// The predicate is host configuration, not parse state. Specialising each
|
|
2910
|
+
// node once keeps it out of the hot path, so its behaviour must stay stable
|
|
2911
|
+
// for this function identity. Replacing the function is supported: the
|
|
2912
|
+
// identity check below installs a fresh cache when it is replaced.
|
|
2913
|
+
if (this.hostPredicate === hostCaptureTrivia) {
|
|
2914
|
+
cache = this.hostAssemblies!
|
|
2915
|
+
}
|
|
2916
|
+
else {
|
|
2917
|
+
cache = Array.from({ length: 64 })
|
|
2918
|
+
this.hostPredicate = hostCaptureTrivia
|
|
2919
|
+
this.hostAssemblies = cache
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
const hit = cache[key]
|
|
2923
|
+
if (hit !== undefined) return hit
|
|
2924
|
+
const made = assemble(this.t, this.prog, {
|
|
2925
|
+
hostCst, hostReadsChildren, hostCaptureTrivia,
|
|
2926
|
+
trackLines, tolerant, coverage, probe,
|
|
2927
|
+
})
|
|
2928
|
+
cache[key] = made
|
|
2929
|
+
return made
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
/**
|
|
2934
|
+
* The ASSEMBLED rule map — the same artifact contract as `tableRules`, run
|
|
2935
|
+
* through linked closures instead of the bytecode interpreter.
|
|
2936
|
+
*
|
|
2937
|
+
* THE ONE config read is HERE, at the boundary, once per entry invocation:
|
|
2938
|
+
* `AssemblyCache.forCtx` turns the `ctx` into a scalar option set and takes the
|
|
2939
|
+
* assembly built for it. Everything past that point is pieces, and no piece body
|
|
2940
|
+
* reads an option — `scripts/check-invariants.mjs` INV-6 asserts it. `forCtx`
|
|
2941
|
+
* carries the argument for why that read is irreducible rather than merely cheap.
|
|
2942
|
+
*/
|
|
2943
|
+
export function tableRules(
|
|
2944
|
+
source: TableProgram | CompactProgram,
|
|
2945
|
+
artifactMetadata: Readonly<Record<symbol, unknown>> = {},
|
|
2946
|
+
): Record<string, TableRule> {
|
|
2947
|
+
const prog = expandCompact(source)
|
|
2948
|
+
const resolved = resolveTable(prog)
|
|
2949
|
+
const cache = new AssemblyCache(prog, resolved)
|
|
2950
|
+
const names = Object.keys(prog.rules)
|
|
2951
|
+
const skipOf = prog.scanSkipOf
|
|
2952
|
+
let last: unknown
|
|
2953
|
+
/**
|
|
2954
|
+
* THE ASSEMBLY THIS ENTRY INVOCATION SELECTED — handed from `scanSkipFor` to
|
|
2955
|
+
* the `runRule` that `stamp.ts`'s entry runs immediately afterwards, on the
|
|
2956
|
+
* SAME `ctx`, so the pair costs one `forCtx` between them rather than two.
|
|
2957
|
+
*
|
|
2958
|
+
* It used to live ACROSS parses ("the lookup is memoised, so this is an array
|
|
2959
|
+
* index after the first parse"), and that was wrong: `scanSkipFor` runs before
|
|
2960
|
+
* `runRule` has re-selected, so a strict parse following a tolerant one was
|
|
2961
|
+
* installed with the TOLERANT assembly's `scanSkip` — verified by object
|
|
2962
|
+
* identity, since the two assemblies wrap their own pieces. Amortising a
|
|
2963
|
+
* per-parse selection over parses is not amortisation, it is a stale answer.
|
|
2964
|
+
*
|
|
2965
|
+
* `runRule` CONSUMES it, so it can never outlive the invocation that set it:
|
|
2966
|
+
* an entry reached without the preceding `scanSkipFor` re-selects, and a
|
|
2967
|
+
* nested entry invocation sets and consumes its own inside the outer
|
|
2968
|
+
* `runRule`, after the outer read.
|
|
2969
|
+
*/
|
|
2970
|
+
let selected: Assembly | undefined
|
|
2971
|
+
return stampRuleMap(prog, {
|
|
2972
|
+
runRule: (ri, input, pos, ctx) => {
|
|
2973
|
+
const a = selected ?? cache.forCtx(ctx)
|
|
2974
|
+
selected = undefined
|
|
2975
|
+
a.begin(ctx)
|
|
2976
|
+
try {
|
|
2977
|
+
const v = a.pieces[names[ri]!]!(input, pos, ctx)
|
|
2978
|
+
if (v === FAIL) return -1
|
|
2979
|
+
last = v
|
|
2980
|
+
return a.end()
|
|
2981
|
+
} finally {
|
|
2982
|
+
// A host callback or direct reducer can run this same map before its
|
|
2983
|
+
// outer entry returns. Restore the assembly frame on every exit.
|
|
2984
|
+
a.finish()
|
|
2985
|
+
}
|
|
2986
|
+
},
|
|
2987
|
+
lastValue: () => last,
|
|
2988
|
+
scanSkipFor: (ri, ctx) => {
|
|
2989
|
+
const a = cache.forCtx(ctx)
|
|
2990
|
+
selected = a
|
|
2991
|
+
return a.scanSkip[skipOf?.[ri] ?? -1]
|
|
2992
|
+
},
|
|
2993
|
+
}, artifactMetadata, resolved)
|
|
2994
|
+
}
|