parseman 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/analysis/choice-cost-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +9929 -7465
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +9929 -7465
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10865 -8535
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10865 -8535
- package/dist/cli/index.js.map +3 -4
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +40 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +47 -53
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +9224 -7203
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9219 -7198
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +311 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +311 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11146 -9593
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11146 -9593
- package/dist/plugin/index.js.map +3 -4
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2898 -1623
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1623
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11375 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11352 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +446 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +493 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +25 -2
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +322 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +221 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +821 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +2994 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1766 -0
- package/src/table/emit.ts +418 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1672 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +459 -0
- package/src/table/program.ts +855 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,1766 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* U4 — THE ASSEMBLY, AS EMITTED SOURCE.
|
|
3
|
+
*
|
|
4
|
+
* `assemble.ts` lowers each reachable site to a closure. This file lowers the
|
|
5
|
+
* same sites to TEXT, compiled once per (grammar × option set) at run start.
|
|
6
|
+
* Semantics are `assemble.ts`'s, site for site; the identity sweep
|
|
7
|
+
* (`bench/table-lowering-identity.ts`) gates the two against each other and
|
|
8
|
+
* against the interpreter, and `expected` is inside the digest it compares.
|
|
9
|
+
*
|
|
10
|
+
* ## Why text, and not a rearrangement of `assemble.ts`
|
|
11
|
+
*
|
|
12
|
+
* V8 attaches inline-cache feedback per FUNCTION LITERAL, not per closure. The
|
|
13
|
+
* second closure minted from a `CreateClosure` site moves its `FeedbackCell` to
|
|
14
|
+
* `kManyClosures`, and the vector is shared from then on. `assemble.ts` mints 18
|
|
15
|
+
* arity-2 `SEQV` closures for `less/stylesheet` from ONE literal, so the two
|
|
16
|
+
* child call sites inside that literal see 18 unrelated callees and go
|
|
17
|
+
* megamorphic — a hash lookup in the megamorphic stub cache, no inlining, no
|
|
18
|
+
* type feedback for the callee body. No rearrangement of a file that MINTS
|
|
19
|
+
* closures can produce a distinct literal per site. Emitting one
|
|
20
|
+
* `function _pf<N>` per site can: inside `_pf12` the call `_pf7(…)` names ONE
|
|
21
|
+
* binding and stays monomorphic.
|
|
22
|
+
*
|
|
23
|
+
* ## What may be shared, and what must be emitted
|
|
24
|
+
*
|
|
25
|
+
* A body containing a call to another PIECE must be emitted per site, or the
|
|
26
|
+
* megamorphism relocates from `assemble.ts` into a shared helper and nothing
|
|
27
|
+
* changes. That is why `nextTerm` is INLINED at every sequence term here rather
|
|
28
|
+
* than called.
|
|
29
|
+
*
|
|
30
|
+
* The rule this file applies is narrower than "no helpers", and it is the whole
|
|
31
|
+
* criterion: **a shared emitted-scope helper is sound exactly when it takes no
|
|
32
|
+
* piece as an argument**, because then it has no call site whose feedback a
|
|
33
|
+
* second caller could pollute. `_skipTrivia` qualifies — it calls the installed
|
|
34
|
+
* trivia scanner, which is runtime state in either engine. `nextTerm` does not.
|
|
35
|
+
*
|
|
36
|
+
* ## Cycles
|
|
37
|
+
*
|
|
38
|
+
* `assemble.ts` needs one forwarding stub per recursive site,
|
|
39
|
+
* `const fwd = (input, pos, ctx) => target!(input, pos, ctx)` — a SINGLE
|
|
40
|
+
* function literal that every back-edge in the process funnels through.
|
|
41
|
+
* Emitted `function` declarations hoist, so a back-edge here is a direct name
|
|
42
|
+
* reference and the stub does not exist.
|
|
43
|
+
*
|
|
44
|
+
* ## Refusal
|
|
45
|
+
*
|
|
46
|
+
* Any construct not lowered raises `Unemittable`, naming it. `assemble.ts`
|
|
47
|
+
* catches that, RECORDS it on the assembly, and falls back to the closure path.
|
|
48
|
+
* The fallback is observable (`Assembly.emitRefusal`): a silent one would make
|
|
49
|
+
* a permanently slow path indistinguishable from a fast one, which is exactly
|
|
50
|
+
* what `encode.ts:1208-1213` refuses to allow for `OP_LIVE`.
|
|
51
|
+
*/
|
|
52
|
+
import type { ParseContext } from '../types.ts'
|
|
53
|
+
import {
|
|
54
|
+
OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
|
|
55
|
+
OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
|
|
56
|
+
OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX,
|
|
57
|
+
OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
|
|
58
|
+
} from './ops.ts'
|
|
59
|
+
import type { ResolvedClass, ResolvedTable, TableProgram } from './program.ts'
|
|
60
|
+
import { emitShapeMatch, scanShapeFromRegex } from './scan-shapes.ts'
|
|
61
|
+
import {
|
|
62
|
+
CAP_OFF, CAP_ON, TRI_NONE, TRI_UNKNOWN, TOP, computeSiteLabels, reachableSites, type SiteLabel,
|
|
63
|
+
} from './site-labels.ts'
|
|
64
|
+
|
|
65
|
+
/** What the compiled factory hands back — the emitted twin of `Assembly`. */
|
|
66
|
+
export type EmittedPiece = (input: string, pos: number, ctx: ParseContext) => unknown
|
|
67
|
+
export type EmittedAssembly = {
|
|
68
|
+
readonly pieces: Record<string, EmittedPiece>
|
|
69
|
+
/** Sites named for `subtreeComb` — the scan pool and the scan-skip sets. */
|
|
70
|
+
readonly byIp: Record<number, EmittedPiece | undefined>
|
|
71
|
+
readonly end: () => number
|
|
72
|
+
readonly begin: (ctx: ParseContext) => void
|
|
73
|
+
readonly finish: () => void
|
|
74
|
+
}
|
|
75
|
+
/** The `new Function` result. Its parameters are `EMITTED_PARAMS`, in order. */
|
|
76
|
+
export type EmittedFactory = (...args: readonly unknown[]) => EmittedAssembly
|
|
77
|
+
|
|
78
|
+
/** A construct the emitter does not lower. Names the CONSTRUCT, never a type. */
|
|
79
|
+
export class Unemittable extends Error {
|
|
80
|
+
readonly construct: string
|
|
81
|
+
constructor(construct: string) {
|
|
82
|
+
super(`table emitter: cannot emit ${construct}`)
|
|
83
|
+
this.name = 'Unemittable'
|
|
84
|
+
this.construct = construct
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The names the emitted scope is closed over, in the order the factory takes
|
|
90
|
+
* them. ONE list, so the parameter list and the argument list cannot drift — a
|
|
91
|
+
* mismatch binds the wrong function to the wrong name and yields a parser that
|
|
92
|
+
* runs and is wrong, which no type in this file would catch.
|
|
93
|
+
*/
|
|
94
|
+
export const EMITTED_PARAMS = [
|
|
95
|
+
'EC', 'FAIL', 'K', 'FX', 'FNS', 'MASK', 'CLS', 'AFX', 'TRIVIA', 'TRIVIALABELS', 'TRIVIASCAN',
|
|
96
|
+
'SCANS', 'DISP', 'DSP', 'EMPTY_FX', 'EMPTY_CH', 'EMPTY_TLOG', 'EMPTY_TL',
|
|
97
|
+
'cstCaptureActive', 'pushCstLeaf', 'pushCstChild', 'rollbackTriviaAt', 'failAt',
|
|
98
|
+
'classHas', 'consumeTrivia', 'buildFieldMap', 'projectChild', 'unwrapChild',
|
|
99
|
+
'demoteCapturedToRaw', 'cstLeavesLen', 'skipTriviaScanned', 'needsDeferredTriviaCommit',
|
|
100
|
+
'scanTrivia', 'advanceTrivia', 'refuseUnclassifiedRootScope', 'spanLines', 'rawEntry', 'lead',
|
|
101
|
+
'asciiFoldKey', 'ROUTED_FX',
|
|
102
|
+
// RECOVERY, bound rather than reimplemented. `SENTS` is the sync sentinel per
|
|
103
|
+
// char-class index — the emitted twin of `assemble.ts`'s `sentinelFor` memo,
|
|
104
|
+
// and an interpreted `Combinator` indexed out of an array for the same reason
|
|
105
|
+
// `SCANS` is. The four functions are `recovery/scan.ts`'s own, so an error's
|
|
106
|
+
// span, expected set and CST embedding are produced by the SAME code in every
|
|
107
|
+
// engine and cannot drift.
|
|
108
|
+
'SENTS', 'matchesAt', 'recoverScan', 'orSentinel', 'captureError',
|
|
109
|
+
] as const
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The state the emitted scope owns, and the helpers that touch it.
|
|
113
|
+
*
|
|
114
|
+
* `EC` is the assembly's end-position cell, INJECTED (see `cell.ts`) rather
|
|
115
|
+
* than a slot this scope owns: a mixed assembly runs emitted pieces beside
|
|
116
|
+
* closure pieces or an `exec.ts` driver, and all three must write one slot or a
|
|
117
|
+
* cross-engine call reports the end of whatever the other engine last did.
|
|
118
|
+
* `_pfScan` is the
|
|
119
|
+
* installed trivia scanner, which `OP_SCOPE` swaps mid-parse and so cannot be a
|
|
120
|
+
* constant in either engine.
|
|
121
|
+
*
|
|
122
|
+
* Every function here is shared by the rule in this file's header: none takes a
|
|
123
|
+
* piece as an argument.
|
|
124
|
+
*/
|
|
125
|
+
const RUNTIME_PRELUDE = `
|
|
126
|
+
let _pfScan=null
|
|
127
|
+
let _pfHost
|
|
128
|
+
let _pfDepth=0
|
|
129
|
+
const _pfFrames=[]
|
|
130
|
+
function _skipTrivia(input,cur,ctx){
|
|
131
|
+
const s=_pfScan
|
|
132
|
+
if(s!==null&&ctx._triviaLog===undefined&&!(ctx.captureTrivia===true&&(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)))return s(input,cur)
|
|
133
|
+
if(s!==null)return skipTriviaScanned(s,input,cur,ctx)
|
|
134
|
+
if(needsDeferredTriviaCommit(ctx)){const sc=scanTrivia(input,cur,ctx);sc.commit();return sc.end}
|
|
135
|
+
return advanceTrivia(input,cur,ctx)
|
|
136
|
+
}
|
|
137
|
+
function _pushLeaf(ctx,value,s,e){pushCstLeaf(ctx,{_tag:'leaf',value,span:{start:s,end:e}})}
|
|
138
|
+
function _pushLeafBuf(ctx,value,s,e){
|
|
139
|
+
const l={_tag:'leaf',value,span:{start:s,end:e}}
|
|
140
|
+
const b=ctx._cstBuf
|
|
141
|
+
if(b.ch!==undefined)b.ch.push(l)
|
|
142
|
+
else if(b.single!==undefined){b.ch=[b.single,l];b.single=undefined}
|
|
143
|
+
else b.single=l
|
|
144
|
+
if(b.raw!==undefined)b.raw.push(l)
|
|
145
|
+
else if(b.rawSingle!==undefined){b.raw=[b.rawSingle,l];b.rawSingle=undefined}
|
|
146
|
+
else b.rawSingle=l
|
|
147
|
+
}
|
|
148
|
+
function _accSet(ax,acc){
|
|
149
|
+
if(ax===undefined||ax.length===0)return acc
|
|
150
|
+
if(acc===undefined)return ax.slice()
|
|
151
|
+
for(const s of ax)acc.push(s)
|
|
152
|
+
return acc
|
|
153
|
+
}
|
|
154
|
+
function _trackLines(ctx,input,end){
|
|
155
|
+
const from=ctx._lineScannedTo??0
|
|
156
|
+
if(end<=from)return
|
|
157
|
+
const starts=ctx._lineStarts
|
|
158
|
+
if(starts===undefined)return
|
|
159
|
+
for(let i=from;i<end;i++)if(input.charCodeAt(i)===10)starts.push(i+1)
|
|
160
|
+
ctx._lineScannedTo=end
|
|
161
|
+
}
|
|
162
|
+
function _rollbackNeeded(ctx){
|
|
163
|
+
return ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined||ctx._cstRawChildren!==undefined||ctx._cstTriviaLog!==undefined||ctx._fields!==undefined||ctx._errors!==undefined||ctx._triviaLog!==undefined||ctx._rootTriviaLog!==undefined
|
|
164
|
+
}
|
|
165
|
+
function _rbBuf(ctx,raw,tl,lv,lg,rt){
|
|
166
|
+
const b=ctx._cstBuf
|
|
167
|
+
const ra=b.raw
|
|
168
|
+
if(ra!==undefined){
|
|
169
|
+
if(raw===0)b.raw=undefined
|
|
170
|
+
else if(raw===1){b.rawSingle=ra[0];b.raw=undefined}
|
|
171
|
+
else if(ra.length!==raw)ra.length=raw
|
|
172
|
+
}else if(raw===0)b.rawSingle=undefined
|
|
173
|
+
const ch=b.ch
|
|
174
|
+
if(ch!==undefined){
|
|
175
|
+
if(lv===0)b.ch=undefined
|
|
176
|
+
else if(lv===1){b.single=ch[0];b.ch=undefined}
|
|
177
|
+
else if(ch.length!==lv)ch.length=lv
|
|
178
|
+
}else if(lv===0)b.single=undefined
|
|
179
|
+
const bt=b.tl
|
|
180
|
+
if(bt!==undefined){
|
|
181
|
+
if(tl===0)b.tl=undefined
|
|
182
|
+
else if(bt.length!==tl)bt.length=tl
|
|
183
|
+
}
|
|
184
|
+
if(ctx._triviaLog!==undefined&&ctx._triviaLog.length!==lg)ctx._triviaLog.length=lg
|
|
185
|
+
if(ctx._rootTriviaLog!==undefined&&ctx._rootTriviaLog.length!==rt)ctx._rootTriviaLog.length=rt
|
|
186
|
+
}
|
|
187
|
+
`
|
|
188
|
+
|
|
189
|
+
function q(s: string): string {
|
|
190
|
+
return JSON.stringify(s)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* WHICH SIDE SINKS A MARK IN THIS ASSEMBLY HAS TO COVER.
|
|
195
|
+
*
|
|
196
|
+
* `_fields` and `_errors` used to be free: `emitRollback`'s own comment argued
|
|
197
|
+
* that neither could grow in an emitted assembly, because `OP_FIELD` and
|
|
198
|
+
* `OP_EXPECT` were both unemittable and a table carrying one fell back whole.
|
|
199
|
+
* Emitting them retires that argument, so the marks come back — but ONLY for a
|
|
200
|
+
* table that actually contains the writer, which is an emit-time fact about the
|
|
201
|
+
* program and not a per-parse test.
|
|
202
|
+
*
|
|
203
|
+
* Per TABLE and not per SITE, deliberately. A site's subtree reaches through
|
|
204
|
+
* `OP_RULE` into the rule graph, so "can a field run under this mark" is very
|
|
205
|
+
* nearly "does the grammar have a field" for every mark that is not a leaf's —
|
|
206
|
+
* and a per-site answer would buy a handful of sites at the cost of a second
|
|
207
|
+
* fixpoint walk over the code array.
|
|
208
|
+
*/
|
|
209
|
+
type Sinks = { readonly fd: boolean; readonly er: boolean }
|
|
210
|
+
const NO_SINKS: Sinks = { fd: false, er: false }
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The rollback-mark prologue as SSA locals, rather than the eight shared slots
|
|
214
|
+
* `assemble.ts` writes and reads back.
|
|
215
|
+
*
|
|
216
|
+
* `markCst` exists in the closure engine because a `{ raw, tl, lv }` return
|
|
217
|
+
* would allocate per mark and marks are the most-executed thing in the driver.
|
|
218
|
+
* Emitted, the question does not arise: a mark is seven locals in a frame the
|
|
219
|
+
* body already has. Same scalarisation `rollbackCstCaptureAt` documents, one
|
|
220
|
+
* step further because the source form can take it.
|
|
221
|
+
*/
|
|
222
|
+
function sinkSlots(t: string, s: Sinks): string {
|
|
223
|
+
return `${s.fd ? `,${t}fd=0` : ''}${s.er ? `,${t}er=0` : ''}`
|
|
224
|
+
}
|
|
225
|
+
function sinkReads(t: string, s: Sinks): string {
|
|
226
|
+
return `${s.fd ? `\n${t}fd=ctx._fields!==undefined?ctx._fields.length:0` : ''}${s.er ? `\n${t}er=ctx._errors!==undefined?ctx._errors.length:0` : ''}`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function emitMark(t: string, buf: boolean, s: Sinks = NO_SINKS, decl = true): string {
|
|
230
|
+
// THE SITE IS INSIDE A NODE. `OP_NODE` opens `ctx._cstBuf` unconditionally and
|
|
231
|
+
// closes it on the way out, so the whole discriminating chain below — which
|
|
232
|
+
// sink is live, and whether a mark is needed at all — has ONE answer here, and
|
|
233
|
+
// the pass knows it. What is left is the five loads a mark actually is.
|
|
234
|
+
//
|
|
235
|
+
// `decl === false` RE-TAKES a mark into locals that already exist —
|
|
236
|
+
// `OP_DISPATCH` marks, rolls back to the selector's mark, and marks again.
|
|
237
|
+
if (buf) {
|
|
238
|
+
return `${decl ? `let ${t}raw=0,${t}tl=0,${t}lv=0,${t}lg=0,${t}rt=0${sinkSlots(t, s)}\n` : ''}{const b=ctx._cstBuf
|
|
239
|
+
const r=b.raw;${t}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
|
|
240
|
+
const h=b.ch;${t}lv=h!==undefined?h.length:b.single!==undefined?1:0
|
|
241
|
+
const l=b.tl;${t}tl=l!==undefined?l.length:0
|
|
242
|
+
${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
243
|
+
${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${sinkReads(t, s)}}`
|
|
244
|
+
}
|
|
245
|
+
return `${decl ? `let ${t}n=false,${t}raw=0,${t}tl=0,${t}lv=0,${t}lg=0,${t}rt=0${sinkSlots(t, s)}\n` : `${t}n=false\n`}{const b=ctx._cstBuf
|
|
246
|
+
if(b!==undefined){
|
|
247
|
+
const r=b.raw;${t}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
|
|
248
|
+
const h=b.ch;${t}lv=h!==undefined?h.length:b.single!==undefined?1:0
|
|
249
|
+
const l=b.tl;${t}tl=l!==undefined?l.length:0
|
|
250
|
+
${t}n=true
|
|
251
|
+
}else if(ctx._cstLeaves!==undefined||ctx._cstRawChildren!==undefined||ctx._cstTriviaLog!==undefined||ctx._fields!==undefined||ctx._errors!==undefined||ctx._triviaLog!==undefined||ctx._rootTriviaLog!==undefined){
|
|
252
|
+
${t}raw=ctx._cstRawChildren!==undefined?ctx._cstRawChildren.length:0
|
|
253
|
+
${t}tl=ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0
|
|
254
|
+
${t}lv=ctx._cstLeaves!==undefined?ctx._cstLeaves.length:0
|
|
255
|
+
${t}n=true
|
|
256
|
+
}
|
|
257
|
+
if(${t}n){
|
|
258
|
+
${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
259
|
+
${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${sinkReads(t, s)}
|
|
260
|
+
}}`
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The matching rollback for a mark emitted with prefix `t`.
|
|
265
|
+
*
|
|
266
|
+
* THE `_fields` AND `_errors` MARKS ARE NOT TAKEN, because in an EMITTED
|
|
267
|
+
* assembly neither sink can grow. `OP_FIELD` is the only writer of `_fields`
|
|
268
|
+
* and `OP_EXPECT`/`recoverScan` the only writers of `_errors`, and none of the
|
|
269
|
+
* three is lowered by this file — a table containing one raises `Unemittable`
|
|
270
|
+
* and the WHOLE assembly falls back to `assemble.ts`'s closures, so no emitted
|
|
271
|
+
* body ever runs beside one. That is what makes this an emit-time fact rather
|
|
272
|
+
* than a per-parse guess, and it is why `assemble.ts`'s `nextTerm` still takes
|
|
273
|
+
* all seven: it is the engine those constructs actually run in.
|
|
274
|
+
*
|
|
275
|
+
* The two sinks are passed differently because `rollbackCstCaptureAt` guards
|
|
276
|
+
* them differently. `_errors` is guarded on `errors !== undefined`, so
|
|
277
|
+
* `undefined` is its established "no mark taken" sentinel and is correct even
|
|
278
|
+
* for a caller-supplied context that arrived with errors already on it.
|
|
279
|
+
* `_fields` has no such guard — `length = undefined` would throw — so it takes
|
|
280
|
+
* the literal `0` that a per-node `_fields` no `OP_FIELD` can push to always has.
|
|
281
|
+
*/
|
|
282
|
+
function emitRollback(t: string, buf: boolean, s: Sinks = NO_SINKS): string {
|
|
283
|
+
// `_rbBuf` is the `_cstBuf` arm of `rollbackCstCaptureAt` plus the two trivia
|
|
284
|
+
// truncations, with the sink discrimination the label already answered removed.
|
|
285
|
+
// It takes no piece, so the header's sharing rule admits it.
|
|
286
|
+
//
|
|
287
|
+
// The two side sinks stay OUT of `_rbBuf` rather than growing its parameter
|
|
288
|
+
// list: they are absent from most tables, and a fixed seven-argument helper
|
|
289
|
+
// would make every grammar pay the two extra pushes to serve the ones that
|
|
290
|
+
// carry a field. Guarded exactly as `rollbackCstCaptureAt:242,233` guards them
|
|
291
|
+
// — `_errors` on a defined mark, because `undefined` is its "no mark taken"
|
|
292
|
+
// sentinel, and `_fields` on the array alone.
|
|
293
|
+
const fd = s.fd ? `\nif(ctx._fields!==undefined&&ctx._fields.length!==${t}fd)ctx._fields.length=${t}fd` : ''
|
|
294
|
+
const er = s.er ? `\nif(ctx._errors!==undefined&&ctx._errors.length!==${t}er)ctx._errors.length=${t}er` : ''
|
|
295
|
+
if (buf) return `_rbBuf(ctx,${t}raw,${t}tl,${t}lv,${t}lg,${t}rt)${fd}${er}`
|
|
296
|
+
if (!s.fd && !s.er) return `if(${t}n)rollbackTriviaAt(ctx,${t}raw,${t}tl,${t}lv,0,undefined,${t}lg,${t}rt)`
|
|
297
|
+
return `if(${t}n){rollbackTriviaAt(ctx,${t}raw,${t}tl,${t}lv,${s.fd ? `${t}fd` : '0'},${s.er ? `${t}er` : 'undefined'},${t}lg,${t}rt)}`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* ONE SEQUENCE TERM — `assemble.ts`'s `nextTerm`, inlined at the site.
|
|
302
|
+
*
|
|
303
|
+
* Reads and writes `cur`; the term's value lands in `dst`; failure returns
|
|
304
|
+
* `FAIL` directly, because emitted here there is no shared return channel to
|
|
305
|
+
* encode a −1 sentinel into.
|
|
306
|
+
*
|
|
307
|
+
* The `ctx.trivia === undefined` test is GONE wherever the site label answers
|
|
308
|
+
* it. It was never an option — `OP_SCOPE` swaps `ctx.trivia` mid-parse, which is
|
|
309
|
+
* why it could not be lifted into `RunCfg` — but it IS a property of where the
|
|
310
|
+
* term sits, and `site-labels.ts` resolves it at encode time. `TRI_NONE` keeps
|
|
311
|
+
* only the first branch, a known slot keeps only the second (with the scope's own
|
|
312
|
+
* scanner already bound into `skip`), and `TRI_UNKNOWN` keeps both.
|
|
313
|
+
*/
|
|
314
|
+
function emitTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string, s: Sinks): string {
|
|
315
|
+
const fast = `const ${t}v=${callee}(input,cur,ctx)
|
|
316
|
+
if(${t}v===FAIL)return FAIL
|
|
317
|
+
${dst}=${t}v
|
|
318
|
+
cur=EC.e`
|
|
319
|
+
if (l.tri === TRI_NONE) return fast
|
|
320
|
+
const scanned = `${emitMark(t, l.buf, s)}
|
|
321
|
+
const ${t}s=${skip}(input,cur,ctx)
|
|
322
|
+
const ${t}v=${callee}(input,${t}s,ctx)
|
|
323
|
+
if(${t}v===FAIL)return FAIL
|
|
324
|
+
${dst}=${t}v
|
|
325
|
+
if(EC.e>${t}s)cur=EC.e
|
|
326
|
+
else{${emitRollback(t, l.buf, s)}}`
|
|
327
|
+
if (l.tri !== TRI_UNKNOWN) return scanned
|
|
328
|
+
return `if(ctx.trivia===undefined){
|
|
329
|
+
${fast}
|
|
330
|
+
}else{
|
|
331
|
+
${scanned}
|
|
332
|
+
}`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The repetition's LEAD-TRIVIA skip, as a BLOCK.
|
|
337
|
+
*
|
|
338
|
+
* A block and not a bare statement because the separator form spells it
|
|
339
|
+
* `} else <this>`, and a label that erases the skip entirely would otherwise
|
|
340
|
+
* leave a dangling `else` to swallow the statement after it.
|
|
341
|
+
*/
|
|
342
|
+
function leadSkip(hasTrivia: string, leadTrivia: string, skip: string): string {
|
|
343
|
+
if (hasTrivia === 'false') return '{}'
|
|
344
|
+
const call = `itemStart=${skip}(input,itemStart,ctx)`
|
|
345
|
+
if (hasTrivia === 'true') return leadTrivia === 'true' ? `{${call}}` : `{if(${leadTrivia})${call}}`
|
|
346
|
+
return `{if(hasTrivia&&${leadTrivia})${call}}`
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* THE THREE POOLS, SAID AS INDICES INTO THE PROGRAM.
|
|
351
|
+
*
|
|
352
|
+
* The pools themselves are `Uint32Array`s, `{ascii, hi}` class objects and
|
|
353
|
+
* string arrays — printable, but at 129 words per mask and 128 bytes per class
|
|
354
|
+
* they would dwarf the table they belong to. Every entry is already IN the
|
|
355
|
+
* program: a class is `cc[i]`, an arm's expected set is `fx[i]`, and a mask is a
|
|
356
|
+
* pure function of its class row. So the plan is three arrays of small integers,
|
|
357
|
+
* and `rebuildPools` turns it back into the pools with allocation only — no
|
|
358
|
+
* string building, and in particular no `Function` constructor.
|
|
359
|
+
*
|
|
360
|
+
* This is what lets the macro pre-compile an assembly: the FACTORY is printed as
|
|
361
|
+
* a real function literal, and its data arguments are rebuilt from this.
|
|
362
|
+
*/
|
|
363
|
+
export type PoolPlan = {
|
|
364
|
+
/** Per `CLS` row: the `cc` index of each entry, `-1` for a null (ungated) arm. */
|
|
365
|
+
readonly classes: readonly (readonly number[])[]
|
|
366
|
+
/** Per `AFX` row: the `fx` index of each arm's expected set. */
|
|
367
|
+
readonly armExpected: readonly (readonly number[])[]
|
|
368
|
+
/** Per `MASK` row: the `CLS` index the mask is computed from. */
|
|
369
|
+
readonly masks: readonly number[]
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Everything the compiled factory needs bound, beside the emitted text. */
|
|
373
|
+
export type EmitResult = {
|
|
374
|
+
readonly source: string
|
|
375
|
+
/** Site offsets the emitter reached — the emitted twin of `Assembly.reached`. */
|
|
376
|
+
readonly reached: ReadonlySet<number>
|
|
377
|
+
/** Hoisted per-choice candidate masks, in `MASK` order. */
|
|
378
|
+
readonly masks: readonly Uint32Array[]
|
|
379
|
+
/** Hoisted per-arm class gates, in `CLS` order. */
|
|
380
|
+
readonly classes: readonly (ResolvedClass | null)[][]
|
|
381
|
+
/** Hoisted per-arm expected sets, in `AFX` order. */
|
|
382
|
+
readonly armExpected: readonly (readonly string[])[][]
|
|
383
|
+
/** The same three pools as indices, for a build-time emitter. */
|
|
384
|
+
readonly plan: PoolPlan
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The candidate mask for one `CLS` row — the ONE definition, shared with the emitter. */
|
|
388
|
+
function maskForClassRow(gates: readonly (ResolvedClass | null)[]): Uint32Array {
|
|
389
|
+
const m = new Uint32Array(129)
|
|
390
|
+
for (let i = 0; i < gates.length; i++) {
|
|
391
|
+
const cls = gates[i]!
|
|
392
|
+
const bit = 1 << i
|
|
393
|
+
if (cls === null) { for (let c = 0; c < 129; c++) m[c]! |= bit; continue }
|
|
394
|
+
for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) m[c]! |= bit
|
|
395
|
+
}
|
|
396
|
+
return m
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Rebuild the three pools a pre-compiled factory takes, from the resolved table
|
|
401
|
+
* and the plan the emitter printed. Allocation only.
|
|
402
|
+
*/
|
|
403
|
+
export function rebuildPools(
|
|
404
|
+
cc: readonly ResolvedClass[],
|
|
405
|
+
fx: readonly (readonly string[])[],
|
|
406
|
+
plan: PoolPlan,
|
|
407
|
+
): { masks: Uint32Array[]; classes: (ResolvedClass | null)[][]; armExpected: (readonly string[])[][] } {
|
|
408
|
+
const classes = plan.classes.map(row => row.map(i => (i < 0 ? null : cc[i] ?? null)))
|
|
409
|
+
return {
|
|
410
|
+
classes,
|
|
411
|
+
armExpected: plan.armExpected.map(row => row.map(i => fx[i] ?? [])),
|
|
412
|
+
masks: plan.masks.map(ci => maskForClassRow(classes[ci]!)),
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Emit the whole assembly for one resolved table and one option set.
|
|
418
|
+
*
|
|
419
|
+
* Throws `Unemittable` for any construct not lowered. It does NOT compile the
|
|
420
|
+
* text — `assemble.ts` does — so a refusal and a compile failure stay two
|
|
421
|
+
* distinguishable outcomes at the call site.
|
|
422
|
+
*/
|
|
423
|
+
export function emitAssemblySource(
|
|
424
|
+
t: ResolvedTable,
|
|
425
|
+
prog: TableProgram,
|
|
426
|
+
cfg: {
|
|
427
|
+
hostCst: boolean
|
|
428
|
+
hostReadsChildren?: boolean
|
|
429
|
+
hostCaptureTrivia?: ((type: string) => boolean) | undefined
|
|
430
|
+
trackLines: boolean
|
|
431
|
+
tolerant: boolean
|
|
432
|
+
coverage: boolean
|
|
433
|
+
probe: boolean
|
|
434
|
+
},
|
|
435
|
+
extraIps: readonly number[] = [],
|
|
436
|
+
): EmitResult {
|
|
437
|
+
const { code, k, disp, dsp, triviaLabelled } = t
|
|
438
|
+
const swapLegal = !cfg.trackLines
|
|
439
|
+
const hostCst = cfg.hostCst
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* RECOVERY IS EMITTED, and the refusal this replaces was wrong in SCOPE.
|
|
443
|
+
*
|
|
444
|
+
* The refusal read: *"`recoverScan`'s protocol has three implementations
|
|
445
|
+
* already and a fourth is how an error span drifts... It is also cold by
|
|
446
|
+
* construction — reached only on the failure of an element — so the speed
|
|
447
|
+
* argument for emitting it is absent."*
|
|
448
|
+
*
|
|
449
|
+
* The drift half is answered by construction, not by argument: NOTHING of the
|
|
450
|
+
* protocol is reimplemented here. `recoverScan`, `matchesAt`, `orSentinel` and
|
|
451
|
+
* `captureError` are BOUND and CALLED, exactly as the closure engine calls
|
|
452
|
+
* them, so there is no fourth implementation to drift — the same reason
|
|
453
|
+
* `OP_SCAN` may bind an interpreted combinator and index it (`SCANS[i]`).
|
|
454
|
+
*
|
|
455
|
+
* The cold half was true of recovery and FALSE of the refusal. This throw sat
|
|
456
|
+
* at the top of the emitter, so it did not decline the cold recovery rows — it
|
|
457
|
+
* declined THE ENTIRE ASSEMBLY. Every tolerant parse in the product therefore
|
|
458
|
+
* ran the array-indexed closure walk for its whole hot path: every literal,
|
|
459
|
+
* every choice, every repetition, none of which is recovery and all of which is
|
|
460
|
+
* as hot as the strict path that is emitted. The speed argument is absent for
|
|
461
|
+
* `recoverScan` and was never absent for the table around it.
|
|
462
|
+
*
|
|
463
|
+
* What stays interpreted is what was already interpreted for the strict path.
|
|
464
|
+
*/
|
|
465
|
+
const REC = prog.rec === 1 && cfg.tolerant
|
|
466
|
+
if (cfg.coverage) throw new Unemittable('a coverage assembly')
|
|
467
|
+
|
|
468
|
+
// THE DOWNWARD PASS, BEFORE ANY LOWERING. The roots are exactly the sites
|
|
469
|
+
// `link` is called on from outside a body — the rule entries and the scan
|
|
470
|
+
// pool's `extraIps` — and each starts at `TOP`, because a caller outside the
|
|
471
|
+
// emitted scope supplies a context this pass cannot see.
|
|
472
|
+
const roots = [...Object.values(prog.rules), ...extraIps]
|
|
473
|
+
const labels = computeSiteLabels(code, roots, hostCst)
|
|
474
|
+
|
|
475
|
+
// THE SIDE-SINK CENSUS, over the same reachable set the labels are computed
|
|
476
|
+
// from. `OP_FIELD` is the only ROW that writes `_fields` and `OP_EXPECT` the
|
|
477
|
+
// only emittable row that writes `_errors` — `recoverScan` is the other, and a
|
|
478
|
+
// recovery assembly is refused above. A table with neither pays for neither.
|
|
479
|
+
//
|
|
480
|
+
// `OP_SCAN` RAISES BOTH, and that is not caution — it is a measured defect.
|
|
481
|
+
// The row runs an INTERPRETED combinator (`scanTo`/`balanced`, rebuilt by
|
|
482
|
+
// `resolveTable`'s pool from subtrees), so its interior is not in this code
|
|
483
|
+
// array and this walk cannot see what it writes. `balanced()`'s unclosed-group
|
|
484
|
+
// failure pushes a `parseError` carrying its closer, and less's `at-rules.less`
|
|
485
|
+
// and `css-3.less` each left several of them on `RunResult.errors` — errors
|
|
486
|
+
// that the CLOSURE engine rolls back, because its mark covers `_errors`
|
|
487
|
+
// unconditionally. The two engines disagreed on the errors facet of a parse
|
|
488
|
+
// that otherwise matched byte for byte.
|
|
489
|
+
//
|
|
490
|
+
// Any future row whose child is not an offset in `code` belongs here for the
|
|
491
|
+
// same reason. `OP_LIVE` is the other such row, and it is unemittable.
|
|
492
|
+
const sinks: Sinks = (() => {
|
|
493
|
+
let fd = false
|
|
494
|
+
// A RECOVERY ASSEMBLY IS AN `_errors` WRITER EVERYWHERE. `recoverScan` pushes
|
|
495
|
+
// through `captureError` from inside any `OP_REP` that recovers, and the
|
|
496
|
+
// recovering row is the repetition itself rather than a distinguishable
|
|
497
|
+
// opcode — so there is no site to census and the answer is the assembly's.
|
|
498
|
+
//
|
|
499
|
+
// This is the `OP_SCAN` lesson applied before it costs anything rather than
|
|
500
|
+
// after: that row raised both sinks because its interior is not in `code`,
|
|
501
|
+
// and the defect it was raised for was precisely an `_errors` rollback the
|
|
502
|
+
// two engines disagreed on while matching byte for byte on the tree.
|
|
503
|
+
let er = REC
|
|
504
|
+
for (const ip of reachableSites(code, roots)) {
|
|
505
|
+
const op = code[ip]
|
|
506
|
+
if (op === OP_SCAN) { fd = true; er = true; break }
|
|
507
|
+
if (op === OP_FIELD) fd = true
|
|
508
|
+
else if (op === OP_EXPECT) er = true
|
|
509
|
+
}
|
|
510
|
+
return { fd, er }
|
|
511
|
+
})()
|
|
512
|
+
|
|
513
|
+
const bodies: string[] = []
|
|
514
|
+
const byIp = new Map<number, string>()
|
|
515
|
+
const reached = new Set<number>()
|
|
516
|
+
const prelude: string[] = []
|
|
517
|
+
const skipDefs: string[] = []
|
|
518
|
+
const skipPool = new Map<string, string>()
|
|
519
|
+
const pool = new Map<string, string>()
|
|
520
|
+
const masks: Uint32Array[] = []
|
|
521
|
+
const classes: (ResolvedClass | null)[][] = []
|
|
522
|
+
const armExpected: (readonly string[])[][] = []
|
|
523
|
+
/**
|
|
524
|
+
* THE POOLS, SAID AS INDICES — see `PoolPlan`. Written in lockstep with the
|
|
525
|
+
* three arrays above so a build-time emitter can print the plan instead of the
|
|
526
|
+
* pools, and `rebuildPools` can reproduce them from the resolved table alone.
|
|
527
|
+
* Kept adjacent to each `push` for exactly the reason `EMITTED_PARAMS` is one
|
|
528
|
+
* list: two orders that must agree cannot be allowed to live apart.
|
|
529
|
+
*/
|
|
530
|
+
const classPlan: number[][] = []
|
|
531
|
+
const armExpectedPlan: number[][] = []
|
|
532
|
+
const maskPlan: number[] = []
|
|
533
|
+
|
|
534
|
+
/** `codegen.ts`'s hoisted pools, same spelling: `_k<N>` / `_fx<N>` / `_fn<N>`. */
|
|
535
|
+
function hoist(kind: string, expr: string): string {
|
|
536
|
+
const hit = pool.get(expr)
|
|
537
|
+
if (hit !== undefined) return hit
|
|
538
|
+
const nm = `_${kind}${pool.size}`
|
|
539
|
+
pool.set(expr, nm)
|
|
540
|
+
prelude.push(`const ${nm}=${expr}`)
|
|
541
|
+
return nm
|
|
542
|
+
}
|
|
543
|
+
const kRef = (i: number): string => hoist('k', `K[${i}]`)
|
|
544
|
+
const fxRef = (i: number): string => hoist('fx', `FX[${i}]`)
|
|
545
|
+
const fnRef = (i: number): string => hoist('fn', `FNS[${i}]`)
|
|
546
|
+
/**
|
|
547
|
+
* The sync sentinel for a char-class index, hoisted once per class.
|
|
548
|
+
*
|
|
549
|
+
* −1 is "no usable sentinel" — the same answer `firstSetSentinel` gives for an
|
|
550
|
+
* `any`/`empty` first set — and it lowers to the literal `undefined` so the
|
|
551
|
+
* publish below collapses to the inherited sentinel with no array read.
|
|
552
|
+
*/
|
|
553
|
+
const sentRef = (cls: number): string => cls < 0 ? 'undefined' : hoist('sent', `SENTS[${cls}]`)
|
|
554
|
+
|
|
555
|
+
/** A fresh local prefix, so two inlined marks in one body cannot collide. */
|
|
556
|
+
let uid = 0
|
|
557
|
+
const tmp = (): string => `_t${uid++}_`
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* THE TRIVIA SCAN FOR ONE SITE LABEL.
|
|
561
|
+
*
|
|
562
|
+
* `_skipTrivia` answers four questions on every call — is a scanner installed,
|
|
563
|
+
* is the global trivia log live, is capture on, is a capture sink open — and
|
|
564
|
+
* three of them are what the label already knows. This mints the arm that
|
|
565
|
+
* survives, once per distinct label, and every site with that label calls it by
|
|
566
|
+
* name.
|
|
567
|
+
*
|
|
568
|
+
* Shared, and the header's rule is why it may be: none of these takes a PIECE.
|
|
569
|
+
* The installed scanner arrives as a hoisted `TRIVIASCAN[ki]` const, not as an
|
|
570
|
+
* argument, so a second caller cannot pollute a call site's feedback. That is
|
|
571
|
+
* the same test `_skipTrivia` passed and `nextTerm` failed.
|
|
572
|
+
*
|
|
573
|
+
* `_triviaLog` is deliberately NOT resolved here. It is neither an option nor a
|
|
574
|
+
* site property — nothing in `src` writes it except a caller-supplied
|
|
575
|
+
* `ParseContext` (`types.ts:524`) — so it stays the one runtime read.
|
|
576
|
+
*/
|
|
577
|
+
function skipFor(l: SiteLabel): string {
|
|
578
|
+
if (l.tri < 0) return '_skipTrivia'
|
|
579
|
+
const ki = l.tri
|
|
580
|
+
// `TRIVIASCAN[ki]` IS NULLABLE. `program.ts:475` maps every trivia through
|
|
581
|
+
// `fastTriviaScanner`, which declines any shape it cannot lower, so the slot
|
|
582
|
+
// holds `null` for those — which is why `OP_SCOPE` installs a scanner it
|
|
583
|
+
// still has to null-test. The presence of a lowering is table data like the
|
|
584
|
+
// label bit beside it, so it belongs in the key rather than in a branch.
|
|
585
|
+
const hasScan = swapLegal && !triviaLabelled[ki]! && t.triviaScan[ki] != null
|
|
586
|
+
const key = `${ki}|${hasScan ? 1 : 0}|${l.buf ? 1 : 0}|${l.cap}`
|
|
587
|
+
const hit = skipPool.get(key)
|
|
588
|
+
if (hit !== undefined) return hit
|
|
589
|
+
const nm = `_sk${skipPool.size}`
|
|
590
|
+
skipPool.set(key, nm)
|
|
591
|
+
if (!hasScan) {
|
|
592
|
+
// No installed scanner: the labelled/`trackLines` path, which records
|
|
593
|
+
// through `scanTrivia`. `needsDeferredTriviaCommit` is implied by an open
|
|
594
|
+
// buffer, so an in-node site skips the call that asks.
|
|
595
|
+
skipDefs.push(l.buf
|
|
596
|
+
? `function ${nm}(input,cur,ctx){const s=scanTrivia(input,cur,ctx);s.commit();return s.end}`
|
|
597
|
+
: `function ${nm}(input,cur,ctx){
|
|
598
|
+
if(needsDeferredTriviaCommit(ctx)){const s=scanTrivia(input,cur,ctx);s.commit();return s.end}
|
|
599
|
+
return advanceTrivia(input,cur,ctx)}`)
|
|
600
|
+
return nm
|
|
601
|
+
}
|
|
602
|
+
const sc = hoist('ts', `TRIVIASCAN[${ki}]`)
|
|
603
|
+
// `ctx.captureTrivia === true && (a sink is open)`, with both conjuncts
|
|
604
|
+
// resolved as far as the label takes them.
|
|
605
|
+
const sink = l.buf ? 'true' : '(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)'
|
|
606
|
+
const capturing = l.cap === CAP_OFF ? 'false' : l.cap === CAP_ON ? sink : `(ctx.captureTrivia===true&&${sink})`
|
|
607
|
+
if (capturing === 'true') {
|
|
608
|
+
// Capture is on and a sink is open: the bare-scanner arm is unreachable.
|
|
609
|
+
skipDefs.push(`function ${nm}(input,cur,ctx){return skipTriviaScanned(${sc},input,cur,ctx)}`)
|
|
610
|
+
return nm
|
|
611
|
+
}
|
|
612
|
+
const guard = capturing === 'false'
|
|
613
|
+
? 'ctx._triviaLog===undefined'
|
|
614
|
+
: `ctx._triviaLog===undefined&&!${capturing}`
|
|
615
|
+
skipDefs.push(`function ${nm}(input,cur,ctx){
|
|
616
|
+
if(${guard})return ${sc}(input,cur)
|
|
617
|
+
return skipTriviaScanned(${sc},input,cur,ctx)}`)
|
|
618
|
+
return nm
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The BODY OF A LOWERED REGEX ROW, or `undefined` when the shape declines.
|
|
623
|
+
*
|
|
624
|
+
* Declining is the safe direction and the only one this can take: a shape it
|
|
625
|
+
* fails to recognise falls through to the regex, which is what the row did
|
|
626
|
+
* before. A shape it recognises WRONGLY is a defect, and the gate for that is
|
|
627
|
+
* `bench/scan-shape-oracle.ts` — every regex constant of every workload
|
|
628
|
+
* grammar driven at every position of its own corpus against the sticky
|
|
629
|
+
* `exec` it replaces, end for end.
|
|
630
|
+
*
|
|
631
|
+
* The value is `input.slice(pos, end)`, which for a STICKY regex is `m[0]`
|
|
632
|
+
* character for character. The `u`/`i`/`m`/`s` flags are handled inside
|
|
633
|
+
* `scanShapeFromRegex`; `u` declines outright, because a code-unit scan is not
|
|
634
|
+
* a code-point one.
|
|
635
|
+
*
|
|
636
|
+
* `captureLeaf` is the SITE'S — it arrives from `lower`, already resolved
|
|
637
|
+
* against that site's label, so a lowered row pushes its leaf through exactly
|
|
638
|
+
* the form `OP_LIT` and `OP_LIT_CI` push theirs. Re-deriving the capture test
|
|
639
|
+
* here would put a second answer to a settled question inside the one body
|
|
640
|
+
* that runs most.
|
|
641
|
+
*/
|
|
642
|
+
function emitScan(
|
|
643
|
+
ki: number,
|
|
644
|
+
xf: string,
|
|
645
|
+
track: boolean,
|
|
646
|
+
captureLeaf: (value: string) => string,
|
|
647
|
+
): string | undefined {
|
|
648
|
+
const re = k[ki]
|
|
649
|
+
if (!(re instanceof RegExp)) return undefined
|
|
650
|
+
const shape = scanShapeFromRegex(re.source, re.flags)
|
|
651
|
+
if (shape === null) return undefined
|
|
652
|
+
let n = 0
|
|
653
|
+
const p = tmp()
|
|
654
|
+
const m = emitShapeMatch(shape, 'pos', (prefix = '_v') => `${p}${prefix}${n++}`, '')
|
|
655
|
+
return `
|
|
656
|
+
${m.setup.join('\n')}
|
|
657
|
+
if(${m.ok}){
|
|
658
|
+
const e=${m.end}
|
|
659
|
+
const v=input.slice(pos,e)
|
|
660
|
+
${captureLeaf('v')}
|
|
661
|
+
${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
|
|
662
|
+
return v
|
|
663
|
+
}
|
|
664
|
+
ctx._fe=pos;ctx._fx=${xf}
|
|
665
|
+
${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
|
|
666
|
+
`
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function link(ip: number): string {
|
|
670
|
+
const hit = byIp.get(ip)
|
|
671
|
+
if (hit !== undefined) return hit
|
|
672
|
+
// An ALIAS site forwards to its child with no body of its own. Resolved
|
|
673
|
+
// before reservation so no name is minted for a function that would only
|
|
674
|
+
// add a call frame — `assemble.ts` makes the same choice by returning the
|
|
675
|
+
// child piece itself.
|
|
676
|
+
const target = resolveAlias(ip)
|
|
677
|
+
const done = byIp.get(target)
|
|
678
|
+
if (done !== undefined) return done
|
|
679
|
+
const fname = `_pf${target}`
|
|
680
|
+
// Reserved BEFORE lowering, so a back-edge into a site still in flight binds
|
|
681
|
+
// to the hoisted declaration rather than to a forwarding stub. This is the
|
|
682
|
+
// whole of `assemble.ts`'s `inFlight` map and its one shared closure.
|
|
683
|
+
byIp.set(target, fname)
|
|
684
|
+
bodies.push(lower(target, fname))
|
|
685
|
+
return fname
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* FOLLOW THE ALIAS CHAIN TO THE FIRST SITE WITH A BODY — iteratively, and
|
|
690
|
+
* BEFORE anything is reserved.
|
|
691
|
+
*
|
|
692
|
+
* The previous form recursed through `link` and parked a PROVISIONAL name
|
|
693
|
+
* `_pf<ip>` in the alias map so that a cycle would terminate. It terminated,
|
|
694
|
+
* and it emitted a call to a function that does not exist: `OP_RULE` is the
|
|
695
|
+
* only back-edge in a table, `OP_RULE` is an alias, so a recursive rule whose
|
|
696
|
+
* cycle re-enters through the alias row still in flight bound to the
|
|
697
|
+
* provisional name, and every grammar with that shape compiled to
|
|
698
|
+
* `_pf1100 is not defined` — a ReferenceError at parse time, from a body that
|
|
699
|
+
* the `new Function` call above accepts as syntactically valid.
|
|
700
|
+
*
|
|
701
|
+
* Resolving the chain first removes the window: no name is ever handed out for
|
|
702
|
+
* a site that will not get a body, because the only name handed out is the
|
|
703
|
+
* CHAIN'S END, which is always lowered.
|
|
704
|
+
*
|
|
705
|
+
* A cycle of alias-only rows — `A` aliasing to `B` aliasing back to `A`, with
|
|
706
|
+
* no body anywhere in between — is a rule that expands to itself and consumes
|
|
707
|
+
* nothing. Refused BY NAME rather than looped on: it is a defect in the
|
|
708
|
+
* grammar, and the closure engine meets it as a stack overflow at parse time.
|
|
709
|
+
*/
|
|
710
|
+
function resolveAlias(ip: number): number {
|
|
711
|
+
let cur = ip
|
|
712
|
+
let seen: Set<number> | undefined
|
|
713
|
+
for (;;) {
|
|
714
|
+
reached.add(cur)
|
|
715
|
+
const next = aliasOf(cur)
|
|
716
|
+
if (next === undefined) return cur
|
|
717
|
+
if (seen === undefined) seen = new Set([cur])
|
|
718
|
+
else if (seen.has(cur)) throw new Unemittable('a cycle of alias-only sites (a rule that expands to itself)')
|
|
719
|
+
else seen.add(cur)
|
|
720
|
+
cur = next
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Sites that forward to a child with no body, decided by option, data, or LABEL. */
|
|
725
|
+
function aliasOf(ip: number): number | undefined {
|
|
726
|
+
const op = code[ip]
|
|
727
|
+
// `OP_GATE` under a probe or a tolerant recovery is a no-op that forwards to
|
|
728
|
+
// its child, exactly as `assemble.ts:952` resolves it.
|
|
729
|
+
if (op === OP_GATE && (cfg.tolerant || cfg.probe)) return code[ip + 2]!
|
|
730
|
+
if (op === OP_RULE) return code[ip + 1]!
|
|
731
|
+
// A SCOPE THAT INSTALLS WHAT IS ALREADY INSTALLED. `encode.ts:520` wraps
|
|
732
|
+
// EVERY rule of a `rules({ trivia }, …)` map in its own `OP_SCOPE`, so a
|
|
733
|
+
// grammar with one ambient trivia re-installs the same slot at every rule
|
|
734
|
+
// entry — six context stores, a scanner swap and their six restores, per
|
|
735
|
+
// call, to arrive at the values already there.
|
|
736
|
+
//
|
|
737
|
+
// The label is what makes this decidable: `tri >= 0` can ONLY have come from
|
|
738
|
+
// an enclosing `OP_SCOPE` carrying that slot, and that scope set
|
|
739
|
+
// `ctx.triviaKindLabels` and `_pfScan` from the same slot, so all three are
|
|
740
|
+
// already the values this row would write. Restricted to plain `OP_SCOPE`
|
|
741
|
+
// with no root-capture policy: `OP_SCOPE_CAP` also raises `captureTrivia`,
|
|
742
|
+
// and the two flag bits are a real refusal and a real save/restore.
|
|
743
|
+
//
|
|
744
|
+
// `ki >= 0` is required rather than implied: `TRI_NONE` and `TRI_UNKNOWN` are
|
|
745
|
+
// themselves negative, so comparing a negative operand against a lattice
|
|
746
|
+
// element would read "unknown" as a match.
|
|
747
|
+
if (op === OP_SCOPE && code[ip + 3]! === 0 && code[ip + 1]! >= 0
|
|
748
|
+
&& labels.at(ip).tri === code[ip + 1]!) {
|
|
749
|
+
return code[ip + 2]!
|
|
750
|
+
}
|
|
751
|
+
return undefined
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function lower(ip: number, fname: string): string {
|
|
755
|
+
const op = code[ip]
|
|
756
|
+
const head = `function ${fname}(input,pos,ctx){`
|
|
757
|
+
const L = labels.at(ip)
|
|
758
|
+
/**
|
|
759
|
+
* THE LEAF CAPTURE TEST, and only the test.
|
|
760
|
+
*
|
|
761
|
+
* `cstCaptureActive` is `_cstBuf !== undefined || _cstLeaves !== undefined`,
|
|
762
|
+
* and `OP_NODE` opens `_cstBuf` on entry regardless of host mode — so under a
|
|
763
|
+
* node the answer is a constant `true`. What that licenses is dropping the
|
|
764
|
+
* CALL, never the capture: the captured leaves feed `kids` → `build(...)`,
|
|
765
|
+
* and eliding them on `hostCst === false` would be a wrong tree, not a fast
|
|
766
|
+
* one. Off-label the test is INLINED rather than called, which is sound
|
|
767
|
+
* everywhere and costs nothing.
|
|
768
|
+
*
|
|
769
|
+
* THE PUSH ITSELF is two cross-module calls — `pushCstLeaf` to decide on
|
|
770
|
+
* `trackLines`, then `pushCstChild` to decide which collector is live — on
|
|
771
|
+
* the most-executed path there is. Both decisions are settled here:
|
|
772
|
+
* `trackLines` is `RunCfg`, and an open `_cstBuf` is the label. `_pushLeafBuf`
|
|
773
|
+
* is that pair with both branches taken, and it takes no piece.
|
|
774
|
+
*/
|
|
775
|
+
const pushLeaf = L.buf && !cfg.trackLines ? '_pushLeafBuf' : '_pushLeaf'
|
|
776
|
+
const captureLeaf = (value: string): string => {
|
|
777
|
+
const call = `${pushLeaf}(ctx,${value},pos,e)`
|
|
778
|
+
return L.buf ? call : `if(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)${call}`
|
|
779
|
+
}
|
|
780
|
+
switch (op) {
|
|
781
|
+
case OP_LIT:
|
|
782
|
+
case OP_LIT_TRACK: {
|
|
783
|
+
const s = k[code[ip + 1]!] as string
|
|
784
|
+
const xf = fxRef(code[ip + 2]!)
|
|
785
|
+
const track = op === OP_LIT_TRACK
|
|
786
|
+
// LENGTH IS TABLE DATA, so the compare is chosen here — the emitted twin
|
|
787
|
+
// of the closure engine's length-keyed literal bodies.
|
|
788
|
+
const test = s.length === 1
|
|
789
|
+
? `input.charCodeAt(pos)===${s.charCodeAt(0)}`
|
|
790
|
+
: s.length === 2
|
|
791
|
+
? `input.charCodeAt(pos)===${s.charCodeAt(0)}&&input.charCodeAt(pos+1)===${s.charCodeAt(1)}`
|
|
792
|
+
: `input.startsWith(${q(s)},pos)`
|
|
793
|
+
return `${head}
|
|
794
|
+
if(${test}){
|
|
795
|
+
const e=pos+${s.length}
|
|
796
|
+
${captureLeaf(q(s))}
|
|
797
|
+
${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
|
|
798
|
+
return ${q(s)}
|
|
799
|
+
}
|
|
800
|
+
ctx._fe=pos;ctx._fx=${xf}
|
|
801
|
+
${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
|
|
802
|
+
}`
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
case OP_LIT_CI:
|
|
806
|
+
case OP_LIT_CI_TRACK: {
|
|
807
|
+
const s = k[code[ip + 1]!] as string
|
|
808
|
+
const xf = fxRef(code[ip + 2]!)
|
|
809
|
+
const track = op === OP_LIT_CI_TRACK
|
|
810
|
+
// THE FOLD, UNROLLED AGAINST CONSTANTS.
|
|
811
|
+
//
|
|
812
|
+
// `assemble.ts:866-873` holds the folded literal in an ARRAY and walks it
|
|
813
|
+
// with a loop and a ternary per character — a load, a bounds check and a
|
|
814
|
+
// branch for a comparison whose right-hand side is known here. Emitted, the
|
|
815
|
+
// literal is not data at all: each character is one or two compares
|
|
816
|
+
// against integers, straight-line, no array and no closure.
|
|
817
|
+
//
|
|
818
|
+
// Semantics are `asciiFoldEq`'s exactly. `foldedLit[i]` is the literal
|
|
819
|
+
// folded, so it is never in A-Z; and `fold` maps A-Z INTO a-z and is the
|
|
820
|
+
// identity everywhere else. So for a folded character `c`:
|
|
821
|
+
// - `c` in a-z — the input matches iff it is `c` or `c - 32`
|
|
822
|
+
// - otherwise — the input matches iff it is exactly `c`, because the
|
|
823
|
+
// only characters fold moves land in a-z
|
|
824
|
+
// Past end of input `charCodeAt` is NaN, which compares unequal to both —
|
|
825
|
+
// the same answer the length test gave.
|
|
826
|
+
//
|
|
827
|
+
// The match VALUE is `input.slice(pos, e)`, the INPUT's casing and not the
|
|
828
|
+
// literal's, exactly as `literal.ts:86` has it — returning the literal
|
|
829
|
+
// would silently normalise case in any node built from it.
|
|
830
|
+
const tests: string[] = []
|
|
831
|
+
const reads: string[] = []
|
|
832
|
+
for (let i = 0; i < s.length; i++) {
|
|
833
|
+
const raw = s.charCodeAt(i)
|
|
834
|
+
const f = raw >= 65 && raw <= 90 ? raw + 32 : raw
|
|
835
|
+
const at = i === 0 ? 'pos' : `pos+${i}`
|
|
836
|
+
if (f >= 97 && f <= 122) {
|
|
837
|
+
reads.push(`const c${i}=input.charCodeAt(${at})`)
|
|
838
|
+
tests.push(`(c${i}===${f}||c${i}===${f - 32})`)
|
|
839
|
+
} else {
|
|
840
|
+
tests.push(`input.charCodeAt(${at})===${f}`)
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return `${head}
|
|
844
|
+
${reads.length > 0 ? `${reads.join('\n')}\n` : ''}if(${tests.join('&&')}){
|
|
845
|
+
const e=pos+${s.length}
|
|
846
|
+
const v=input.slice(pos,e)
|
|
847
|
+
${captureLeaf('v')}
|
|
848
|
+
${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
|
|
849
|
+
return v
|
|
850
|
+
}
|
|
851
|
+
ctx._fe=pos;ctx._fx=${xf}
|
|
852
|
+
${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
|
|
853
|
+
}`
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
case OP_RX:
|
|
857
|
+
case OP_RX_TRACK: {
|
|
858
|
+
const xf = fxRef(code[ip + 2]!)
|
|
859
|
+
const track = op === OP_RX_TRACK
|
|
860
|
+
// THE MATCH ARRAY IS THE COST, not the matching. `re.exec` allocates one
|
|
861
|
+
// per row — 6,005 rows per `json/document` parse, 12.9% of everything
|
|
862
|
+
// executed — and every field of it but `[0]` is discarded here. A shape
|
|
863
|
+
// that lowers replaces the call with a straight-line scan over
|
|
864
|
+
// `charCodeAt`, and the ranges are FOLDED INTO THE SOURCE by
|
|
865
|
+
// `classCond`/`litCond`/`foldEq` rather than read from a table at run
|
|
866
|
+
// time: emitted as a static body consulting `inRanges`, this would
|
|
867
|
+
// reproduce the per-character loop it exists to remove.
|
|
868
|
+
const scanned = emitScan(code[ip + 1]!, xf, track, captureLeaf)
|
|
869
|
+
if (scanned !== undefined) return `${head}${scanned}}`
|
|
870
|
+
// STICKINESS IS THE PRECONDITION OF BOTH FORMS BELOW, and it is checked
|
|
871
|
+
// rather than assumed. `encode.ts:588` appends `y` to every regex row it
|
|
872
|
+
// emits, so a non-sticky constant here would be a new encoder path — and
|
|
873
|
+
// `lastIndex=pos` would already have been meaningless for it, silently
|
|
874
|
+
// matching from wherever the last row left off.
|
|
875
|
+
const rxk = k[code[ip + 1]!]
|
|
876
|
+
if (!(rxk instanceof RegExp) || !rxk.sticky) throw new Unemittable('a non-sticky regex row')
|
|
877
|
+
// A ROW THAT DOES NOT LOWER STILL NEED NOT ALLOCATE. `test` and `exec`
|
|
878
|
+
// run the identical match; `exec` additionally materialises a
|
|
879
|
+
// JSRegExpResult, and V8's `test` fast path does not. For a STICKY
|
|
880
|
+
// regex `lastIndex` IS the match end on success (RegExpBuiltinExec
|
|
881
|
+
// step 15), so `input.slice(pos, lastIndex)` is `m[0]` character for
|
|
882
|
+
// character, and the array was the only thing dropped. Counted, not
|
|
883
|
+
// reasoned: 400×~2k sticky matches of json's unlowered string body take
|
|
884
|
+
// 99 scavenges through `exec` and 10 through `test`, reproduced.
|
|
885
|
+
const re = kRef(code[ip + 1]!)
|
|
886
|
+
return `${head}
|
|
887
|
+
${re}.lastIndex=pos
|
|
888
|
+
if(${re}.test(input)){
|
|
889
|
+
const e=${re}.lastIndex
|
|
890
|
+
const v=input.slice(pos,e)
|
|
891
|
+
${captureLeaf('v')}
|
|
892
|
+
${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
|
|
893
|
+
return v
|
|
894
|
+
}
|
|
895
|
+
ctx._fe=pos;ctx._fx=${xf}
|
|
896
|
+
${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
|
|
897
|
+
}`
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
case OP_EMPTY:
|
|
901
|
+
// `''`, not `null` — the zero-width match's value. The other three engines
|
|
902
|
+
// (exec.ts, exec-baseline.ts, assemble.ts) all return `''`; this one returned
|
|
903
|
+
// `null`, so the SAME program yielded a different `value` depending only on
|
|
904
|
+
// which engine ran. Unreached from the combinator API today — `OP_EMPTY` is
|
|
905
|
+
// emitted only as `finish()` padding for an EMPTY rule map, which then has no
|
|
906
|
+
// walk roots for the emitter to visit — but that is an accident of two
|
|
907
|
+
// unguarded facts, not an invariant, and it is already reachable through the
|
|
908
|
+
// hand-built-program idiom this file's driver tests use.
|
|
909
|
+
return `${head}EC.e=pos;return ''}`
|
|
910
|
+
|
|
911
|
+
case OP_GATE: {
|
|
912
|
+
const child = link(code[ip + 2]!)
|
|
913
|
+
const ci = classes.push([t.cc[code[ip + 1]!]!]) - 1
|
|
914
|
+
classPlan.push([code[ip + 1]!])
|
|
915
|
+
const cls = hoist('cl', `CLS[${ci}][0]`)
|
|
916
|
+
const xf = fxRef(code[ip + 3]!)
|
|
917
|
+
return `${head}
|
|
918
|
+
if(!classHas(${cls},lead(input,pos))){ctx._fe=pos;ctx._fx=${xf};return FAIL}
|
|
919
|
+
return ${child}(input,pos,ctx)
|
|
920
|
+
}`
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
case OP_XFORM: {
|
|
924
|
+
const fn = fnRef(code[ip + 1]!)
|
|
925
|
+
const child = link(code[ip + 2]!)
|
|
926
|
+
return `${head}
|
|
927
|
+
const v=${child}(input,pos,ctx)
|
|
928
|
+
if(v===FAIL)return FAIL
|
|
929
|
+
return ${fn}(v,{start:pos,end:EC.e})
|
|
930
|
+
}`
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
case OP_LABEL: {
|
|
934
|
+
const child = link(code[ip + 1]!)
|
|
935
|
+
const xf = fxRef(code[ip + 2]!)
|
|
936
|
+
// `_fe` IS NOT TOUCHED — `map.ts:84` keeps the child's span and replaces
|
|
937
|
+
// only the expected set. A label that moved the failure position would
|
|
938
|
+
// report the diagnostic at the label's start rather than where the input
|
|
939
|
+
// actually stopped.
|
|
940
|
+
return `${head}
|
|
941
|
+
const v=${child}(input,pos,ctx)
|
|
942
|
+
if(v===FAIL)ctx._fx=${xf}
|
|
943
|
+
return v
|
|
944
|
+
}`
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
case OP_FIELD: {
|
|
948
|
+
const name = q(k[code[ip + 1]!] as string)
|
|
949
|
+
const child = link(code[ip + 2]!)
|
|
950
|
+
// `ctx._fields?.push(…)` — conditional on a field-reading node being
|
|
951
|
+
// open, exactly as `map.ts` has it, so a field outside one costs a load
|
|
952
|
+
// and a branch. The ARRAY is what `OP_NODE` hands to `buildFieldMap`.
|
|
953
|
+
return `${head}
|
|
954
|
+
const v=${child}(input,pos,ctx)
|
|
955
|
+
if(v===FAIL)return FAIL
|
|
956
|
+
const f=ctx._fields
|
|
957
|
+
if(f!==undefined)f.push({name:${name},value:v,span:{start:pos,end:EC.e}})
|
|
958
|
+
return v
|
|
959
|
+
}`
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
case OP_EXPECT: {
|
|
963
|
+
const child = link(code[ip + 1]!)
|
|
964
|
+
const xf = fxRef(code[ip + 2]!)
|
|
965
|
+
// TWO ARMS, mirroring `assemble.ts:1050`. A TOLERANT `expect()` EMBEDS
|
|
966
|
+
// ITS ERROR IN THE TREE and not only in the flat `_errors` side-channel
|
|
967
|
+
// (`combinators/expect.ts:150`, codegen's `_ctx._rec.capture` at
|
|
968
|
+
// codegen.ts:4470) — so a tree walk finds every diagnostic and the node
|
|
969
|
+
// survives incremental subtree reuse. `captureError` is the shared
|
|
970
|
+
// function, called; the embedding is not restated here.
|
|
971
|
+
//
|
|
972
|
+
// `_fc` IS CLEARED IN BOTH: a recovered failure is no longer a failure,
|
|
973
|
+
// and the commit bit the child raised must not survive it and cut an
|
|
974
|
+
// enclosing choice (`assemble.ts:1058-1066`).
|
|
975
|
+
return `${head}
|
|
976
|
+
const v=${child}(input,pos,ctx)
|
|
977
|
+
if(v!==FAIL)return v
|
|
978
|
+
const err={_tag:'parseError',span:${prog.lines === 1 ? 'spanLines(ctx,pos,pos)' : '{start:pos,end:pos}'},expected:${xf}}
|
|
979
|
+
const es=ctx._errors
|
|
980
|
+
if(es!==undefined)es.push(err)
|
|
981
|
+
${REC ? 'if(ctx._tolerant===true)captureError(ctx,err)\n' : ''}ctx._fc=false
|
|
982
|
+
EC.e=pos
|
|
983
|
+
return err
|
|
984
|
+
}`
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
case OP_ROUTED: {
|
|
988
|
+
const fb = code[ip + 1]!
|
|
989
|
+
const fallback = fb >= 0 ? link(fb) : undefined
|
|
990
|
+
// The SPAN is the routed item's own object, not a copy — `assemble.ts`
|
|
991
|
+
// hands the same one to the leaf, and a fresh `{start,end}` here would
|
|
992
|
+
// be a different tree for any consumer comparing by identity.
|
|
993
|
+
const push = `pushCstLeaf(ctx,{_tag:'leaf',value:it.value,span:it.span})`
|
|
994
|
+
return `${head}
|
|
995
|
+
const it=ctx._routed
|
|
996
|
+
if(it===undefined||pos!==it.span.start){
|
|
997
|
+
${fallback !== undefined ? `return ${fallback}(input,pos,ctx)` : 'ctx._fe=pos;ctx._fx=ROUTED_FX;return FAIL'}
|
|
998
|
+
}
|
|
999
|
+
${L.buf ? push : `if(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)${push}`}
|
|
1000
|
+
EC.e=it.span.end
|
|
1001
|
+
return it.value
|
|
1002
|
+
}`
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
case OP_SCAN: {
|
|
1006
|
+
const si = code[ip + 1]!
|
|
1007
|
+
// The scan pool is built FROM subtrees, so it is populated after this
|
|
1008
|
+
// text compiles. The array is passed by reference and indexed on
|
|
1009
|
+
// execution, which is what the closure engine's lazy bind amounts to.
|
|
1010
|
+
return `${head}
|
|
1011
|
+
const r=SCANS[${si}].parse(input,pos,ctx)
|
|
1012
|
+
if(!r.ok){ctx._fe=r.span.start;ctx._fx=r.expected??EMPTY_FX;return FAIL}
|
|
1013
|
+
EC.e=r.span.end
|
|
1014
|
+
return r.value
|
|
1015
|
+
}`
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
case OP_SCOPE:
|
|
1019
|
+
case OP_SCOPE_CAP: {
|
|
1020
|
+
const ki = code[ip + 1]!
|
|
1021
|
+
const cap = op === OP_SCOPE_CAP
|
|
1022
|
+
const flags = code[ip + 3]!
|
|
1023
|
+
const child = link(code[ip + 2]!)
|
|
1024
|
+
// THE SWAP, RESOLVED AT EMIT. `swapLegal` is `!trackLines`, an option;
|
|
1025
|
+
// the other two are table data. All three are known here, so the body
|
|
1026
|
+
// holds the scanner it installs as a hoisted const.
|
|
1027
|
+
const scanFor = swapLegal && ki >= 0 && !triviaLabelled[ki]! ? hoist('ts', `TRIVIASCAN[${ki}]`) : 'null'
|
|
1028
|
+
const tri = ki < 0 ? 'undefined' : hoist('tv', `TRIVIA[${ki}]`)
|
|
1029
|
+
const lab = ki < 0 ? 'undefined' : hoist('tl', `TRIVIA[${ki}]._meta.triviaKindLabels`)
|
|
1030
|
+
// The two root-capture policies are TABLE DATA, so they are emitted INTO
|
|
1031
|
+
// the body. `scopeRootPolicy`'s two wrapper closures and their two extra
|
|
1032
|
+
// call frames do not exist here.
|
|
1033
|
+
const strict = (flags & 2) !== 0 ? 'refuseUnclassifiedRootScope(ctx._rootTriviaStrictScopes)\n' : ''
|
|
1034
|
+
const rootCap = (flags & 1) !== 0
|
|
1035
|
+
return `${head}
|
|
1036
|
+
${strict}${rootCap ? 'const sR=ctx._rootTriviaCapture\nctx._rootTriviaCapture=false\n' : ''}const sT=ctx.trivia,sL=ctx.triviaKindLabels,sS=_pfScan${cap ? ',sC=ctx.captureTrivia' : ''}
|
|
1037
|
+
_pfScan=${scanFor}
|
|
1038
|
+
ctx.trivia=${tri}
|
|
1039
|
+
ctx.triviaKindLabels=${lab}
|
|
1040
|
+
${cap ? 'ctx.captureTrivia=true\n' : ''}const v=${child}(input,pos,ctx)
|
|
1041
|
+
${cap ? 'ctx.captureTrivia=sC\n' : ''}ctx.trivia=sT
|
|
1042
|
+
ctx.triviaKindLabels=sL
|
|
1043
|
+
_pfScan=sS
|
|
1044
|
+
${rootCap ? 'ctx._rootTriviaCapture=sR\n' : ''}return v
|
|
1045
|
+
}`
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/* ── boundaries ──────────────────────────────────────────────────────── */
|
|
1049
|
+
|
|
1050
|
+
case OP_TOKEN:
|
|
1051
|
+
case OP_LEAF: {
|
|
1052
|
+
const isToken = op === OP_TOKEN
|
|
1053
|
+
const fn = isToken ? undefined : fnRef(code[ip + 1]!)
|
|
1054
|
+
const child = link(isToken ? code[ip + 1]! : code[ip + 2]!)
|
|
1055
|
+
// `try`/`finally`, AS `assemble.ts` HAS IT. `OP_SCOPE` restores linearly
|
|
1056
|
+
// and this does not, and the difference is not an oversight in either:
|
|
1057
|
+
// `test/unit/token.test.ts:198` pins CONTEXT RESTORATION ON A THROWING
|
|
1058
|
+
// BODY across the interpreter and `compile`, and jess's reducers
|
|
1059
|
+
// throw on purpose — that is how its dialects reject illegal constructs
|
|
1060
|
+
// (`bench/jess/digest.ts:64`). A boundary that leaked `ctx.trivia` and
|
|
1061
|
+
// five capture sinks on the way out would corrupt every subsequent parse
|
|
1062
|
+
// through the same context, so the handler IS the semantics here.
|
|
1063
|
+
//
|
|
1064
|
+
// `cstCaptureActive` IS THE SAVED STATE, read BEFORE the sinks are
|
|
1065
|
+
// cleared — the leaf this body contributes goes to the OUTER collector.
|
|
1066
|
+
// Inside a node the answer is the label's, and the call disappears.
|
|
1067
|
+
const wasCap = L.buf ? 'true' : '(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)'
|
|
1068
|
+
// `token()` additionally clears the TRIVIA scope and the root trivia
|
|
1069
|
+
// log: its whole point is that the child sees no ambient skipping, so
|
|
1070
|
+
// the match is one contiguous run of input (`ops.ts:180-190`).
|
|
1071
|
+
return `${head}
|
|
1072
|
+
const sBuf=ctx._cstBuf,sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
|
|
1073
|
+
const sOtl=ctx._triviaLog
|
|
1074
|
+
${isToken ? 'const sTri=ctx.trivia,sKinds=ctx.triviaKindLabels,sScan=_pfScan,sRtl=ctx._rootTriviaLog\n' : ''}const wasCap=${wasCap}
|
|
1075
|
+
${isToken ? `_pfScan=null
|
|
1076
|
+
ctx.trivia=undefined
|
|
1077
|
+
ctx.triviaKindLabels=undefined
|
|
1078
|
+
` : ''}ctx._cstBuf=undefined
|
|
1079
|
+
ctx._cstChildren=undefined
|
|
1080
|
+
ctx._cstLeaves=undefined
|
|
1081
|
+
ctx._cstRawChildren=undefined
|
|
1082
|
+
ctx._cstTriviaLog=undefined
|
|
1083
|
+
ctx._triviaLog=undefined
|
|
1084
|
+
${isToken ? 'ctx._rootTriviaLog=undefined\n' : ''}let v
|
|
1085
|
+
try{v=${child}(input,pos,ctx)}finally{
|
|
1086
|
+
${isToken ? `_pfScan=sScan
|
|
1087
|
+
ctx.trivia=sTri
|
|
1088
|
+
ctx.triviaKindLabels=sKinds
|
|
1089
|
+
ctx._rootTriviaLog=sRtl
|
|
1090
|
+
` : ''}ctx._cstBuf=sBuf
|
|
1091
|
+
ctx._cstChildren=sCh
|
|
1092
|
+
ctx._cstLeaves=sLv
|
|
1093
|
+
ctx._cstRawChildren=sRaw
|
|
1094
|
+
ctx._cstTriviaLog=sTl
|
|
1095
|
+
ctx._triviaLog=sOtl
|
|
1096
|
+
}
|
|
1097
|
+
if(v===FAIL)return FAIL
|
|
1098
|
+
const e=EC.e
|
|
1099
|
+
const out=${isToken ? 'input.slice(pos,e)' : `${fn}(v,{start:pos,end:e})`}
|
|
1100
|
+
if(wasCap)pushCstLeaf(ctx,{_tag:'leaf',value:out,span:{start:pos,end:e}})
|
|
1101
|
+
EC.e=e
|
|
1102
|
+
return out
|
|
1103
|
+
}`
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/* ── transaction ─────────────────────────────────────────────────────── */
|
|
1107
|
+
|
|
1108
|
+
case OP_ATTEMPT: {
|
|
1109
|
+
const child = link(code[ip + 1]!)
|
|
1110
|
+
const p = tmp()
|
|
1111
|
+
return `${head}
|
|
1112
|
+
${emitMark(p, L.buf, sinks)}
|
|
1113
|
+
const v=${child}(input,pos,ctx)
|
|
1114
|
+
if(v!==FAIL)return v
|
|
1115
|
+
${emitRollback(p, L.buf, sinks)}
|
|
1116
|
+
if(ctx._fc===true)return FAIL
|
|
1117
|
+
ctx._fe=pos
|
|
1118
|
+
return FAIL
|
|
1119
|
+
}`
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
case OP_NOT: {
|
|
1123
|
+
const child = link(code[ip + 1]!)
|
|
1124
|
+
const xf = fxRef(code[ip + 2]!)
|
|
1125
|
+
const p = tmp()
|
|
1126
|
+
return `${head}
|
|
1127
|
+
${emitMark(p, L.buf, sinks)}
|
|
1128
|
+
const v=${child}(input,pos,ctx)
|
|
1129
|
+
${emitRollback(p, L.buf, sinks)}
|
|
1130
|
+
if(v===FAIL){EC.e=pos;return null}
|
|
1131
|
+
ctx._fe=pos
|
|
1132
|
+
ctx._fx=${xf}
|
|
1133
|
+
return FAIL
|
|
1134
|
+
}`
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
case OP_PEEK: {
|
|
1138
|
+
const child = link(code[ip + 1]!)
|
|
1139
|
+
const xf = fxRef(code[ip + 2]!)
|
|
1140
|
+
const p = tmp()
|
|
1141
|
+
return `${head}
|
|
1142
|
+
${emitMark(p, L.buf, sinks)}
|
|
1143
|
+
const v=${child}(input,pos,ctx)
|
|
1144
|
+
${emitRollback(p, L.buf, sinks)}
|
|
1145
|
+
if(v===FAIL){ctx._fe=pos;ctx._fx=${xf};return FAIL}
|
|
1146
|
+
EC.e=pos
|
|
1147
|
+
return null
|
|
1148
|
+
}`
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
case OP_OPT: {
|
|
1152
|
+
const child = link(code[ip + 1]!)
|
|
1153
|
+
const p = tmp()
|
|
1154
|
+
return `${head}
|
|
1155
|
+
${emitMark(p, L.buf, sinks)}
|
|
1156
|
+
ctx._fc=false
|
|
1157
|
+
const v=${child}(input,pos,ctx)
|
|
1158
|
+
if(v===FAIL){
|
|
1159
|
+
if(ctx._fc===true)return FAIL
|
|
1160
|
+
${emitRollback(p, L.buf, sinks)}
|
|
1161
|
+
EC.e=pos
|
|
1162
|
+
return null
|
|
1163
|
+
}
|
|
1164
|
+
return v
|
|
1165
|
+
}`
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
case OP_SEQ:
|
|
1169
|
+
case OP_SEQV:
|
|
1170
|
+
case OP_SEQX: {
|
|
1171
|
+
const fused = op === OP_SEQX
|
|
1172
|
+
const base = fused ? ip + 3 : ip + 2
|
|
1173
|
+
const n = code[fused ? ip + 2 : ip + 1]!
|
|
1174
|
+
const wantValues = op !== OP_SEQV
|
|
1175
|
+
// ADJACENCY changes the PARENT's term-boundary emission: the assertion
|
|
1176
|
+
// must be answered at the cursor, BEFORE the ambient trivia scan.
|
|
1177
|
+
// Refused rather than lowered wrong — a piece handed the post-scan
|
|
1178
|
+
// position finds the gap already consumed and answers "adjacent" every
|
|
1179
|
+
// time, SILENTLY (`ops.ts:226-251`).
|
|
1180
|
+
for (let i = 1; i < n; i++) {
|
|
1181
|
+
if (code[code[base + i]!] === OP_ADJ) {
|
|
1182
|
+
throw new Unemittable('a sequence carrying an adjacency assertion (OP_ADJ)')
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const fn = fused ? fnRef(code[ip + 1]!) : undefined
|
|
1186
|
+
const kids: string[] = []
|
|
1187
|
+
for (let i = 0; i < n; i++) kids.push(link(code[base + i]!))
|
|
1188
|
+
/**
|
|
1189
|
+
* A RECOVERY SEQUENCE PUBLISHES `ctx._sync`, and that is the WHOLE of
|
|
1190
|
+
* "recovery config" — no grammar carries any (`assemble.ts:1446`).
|
|
1191
|
+
*
|
|
1192
|
+
* Before term `i`, the sentinel for the union of every LATER term's first
|
|
1193
|
+
* set becomes the sync point a list nested in that term resyncs to, so a
|
|
1194
|
+
* `sepBy` inside `sequence('{', list, '}')` finds `}` with nothing
|
|
1195
|
+
* annotated. Where there is no usable local follow — the last term, an
|
|
1196
|
+
* `any` first set — the INHERITED sentinel stays published, which is how
|
|
1197
|
+
* an enclosing delimiter reaches across a rule boundary. `sentRef` emits
|
|
1198
|
+
* the literal `undefined` for that case, so it costs no array read.
|
|
1199
|
+
*
|
|
1200
|
+
* THE `n` SYNC OPERANDS SIT AFTER THE `n` CHILD OPERANDS, laid down by
|
|
1201
|
+
* `encodeTable({ recovery })` — the same operands `assemble.ts:1468`
|
|
1202
|
+
* reads, at the same offsets.
|
|
1203
|
+
*
|
|
1204
|
+
* RESTORED IN A `finally`, matching `sequence()`'s own
|
|
1205
|
+
* (`combinators/sequence.ts:105`) and the closure engine's. `emitTerm`
|
|
1206
|
+
* emits `return FAIL` INLINE at several points, so a restore placed on
|
|
1207
|
+
* the fall-through paths alone would leak a stale sentinel out of exactly
|
|
1208
|
+
* the failure the recovery path is about to read. `finally` is the only
|
|
1209
|
+
* placement that covers an inlined return, and it is on the tolerant
|
|
1210
|
+
* assembly only — the strict one emits none of this.
|
|
1211
|
+
*/
|
|
1212
|
+
const sy = REC ? `${tmp()}sy` : ''
|
|
1213
|
+
const pub = (i: number): string => {
|
|
1214
|
+
const s = sentRef(code[base + n + i]!)
|
|
1215
|
+
return s === 'undefined' ? `ctx._sync=${sy}` : `ctx._sync=${s}??${sy}`
|
|
1216
|
+
}
|
|
1217
|
+
const parts: string[] = [head]
|
|
1218
|
+
if (REC) parts.push(`const ${sy}=ctx._sync`, 'try{', pub(0))
|
|
1219
|
+
parts.push(`const v0=${kids[0]}(input,pos,ctx)`, 'if(v0===FAIL)return FAIL')
|
|
1220
|
+
const close = (): string => REC ? `${parts.join('\n')}\n}finally{ctx._sync=${sy}}\n}` : `${parts.join('\n')}\n}`
|
|
1221
|
+
if (n === 1) {
|
|
1222
|
+
parts.push(fused ? `return ${fn}([v0],{start:pos,end:EC.e})` : wantValues ? 'return [v0]' : 'return undefined')
|
|
1223
|
+
return close()
|
|
1224
|
+
}
|
|
1225
|
+
parts.push('let cur=EC.e')
|
|
1226
|
+
const names: string[] = ['v0']
|
|
1227
|
+
for (let i = 1; i < n; i++) {
|
|
1228
|
+
const vn = `v${i}`
|
|
1229
|
+
parts.push(`let ${vn}`)
|
|
1230
|
+
if (REC) parts.push(pub(i))
|
|
1231
|
+
parts.push(emitTerm(kids[i]!, vn, tmp(), L, skipFor(L), sinks))
|
|
1232
|
+
names.push(vn)
|
|
1233
|
+
}
|
|
1234
|
+
parts.push('EC.e=cur')
|
|
1235
|
+
parts.push(fused
|
|
1236
|
+
? `return ${fn}([${names.join(',')}],{start:pos,end:cur})`
|
|
1237
|
+
: wantValues ? `return [${names.join(',')}]` : 'return undefined')
|
|
1238
|
+
return close()
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
case OP_CHOICE: {
|
|
1242
|
+
const table = disp[code[ip + 1]!]!
|
|
1243
|
+
const n = code[ip + 2]!
|
|
1244
|
+
const choiceFx = fxRef(code[ip + 3]!)
|
|
1245
|
+
const base = ip + 4
|
|
1246
|
+
const arms: string[] = []
|
|
1247
|
+
for (let i = 0; i < n; i++) arms.push(link(code[base + i]!))
|
|
1248
|
+
const axi = armExpected.push(
|
|
1249
|
+
Array.from({ length: n }, (_, i) => t.fx[code[base + n + i]!] as readonly string[]),
|
|
1250
|
+
) - 1
|
|
1251
|
+
armExpectedPlan.push(Array.from({ length: n }, (_, i) => code[base + n + i]!))
|
|
1252
|
+
const afx = hoist('afx', `AFX[${axi}]`)
|
|
1253
|
+
|
|
1254
|
+
if (table.exclusive) {
|
|
1255
|
+
// NO OPEN ARMS EXIST under `exclusive` — `resolveDispatch` clears the
|
|
1256
|
+
// flag for any arm whose class is −1 — so there is no fallback loop.
|
|
1257
|
+
const di = code[ip + 1]!
|
|
1258
|
+
const asc = hoist('as', `DISP[${di}].ascii`)
|
|
1259
|
+
const hiArr = table.hi
|
|
1260
|
+
// Arms named, one `case` each: the interpreter's `arms[arm](…)` is an
|
|
1261
|
+
// array index feeding ONE call site, which is the megamorphic site
|
|
1262
|
+
// this unit exists to remove.
|
|
1263
|
+
const armSwitch = arms.map((a, i) => `case ${i}:v=${a}(input,pos,ctx);break`).join('\n')
|
|
1264
|
+
return `${head}
|
|
1265
|
+
const c=lead(input,pos)
|
|
1266
|
+
let arm=-1
|
|
1267
|
+
if(c>=0&&c<128){const a=${asc}[c];if(a!==0)arm=a-1}
|
|
1268
|
+
${hiArr.length === 0 ? '' : `else if(c>=128){const h=${hoist('hi', `DISP[${di}].hi`)}
|
|
1269
|
+
for(let i=0;i<h.length;i+=3){if(c>=h[i]&&c<=h[i+1]){arm=h[i+2];break}}}`}
|
|
1270
|
+
if(arm>=0){
|
|
1271
|
+
ctx._fc=false
|
|
1272
|
+
let v
|
|
1273
|
+
switch(arm){
|
|
1274
|
+
${armSwitch}
|
|
1275
|
+
}
|
|
1276
|
+
if(v!==FAIL)return v
|
|
1277
|
+
if(ctx._fc===true)return FAIL
|
|
1278
|
+
const af=ctx._fx
|
|
1279
|
+
if(af!==undefined&&af.length>0){ctx._fe=pos;return FAIL}
|
|
1280
|
+
}
|
|
1281
|
+
ctx._fe=pos;ctx._fx=${choiceFx}
|
|
1282
|
+
return FAIL
|
|
1283
|
+
}`
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// THE PER-ARM GATE. `assemble.ts` precomputes a candidate bitmask so one
|
|
1287
|
+
// `Uint32Array` load replaces `n` class tests, then walks the set bits
|
|
1288
|
+
// through `arms[i]`. The mask survives here; the indexed CALL does not.
|
|
1289
|
+
const ci = classes.push(Array.from({ length: n }, (_, i) => table.armCls[i] ?? null)) - 1
|
|
1290
|
+
// `resolveDispatch` builds `armCls[a]` as `cc[disp[di][a]]`, or null when
|
|
1291
|
+
// that index is negative (program.ts:419-423), so the arm's CLASS INDEX
|
|
1292
|
+
// is the dispatch row itself. Nothing is re-derived here.
|
|
1293
|
+
classPlan.push(Array.from({ length: n }, (_, i) => prog.disp[code[ip + 1]!]![i] ?? -1))
|
|
1294
|
+
const gates = classes[ci]!
|
|
1295
|
+
const gRef = hoist('g', `CLS[${ci}]`)
|
|
1296
|
+
const maskable = n <= 32
|
|
1297
|
+
let maskName = ''
|
|
1298
|
+
if (maskable) {
|
|
1299
|
+
maskName = hoist('mk', `MASK[${masks.push(maskForClassRow(gates)) - 1}]`)
|
|
1300
|
+
// The mask is a pure function of `gates`, i.e. of `CLS[ci]`, so the
|
|
1301
|
+
// plan records the class-pool index rather than 129 words per site.
|
|
1302
|
+
maskPlan.push(ci)
|
|
1303
|
+
}
|
|
1304
|
+
const p = tmp()
|
|
1305
|
+
// THE SKIPPED ARMS' SETS STAY LAZY. `prev` and the catch-up loop are the
|
|
1306
|
+
// closure engine's, kept verbatim: they run only on a FAILURE path, and
|
|
1307
|
+
// an arm that MATCHES returns without paying for any of it. Unrolling
|
|
1308
|
+
// them would put an `_accSet` — and its `slice()` — on the success path.
|
|
1309
|
+
const maskArms = arms.map((a, i) => `
|
|
1310
|
+
if((bits&${1 << i})!==0){
|
|
1311
|
+
ctx._fc=false
|
|
1312
|
+
{const v=${a}(input,pos,ctx)
|
|
1313
|
+
if(v!==FAIL)return v}
|
|
1314
|
+
for(let j=prev;j<${i};j++)if((bits&(1<<j))===0)acc=_accSet(${afx}[j],acc)
|
|
1315
|
+
prev=${i + 1}
|
|
1316
|
+
acc=_accSet(ctx._fx,acc)
|
|
1317
|
+
if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
|
|
1318
|
+
${emitRollback(p, L.buf, sinks)}
|
|
1319
|
+
}`).join('')
|
|
1320
|
+
|
|
1321
|
+
const generalArms = arms.map((a, i) => `
|
|
1322
|
+
if(${gRef}[${i}]===null||classHas(${gRef}[${i}],c)){
|
|
1323
|
+
ctx._fc=false
|
|
1324
|
+
{const v=${a}(input,pos,ctx)
|
|
1325
|
+
if(v!==FAIL)return v}
|
|
1326
|
+
acc=_accSet(ctx._fx,acc)
|
|
1327
|
+
if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
|
|
1328
|
+
${emitRollback(p, L.buf, sinks)}
|
|
1329
|
+
}else acc=_accSet(${afx}[${i}],acc)`).join('')
|
|
1330
|
+
|
|
1331
|
+
return `${head}
|
|
1332
|
+
const c=lead(input,pos)
|
|
1333
|
+
${emitMark(p, L.buf, sinks)}
|
|
1334
|
+
let acc
|
|
1335
|
+
${maskable ? `if(c<128){
|
|
1336
|
+
const bits=${maskName}[c<0?128:c]
|
|
1337
|
+
let prev=0
|
|
1338
|
+
${maskArms}
|
|
1339
|
+
for(let j=prev;j<${n};j++)if((bits&(1<<j))===0)acc=_accSet(${afx}[j],acc)
|
|
1340
|
+
ctx._fe=pos;ctx._fx=acc??${choiceFx}
|
|
1341
|
+
return FAIL
|
|
1342
|
+
}
|
|
1343
|
+
` : ''}${generalArms}
|
|
1344
|
+
ctx._fe=pos;ctx._fx=acc??${choiceFx}
|
|
1345
|
+
return FAIL
|
|
1346
|
+
}`
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
case OP_DISPATCH: {
|
|
1350
|
+
const di = code[ip + 2]!
|
|
1351
|
+
const spec = dsp[di]!
|
|
1352
|
+
const selector = link(code[ip + 1]!)
|
|
1353
|
+
const otherIp = code[ip + 3]!
|
|
1354
|
+
const other = otherIp >= 0 ? link(otherIp) : undefined
|
|
1355
|
+
const otherRouted = code[ip + 4]! === 1
|
|
1356
|
+
const n = code[ip + 5]!
|
|
1357
|
+
const armBase = ip + 6
|
|
1358
|
+
const arms: string[] = []
|
|
1359
|
+
for (let i = 0; i < n; i++) arms.push(link(code[armBase + i]!))
|
|
1360
|
+
const bk = hoist('bk', `DSP[${di}].byKey`)
|
|
1361
|
+
const rt = hoist('rt', `DSP[${di}].routed`)
|
|
1362
|
+
const dx = hoist('dx', `DSP[${di}].expected`)
|
|
1363
|
+
// THE MATCHER ARMS, AS SOURCE. `exec.ts`'s `linkMatcher` mints one
|
|
1364
|
+
// closure per arm from four literals it already has in hand; the four
|
|
1365
|
+
// are TABLE DATA, so here they are the test itself and no closure, no
|
|
1366
|
+
// pool and no indexed call exist. Kinds are `matcherClaims`'s exactly
|
|
1367
|
+
// — 0/1 raw prefix/suffix, 3/4 the pre-folded pair, anything else a
|
|
1368
|
+
// regex. The regex is built PER TEST, deliberately: a hoisted one
|
|
1369
|
+
// carries `lastIndex` across parses whenever the author's pattern is
|
|
1370
|
+
// sticky or global (`exec.ts:140-144`).
|
|
1371
|
+
const claims = (m: readonly [number, string, string, number]): string => {
|
|
1372
|
+
switch (m[0]) {
|
|
1373
|
+
case 0: return `key.startsWith(${q(m[1])})`
|
|
1374
|
+
case 1: return `key.endsWith(${q(m[1])})`
|
|
1375
|
+
case 3: return `asciiFoldKey(key).startsWith(${q(m[1])})`
|
|
1376
|
+
case 4: return `asciiFoldKey(key).endsWith(${q(m[1])})`
|
|
1377
|
+
default: return `new RegExp(${q(m[1])},${q(m[2])}).test(key)`
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const chain = spec.match.length === 0
|
|
1381
|
+
? ''
|
|
1382
|
+
: `if(arm===undefined){\n${spec.match.map((m, i) => `${i === 0 ? '' : 'else '}if(${claims(m)})arm=${m[3]}`).join('\n')}\n}\n`
|
|
1383
|
+
const fold = spec.byFold.size > 0
|
|
1384
|
+
? `if(arm===undefined)arm=${hoist('bf', `DSP[${di}].byFold`)}.get(asciiFoldKey(key))\n`
|
|
1385
|
+
: ''
|
|
1386
|
+
const m1 = tmp()
|
|
1387
|
+
const m2 = tmp()
|
|
1388
|
+
// THE SELECTOR RUNS ONCE and the key it returns picks the arm — that is
|
|
1389
|
+
// what `dispatch()` buys over a choice of arms that each re-parse the
|
|
1390
|
+
// opener. A routed arm rewinds the selector's trivia capture and gets
|
|
1391
|
+
// the token handed to it (`OP_ROUTED`) instead of re-matching it.
|
|
1392
|
+
return `${head}
|
|
1393
|
+
${emitMark(m1, L.buf, sinks)}
|
|
1394
|
+
const sv=${selector}(input,pos,ctx)
|
|
1395
|
+
if(sv===FAIL)return FAIL
|
|
1396
|
+
const selEnd=EC.e
|
|
1397
|
+
const key=sv
|
|
1398
|
+
let arm=${bk}.get(key)
|
|
1399
|
+
${fold}${chain}let ur
|
|
1400
|
+
if(arm===undefined){
|
|
1401
|
+
${other === undefined ? `ctx._fe=selEnd;ctx._fx=${dx};return FAIL` : `ur=${String(otherRouted)}`}
|
|
1402
|
+
}else ur=${rt}[arm]===1
|
|
1403
|
+
const savedRouted=ctx._routed
|
|
1404
|
+
${emitMark(m2, L.buf, sinks)}
|
|
1405
|
+
if(ur){
|
|
1406
|
+
${emitRollback(m1, L.buf, sinks)}
|
|
1407
|
+
${emitMark(m2, L.buf, sinks, false)}
|
|
1408
|
+
ctx._routed={value:key,span:{start:pos,end:selEnd}}
|
|
1409
|
+
}
|
|
1410
|
+
const at=ur?pos:selEnd
|
|
1411
|
+
let v
|
|
1412
|
+
switch(arm){
|
|
1413
|
+
${arms.map((a, i) => `case ${i}:v=${a}(input,at,ctx);break`).join('\n')}
|
|
1414
|
+
${other === undefined ? '' : `default:v=${other}(input,at,ctx)`}
|
|
1415
|
+
}
|
|
1416
|
+
if(ur)ctx._routed=savedRouted
|
|
1417
|
+
if(v===FAIL){
|
|
1418
|
+
${emitRollback(m2, L.buf, sinks)}
|
|
1419
|
+
ctx._fc=true
|
|
1420
|
+
return FAIL
|
|
1421
|
+
}
|
|
1422
|
+
return [key,v]
|
|
1423
|
+
}`
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
case OP_REP:
|
|
1427
|
+
case OP_REPV: {
|
|
1428
|
+
const child = link(code[ip + 1]!)
|
|
1429
|
+
const min = code[ip + 2]!
|
|
1430
|
+
const max = code[ip + 3]!
|
|
1431
|
+
const sepIp = code[ip + 4]!
|
|
1432
|
+
const sep = sepIp >= 0 ? link(sepIp) : undefined
|
|
1433
|
+
const flags = code[ip + 5]!
|
|
1434
|
+
const keepSeparators = (flags & 2) !== 0
|
|
1435
|
+
const trailingAllowed = (flags & 1) !== 0
|
|
1436
|
+
const reportItem = (flags & 4) !== 0
|
|
1437
|
+
const itemFx = reportItem ? fxRef(code[ip + 6]!) : 'EMPTY_FX'
|
|
1438
|
+
const collect = op === OP_REP
|
|
1439
|
+
const skipBeforeFirst = sepIp < 0 && min === 0
|
|
1440
|
+
const p = tmp()
|
|
1441
|
+
// `viaRepItem` is `sep === undefined && count >= min && (count > 0 ||
|
|
1442
|
+
// skipBeforeFirst)`. Two of its three conjuncts are table data, so only
|
|
1443
|
+
// what survives them is emitted.
|
|
1444
|
+
const via = sep !== undefined
|
|
1445
|
+
? 'false'
|
|
1446
|
+
: min === 0
|
|
1447
|
+
? (skipBeforeFirst ? 'true' : 'count>0')
|
|
1448
|
+
: `count>=${min}&&count>0`
|
|
1449
|
+
const leadTrivia = skipBeforeFirst ? 'true' : 'count>0'
|
|
1450
|
+
// THE TWO LOOP-INVARIANT HOISTS, RESOLVED AT EMIT WHERE THE LABEL ANSWERS
|
|
1451
|
+
// THEM. `hasTrivia` is the site's trivia scope; `needMark` is implied by
|
|
1452
|
+
// an open `_cstBuf`. Both were read once per repetition SITE, which is
|
|
1453
|
+
// cheap on its own and is not the point — what they gated was a branch
|
|
1454
|
+
// per item and a branch at every one of the five rollback points below.
|
|
1455
|
+
const skip = skipFor(L)
|
|
1456
|
+
const knownTrivia = L.tri === TRI_NONE ? false : L.tri !== TRI_UNKNOWN ? true : undefined
|
|
1457
|
+
const hasTrivia = knownTrivia === undefined ? 'hasTrivia' : String(knownTrivia)
|
|
1458
|
+
const needMark = L.buf ? 'true' : 'needMark'
|
|
1459
|
+
// THE `_fields` AND `_errors` MARKS ARE TAKEN ONLY WHERE THE TABLE HAS A
|
|
1460
|
+
// WRITER, which is the same emit-time census `emitMark` applies to the
|
|
1461
|
+
// non-loop marks. Before `OP_FIELD` and `OP_EXPECT` were emittable the
|
|
1462
|
+
// answer was "never"; it is now "per table", and a grammar with no field
|
|
1463
|
+
// still pays neither the two loads nor the two stores per item.
|
|
1464
|
+
const pfd = sinks.fd ? `${p}fd` : '0'
|
|
1465
|
+
const per = sinks.er ? `${p}er` : 'undefined'
|
|
1466
|
+
const rb = L.buf
|
|
1467
|
+
? `_rbBuf(ctx,${p}raw,${p}tl,${p}lv,${p}lg,${p}rt)${sinks.fd ? `\nif(ctx._fields!==undefined&&ctx._fields.length!==${p}fd)ctx._fields.length=${p}fd` : ''}${sinks.er ? `\nif(ctx._errors!==undefined&&ctx._errors.length!==${p}er)ctx._errors.length=${p}er` : ''}`
|
|
1468
|
+
: `if(needMark)rollbackTriviaAt(ctx,${p}raw,${p}tl,${p}lv,${pfd},${per},${p}lg,${p}rt)`
|
|
1469
|
+
const markSinks = `${sinks.fd ? `\n${p}fd=ctx._fields!==undefined?ctx._fields.length:0` : ''}${sinks.er ? `\n${p}er=ctx._errors!==undefined?ctx._errors.length:0` : ''}`
|
|
1470
|
+
const markBody = L.buf
|
|
1471
|
+
? `const b=ctx._cstBuf
|
|
1472
|
+
const r=b.raw;${p}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
|
|
1473
|
+
const h=b.ch;${p}lv=h!==undefined?h.length:b.single!==undefined?1:0
|
|
1474
|
+
const l=b.tl;${p}tl=l!==undefined?l.length:0
|
|
1475
|
+
${p}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
1476
|
+
${p}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${markSinks}`
|
|
1477
|
+
: `if(needMark){
|
|
1478
|
+
const b=ctx._cstBuf
|
|
1479
|
+
if(b!==undefined){
|
|
1480
|
+
const r=b.raw;${p}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
|
|
1481
|
+
const h=b.ch;${p}lv=h!==undefined?h.length:b.single!==undefined?1:0
|
|
1482
|
+
const l=b.tl;${p}tl=l!==undefined?l.length:0
|
|
1483
|
+
}else{
|
|
1484
|
+
${p}raw=ctx._cstRawChildren!==undefined?ctx._cstRawChildren.length:0
|
|
1485
|
+
${p}tl=ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0
|
|
1486
|
+
${p}lv=ctx._cstLeaves!==undefined?ctx._cstLeaves.length:0
|
|
1487
|
+
}
|
|
1488
|
+
${p}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
1489
|
+
${p}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${markSinks}
|
|
1490
|
+
}`
|
|
1491
|
+
/**
|
|
1492
|
+
* TOLERANT RECOVERY — the SAME functions the other three engines call
|
|
1493
|
+
* (`recovery/scan.ts`), so an error's span, its expected set and its CST
|
|
1494
|
+
* embedding are produced once and cannot drift between engines.
|
|
1495
|
+
*
|
|
1496
|
+
* Reached only in a recovery table, and inside it only on the FAILURE of
|
|
1497
|
+
* an element, so a matching item pays one `_sync` read at entry and
|
|
1498
|
+
* nothing else. Everything else in this loop is the emitted strict loop
|
|
1499
|
+
* unchanged — which is the whole point, and what the top-level refusal
|
|
1500
|
+
* this replaces was giving up.
|
|
1501
|
+
*
|
|
1502
|
+
* THE FOUR AGREEMENTS WITH `assemble.ts:2036`, each load-bearing:
|
|
1503
|
+
* - a MANDATORY item of a separator-less repeat does NOT recover, so the
|
|
1504
|
+
* gate is `sep !== undefined || count >= min`. Both conjuncts are
|
|
1505
|
+
* table data here, so the test is CONSTANT-FOLDED rather than run.
|
|
1506
|
+
* - the check is `matchesAt(mySync, itemStart)`: sitting ON the sync
|
|
1507
|
+
* token is a clean list end, not junk, and `itemStart` is past any
|
|
1508
|
+
* leading trivia so trivia is never swallowed into the error span.
|
|
1509
|
+
* - a separated list scans to its OWN separator or the enclosing
|
|
1510
|
+
* delimiter (`orSentinel`); a separator-less one to the inherited
|
|
1511
|
+
* sentinel alone.
|
|
1512
|
+
* - the separator-less path rolls leading trivia back BEFORE recovering
|
|
1513
|
+
* and the separated path does NOT — a consumed separator and the error
|
|
1514
|
+
* after it both belong to the list (repeat.ts:533). This is why the
|
|
1515
|
+
* strict loop's unconditional `${'${rb}'}` could not simply be reused.
|
|
1516
|
+
*
|
|
1517
|
+
* `mySync` IS CAPTURED AT ENTRY, not read per item: an element's own
|
|
1518
|
+
* sequence publishes over `_sync` while it runs. The interpreter restores
|
|
1519
|
+
* in a `finally`, codegen saves at entry — same value, and so does this.
|
|
1520
|
+
*/
|
|
1521
|
+
const my = `${p}my`
|
|
1522
|
+
// `reportItem` gates the FAILURE report only. `recoverScan`'s expected set
|
|
1523
|
+
// is taken unconditionally by the closure engine (`assemble.ts:2067`), so
|
|
1524
|
+
// it is resolved separately from `itemFx` — reusing that one would hand
|
|
1525
|
+
// `EMPTY_FX` to every recovered error in a grammar that does not report.
|
|
1526
|
+
const recFx = REC ? fxRef(code[ip + 6]!) : ''
|
|
1527
|
+
const sepSent = REC ? sentRef(code[ip + 7]!) : 'undefined'
|
|
1528
|
+
const recSent = sep === undefined
|
|
1529
|
+
? my
|
|
1530
|
+
: sepSent === 'undefined'
|
|
1531
|
+
? `orSentinel(${my},undefined)`
|
|
1532
|
+
: `orSentinel(${sepSent}??${my},${sepSent}===undefined?undefined:${my})`
|
|
1533
|
+
const recGate = sep !== undefined || min === 0 ? '' : `count>=${min}&&`
|
|
1534
|
+
const recBranch = !REC ? '' : `if(ctx._tolerant===true&&${my}!==undefined&&${recGate}!matchesAt(${my},input,itemStart,ctx)){
|
|
1535
|
+
${sep === undefined ? `${rb}\n` : ''}const rr=recoverScan(input,itemStart,ctx,${recSent},${recFx})
|
|
1536
|
+
${collect ? 'out.push(rr.error)\n' : ''}captureError(ctx,rr.error)
|
|
1537
|
+
count++
|
|
1538
|
+
cur=rr.end
|
|
1539
|
+
continue
|
|
1540
|
+
}
|
|
1541
|
+
`
|
|
1542
|
+
return `${head}
|
|
1543
|
+
const out=${collect ? '[]' : 'undefined'}
|
|
1544
|
+
${knownTrivia === undefined ? 'const hasTrivia=ctx.trivia!==undefined\n' : ''}${L.buf ? '' : 'const needMark=_rollbackNeeded(ctx)\n'}${REC ? `const ${my}=ctx._sync\n` : ''}let cur=pos
|
|
1545
|
+
let count=0
|
|
1546
|
+
for(;;){
|
|
1547
|
+
${max >= 0 ? `if(count>=${max})break\n` : ''}${sep !== undefined ? `if(count>0&&count>=${min}&&cur>=input.length)break\n` : ''}let ${p}raw=0,${p}tl=0,${p}lv=0,${p}lg=0,${p}rt=0${sinks.fd ? `,${p}fd=0` : ''}${sinks.er ? `,${p}er=0` : ''}
|
|
1548
|
+
${markBody}
|
|
1549
|
+
let itemStart=cur
|
|
1550
|
+
let sepEnd=-1
|
|
1551
|
+
${sep !== undefined ? `if(count>0){
|
|
1552
|
+
const lb=cstLeavesLen(ctx)
|
|
1553
|
+
let sp=cur
|
|
1554
|
+
${hasTrivia === 'false' ? '' : hasTrivia === 'true' ? `sp=${skip}(input,sp,ctx)\n` : `if(hasTrivia)sp=${skip}(input,sp,ctx)\n`}ctx._fc=false
|
|
1555
|
+
const sv=${sep}(input,sp,ctx)
|
|
1556
|
+
if(sv===FAIL){
|
|
1557
|
+
${rb}
|
|
1558
|
+
if(ctx._fc===true)return FAIL
|
|
1559
|
+
break
|
|
1560
|
+
}
|
|
1561
|
+
${keepSeparators ? '' : 'demoteCapturedToRaw(ctx,lb)\n'}sepEnd=EC.e
|
|
1562
|
+
itemStart=${hasTrivia === 'false' ? 'EC.e' : hasTrivia === 'true' ? `${skip}(input,EC.e,ctx)` : `hasTrivia?${skip}(input,EC.e,ctx):EC.e`}
|
|
1563
|
+
}else ${leadSkip(hasTrivia, leadTrivia, skip)}
|
|
1564
|
+
` : `${leadSkip(hasTrivia, leadTrivia, skip)}
|
|
1565
|
+
`}if(itemStart>=input.length&&${via}){
|
|
1566
|
+
${rb}
|
|
1567
|
+
${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
|
|
1568
|
+
}
|
|
1569
|
+
ctx._fc=false
|
|
1570
|
+
const v=${child}(input,itemStart,ctx)
|
|
1571
|
+
if(v===FAIL){
|
|
1572
|
+
${REC
|
|
1573
|
+
// THE ROLLBACK MOVES INSIDE THE ARMS. The strict loop rolls back
|
|
1574
|
+
// unconditionally before testing `_fc`, which is sound because every path out
|
|
1575
|
+
// of it rolls back. The recovery arm is the one path that must NOT: a
|
|
1576
|
+
// separated list keeps the consumed separator and the trivia after it, both of
|
|
1577
|
+
// which belong to the list (repeat.ts:533). So `committed` is tested first and
|
|
1578
|
+
// each arm takes its own rollback — the same order as `assemble.ts:2113`.
|
|
1579
|
+
? `if(ctx._fc===true){
|
|
1580
|
+
${rb}
|
|
1581
|
+
return FAIL
|
|
1582
|
+
}
|
|
1583
|
+
${recBranch}${rb}`
|
|
1584
|
+
: `${rb}
|
|
1585
|
+
if(ctx._fc===true)return FAIL`}
|
|
1586
|
+
${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
|
|
1587
|
+
}
|
|
1588
|
+
if(EC.e===itemStart&&${via}){
|
|
1589
|
+
${rb}
|
|
1590
|
+
break
|
|
1591
|
+
}
|
|
1592
|
+
${collect ? 'out.push(v)\n' : ''}cur=EC.e
|
|
1593
|
+
count++
|
|
1594
|
+
}
|
|
1595
|
+
${min > 0 ? `if(count<${min}){
|
|
1596
|
+
${reportItem ? `ctx._fe=cur;ctx._fx=${itemFx}\n` : ''}return FAIL
|
|
1597
|
+
}
|
|
1598
|
+
` : ''}EC.e=cur
|
|
1599
|
+
return out
|
|
1600
|
+
}`
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
case OP_NODE:
|
|
1604
|
+
case OP_NODE_TRACK: {
|
|
1605
|
+
const flags = code[ip + 3]!
|
|
1606
|
+
const child = link(code[ip + 2]!)
|
|
1607
|
+
const proj = code[ip + 4]!
|
|
1608
|
+
const buildIdx = code[ip + 1]!
|
|
1609
|
+
const type = k[code[ip + 5]!] as string
|
|
1610
|
+
const tagIdx = code[ip + 6]!
|
|
1611
|
+
const tags = tagIdx < 0 ? 'undefined' : kRef(tagIdx)
|
|
1612
|
+
const tracked = op === OP_NODE_TRACK
|
|
1613
|
+
const readsTrivia = (flags & 4) !== 0
|
|
1614
|
+
const readsState = (flags & 8) !== 0
|
|
1615
|
+
const hasFields = (flags & 16) !== 0
|
|
1616
|
+
const collapse = (flags & 32) !== 0
|
|
1617
|
+
const unwrap = (flags & 64) !== 0
|
|
1618
|
+
const trailingTrivia = (flags & 128) !== 0
|
|
1619
|
+
// HOST MODE IS AN OPTION, and it decided five runtime ternaries in the
|
|
1620
|
+
// interpreter's node case — the most-executed non-terminal in any of
|
|
1621
|
+
// these grammars. It selects the emitted shape instead.
|
|
1622
|
+
const build = buildIdx >= 0 ? fnRef(buildIdx) : undefined
|
|
1623
|
+
const structural = build === undefined && proj < 0
|
|
1624
|
+
const grammarCapture = (flags & 1) !== 0 || trailingTrivia
|
|
1625
|
+
const hostCapturesThisType = structural && cfg.hostCaptureTrivia !== undefined
|
|
1626
|
+
? cfg.hostCaptureTrivia(type)
|
|
1627
|
+
: undefined
|
|
1628
|
+
const wantFields = hasFields || hostCst
|
|
1629
|
+
const captureWide = readsTrivia || hostCst
|
|
1630
|
+
? !structural || grammarCapture || hostCapturesThisType !== false
|
|
1631
|
+
: hostCapturesThisType === true
|
|
1632
|
+
const keepChildren = !structural || cfg.hostReadsChildren !== false || collapse || unwrap
|
|
1633
|
+
const ty = q(type)
|
|
1634
|
+
const stArg = readsState ? 'st' : '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)'
|
|
1635
|
+
const hostCall = `_pfHost(${ty},hostKids,fieldMap,span,rawKids,tlog,${stArg},${tags})`
|
|
1636
|
+
let value: string
|
|
1637
|
+
if (proj >= 0) {
|
|
1638
|
+
value = hostCst
|
|
1639
|
+
? `nd=_pfHost!==undefined?${hostCall}:projectChild(kids,${proj},${ty})`
|
|
1640
|
+
: `nd=projectChild(kids,${proj},${ty})`
|
|
1641
|
+
} else if (build !== undefined) {
|
|
1642
|
+
// A direct builder is bypassed under a CST host.
|
|
1643
|
+
const direct = `${build}(kids,fieldMap,span,rawKids,${captureWide ? 'tlog' : 'EMPTY_TL'},st)`
|
|
1644
|
+
value = hostCst ? `nd=_pfHost!==undefined?${hostCall}:${direct}` : `nd=${direct}`
|
|
1645
|
+
} else {
|
|
1646
|
+
value = `nd=_pfHost!==undefined?_pfHost(${ty},hostKids,fieldMap,span,rawKids,tlog,st,${tags}):{_tag:'node',type:${ty},span,state:st??null,children:kids}`
|
|
1647
|
+
}
|
|
1648
|
+
// HOST COLLAPSE applies wherever the node's VALUE comes from the host —
|
|
1649
|
+
// any node under a CST host, not only the builder-less ones.
|
|
1650
|
+
const collapsible = keepChildren && (hostCst || (build === undefined && proj < 0))
|
|
1651
|
+
const openCapture = keepChildren
|
|
1652
|
+
? `const buf={}
|
|
1653
|
+
ctx._cstBuf=buf
|
|
1654
|
+
ctx._cstChildren=undefined
|
|
1655
|
+
ctx._cstLeaves=undefined
|
|
1656
|
+
ctx._cstRawChildren=undefined
|
|
1657
|
+
ctx._cstTriviaLog=undefined`
|
|
1658
|
+
: `const buf={rawOnly:true}
|
|
1659
|
+
ctx._cstBuf=buf
|
|
1660
|
+
ctx._cstChildren=undefined
|
|
1661
|
+
ctx._cstLeaves=undefined
|
|
1662
|
+
ctx._cstRawChildren=undefined
|
|
1663
|
+
ctx._cstTriviaLog=undefined`
|
|
1664
|
+
const finishCapture = keepChildren
|
|
1665
|
+
? `const kids=buf.ch??(buf.single!==undefined?[buf.single]:EMPTY_CH)
|
|
1666
|
+
const hostKids=kids
|
|
1667
|
+
const rawKids=buf.raw??(buf.rawSingle!==undefined?[buf.rawSingle]:EMPTY_CH)
|
|
1668
|
+
const tlog=buf.tl??EMPTY_TLOG`
|
|
1669
|
+
: `const kids=EMPTY_CH
|
|
1670
|
+
const hostKids=kids
|
|
1671
|
+
const rawKids=buf.raw??(buf.rawSingle!==undefined?[buf.rawSingle]:EMPTY_CH)
|
|
1672
|
+
const tlog=buf.tl??EMPTY_TLOG`
|
|
1673
|
+
return `${head}
|
|
1674
|
+
const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
|
|
1675
|
+
const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf
|
|
1676
|
+
${openCapture}
|
|
1677
|
+
ctx.captureTrivia=${captureWide}
|
|
1678
|
+
const savedFields=ctx._fields
|
|
1679
|
+
ctx._fields=${wantFields ? '[]' : 'undefined'}
|
|
1680
|
+
${structural ? `const savedMask=ctx._triviaCaptureMask
|
|
1681
|
+
if(_pfHost!==undefined&&_pfHost._parsemanTriviaKinds!==undefined)ctx._triviaCaptureMask=_pfHost._parsemanTriviaKinds(${ty})
|
|
1682
|
+
` : ''}const v=${child}(input,pos,ctx)
|
|
1683
|
+
${trailingTrivia && L.tri !== TRI_NONE
|
|
1684
|
+
? `if(v!==FAIL${L.tri === TRI_UNKNOWN ? '&&ctx.trivia!==undefined' : ''})EC.e=consumeTrivia(input,EC.e,ctx)\n`
|
|
1685
|
+
: ''}const fieldMap=${wantFields ? 'buildFieldMap(ctx._fields)' : 'undefined'}
|
|
1686
|
+
ctx._fields=savedFields
|
|
1687
|
+
${structural ? 'ctx._triviaCaptureMask=savedMask\n' : ''}${finishCapture}
|
|
1688
|
+
ctx._cstBuf=sBuf
|
|
1689
|
+
ctx._cstChildren=sCh
|
|
1690
|
+
ctx._cstLeaves=sLv
|
|
1691
|
+
ctx._cstRawChildren=sRaw
|
|
1692
|
+
ctx._cstTriviaLog=sTl
|
|
1693
|
+
ctx.captureTrivia=sCap
|
|
1694
|
+
if(v===FAIL)return FAIL
|
|
1695
|
+
const end=EC.e
|
|
1696
|
+
const span=${tracked ? 'spanLines(ctx,pos,end)' : '{start:pos,end}'}
|
|
1697
|
+
const st=${readsState ? '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)' : 'undefined'}
|
|
1698
|
+
let nd
|
|
1699
|
+
${unwrap ? 'if(kids.length===1)nd=unwrapChild(kids[0])\nelse ' : ''}${collapse ? 'if(kids.length===1)nd=kids[0]\nelse ' : ''}${collapsible ? `if(_pfHost!==undefined&&_pfHost._parsemanCstCollapse!==undefined&&kids.length===1&&rawKids.length===1&&_pfHost._parsemanCstCollapse(${ty},kids[0],kids,rawKids))nd=kids[0]
|
|
1700
|
+
else ` : ''}{${value}}
|
|
1701
|
+
${L.buf
|
|
1702
|
+
// The OUTER buffer, which this body saved into `sBuf` before opening its own —
|
|
1703
|
+
// so an in-node site's parent collector is present by the same fact.
|
|
1704
|
+
? 'pushCstChild(ctx,nd,rawEntry(nd,input,pos,end))'
|
|
1705
|
+
: 'if(sBuf!==undefined||sCh!==undefined)pushCstChild(ctx,nd,rawEntry(nd,input,pos,end))'}
|
|
1706
|
+
EC.e=end
|
|
1707
|
+
return nd
|
|
1708
|
+
}`
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
default:
|
|
1712
|
+
throw new Unemittable(`OP_${OP_NAMES[op!] ?? String(op)}`)
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const ruleEntries: string[] = []
|
|
1717
|
+
for (const [rname, entryIp] of Object.entries(prog.rules)) {
|
|
1718
|
+
// `_r_<Name>` — the composition surface, in `codegen.ts`'s own spelling and
|
|
1719
|
+
// deliberately NOT namespaced, so a sibling calls it by name.
|
|
1720
|
+
const target = link(entryIp)
|
|
1721
|
+
const rn = `_r_${rname.replace(/[^A-Za-z0-9_$]/g, '_')}`
|
|
1722
|
+
bodies.push(`const ${rn}=${target}`)
|
|
1723
|
+
ruleEntries.push(`${q(rname)}:${rn}`)
|
|
1724
|
+
}
|
|
1725
|
+
// Sites the SCAN pool and the scan-skip sets reference. They are linked
|
|
1726
|
+
// through `subtreeComb` outside the emitted scope, so they need names.
|
|
1727
|
+
const extra: string[] = []
|
|
1728
|
+
for (const ip of extraIps) extra.push(`${ip}:${link(ip)}`)
|
|
1729
|
+
|
|
1730
|
+
// The per-label trivia scans sit AFTER the hoisted pool they close over: they
|
|
1731
|
+
// are `function` declarations, so a body may call one that is textually below
|
|
1732
|
+
// it, but the `const _ts<N>` each one reads must be initialised before any
|
|
1733
|
+
// parse runs, not merely before the declaration is evaluated.
|
|
1734
|
+
const source = `${RUNTIME_PRELUDE}
|
|
1735
|
+
${prelude.join('\n')}
|
|
1736
|
+
${skipDefs.join('\n')}
|
|
1737
|
+
${bodies.join('\n')}
|
|
1738
|
+
function _begin(ctx){
|
|
1739
|
+
const host=ctx.build
|
|
1740
|
+
if(_pfDepth>0)_pfFrames.push([_pfScan,_pfHost,EC.e])
|
|
1741
|
+
_pfDepth++
|
|
1742
|
+
_pfScan=null
|
|
1743
|
+
_pfHost=host
|
|
1744
|
+
}
|
|
1745
|
+
function _finish(){
|
|
1746
|
+
if(_pfDepth<=0)throw new Error('parseman emitted table assembly frame underflow')
|
|
1747
|
+
_pfDepth--
|
|
1748
|
+
if(_pfDepth===0)return
|
|
1749
|
+
const prior=_pfFrames.pop()
|
|
1750
|
+
_pfScan=prior[0]
|
|
1751
|
+
_pfHost=prior[1]
|
|
1752
|
+
EC.e=prior[2]
|
|
1753
|
+
}
|
|
1754
|
+
return{
|
|
1755
|
+
pieces:{${ruleEntries.join(',')}},
|
|
1756
|
+
byIp:{${extra.join(',')}},
|
|
1757
|
+
end:function(){return EC.e},
|
|
1758
|
+
begin:_begin,
|
|
1759
|
+
finish:_finish
|
|
1760
|
+
}`
|
|
1761
|
+
|
|
1762
|
+
return {
|
|
1763
|
+
source, reached, masks, classes, armExpected,
|
|
1764
|
+
plan: { classes: classPlan, armExpected: armExpectedPlan, masks: maskPlan },
|
|
1765
|
+
}
|
|
1766
|
+
}
|