parseman 0.46.0 → 0.47.1
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 +10339 -7787
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +10339 -7787
- 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 +10924 -8506
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10924 -8506
- 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 +45 -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 +9290 -7181
- 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 +9300 -7191
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +313 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +313 -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 +11175 -9534
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11175 -9534
- 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 +2934 -1611
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2934 -1611
- 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 +11463 -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 +11440 -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 +453 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +496 -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 +26 -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 +348 -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 +237 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +852 -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 +3006 -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 +1777 -0
- package/src/table/emit.ts +429 -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 +1680 -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 +467 -0
- package/src/table/program.ts +876 -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,1569 @@
|
|
|
1
|
+
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
2
|
+
import { firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
3
|
+
import { childrenOf } from '../analysis/gating.ts'
|
|
4
|
+
import { getCoreLiteralValue } from '../combinators/choice.ts'
|
|
5
|
+
import { deriveExpected } from '../combinators/expect.ts'
|
|
6
|
+
import { buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
|
|
7
|
+
import { buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
|
|
8
|
+
import { asciiFoldKey, branchUsesRouted, parserUsesRouted } from '../combinators/dispatch.ts'
|
|
9
|
+
import {
|
|
10
|
+
OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
|
|
11
|
+
OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
|
|
12
|
+
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
13
|
+
OP_LIVE, OP_ATTEMPT, OP_LABEL,
|
|
14
|
+
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
|
|
15
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_COV,
|
|
16
|
+
} from './ops.ts'
|
|
17
|
+
import { adjacencyExpected } from '../combinators/adjacency.ts'
|
|
18
|
+
import { resolveAdjacencyKindMask } from '../cst/trivia-kinds.ts'
|
|
19
|
+
import { missingInferredType } from '../combinators/node.ts'
|
|
20
|
+
import { hasOwnTriviaBoundary } from '../combinators/trivia-boundary.ts'
|
|
21
|
+
import type { BalancedSpec } from '../combinators/scanTo.ts'
|
|
22
|
+
import type { DispatchSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
|
|
23
|
+
import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
24
|
+
import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Can `emitConst` print this? Mirrors the guard in `emit.ts` — scalars, arrays
|
|
28
|
+
* of scalars, and plain objects of those. Kept as a predicate here so the
|
|
29
|
+
* encoder can record a NAMED runtime-only reason instead of letting the printer
|
|
30
|
+
* throw a bare TypeError at emit time.
|
|
31
|
+
*/
|
|
32
|
+
function emittableConst(v: unknown): boolean {
|
|
33
|
+
const scalar = (x: unknown): boolean =>
|
|
34
|
+
x === null || typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean'
|
|
35
|
+
if (scalar(v) || v instanceof RegExp) return true
|
|
36
|
+
if (Array.isArray(v)) return v.every(scalar)
|
|
37
|
+
if (typeof v === 'object' && v !== null) {
|
|
38
|
+
const proto = Object.getPrototypeOf(v)
|
|
39
|
+
if (proto !== Object.prototype && proto !== null) return false
|
|
40
|
+
return Object.values(v).every(x => scalar(x) || (Array.isArray(x) && x.every(scalar)))
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Does the rule-map entry `winner` bottom out AT the reference `p`?
|
|
47
|
+
*
|
|
48
|
+
* The question `Encoder.winners` has to ask before resolving a `g.X` by name. A
|
|
49
|
+
* map entry that IS the reference — directly, or under the scope wrappers that
|
|
50
|
+
* `rules()` puts there — is not an override to resolve to; it is the reference's
|
|
51
|
+
* own binding, and the only thing that reaches the rule's BODY from there is the
|
|
52
|
+
* ref's thunk. Resolving by name instead hands back the row already in flight for
|
|
53
|
+
* that entry, which is an alias cycle rather than a parser (see `winners`).
|
|
54
|
+
*
|
|
55
|
+
* ONLY WRAPPERS `rules()` ITSELF INTRODUCES are unwrapped — `parser()`/`noTrivia`
|
|
56
|
+
* scopes (`grammar`) and `trivia()`. Both are single-child and both can encode to
|
|
57
|
+
* NO ROW, which is what makes the cycle degenerate. Walking further, into a
|
|
58
|
+
* `seq()` or a `choice()` arm, would answer a different question: a rule whose
|
|
59
|
+
* body legitimately contains a reference to itself is ordinary recursion and must
|
|
60
|
+
* keep resolving by name.
|
|
61
|
+
*/
|
|
62
|
+
function wrapsRef(winner: Combinator<unknown>, p: Combinator<unknown>): boolean {
|
|
63
|
+
let cur = winner
|
|
64
|
+
// Bounded rather than `while (true)`: `_def.parser` is author-supplied and a
|
|
65
|
+
// hand-built cycle of scopes would otherwise hang the encoder. Nothing legal
|
|
66
|
+
// nests these more than a couple deep.
|
|
67
|
+
for (let n = 0; n < 16; n++) {
|
|
68
|
+
if (cur === p) return true
|
|
69
|
+
const d = cur._def as ParserDef
|
|
70
|
+
if (d.tag !== 'grammar' && d.tag !== 'trivia') return false
|
|
71
|
+
cur = d.parser
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Raised when a construct has no opcode yet. Prototype scope is explicit. */
|
|
77
|
+
export class UnsupportedConstruct extends Error {
|
|
78
|
+
readonly tag: string
|
|
79
|
+
constructor(tag: string) { super(`table lowering: no opcode for '${tag}'`); this.tag = tag }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Settings that select the TABLE'S CONTENTS. They never reach the driver. */
|
|
83
|
+
export type TableSettings = {
|
|
84
|
+
readonly hostMode?: 'ast' | 'cst'
|
|
85
|
+
readonly trackLines?: boolean
|
|
86
|
+
/**
|
|
87
|
+
* ACCEPTED AND IGNORED. RECOVERY IS ALWAYS LOWERED (owner ruling).
|
|
88
|
+
*
|
|
89
|
+
* It used to be opt-in because the sync sentinel a list resyncs to is derived
|
|
90
|
+
* from grammar structure and so has to be known before the table exists — a
|
|
91
|
+
* BUILD setting for the same reason `hostMode` is. The consequence was a
|
|
92
|
+
* `CompiledParser` whose `parseWithErrors()` THREW unless the caller had
|
|
93
|
+
* happened to pass a flag `compile()` does not require, which is a contract
|
|
94
|
+
* break against the source lowering, not a trade.
|
|
95
|
+
*
|
|
96
|
+
* The cost of always lowering it is the extra rows, measured on the emitted
|
|
97
|
+
* module: json 1,081 → 1,214 B (+12.3%), graphql 2,925 → 3,397 B (+16.1%).
|
|
98
|
+
* Against codegen's 15,138 B for the same json root the table is still ~12×
|
|
99
|
+
* smaller, so the size argument for making it optional no longer holds.
|
|
100
|
+
*
|
|
101
|
+
* LOWERING RECOVERY IS NOT RUNNING IT. Every recovery path stays gated on
|
|
102
|
+
* `ctx._tolerant` — the same dormancy the source lowering emits
|
|
103
|
+
* (`codegen.ts:3153`) and the interpreter tests (`repeat.ts:163`) — so a strict
|
|
104
|
+
* parse is strict, and `test/parity/table-recovery-always.test.ts` pins that.
|
|
105
|
+
*
|
|
106
|
+
* The field is kept rather than deleted so `compile` stays signature-
|
|
107
|
+
* compatible with `compile()`, whose `{ recovery: true }` is real. Passing it
|
|
108
|
+
* is harmless; passing `false` does NOT switch recovery off, and saying so here
|
|
109
|
+
* is the point — a setting that silently means nothing is the defect class this
|
|
110
|
+
* project keeps finding.
|
|
111
|
+
*/
|
|
112
|
+
readonly recovery?: boolean
|
|
113
|
+
/**
|
|
114
|
+
* THE COVERAGE PLAN, when this table is being encoded for grammar coverage.
|
|
115
|
+
*
|
|
116
|
+
* Its maps are keyed by COMBINATOR IDENTITY, which is what makes threading the
|
|
117
|
+
* plan through the encoder exact: at the moment a choice, dispatch, label or
|
|
118
|
+
* rule body is laid down, the combinator being encoded IS the key, so no code
|
|
119
|
+
* offset has to be matched back to a structural path after the fact. `OP_COV`
|
|
120
|
+
* rows go in beside those sites and the plan's definitions become `prog.cov`.
|
|
121
|
+
*
|
|
122
|
+
* A plan is a BUILD input for the same reason `hostMode` is: the rows either
|
|
123
|
+
* exist in the table or they do not, and no per-parse flag can conjure them.
|
|
124
|
+
* Which of the resulting assemblies actually counts is the separate,
|
|
125
|
+
* assembly-selected question `RunCfg.coverage` answers.
|
|
126
|
+
*/
|
|
127
|
+
readonly coverage?: GrammarCoveragePlan
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type Emitted = { readonly ip: number }
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Encoder {
|
|
134
|
+
code: number[] = []
|
|
135
|
+
k: unknown[] = []
|
|
136
|
+
fns: unknown[] = []
|
|
137
|
+
/**
|
|
138
|
+
* SOURCE TEXT per `fns` entry, or `null` where the author's callback reached
|
|
139
|
+
* the encoder as a live closure with no source (the runtime path).
|
|
140
|
+
*
|
|
141
|
+
* Parallel to `fns`, in the same order, because that is the order the emitters
|
|
142
|
+
* print `f:[…]` in. Without it a macro build has no way to print an author
|
|
143
|
+
* reducer at all: `emitTableModule` falls back to `() => {}` per entry, which
|
|
144
|
+
* produces a module that LOADS and returns the wrong tree — the exact class of
|
|
145
|
+
* failure that a `null` return from `compileRuleMap` (its `mfCovered &&
|
|
146
|
+
* buildCovered` gate) exists to prevent on the source-lowering side. The table
|
|
147
|
+
* counterpart cannot honour that same all-or-nothing contract without this.
|
|
148
|
+
*
|
|
149
|
+
* NOT part of `TableProgram`: it is build-time metadata for the printer, never
|
|
150
|
+
* data the driver reads, and it must not travel in an emitted artifact.
|
|
151
|
+
*/
|
|
152
|
+
fnSrcs: (string | null)[] = []
|
|
153
|
+
cc: string[] = []
|
|
154
|
+
fx: (readonly string[])[] = []
|
|
155
|
+
disp: (readonly number[])[] = []
|
|
156
|
+
dsp: DispatchSpec[] = []
|
|
157
|
+
labels: readonly string[] | undefined = undefined
|
|
158
|
+
/** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
|
|
159
|
+
activeTrivia: Combinator<unknown> | undefined = undefined
|
|
160
|
+
classified = false
|
|
161
|
+
scans: ScanSpec[] = []
|
|
162
|
+
/** Ambient `scanSkip` sets, pooled by the ARRAY's identity (stable per grammar). */
|
|
163
|
+
scanSkipSets: (readonly SubtreeRef[])[] = []
|
|
164
|
+
private scanSkipIndex = new Map<readonly Combinator<unknown>[], number>()
|
|
165
|
+
/** Per rule, in `rules` order: which pooled set its entry installs, or −1. */
|
|
166
|
+
scanSkipOf: number[] = []
|
|
167
|
+
/** Reasons this program can RUN but not be EMITTED. */
|
|
168
|
+
runtimeOnly = new Set<string>()
|
|
169
|
+
/**
|
|
170
|
+
* The rule map being encoded, so a named reference resolves BY NAME.
|
|
171
|
+
*
|
|
172
|
+
* This is what makes a MERGED rule map — the shape composition produces, where
|
|
173
|
+
* a later piece overrides an earlier piece's rule — encode to the parser
|
|
174
|
+
* `compose()` means. Without it `case 'lazy'` resolves a reference by calling
|
|
175
|
+
* its thunk, and a thunk closes over the piece that DEFINED the reference: a
|
|
176
|
+
* base piece's internal `g.Atom` keeps reaching the base's own `Atom` even
|
|
177
|
+
* after the merged map binds that name to an override. The encode succeeds,
|
|
178
|
+
* the table parses, and it silently returns the base's tree — open recursion,
|
|
179
|
+
* which `compose()` is largely FOR, quietly absent.
|
|
180
|
+
*
|
|
181
|
+
* The source lowering never had this problem because it compiles every rule to
|
|
182
|
+
* a canonical `_r_<Name>` and fusion drops them into one scope, so a reference
|
|
183
|
+
* resolves by name for free (`compiler/linker.ts:1`). The table has no shared
|
|
184
|
+
* scope, so the name lookup has to be explicit and is done here.
|
|
185
|
+
*
|
|
186
|
+
* For a NON-merged map this is a no-op: the name resolves to the same
|
|
187
|
+
* combinator the thunk returns. The self-resolution guard is load-bearing — a
|
|
188
|
+
* rule map entry may BE the named lazy proxy for its own body, and resolving
|
|
189
|
+
* that to itself would return the in-progress memoized row and never encode
|
|
190
|
+
* the body at all.
|
|
191
|
+
*
|
|
192
|
+
* THAT GUARD CANNOT BE OBJECT IDENTITY. `rules({ trackLines: true }, …)`
|
|
193
|
+
* REPLACES every map entry with `parser({ trackLines: true }, entry)` carrying
|
|
194
|
+
* the SAME `_ruleName` (`combinators/parser.ts:228-241`), so for all four
|
|
195
|
+
* `*PositionsGrammar` exports the entry is a WRAPPER around the rule's own lazy
|
|
196
|
+
* proxy. `winner !== p` is then trivially true, the proxy resolves by name to
|
|
197
|
+
* the wrapper that is already in flight, `node()` hands back the recursion
|
|
198
|
+
* trampoline, and `case 'grammar'` — a `parser()` scope with no trivia, no
|
|
199
|
+
* capture and no root policy emits NO ROW — passes that trampoline straight out
|
|
200
|
+
* as the wrapper's own offset. The trampoline is patched to itself, the rule's
|
|
201
|
+
* real body is never encoded, and every parse dies on its first byte with
|
|
202
|
+
* `Maximum call stack size exceeded`: 87/87 css, 314/314 less, 24/24 jess and
|
|
203
|
+
* 2408/2408 scss files, all four `-lines` variants, from `dccb7fa` (which added
|
|
204
|
+
* this map) to `90aa867`. So the question is not "is the winner this object"
|
|
205
|
+
* but "does the winner bottom out AT this reference", and `wrapsRef` asks that.
|
|
206
|
+
*/
|
|
207
|
+
winners: Readonly<Record<string, Combinator<unknown>>> | undefined = undefined
|
|
208
|
+
triviaSpecs: TriviaSpec[] = []
|
|
209
|
+
private triviaIndex = new Map<Combinator<unknown>, number>()
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A trivia combinator as DATA where its shape allows.
|
|
213
|
+
*
|
|
214
|
+
* `classifiedTrivia()` builds exactly `trivia(oneOrMore(choice(label(name,
|
|
215
|
+
* arm)…)))` (src/combinators/map.ts) and the four shipping grammars give it
|
|
216
|
+
* regex arms, so the whole thing round-trips through `[label, source, flags]`
|
|
217
|
+
* triples. Anything else is kept LIVE and recorded, so the program runs and
|
|
218
|
+
* emit refuses by name.
|
|
219
|
+
*/
|
|
220
|
+
private triviaSlot(t: Combinator<unknown>): number {
|
|
221
|
+
const hit = this.triviaIndex.get(t)
|
|
222
|
+
if (hit !== undefined) return hit
|
|
223
|
+
const idx = this.triviaSpecs.length
|
|
224
|
+
this.triviaIndex.set(t, idx)
|
|
225
|
+
this.triviaSpecs.push(this.triviaSpecOf(t))
|
|
226
|
+
return idx
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private triviaSpecOf(t: Combinator<unknown>): TriviaSpec {
|
|
230
|
+
const unlowered = (why: string): TriviaSpec => {
|
|
231
|
+
this.runtimeOnly.add(`rules({ trivia }) — ${why}`)
|
|
232
|
+
return { arms: [], live: t }
|
|
233
|
+
}
|
|
234
|
+
const d = t._def
|
|
235
|
+
// THE `trivia()` WRAPPER IS A MARKER, NOT THE SHAPE. It sets `_meta.isTrivia`
|
|
236
|
+
// and delegates; `parser({ trivia })` / `rules({ trivia })` store whatever they
|
|
237
|
+
// are handed verbatim (`combinators/grammar.ts:71`), and the only consumers —
|
|
238
|
+
// `advanceTrivia` / `scanTrivia` — read `span.end` off it without ever looking
|
|
239
|
+
// at the tag. So demanding the wrapper refused grammars every other engine runs:
|
|
240
|
+
// `parser({ trivia: regex(/ +/) })` is ordinary authoring and it fell all the way
|
|
241
|
+
// back to the interpreter, silently, with the parseman import still in place.
|
|
242
|
+
// Unwrap it WHEN PRESENT and classify the body either way.
|
|
243
|
+
let inner = d.tag === 'trivia' ? d.parser._def : d
|
|
244
|
+
// A `transform()` over a trivia body is a VALUE map, and a trivia VALUE is
|
|
245
|
+
// never observed by anything: `advanceTrivia` and `scanTrivia`
|
|
246
|
+
// (combinators/trivia-skip.ts:189, :219) are the only consumers of `ctx.trivia`
|
|
247
|
+
// and both read `span.end` alone — the parsed value is dropped on every path.
|
|
248
|
+
// So the wrapper is UNWRAPPED rather than refused. This is not "close enough":
|
|
249
|
+
// the recognition it wraps is the entire observable contract of trivia, and
|
|
250
|
+
// dropping a reducer whose result provably cannot escape loses nothing.
|
|
251
|
+
// (`examples/json/jsonc.ts` writes exactly `transform(many(…), () => '')`.)
|
|
252
|
+
while (inner.tag === 'transform') inner = inner.parser._def
|
|
253
|
+
if (inner.tag === 'regex') return { arms: [], plain: [inner.source, inner.flags] }
|
|
254
|
+
// The repetition around the alternation. `oneOrMore` is the tag min>=1 always
|
|
255
|
+
// carries (repeat.ts:201) and `many` is the nullable min-0 loop; both lower,
|
|
256
|
+
// and the floor travels in the spec so the rebuild is the same combinator
|
|
257
|
+
// rather than a same-shaped one. A BOUNDED repeat does not lower — `max` has
|
|
258
|
+
// nowhere to go in the spec and silently unbounding it would accept input the
|
|
259
|
+
// grammar rejects.
|
|
260
|
+
if (inner.tag !== 'oneOrMore' && inner.tag !== 'many') {
|
|
261
|
+
return unlowered(`unrecognised trivia body '${inner.tag}'`)
|
|
262
|
+
}
|
|
263
|
+
if (inner.max !== undefined) {
|
|
264
|
+
return unlowered(`trivia body is a BOUNDED repeat (max ${inner.max}), which the spec cannot carry`)
|
|
265
|
+
}
|
|
266
|
+
const min = inner.min
|
|
267
|
+
const rep = inner.parser._def
|
|
268
|
+
// A single un-alternated body (`trivia(many(regex))`) is the one-arm case.
|
|
269
|
+
const parsers = rep.tag === 'choice' ? rep.parsers : [inner.parser]
|
|
270
|
+
// Two shapes, and an arm set must be wholly one or the other. LABELLED is
|
|
271
|
+
// `classifiedTrivia()`'s exact output. UNLABELLED is the plain
|
|
272
|
+
// `trivia(oneOrMore(choice(ws, comment)))` that `examples/css/parser.ts`
|
|
273
|
+
// writes and that had no lowering at all. Mixing them is refused rather than
|
|
274
|
+
// half-labelled: `classifiedTrivia` requires a label per arm, so there is no
|
|
275
|
+
// rebuild that keeps both, and inventing labels would put names in the CST
|
|
276
|
+
// trivia log that the grammar never wrote.
|
|
277
|
+
const labelled: Array<readonly [string, string, string]> = []
|
|
278
|
+
const bare: Array<readonly [string, string]> = []
|
|
279
|
+
for (const arm of parsers) {
|
|
280
|
+
const a = arm._def
|
|
281
|
+
if (a.tag === 'regex') {
|
|
282
|
+
bare.push([a.source, a.flags])
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
if (a.tag !== 'label') return unlowered(`trivia arm is '${a.tag}', not a labelled arm or a regex`)
|
|
286
|
+
const body = a.parser._def
|
|
287
|
+
if (body.tag !== 'regex') return unlowered(`trivia arm ${JSON.stringify(a.label)} is '${body.tag}', not a regex`)
|
|
288
|
+
labelled.push([a.label, body.source, body.flags])
|
|
289
|
+
}
|
|
290
|
+
if (labelled.length > 0 && bare.length > 0) {
|
|
291
|
+
return unlowered('trivia arms are a MIX of labelled and bare regexes, which has no single rebuild')
|
|
292
|
+
}
|
|
293
|
+
if (labelled.length > 0) {
|
|
294
|
+
// `classifiedTrivia()` builds min-1 by construction, so a min-0 labelled
|
|
295
|
+
// body cannot be rebuilt through it and is not quietly promoted.
|
|
296
|
+
if (min !== 1) return unlowered(`labelled trivia arms need a min-1 repeat, got min ${min}`)
|
|
297
|
+
return { arms: labelled }
|
|
298
|
+
}
|
|
299
|
+
return { arms: [], alts: bare, min }
|
|
300
|
+
}
|
|
301
|
+
rules: Record<string, number> = {}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* THE COVERAGE DEFINITION POOL, in `plan.definitions` order, or `undefined`
|
|
305
|
+
* when this is an ordinary encode. Ordinary is the default and it must stay
|
|
306
|
+
* byte-for-byte the table it always was, so every coverage member is absent —
|
|
307
|
+
* not empty — unless a plan was passed.
|
|
308
|
+
*/
|
|
309
|
+
private readonly cov: (readonly [string, 0 | 1 | 2 | 3])[] | undefined
|
|
310
|
+
private readonly covIndex: ReadonlyMap<string, number>
|
|
311
|
+
readonly plan: GrammarCoveragePlan | undefined
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Wrap one site in its counter row.
|
|
315
|
+
*
|
|
316
|
+
* `id` comes from the plan and is therefore already a definition; an id the
|
|
317
|
+
* pool does not know is a plan/encoder disagreement and is refused rather than
|
|
318
|
+
* appended, because a hit on an id outside the denominator is a hit the
|
|
319
|
+
* collector silently drops (`createGrammarCoverageCollector.hit` tests `known`)
|
|
320
|
+
* — a site that reads as instrumented and counts nothing.
|
|
321
|
+
*/
|
|
322
|
+
private covWrap(child: number, id: string | undefined, on: 0 | 1): number {
|
|
323
|
+
if (id === undefined || this.cov === undefined) return child
|
|
324
|
+
const slot = this.covIndex.get(id)
|
|
325
|
+
if (slot === undefined) {
|
|
326
|
+
throw new UnsupportedConstruct(`coverage(id ${JSON.stringify(id)} is not in the plan's definitions)`)
|
|
327
|
+
}
|
|
328
|
+
return this.emit(OP_COV, child, slot, on)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private kIndex = new Map<unknown, number>()
|
|
332
|
+
private ccIndex = new Map<string, number>()
|
|
333
|
+
private fxIndex = new Map<string, number>()
|
|
334
|
+
/** Memoized by combinator identity: a shared sub-combinator is ONE row. */
|
|
335
|
+
memo = new Map<Combinator<unknown>, number>()
|
|
336
|
+
/**
|
|
337
|
+
* Memo for `lazy` REFERENCES, keyed by the trivia scope the reference is made
|
|
338
|
+
* from. A cross-rule reference's row depends on that scope (`scopedRef`), so the
|
|
339
|
+
* single `memo` slot every other construct uses would freeze the FIRST
|
|
340
|
+
* reference's scope onto every later one and undo the fix.
|
|
341
|
+
*/
|
|
342
|
+
refMemo = new Map<Combinator<unknown>, Map<Combinator<unknown> | undefined, number>>()
|
|
343
|
+
pending = new Map<Combinator<unknown>, number[]>()
|
|
344
|
+
|
|
345
|
+
readonly settings: TableSettings
|
|
346
|
+
/** Resolved once, HERE, at table-build time — never consulted at run time. */
|
|
347
|
+
readonly track: boolean
|
|
348
|
+
/**
|
|
349
|
+
* Decides the extra operands laid down below. ALWAYS TRUE — see
|
|
350
|
+
* `TableSettings.recovery`. Kept as a field rather than folded away because it
|
|
351
|
+
* is what every emit site reads, and the emit sites are where the shape of the
|
|
352
|
+
* row is stated.
|
|
353
|
+
*/
|
|
354
|
+
readonly rec: boolean
|
|
355
|
+
constructor(settings: TableSettings) {
|
|
356
|
+
this.settings = settings
|
|
357
|
+
this.track = settings.trackLines === true
|
|
358
|
+
this.rec = true
|
|
359
|
+
this.plan = settings.coverage
|
|
360
|
+
this.cov = settings.coverage === undefined
|
|
361
|
+
? undefined
|
|
362
|
+
: settings.coverage.definitions.map(d => [d.id, covKindCode(d.kind)] as const)
|
|
363
|
+
this.covIndex = new Map((this.cov ?? []).map((d, i) => [d[0], i]))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The class of the follow set of term `i` in a sequence — the sync sentinel a
|
|
368
|
+
* list nested in that term resyncs to, INFERRED from structure exactly as
|
|
369
|
+
* `sequence()` (combinators/sequence.ts:58) and `emitSeqValues`
|
|
370
|
+
* (compiler/codegen.ts:1756) infer it: the union of every LATER term's first
|
|
371
|
+
* set, not merely up to the first non-nullable one, so a mandatory middle term
|
|
372
|
+
* cannot hide a later close. −1 where there is no usable sentinel, which is the
|
|
373
|
+
* same set of cases `firstSetSentinel` answers `null` for.
|
|
374
|
+
*/
|
|
375
|
+
private followClass(parsers: readonly Combinator<unknown>[], i: number): number {
|
|
376
|
+
let fs: FirstSet = { kind: 'empty' }
|
|
377
|
+
const rr = this.refResolver()
|
|
378
|
+
for (let j = i + 1; j < parsers.length; j++) fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr))
|
|
379
|
+
return this.charClass(fs)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private constant(v: unknown): number {
|
|
383
|
+
const key = v instanceof RegExp ? `re:${JSON.stringify([v.source, v.flags])}` : v
|
|
384
|
+
const hit = this.kIndex.get(key)
|
|
385
|
+
if (hit !== undefined) return hit
|
|
386
|
+
const i = this.k.length
|
|
387
|
+
this.k.push(v)
|
|
388
|
+
this.kIndex.set(key, i)
|
|
389
|
+
return i
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* A `notAdjacent({ kinds })` filter, pooled by its CONTENTS.
|
|
394
|
+
*
|
|
395
|
+
* `constant()` keys an array by identity, and every `notAdjacent()` call
|
|
396
|
+
* builds a fresh one — so `calc()`'s four `{ kinds: ['whitespace'] }` sites
|
|
397
|
+
* would each park an identical array in the pool.
|
|
398
|
+
*/
|
|
399
|
+
private kindsIndex = new Map<string, number>()
|
|
400
|
+
private kindsSlot(kinds: readonly string[]): number {
|
|
401
|
+
const key = JSON.stringify(kinds)
|
|
402
|
+
const hit = this.kindsIndex.get(key)
|
|
403
|
+
if (hit !== undefined) return hit
|
|
404
|
+
const i = this.k.length
|
|
405
|
+
this.k.push([...kinds])
|
|
406
|
+
this.kindsIndex.set(key, i)
|
|
407
|
+
return i
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Park an author callback, WITH its source when the caller has it.
|
|
412
|
+
*
|
|
413
|
+
* `src` is required at every call site rather than optional so that adding a
|
|
414
|
+
* new `fn()`-bearing lowering forces a decision about its source instead of
|
|
415
|
+
* silently defaulting to "unprintable" — which reads as a working encode and
|
|
416
|
+
* emits a `() => {}` placeholder downstream.
|
|
417
|
+
*/
|
|
418
|
+
private fn(v: unknown, src: string | null): number {
|
|
419
|
+
const i = this.fns.length
|
|
420
|
+
this.fns.push(v)
|
|
421
|
+
this.fnSrcs.push(src)
|
|
422
|
+
return i
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private expected(list: readonly string[]): number {
|
|
426
|
+
const key = JSON.stringify(list)
|
|
427
|
+
const hit = this.fxIndex.get(key)
|
|
428
|
+
if (hit !== undefined) return hit
|
|
429
|
+
const i = this.fx.length
|
|
430
|
+
this.fx.push(list)
|
|
431
|
+
this.fxIndex.set(key, i)
|
|
432
|
+
return i
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* THE GATING ANALYSIS'S VIEW OF A CROSS-PIECE HOLE — `winners`, the same map
|
|
437
|
+
* emission already resolves those holes against.
|
|
438
|
+
*
|
|
439
|
+
* `firstSetOf` and `matchesEmpty` degrade a `lazy` whose thunk THROWS to `any`
|
|
440
|
+
* / nullable. A grammar loaded at runtime never hits that: `composeLeaf` binds
|
|
441
|
+
* every `g.X` before the encoder sees the map, so the thunks resolve. A grammar
|
|
442
|
+
* lowered by the MACRO does hit it — `plugin/evaluator.ts` mints an
|
|
443
|
+
* `externalRefs` slot for any `g.X` this `rules()` call does not itself define
|
|
444
|
+
* and never `.define()`s it, because the definition lives in another piece and
|
|
445
|
+
* is merged afterwards. The ref still ENCODES correctly (`case 'lazy'` resolves
|
|
446
|
+
* it through `winners` at `:1169`), so the artifact parses the same input — it
|
|
447
|
+
* simply parses it with the gate switched off at every choice arm that reaches
|
|
448
|
+
* a hole.
|
|
449
|
+
*
|
|
450
|
+
* Measured on jess's less grammar (`bench/jess/macro-program-diff.ts`): 195 of
|
|
451
|
+
* 562 reachable choice arms carried NO first set in the shipped artifact
|
|
452
|
+
* against 103 of 540 in the runtime encode, and 10 dispatch sites lost the O(1)
|
|
453
|
+
* `exclusive` piece (`assemble.ts:1758`) for the candidate-mask loop. With this
|
|
454
|
+
* resolver both figures land exactly on the runtime encode's: 103 and 41.
|
|
455
|
+
*
|
|
456
|
+
* SOUNDNESS. `firstSetOf` warns that deep resolution is sound only where refs
|
|
457
|
+
* are FINAL, because a compose OVERRIDE could widen a referenced rule's set
|
|
458
|
+
* after the fact. `winners` IS the final map — it is the merged rule map being
|
|
459
|
+
* encoded, the one `case 'lazy'` points the arm's CALL at. Gating an arm on the
|
|
460
|
+
* first set of the very row it will call cannot skip a match that row would
|
|
461
|
+
* accept. And this only ever runs in the CATCH branch: where a thunk resolves,
|
|
462
|
+
* nothing consults the resolver and nothing changes, which is why the runtime
|
|
463
|
+
* leg's counts are byte-for-byte what they were.
|
|
464
|
+
*/
|
|
465
|
+
private refResolver(): RefResolver | undefined {
|
|
466
|
+
const w = this.winners
|
|
467
|
+
return w === undefined ? undefined : (name: string) => w[name]
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** A first set becomes a char class string, or −1 for `any`. */
|
|
471
|
+
private charClass(fs: FirstSet): number {
|
|
472
|
+
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
473
|
+
// `fromCharCode` truncates every endpoint to 16 bits. That silently turned
|
|
474
|
+
// U+1F600 into U+F600 in the dispatch table: `lead()` correctly produced the
|
|
475
|
+
// astral code point, but no encoded class could claim it. The shared codec
|
|
476
|
+
// keeps BMP endpoints compact and escapes astral endpoints unambiguously.
|
|
477
|
+
const spec = encodeClassSpec(fs.ranges)
|
|
478
|
+
const hit = this.ccIndex.get(spec)
|
|
479
|
+
if (hit !== undefined) return hit
|
|
480
|
+
const i = this.cc.length
|
|
481
|
+
this.cc.push(spec)
|
|
482
|
+
this.ccIndex.set(spec, i)
|
|
483
|
+
return i
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Wrap one choice arm in its `autoNot` checks (`OP_REJECT`).
|
|
488
|
+
*
|
|
489
|
+
* A `firstSet` check's class comes from `codesToFirstSet` (choice.ts), which
|
|
490
|
+
* only ever builds a non-empty `ranges` set over codes 1..127 — so `charClass`
|
|
491
|
+
* cannot answer −1 here. It is asserted rather than assumed, because a −1 would
|
|
492
|
+
* index past `cc` and silently stop rejecting.
|
|
493
|
+
*/
|
|
494
|
+
private reject(child: number, checks: readonly AutoNotCheck[]): number {
|
|
495
|
+
const ops: number[] = []
|
|
496
|
+
for (const c of checks) {
|
|
497
|
+
if (c.kind === 'startsWith') { ops.push(0, this.constant(c.value)); continue }
|
|
498
|
+
const cls = this.charClass(c.set)
|
|
499
|
+
if (cls < 0) throw new UnsupportedConstruct('choice(autoNot: unmappable first set)')
|
|
500
|
+
ops.push(1, cls)
|
|
501
|
+
}
|
|
502
|
+
const head = this.emitHead(OP_REJECT, 2 + ops.length)
|
|
503
|
+
this.code[head + 1] = child
|
|
504
|
+
this.code[head + 2] = ops.length >> 1
|
|
505
|
+
for (let i = 0; i < ops.length; i++) this.code[head + 3 + i] = ops[i]!
|
|
506
|
+
return head
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private emit(...words: number[]): number {
|
|
510
|
+
const ip = this.code.length
|
|
511
|
+
for (const w of words) this.code.push(w)
|
|
512
|
+
return ip
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Reserve `n` operand slots to patch once children are laid out. */
|
|
516
|
+
private emitHead(op: number, n: number): number {
|
|
517
|
+
const ip = this.code.length
|
|
518
|
+
this.code.push(op)
|
|
519
|
+
for (let i = 0; i < n; i++) this.code.push(0)
|
|
520
|
+
return ip
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* A combinator carried as a table SUBTREE plus its first set.
|
|
525
|
+
*
|
|
526
|
+
* The first set is not decoration: `buildBalancedInterior` reads each skipper's
|
|
527
|
+
* to decide whether the interior content run can be one bounded regex
|
|
528
|
+
* (scanTo.ts:280). A reference that reported `any` would rebuild a different —
|
|
529
|
+
* one-character-at-a-time — interior than the grammar's own.
|
|
530
|
+
*/
|
|
531
|
+
private subtree(c: Combinator<unknown>): SubtreeRef {
|
|
532
|
+
const ip = this.node(c).ip
|
|
533
|
+
const fs = firstSetOf(c, new Set(), this.refResolver())
|
|
534
|
+
return [ip, fs.kind === 'empty' ? -2 : this.charClass(fs)]
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Pool one scan spec. One per combinator: `node()` memoizes by identity. */
|
|
538
|
+
private scanSlot(spec: ScanSpec): number {
|
|
539
|
+
const i = this.scans.length
|
|
540
|
+
this.scans.push(spec)
|
|
541
|
+
return i
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private scanSkipSlot(set: readonly Combinator<unknown>[]): number {
|
|
545
|
+
const hit = this.scanSkipIndex.get(set)
|
|
546
|
+
if (hit !== undefined) return hit
|
|
547
|
+
const i = this.scanSkipSets.length
|
|
548
|
+
// Reserve the slot BEFORE encoding: an ambient skipper can be a `balanced()`
|
|
549
|
+
// whose own encoding reaches back here through the same array.
|
|
550
|
+
this.scanSkipIndex.set(set, i)
|
|
551
|
+
this.scanSkipSets.push([])
|
|
552
|
+
this.scanSkipSets[i] = set.map(c => this.subtree(c))
|
|
553
|
+
return i
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Take trivia labels / classification off one carrier, first one wins. */
|
|
557
|
+
private carryTriviaMeta(c: Combinator<unknown> | undefined): void {
|
|
558
|
+
if (c === undefined) return
|
|
559
|
+
this.labels ??= c._meta.triviaKindLabels
|
|
560
|
+
if (c._meta.rootTriviaClassified === true) this.classified = true
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
encodeRule(name: string, p: Combinator<unknown>): void {
|
|
564
|
+
// `rules({ trivia }, …)` is ambient for the whole rule body, so it is in
|
|
565
|
+
// scope for a `notAdjacent({ kinds })` anywhere inside it — see the same
|
|
566
|
+
// save/restore in `case 'grammar'`.
|
|
567
|
+
this.activeTrivia = p._meta.grammarTrivia
|
|
568
|
+
const body = this.node(p).ip
|
|
569
|
+
this.activeTrivia = undefined
|
|
570
|
+
// GRAMMAR-LEVEL AMBIENT TRIVIA (`rules({ trivia }, …)`).
|
|
571
|
+
//
|
|
572
|
+
// `run()` installs this from `entry._meta.grammarTrivia`, but ONLY for a
|
|
573
|
+
// combinator entry — a compiled entry is a FUNCTION and is expected to have
|
|
574
|
+
// baked it in (src/functional/run.ts:269). A table entry is also a function,
|
|
575
|
+
// so without this the whole grammar parses with NO ambient trivia and every
|
|
576
|
+
// whitespace-bearing input silently fails to match the same way in every
|
|
577
|
+
// path — which makes a three-way identity check AGREE while proving nothing.
|
|
578
|
+
// Bake it, exactly as the compiled path does.
|
|
579
|
+
// AMBIENT `scanSkip`, PER RULE — the granularity `run()` uses, because it
|
|
580
|
+
// reads the ENTRY rule's own `_meta.grammarScanSkip` (grammar.ts:203). It is
|
|
581
|
+
// stamped by `rules({ scanSkip }, …)` on that map's rules only
|
|
582
|
+
// (parser.ts:210); a `parser()` scope has no `scanSkip` at all, so a rule is
|
|
583
|
+
// the finest scope that can carry one. In a `composeLeaf` grammar the pieces
|
|
584
|
+
// disagree — 67 of jess's 195 css rules carry no set — and taking the first
|
|
585
|
+
// one for the whole program handed those entries a skip list the interpreter
|
|
586
|
+
// does not give them.
|
|
587
|
+
const gss = p._meta.grammarScanSkip
|
|
588
|
+
this.scanSkipOf.push(gss === undefined ? -1 : this.scanSkipSlot(gss))
|
|
589
|
+
const amb = p._meta.grammarTrivia
|
|
590
|
+
// Carried so `tableRules` can stamp the entry — see TableProgram.labels.
|
|
591
|
+
//
|
|
592
|
+
// THREE PLACES, because that is where `run()` looks (`triviaKindLabelsFromRunnable`
|
|
593
|
+
// / `rootTriviaClassifiedFromRunnable`, functional/run.ts): `rules({ trivia }, …)`
|
|
594
|
+
// leaves it on `_meta.grammarTrivia`, `classifiedTrivia()` leaves it on the
|
|
595
|
+
// combinator's OWN `_meta`, and `parser({ trivia }, …)` leaves it on
|
|
596
|
+
// `_def.triviaParser` with nothing on `_meta` at all.
|
|
597
|
+
//
|
|
598
|
+
// Reading only the first meant a table lowered from a `parser({ trivia:
|
|
599
|
+
// classifiedTrivia(…) }, …)` root PARSED correctly and then reported no
|
|
600
|
+
// labels, so `run({ rootTrivia: { select } })` rejected a grammar that plainly
|
|
601
|
+
// has labelled trivia — and a bare `classifiedTrivia()` passed as
|
|
602
|
+
// `options.trivia` lost its labels the same way. Neither is visible to a
|
|
603
|
+
// value-identity sweep: the tree is the same, only the metadata is gone.
|
|
604
|
+
this.carryTriviaMeta(amb)
|
|
605
|
+
this.carryTriviaMeta(p)
|
|
606
|
+
if (p._def.tag === 'grammar') this.carryTriviaMeta(p._def.triviaParser)
|
|
607
|
+
// `body` is ALREADY the counter row when this rule has one — `node()` wraps
|
|
608
|
+
// it there, not here. Wrapping the rule ENTRY instead would credit the rule
|
|
609
|
+
// only when it is the start rule: an internal `g.X` reference resolves
|
|
610
|
+
// straight to the body's offset (`case 'lazy'`), so every recursive call and
|
|
611
|
+
// every cross-rule reference would jump past the counter, and a grammar with
|
|
612
|
+
// one entry would report one hit rule however much of it ran.
|
|
613
|
+
this.rules[name] = amb === undefined ? body : this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), body)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* The row a cross-rule `g.X` reference jumps to — the target's body, RE-WRAPPED
|
|
618
|
+
* in the target rule's own ambient trivia when that differs from the trivia
|
|
619
|
+
* lexically active at the reference.
|
|
620
|
+
*
|
|
621
|
+
* `encodeRule` wraps a rule ENTRY in `OP_SCOPE`, but only the entry: a `g.X`
|
|
622
|
+
* reference resolves straight to the body's offset and jumps past it (the note
|
|
623
|
+
* at `encodeRule`). So a rule referenced from inside a `noTrivia(...)` region ran
|
|
624
|
+
* with NO ambient trivia of its own — the table scoped trivia DYNAMICALLY, by
|
|
625
|
+
* caller, where codegen scopes it LEXICALLY, per rule, by binding each rule's
|
|
626
|
+
* trivia scanner at compile time.
|
|
627
|
+
*
|
|
628
|
+
* jess's Less grammar sits on that seam: `StandardDeclaration` wraps its value in
|
|
629
|
+
* `noTrivia(...)` while the `!important` tail lives in the referenced rule
|
|
630
|
+
* `ValueListWithPriority`, so `color: red !important` could not cross the space
|
|
631
|
+
* before `!` — and a 107 KB stylesheet stopped at 68.5% reporting `ok: true`.
|
|
632
|
+
*
|
|
633
|
+
* ONLY WHERE THE SCOPE WAS CLEARED. `activeTrivia` is the encoder's lexical scope
|
|
634
|
+
* tracker (maintained by `case 'grammar'` across `parser()` / `noTrivia`), and a
|
|
635
|
+
* reference made under ANY live trivia stays a bare jump, so the table gains no
|
|
636
|
+
* rows anywhere except inside a cleared region. The broader form — restore
|
|
637
|
+
* wherever the scope merely DIFFERS — also overrides a caller that set its own
|
|
638
|
+
* live trivia, and that regressed `@less/test-data`'s `selectors.less` from 3791
|
|
639
|
+
* bytes to 1784 while repairing nothing extra.
|
|
640
|
+
*
|
|
641
|
+
* ONLY WHERE THE TARGET HAS A BOUNDARY TO REPAIR — `hasOwnTriviaBoundary`, the
|
|
642
|
+
* same gate `ref.ts` applies, because the two engines must ask the same question
|
|
643
|
+
* or they parse different languages again. A rule whose body is a bare
|
|
644
|
+
* alternation, a dispatch, or a single terminal never consults an ambient
|
|
645
|
+
* scanner itself, so the scope row cannot repair it and only leaks past its arms.
|
|
646
|
+
* jess's SCSS `MathUnary` — `choice(noTrivia(…), noTrivia(…), g.ValueAtom)` — is
|
|
647
|
+
* exactly that shape: the row it gained handed its third arm a live scope, and
|
|
648
|
+
* `KeywordOrInterpolatedValue`'s `many()` then skipped the space between two
|
|
649
|
+
* identifiers. `gen-workload.scss` stopped at byte 218 of 287543 and `a{b: c d}`
|
|
650
|
+
* silently produced the ONE keyword `bc`.
|
|
651
|
+
*/
|
|
652
|
+
private scopedRef(p: Combinator<unknown>, target: Combinator<unknown>): number {
|
|
653
|
+
const ip = this.node(target).ip
|
|
654
|
+
// `rules()` stamps `grammarTrivia` on the map entry, which for a proxied rule
|
|
655
|
+
// is the REF itself; a composed `winners` entry carries it on the target.
|
|
656
|
+
const amb = p._meta.grammarTrivia ?? target._meta.grammarTrivia
|
|
657
|
+
if (amb === undefined || this.activeTrivia !== undefined) return ip
|
|
658
|
+
if (!hasOwnTriviaBoundary(target)) return ip
|
|
659
|
+
return this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), ip)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
node(p: Combinator<unknown>): Emitted {
|
|
663
|
+
// See `refMemo`: a reference is memoised per REFERENCING trivia scope, every
|
|
664
|
+
// other construct once for the program.
|
|
665
|
+
const lazyRef = (p._def as ParserDef).tag === 'lazy'
|
|
666
|
+
const scopeKey = lazyRef ? this.activeTrivia : undefined
|
|
667
|
+
const refMemo = this.refMemo
|
|
668
|
+
const hit = lazyRef ? refMemo.get(p)?.get(scopeKey) : this.memo.get(p)
|
|
669
|
+
if (hit !== undefined) return { ip: hit }
|
|
670
|
+
// Recursion: reserve a trampoline row now, patch its target when the real
|
|
671
|
+
// body lands. One extra indirection per recursive rule, zero per call site.
|
|
672
|
+
const inFlight = this.pending.get(p)
|
|
673
|
+
if (inFlight !== undefined) {
|
|
674
|
+
const ip = this.emitHead(OP_RULE, 1)
|
|
675
|
+
inFlight.push(ip + 1)
|
|
676
|
+
return { ip }
|
|
677
|
+
}
|
|
678
|
+
const patches: number[] = []
|
|
679
|
+
this.pending.set(p, patches)
|
|
680
|
+
// A RULE IS CREDITED ON ENTRY, not on success — `codegen.ts:4529` emits the
|
|
681
|
+
// hook as the first statement of the named function body, so a rule that is
|
|
682
|
+
// reached and then fails still counts as exercised. The counter goes on the
|
|
683
|
+
// MEMOISED offset, which is the one every reference to this combinator lands
|
|
684
|
+
// on, and the recursion trampolines below are patched with it too.
|
|
685
|
+
const ip = this.covWrap(this.encodeDef(p), this.plan?.rules.get(p), 0)
|
|
686
|
+
this.pending.delete(p)
|
|
687
|
+
if (lazyRef) {
|
|
688
|
+
let byScope = refMemo.get(p)
|
|
689
|
+
if (byScope === undefined) { byScope = new Map(); refMemo.set(p, byScope) }
|
|
690
|
+
byScope.set(scopeKey, ip)
|
|
691
|
+
} else {
|
|
692
|
+
this.memo.set(p, ip)
|
|
693
|
+
}
|
|
694
|
+
for (const slot of patches) {
|
|
695
|
+
// A TRAMPOLINE PATCHED TO ITSELF IS NOT A TABLE. `OP_RULE ip → ip` makes no
|
|
696
|
+
// progress in either driver: `exec.ts`'s `case OP_RULE` recurses on the same
|
|
697
|
+
// `ip`, and `assemble.ts` builds a piece that calls itself, so the first byte
|
|
698
|
+
// of the first file is `Maximum call stack size exceeded`. It is produced
|
|
699
|
+
// when a combinator's whole encoding emits NO row of its own and resolves
|
|
700
|
+
// back to the trampoline reserved for it — an ALIAS CYCLE, never a grammar
|
|
701
|
+
// this table can run. Raised here, at the one line that can write it, so the
|
|
702
|
+
// encoder names the construct instead of every parse dying at run time.
|
|
703
|
+
if (slot - 1 === ip) {
|
|
704
|
+
const rule = (p as unknown as { _ruleName?: string })._ruleName
|
|
705
|
+
throw new Error(
|
|
706
|
+
`table lowering: alias cycle — ${p._tag}${rule === undefined ? '' : ` (rule '${rule}')`} encodes to its own recursion trampoline at ${ip}`,
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
this.code[slot] = ip
|
|
710
|
+
}
|
|
711
|
+
return { ip }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private encodeDef(p: Combinator<unknown>): number {
|
|
715
|
+
const d = p._def as ParserDef
|
|
716
|
+
// THE SCANNING CONSTRUCTS, as their constructor arguments.
|
|
717
|
+
//
|
|
718
|
+
// Neither is recoverable from `_def` alone — `balanced()` overrides `.parse`
|
|
719
|
+
// and leaves `_def` as its EAGER interior, so a structural encoding builds a
|
|
720
|
+
// parser that skips the wrong things and reports nothing. Both are fully
|
|
721
|
+
// described by what they were CONSTRUCTED with, which is what `ScanSpec`
|
|
722
|
+
// carries; `resolveTable` hands that back to `scanTo()`/`balanced()`.
|
|
723
|
+
//
|
|
724
|
+
// The `balanced()` test reads `_balancedSpec` on the object `balanced()`
|
|
725
|
+
// RETURNS. That object is a `token()` and the ambient marker lives on the
|
|
726
|
+
// combinator INSIDE it, so testing `_balancedAmbient` here would either miss
|
|
727
|
+
// the construct or reach past the `token()` that gives it its one leaf.
|
|
728
|
+
const bal = (p as BalancedSpec)._balancedSpec
|
|
729
|
+
if (bal !== undefined) return this.emit(OP_SCAN, this.scanSlot({
|
|
730
|
+
kind: 1,
|
|
731
|
+
flags: (bal.raw ? 1 : 0) | (bal.strict ? 4 : 0),
|
|
732
|
+
skip: bal.ownSkip.map(c => this.subtree(c)),
|
|
733
|
+
open: bal.open,
|
|
734
|
+
close: bal.close,
|
|
735
|
+
}))
|
|
736
|
+
if (d.tag === 'scanTo') {
|
|
737
|
+
const sentDef = d.sentinel._def
|
|
738
|
+
return this.emit(OP_SCAN, this.scanSlot({
|
|
739
|
+
kind: 0,
|
|
740
|
+
flags: (d.raw ? 1 : 0) | (d.orEOF ? 2 : 0),
|
|
741
|
+
skip: d.skip.map(c => this.subtree(c)),
|
|
742
|
+
sentinel: this.subtree(d.sentinel),
|
|
743
|
+
sent: sentDef.tag === 'literal' ? sentDef.value : null,
|
|
744
|
+
}))
|
|
745
|
+
}
|
|
746
|
+
switch (d.tag) {
|
|
747
|
+
case 'literal': {
|
|
748
|
+
if (d.caseInsensitive) {
|
|
749
|
+
return this.emit(this.track ? OP_LIT_CI_TRACK : OP_LIT_CI, this.constant(d.value), this.expected(deriveExpected(p)))
|
|
750
|
+
}
|
|
751
|
+
return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(deriveExpected(p)))
|
|
752
|
+
}
|
|
753
|
+
case 'regex': {
|
|
754
|
+
const flags = d.flags.includes('y') ? d.flags : `${d.flags}y`
|
|
755
|
+
return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(deriveExpected(p)))
|
|
756
|
+
}
|
|
757
|
+
case 'keywords': {
|
|
758
|
+
// `keywords()` compiles to ONE sticky regex and pushes a leaf — which is
|
|
759
|
+
// exactly what OP_RX does. Rebuilding the same regex here needs no new
|
|
760
|
+
// opcode. Construction mirrors src/combinators/keywords.ts:87-106; the
|
|
761
|
+
// words arrive already sorted longest-first on the def.
|
|
762
|
+
const alt = d.words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
|
|
763
|
+
const boundary = d.boundary ? `(?![${d.boundary}])` : ''
|
|
764
|
+
const flags = d.caseInsensitive ? 'iy' : 'uy'
|
|
765
|
+
return this.emit(
|
|
766
|
+
this.track ? OP_RX_TRACK : OP_RX,
|
|
767
|
+
this.constant(new RegExp(`(?:${alt})${boundary}`, flags)),
|
|
768
|
+
// `['keyword']`, NOT `deriveExpected(p)`. `keywords.ts:137` reports the
|
|
769
|
+
// one word `keyword` — the whole point of the combinator is that a
|
|
770
|
+
// failing keyword arm names the CATEGORY rather than reciting every
|
|
771
|
+
// literal in the family. `deriveExpected` reports the literals, so the
|
|
772
|
+
// table said `'"media"'` where both other engines say `'keyword'`.
|
|
773
|
+
this.expected(['keyword']),
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
case 'expect': {
|
|
777
|
+
// `expect()` NEVER fails: on a failed child it yields a ParseError value
|
|
778
|
+
// at zero width. That is a distinct outcome, so it gets its own row.
|
|
779
|
+
return this.emit(OP_EXPECT, this.node(d.parser).ip, this.expected([...d.expected]))
|
|
780
|
+
}
|
|
781
|
+
case 'sequence': {
|
|
782
|
+
const kids = d.parsers.map(c => this.node(c).ip)
|
|
783
|
+
// A RECOVERY table appends one follow-set class per term AFTER the child
|
|
784
|
+
// slots. Appending keeps every existing read (`ip + 2 + i`) and the
|
|
785
|
+
// `collapseIndirection` slot map exactly as they were, so a strict table
|
|
786
|
+
// is word-for-word the table it always was.
|
|
787
|
+
const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + kids.length + (this.rec ? kids.length : 0))
|
|
788
|
+
this.code[head + 1] = kids.length
|
|
789
|
+
for (let i = 0; i < kids.length; i++) this.code[head + 2 + i] = kids[i]!
|
|
790
|
+
if (this.rec) {
|
|
791
|
+
for (let i = 0; i < kids.length; i++) this.code[head + 2 + kids.length + i] = this.followClass(d.parsers, i)
|
|
792
|
+
}
|
|
793
|
+
return head
|
|
794
|
+
}
|
|
795
|
+
case 'choice': {
|
|
796
|
+
// `greedyClassify` is NOT a choice at all — one arm runs and another arm
|
|
797
|
+
// is credited — so it gets its own row rather than an arm ordering.
|
|
798
|
+
if (d.strategy.tag === 'greedyClassify') {
|
|
799
|
+
const superIndex = d.strategy.superIndex
|
|
800
|
+
const sup = this.node(d.parsers[superIndex]!).ip
|
|
801
|
+
// Same construction as `choice()`'s own `greedyLitMap` (choice.ts:64-70):
|
|
802
|
+
// every NON-super arm that has a core literal value, keyed by that value.
|
|
803
|
+
// An arm without one is unreachable through classification and is left
|
|
804
|
+
// to the super arm exactly as the interpreter leaves it.
|
|
805
|
+
// COVERAGE CREDITS THE CLASSIFIED ARM AND NOT THE SUPER ARM, which is
|
|
806
|
+
// what the interpreter reference does: `coverage.ts:230` suppresses the
|
|
807
|
+
// per-arm hit for a `greedyClassify` site precisely because the super
|
|
808
|
+
// arm runs as an implementation detail, and only the arm the
|
|
809
|
+
// classification SELECTS is a semantic winner. Here the selected arm is
|
|
810
|
+
// re-run (`assemble.ts` `OP_GREEDY`), so wrapping it is exact.
|
|
811
|
+
//
|
|
812
|
+
// THE SUPER ARM'S OWN ID IS NOT INSTRUMENTED, and that is a stated gap,
|
|
813
|
+
// not an oversight: it is credited when classification finds nothing,
|
|
814
|
+
// and that outcome is decided INSIDE the greedy row rather than by any
|
|
815
|
+
// child of it. Instrumenting the super arm's offset would credit it on
|
|
816
|
+
// every invocation, including the ones the interpreter credits to a
|
|
817
|
+
// literal arm — an over-count, which inflates the ratio. Leaving it
|
|
818
|
+
// uninstrumented under-counts, which is the direction every other
|
|
819
|
+
// coverage decision in this codebase fails in.
|
|
820
|
+
const covIds = this.plan?.choices.get(p)
|
|
821
|
+
const pairs: number[] = []
|
|
822
|
+
for (let i = 0; i < d.parsers.length; i++) {
|
|
823
|
+
if (i === superIndex) continue
|
|
824
|
+
const lit = getCoreLiteralValue(d.parsers[i]!)
|
|
825
|
+
if (lit === null) continue
|
|
826
|
+
pairs.push(this.constant(lit), this.covWrap(this.node(d.parsers[i]!).ip, covIds?.[i], 1))
|
|
827
|
+
}
|
|
828
|
+
const head = this.emitHead(OP_GREEDY, 2 + pairs.length)
|
|
829
|
+
this.code[head + 1] = sup
|
|
830
|
+
this.code[head + 2] = pairs.length >> 1
|
|
831
|
+
for (let i = 0; i < pairs.length; i++) this.code[head + 3 + i] = pairs[i]!
|
|
832
|
+
return head
|
|
833
|
+
}
|
|
834
|
+
// Arm ORDER is semantics. `literalsLongestFirst` / `sharedPrefix` reorder
|
|
835
|
+
// the arms, so encoding them as a declared-order choice would pick a
|
|
836
|
+
// different arm and build a different tree while still parsing. ORDER is
|
|
837
|
+
// the only thing they change, though, and order is table data:
|
|
838
|
+
// `literalsLongestFirst` carries its order explicitly, and `sharedPrefix`
|
|
839
|
+
// is documented in choice.ts:52 as a firstMatch specialization, so
|
|
840
|
+
// declared order is already correct for it.
|
|
841
|
+
const order = d.strategy.tag === 'literalsLongestFirst'
|
|
842
|
+
? d.strategy.sortedIndices
|
|
843
|
+
: d.parsers.map((_, i) => i)
|
|
844
|
+
const arms = order.map(i => d.parsers[i]!)
|
|
845
|
+
// `autoNot` rejects an arm that ALREADY MATCHED when a later arm would
|
|
846
|
+
// have consumed more, so a matched arm loses. It wraps the arm's row and
|
|
847
|
+
// does not touch the arm's identity in `memo`: the same combinator used
|
|
848
|
+
// at a site with no `autoNot` still reaches the bare row.
|
|
849
|
+
//
|
|
850
|
+
// A GATED arm (`choice({ gate, combinator })`) is wrapped OUTSIDE its
|
|
851
|
+
// `autoNot` wrapper, mirroring the interpreter's order: the gate is
|
|
852
|
+
// consulted before the arm runs (choice.ts:150) and `autoNot` only after
|
|
853
|
+
// it has already matched (choice.ts:160-164). Wrapping — rather than
|
|
854
|
+
// splicing the predicate into the arm — is what preserves the arm's own
|
|
855
|
+
// first set for `classes` below, and with it the site's O(1) dispatch.
|
|
856
|
+
// ARM IDS ARE INDEXED BY DECLARED POSITION, not by encoded position:
|
|
857
|
+
// `buildGrammarPlan` mints `choice:…/arm:i` off `def.parsers`, and `order`
|
|
858
|
+
// is a permutation of that. `src` is the declared index, so the id and the
|
|
859
|
+
// `autoNot`/`gates` entries are all taken with the same subscript.
|
|
860
|
+
const armCovIds = this.plan?.choices.get(p)
|
|
861
|
+
const kids = arms.map((c, k) => {
|
|
862
|
+
const src = order[k]!
|
|
863
|
+
const checks = d.autoNot[src]
|
|
864
|
+
const inner = this.node(c).ip
|
|
865
|
+
const ip = checks === null || checks === undefined || checks.length === 0
|
|
866
|
+
? inner
|
|
867
|
+
: this.reject(inner, checks)
|
|
868
|
+
const gate = d.gates[src]
|
|
869
|
+
const gated = gate === null || gate === undefined
|
|
870
|
+
? ip
|
|
871
|
+
: this.emit(OP_ARMGATE, this.fn(gate, d.gateSrcs?.[src] ?? null), ip, this.expected(deriveExpected(c)))
|
|
872
|
+
// OUTSIDE both wrappers, because an arm is credited once it has WON —
|
|
873
|
+
// after its gate admitted it and after `autoNot` declined to reject it.
|
|
874
|
+
// Inside either, a gate-rejected or auto-rejected arm would count as
|
|
875
|
+
// covered on a parse where the choice picked somebody else.
|
|
876
|
+
return this.covWrap(gated, armCovIds?.[src], 1)
|
|
877
|
+
})
|
|
878
|
+
// O(1) first-char dispatch is only SOUND when the arms are disjoint.
|
|
879
|
+
// With overlapping first sets, "the first arm whose class contains this
|
|
880
|
+
// char" is not "the first arm that matches": a `Keyword` arm whose first
|
|
881
|
+
// set over-approximates to include digits was selected ahead of `Num`
|
|
882
|
+
// for the input `0` — the parse still succeeded, and only the TREE moved.
|
|
883
|
+
// Disjointness is computed HERE, not read off `d.disjoint`. That flag is
|
|
884
|
+
// set when `choice()` is CONSTRUCTED, at which point `g.X` arms are
|
|
885
|
+
// unresolved refs whose first set is `any` — so every recursive grammar
|
|
886
|
+
// reports non-disjoint and loses its dispatch. (Codegen has the same
|
|
887
|
+
// problem and solves it the same way: a placeholder resolved at fuse.)
|
|
888
|
+
// EVERY choice now carries its arms' classes. The old code recorded them
|
|
889
|
+
// only when the whole site qualified for O(1) selection — all arms
|
|
890
|
+
// non-nullable, pairwise disjoint and mappable — and a single failure
|
|
891
|
+
// left the site with no gate of any kind, so all its arms were entered
|
|
892
|
+
// at every position it was reached.
|
|
893
|
+
//
|
|
894
|
+
// An arm's class is its OWN gate and is sound on its own: a
|
|
895
|
+
// non-nullable arm whose first set excludes the char at `pos` cannot
|
|
896
|
+
// match there, whatever the other arms do. `firstSetOf` already skips a
|
|
897
|
+
// leading zero-width assertion so `not(X) Y` reports firstSet(Y) rather
|
|
898
|
+
// than `any` (`first-set.ts` `isZeroWidthAssertion`), and `charClass`
|
|
899
|
+
// returns −1 for anything it cannot represent — both directions are the
|
|
900
|
+
// safe one. A NULLABLE arm is never gated: it can match at a position
|
|
901
|
+
// its first set does not contain, by consuming nothing.
|
|
902
|
+
//
|
|
903
|
+
// `resolveDispatch` decides from these classes whether the site keeps
|
|
904
|
+
// the O(1) table (`exclusive`) or falls to the ordered per-arm path.
|
|
905
|
+
// Arm ORDER is preserved on both, which is what makes this a PEG-safe
|
|
906
|
+
// change rather than a reordering.
|
|
907
|
+
const rr = this.refResolver()
|
|
908
|
+
const classes = arms.map(a => matchesEmpty(a, new Set(), rr) ? -1 : this.charClass(firstSetOf(a, new Set(), rr)))
|
|
909
|
+
const dispIdx = this.disp.length
|
|
910
|
+
this.disp.push(classes)
|
|
911
|
+
// PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. The ordered
|
|
912
|
+
// path reports the CONCATENATION of the arms' sets (`choice.ts:167`), and
|
|
913
|
+
// it must include the arms the driver's char gate declined to enter — the
|
|
914
|
+
// interpreter has no such gate, runs them, and gets exactly their static
|
|
915
|
+
// opener set back. Without these words a partially-gated site reported
|
|
916
|
+
// only the arms that happened to run.
|
|
917
|
+
const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
|
|
918
|
+
this.code[head + 1] = dispIdx
|
|
919
|
+
this.code[head + 2] = kids.length
|
|
920
|
+
// THE CHOICE'S OWN SET IS THE ARMS' FLATMAP, NOT `deriveExpected(p)`.
|
|
921
|
+
// `deriveExpected` DEDUPES, and neither engine does: `choice.ts:110-118`
|
|
922
|
+
// is `parsers.flatMap(p => p.parse(...).expected)`, so a shared-prefix
|
|
923
|
+
// choice whose two arms both open with `/::?/` reports it TWICE. The
|
|
924
|
+
// deduped union silently collapsed that to one.
|
|
925
|
+
this.code[head + 3] = this.expected(arms.flatMap(a => deriveExpected(a)))
|
|
926
|
+
for (let i = 0; i < kids.length; i++) {
|
|
927
|
+
this.code[head + 4 + i] = kids[i]!
|
|
928
|
+
this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
|
|
929
|
+
}
|
|
930
|
+
return head
|
|
931
|
+
}
|
|
932
|
+
case 'many':
|
|
933
|
+
case 'oneOrMore': {
|
|
934
|
+
const child = this.node(d.parser).ip
|
|
935
|
+
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
936
|
+
const min = d.tag === 'many' ? 0 : d.min
|
|
937
|
+
// ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
|
|
938
|
+
// class (−1: there is no separator). Both are only laid down for a
|
|
939
|
+
// recovery table, where the resync error's payload has to be the ITEM's
|
|
940
|
+
// set — `deriveExpected(combinator)` in repeat.ts:170, and
|
|
941
|
+
// `deriveExpectedArr([def.parser])` in codegen's emitMany.
|
|
942
|
+
return this.rec
|
|
943
|
+
? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), -1)
|
|
944
|
+
: this.emit(op, child, min, d.max ?? -1, -1, 0)
|
|
945
|
+
}
|
|
946
|
+
case 'sepBy': {
|
|
947
|
+
const child = this.node(d.parser).ip
|
|
948
|
+
const sep = this.node(d.separator).ip
|
|
949
|
+
// A list contributes its ITEMS and nothing else (release/0.47.0
|
|
950
|
+
// `7cb528e feat(lists)!`). The separator is demoted out of `children`
|
|
951
|
+
// after it matches unless the author opted in with `keepSeparator()`.
|
|
952
|
+
// Bit 2: an ITEM-expected fx index follows at `ip + 6`. A list that ends
|
|
953
|
+
// under `min` reports what would have let it CONTINUE — the ITEM, which
|
|
954
|
+
// is what `failAt` (repeat.ts) and `deriveExpectedArr([def.parser])`
|
|
955
|
+
// (codegen emitSepBy) both use; the driver otherwise reports whatever
|
|
956
|
+
// sub-parse failed last, which is the SEPARATOR. Only `min >= 2` can
|
|
957
|
+
// reach that end under `min` with items already taken — at `min === 1`
|
|
958
|
+
// it means the FIRST item failed, and that failure already set the
|
|
959
|
+
// item's own set. So no committed grammar grows by a word.
|
|
960
|
+
const flags = (d.trailing === 'allow' ? 1 : 0) | (d.keepSeparators === true ? 2 : 0) | (d.min >= 2 ? 4 : 0)
|
|
961
|
+
// A RECOVERY table always carries ip + 6 (the ITEM's expected set, which
|
|
962
|
+
// the resync error reports) and ip + 7 (the SEPARATOR's sentinel class, so
|
|
963
|
+
// a tolerant list resyncs to its own separator OR the enclosing
|
|
964
|
+
// delimiter — `orSentinel(separator, term)` in repeat.ts:454 and
|
|
965
|
+
// `_rec.or(sepSent, mySync)` in codegen's emitSepBy).
|
|
966
|
+
if (this.rec) {
|
|
967
|
+
return this.emit(
|
|
968
|
+
OP_REP, child, d.min, d.max ?? -1, sep, flags,
|
|
969
|
+
this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver())),
|
|
970
|
+
)
|
|
971
|
+
}
|
|
972
|
+
return d.min >= 2
|
|
973
|
+
? this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags, this.expected(deriveExpected(d.parser)))
|
|
974
|
+
: this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
|
|
975
|
+
}
|
|
976
|
+
case 'optional':
|
|
977
|
+
return this.emit(OP_OPT, this.node(d.parser).ip)
|
|
978
|
+
case 'transform': {
|
|
979
|
+
// Declared on the def and not lowered here. Refuse rather than assume it is
|
|
980
|
+
// inert: a recognition-only transform suppresses its value, and a table that
|
|
981
|
+
// produced one anyway would differ from both other engines.
|
|
982
|
+
// FUSE `transform(sequence(...))` — the dominant shape — into one row.
|
|
983
|
+
// Emitted separately it costs two dispatches and two call frames per
|
|
984
|
+
// rule invocation. The inner sequence must not be shared with anything
|
|
985
|
+
// else, or fusing it here would steal it from the other reference.
|
|
986
|
+
const inner = d.parser._def as ParserDef
|
|
987
|
+
if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
|
|
988
|
+
const kids = inner.parsers.map(c => this.node(c).ip)
|
|
989
|
+
const head = this.emitHead(OP_SEQX, 2 + kids.length + (this.rec ? kids.length : 0))
|
|
990
|
+
this.code[head + 1] = this.fn(d.fn, d.fnSrc ?? null)
|
|
991
|
+
this.code[head + 2] = kids.length
|
|
992
|
+
for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
|
|
993
|
+
if (this.rec) {
|
|
994
|
+
for (let i = 0; i < kids.length; i++) this.code[head + 3 + kids.length + i] = this.followClass(inner.parsers, i)
|
|
995
|
+
}
|
|
996
|
+
return head
|
|
997
|
+
}
|
|
998
|
+
const child = this.node(d.parser).ip
|
|
999
|
+
return this.emit(OP_XFORM, this.fn(d.fn, d.fnSrc ?? null), child)
|
|
1000
|
+
}
|
|
1001
|
+
case 'leaf': {
|
|
1002
|
+
const child = this.node(d.parser).ip
|
|
1003
|
+
return this.emit(OP_LEAF, this.fn(d.fn, d.fnSrc ?? null), child)
|
|
1004
|
+
}
|
|
1005
|
+
case 'node': {
|
|
1006
|
+
// A node legally has NO builder when its value comes from a selection:
|
|
1007
|
+
// `project` replaces the builder outright, and `collapse`/`unwrap` make
|
|
1008
|
+
// the single captured child the value. A node with none of those and no
|
|
1009
|
+
// builder is STRUCTURAL — its value comes from a `ctx.build` host.
|
|
1010
|
+
//
|
|
1011
|
+
// This USED to refuse, on the belief that the driver had no host. It
|
|
1012
|
+
// does: `assemble.ts` reads `ctx.build` once per parse in `begin()` and
|
|
1013
|
+
// bakes host-ness into which pieces the assembly holds, and `exec.ts`
|
|
1014
|
+
// has the same branch. The refusal was over-broad, not protective —
|
|
1015
|
+
// verified by differential against the interpreter, with a host and
|
|
1016
|
+
// without, on match and on failure.
|
|
1017
|
+
// An AUTHORING error, not a table gap — raise what the other two engines do.
|
|
1018
|
+
if (d.type === undefined) missingInferredType()
|
|
1019
|
+
const child = this.node(d.parser).ip
|
|
1020
|
+
// Capture flags, resolved HERE from the reducer's declared arity using the
|
|
1021
|
+
// same analysis codegen runs (`src/compiler/build-arity.ts`). `hostMode:
|
|
1022
|
+
// 'cst'` forces them on, exactly as the emitted `cstOut` path does. The
|
|
1023
|
+
// driver reads a bit; it re-derives nothing and sees no setting.
|
|
1024
|
+
const cstOut = this.settings.hostMode === 'cst'
|
|
1025
|
+
// THE THREE DERIVED CAPTURE BITS, mirroring `node.ts:215` term for term:
|
|
1026
|
+
//
|
|
1027
|
+
// capturesTrivia = captureTrivia || trailingTrivia
|
|
1028
|
+
// || (build ? buildReadsTrivia(def) : project === undefined)
|
|
1029
|
+
//
|
|
1030
|
+
// `cstOut` is the static stand-in for "a CST host is coming", which the
|
|
1031
|
+
// interpreter reaches dynamically off `ctx.build`.
|
|
1032
|
+
//
|
|
1033
|
+
// Two of these terms were MISSING and each was a real divergence:
|
|
1034
|
+
//
|
|
1035
|
+
// `captureTrivia` — an explicit request the arity analysis cannot
|
|
1036
|
+
// express (an author can ask for capture on a 3-argument reducer).
|
|
1037
|
+
// This used to REFUSE rather than lower, so a documented option no
|
|
1038
|
+
// grammar could use through the table.
|
|
1039
|
+
// `trailingTrivia` — trivia consumed INSIDE the node's capture scope
|
|
1040
|
+
// lands in THIS node's log. The interpreter counts it; the table did
|
|
1041
|
+
// not, so a node with `trailingTrivia` and a non-trivia-reading
|
|
1042
|
+
// reducer captured under the interpreter and dropped under the table.
|
|
1043
|
+
//
|
|
1044
|
+
// The `build ? … : project === undefined` split matters now that a
|
|
1045
|
+
// STRUCTURAL node lowers: with no builder the interpreter captures unless
|
|
1046
|
+
// a `project` replaces the value outright.
|
|
1047
|
+
const noBuildCaptures = d.project === undefined
|
|
1048
|
+
const derivedTrivia = d.build !== undefined ? buildReadsTrivia(d) : noBuildCaptures
|
|
1049
|
+
const derivedState = d.build !== undefined ? buildReadsState(d) : noBuildCaptures
|
|
1050
|
+
const derivedFields = d.build !== undefined ? buildReadsFields(d) : noBuildCaptures
|
|
1051
|
+
// Field capture additionally requires the body to CONTAIN `field()`
|
|
1052
|
+
// captures: a node that reads fields but has none allocates nothing.
|
|
1053
|
+
const wantsFields = parserHasOwnFields(d.parser) && (cstOut || derivedFields)
|
|
1054
|
+
const flags = (cstOut || d.captureTrivia === true || d.trailingTrivia === true || derivedTrivia ? 4 : 0)
|
|
1055
|
+
| (cstOut || derivedState ? 8 : 0)
|
|
1056
|
+
| (wantsFields ? 16 : 0)
|
|
1057
|
+
| (d.collapse === true ? 32 : 0)
|
|
1058
|
+
| (d.unwrap === true ? 64 : 0)
|
|
1059
|
+
| (d.trailingTrivia === true ? 128 : 0)
|
|
1060
|
+
// Distinguish grammar-owned capture from a structural node's default
|
|
1061
|
+
// capture. A runtime host predicate may narrow only the latter.
|
|
1062
|
+
| (d.captureTrivia === true ? 1 : 0)
|
|
1063
|
+
const body = this.emit(
|
|
1064
|
+
this.track ? OP_NODE_TRACK : OP_NODE,
|
|
1065
|
+
d.build === undefined ? -1 : this.fn(d.build, d.buildSrc ?? null),
|
|
1066
|
+
child, flags,
|
|
1067
|
+
d.project ?? -1,
|
|
1068
|
+
this.constant(d.type),
|
|
1069
|
+
// `tags` reaches a `ctx.build` host as its 8th argument. jess's
|
|
1070
|
+
// `cssCstBuildHost` is built with `{ tags: true }` and puts them on
|
|
1071
|
+
// every CST node, so this is load-bearing, not reflection trivia.
|
|
1072
|
+
d.tags === undefined ? -1 : this.constant(d.tags),
|
|
1073
|
+
)
|
|
1074
|
+
// The rule's own first-set gate — the emitted code's `_ngc` test, as data.
|
|
1075
|
+
// A NULLABLE rule has no gate: it succeeds on input its first set does
|
|
1076
|
+
// not contain (including EOF), so gating it would reject a legal empty
|
|
1077
|
+
// match. Caught by the ladder's `empty` and `garbage` cases.
|
|
1078
|
+
// THE BODY'S first set, not the node's. `node.ts:232` reads
|
|
1079
|
+
// `combinator._meta.firstSet` where `combinator` is the BODY; a `node()`
|
|
1080
|
+
// reports `any` for itself, so `firstSetOf(p)` was −1 here and NO node
|
|
1081
|
+
// ever got the gate the interpreter applies. That is not just a lost
|
|
1082
|
+
// fast path: when the guard fires it reports the STATIC
|
|
1083
|
+
// `deriveExpected(body)`, which for a body with nullable leading terms
|
|
1084
|
+
// names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
|
|
1085
|
+
// running the body reports `['"x"']` alone.
|
|
1086
|
+
if (matchesEmpty(d.parser)) return body
|
|
1087
|
+
const fs = d.parser._meta.firstSet
|
|
1088
|
+
if (fs.kind === 'any') return body
|
|
1089
|
+
const cls = this.charClass(fs)
|
|
1090
|
+
if (cls < 0) return body
|
|
1091
|
+
const e = deriveExpected(d.parser)
|
|
1092
|
+
return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
|
|
1093
|
+
}
|
|
1094
|
+
case 'token':
|
|
1095
|
+
return this.emit(OP_TOKEN, this.node(d.parser).ip)
|
|
1096
|
+
case 'routed':
|
|
1097
|
+
return this.emit(OP_ROUTED, d.fallback === undefined ? -1 : this.node(d.fallback).ip)
|
|
1098
|
+
case 'dispatch': {
|
|
1099
|
+
// A `routed()` in the SELECTOR is a hard error in the interpreter
|
|
1100
|
+
// (dispatch.ts:318) — it would ask for a token that has not been read yet.
|
|
1101
|
+
if (parserUsesRouted(d.selector as Combinator<unknown>)) {
|
|
1102
|
+
throw new UnsupportedConstruct('dispatch(routed() in selector)')
|
|
1103
|
+
}
|
|
1104
|
+
const sel = this.node(d.selector as Combinator<unknown>).ip
|
|
1105
|
+
// `dispatchArmIds` mints ids in RESOLUTION ORDER — every case (one id per
|
|
1106
|
+
// case, not per key), then every matcher, then `otherwise` — which is the
|
|
1107
|
+
// order the two loops below push arms in, with the fallback last. A
|
|
1108
|
+
// running index is what keeps the two in step; taking `ids[armIndex]`
|
|
1109
|
+
// inside the key loop would consume one id per KEY and slide every
|
|
1110
|
+
// subsequent arm onto somebody else's identity.
|
|
1111
|
+
const dispCovIds = this.plan?.dispatches.get(p)
|
|
1112
|
+
let dispCov = 0
|
|
1113
|
+
const arms: number[] = []
|
|
1114
|
+
const routed: number[] = []
|
|
1115
|
+
const key: string[] = [], keyArm: number[] = []
|
|
1116
|
+
const fold: string[] = [], foldArm: number[] = []
|
|
1117
|
+
const match: Array<readonly [number, string, string, number]> = []
|
|
1118
|
+
const expected: string[] = []
|
|
1119
|
+
// ARM ORDER IS RESOLUTION ORDER: exact key, then ASCII-folded key, then
|
|
1120
|
+
// matchers in declaration order, then otherwise. Mirrors dispatch.ts:325.
|
|
1121
|
+
for (const c of d.cases) {
|
|
1122
|
+
const arm = arms.length
|
|
1123
|
+
arms.push(this.covWrap(this.node(c.parser).ip, dispCovIds?.[dispCov++], 1))
|
|
1124
|
+
routed.push(branchUsesRouted(c) ? 1 : 0)
|
|
1125
|
+
for (const kk of c.keys) {
|
|
1126
|
+
expected.push(JSON.stringify(kk))
|
|
1127
|
+
if (c.caseInsensitive) { fold.push(asciiFoldKey(kk)); foldArm.push(arm) }
|
|
1128
|
+
else { key.push(kk); keyArm.push(arm) }
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const KIND = { startsWith: 0, endsWith: 1, matches: 2 } as const
|
|
1132
|
+
for (const m of d.matchers ?? []) {
|
|
1133
|
+
const arm = arms.length
|
|
1134
|
+
arms.push(this.covWrap(this.node(m.parser).ip, dispCovIds?.[dispCov++], 1))
|
|
1135
|
+
routed.push(branchUsesRouted(m) ? 1 : 0)
|
|
1136
|
+
// `{ caseInsensitive: true }` ON A MATCHER ARM IS FOLDED INTO THE
|
|
1137
|
+
// OPERANDS HERE, not tested per parse in the driver. It was dropped
|
|
1138
|
+
// entirely — both drivers built the matcher shape with a hardcoded
|
|
1139
|
+
// `caseInsensitive: false` — so `when(startsWith('pre'), …, { caseInsensitive: true })`
|
|
1140
|
+
// never claimed `PRElude` and the parse fell through to `otherwise()`.
|
|
1141
|
+
// A wrong ARM is a wrong parse, and no key-path test covered it because
|
|
1142
|
+
// the folded-KEY path (`fold`/`foldArm` above) is a different mechanism.
|
|
1143
|
+
//
|
|
1144
|
+
// `matches` folds into the regex's own `i` flag, which is what
|
|
1145
|
+
// `matchesDispatchMatcher` does (it tests the RAW value with `i`
|
|
1146
|
+
// appended). `startsWith`/`endsWith` fold BOTH sides, so the stored
|
|
1147
|
+
// value is pre-folded and kinds 3/4 tell the driver to fold the key.
|
|
1148
|
+
if (!m.caseInsensitive) { match.push([KIND[m.kind], m.value, m.flags ?? '', arm]); continue }
|
|
1149
|
+
if (m.kind === 'matches') {
|
|
1150
|
+
const f = m.flags ?? ''
|
|
1151
|
+
match.push([2, m.value, f.includes('i') ? f : `${f}i`, arm])
|
|
1152
|
+
continue
|
|
1153
|
+
}
|
|
1154
|
+
match.push([m.kind === 'startsWith' ? 3 : 4, asciiFoldKey(m.value), '', arm])
|
|
1155
|
+
}
|
|
1156
|
+
const other = d.otherwise === undefined
|
|
1157
|
+
? -1
|
|
1158
|
+
: this.covWrap(this.node(d.otherwise).ip, dispCovIds?.[dispCov++], 1)
|
|
1159
|
+
const dspIdx = this.dsp.length
|
|
1160
|
+
this.dsp.push({ key, keyArm, fold, foldArm, match, routed, expected })
|
|
1161
|
+
const head = this.emitHead(OP_DISPATCH, 5 + arms.length)
|
|
1162
|
+
this.code[head + 1] = sel
|
|
1163
|
+
this.code[head + 2] = dspIdx
|
|
1164
|
+
this.code[head + 3] = other
|
|
1165
|
+
// THE FALLBACK'S ROUTED BIT IS WALKED, NOT READ OFF THE DEF.
|
|
1166
|
+
//
|
|
1167
|
+
// `d.otherwiseUsesRouted` is computed when `otherwise()` is CONSTRUCTED
|
|
1168
|
+
// (dispatch.ts:189), and at that moment a `g.X` inside the arm is an
|
|
1169
|
+
// unresolved ref whose thunk throws — `parserUsesRouted` catches and
|
|
1170
|
+
// answers `false`. The interpreter never reads the stored flag alone: it
|
|
1171
|
+
// calls `branchUsesRouted` at PARSE time (dispatch.ts:335), which ORs the
|
|
1172
|
+
// flag with a live walk of the now-resolved graph.
|
|
1173
|
+
//
|
|
1174
|
+
// Reading only the flag here is why `@charset "UTF-8";` — one line of
|
|
1175
|
+
// plain CSS — failed under the table with `expected: ["routed()"]`: the
|
|
1176
|
+
// fallback ran at the selector's END with no routed token, so the
|
|
1177
|
+
// `routed()` inside it had nothing to yield. Every case arm was already
|
|
1178
|
+
// walked (`branchUsesRouted(c)` above); the fallback was the one branch
|
|
1179
|
+
// that was not.
|
|
1180
|
+
const otherRouted = d.otherwise === undefined
|
|
1181
|
+
? false
|
|
1182
|
+
: branchUsesRouted({ parser: d.otherwise as Combinator<unknown>, usesRouted: d.otherwiseUsesRouted === true })
|
|
1183
|
+
this.code[head + 4] = otherRouted ? 1 : 0
|
|
1184
|
+
this.code[head + 5] = arms.length
|
|
1185
|
+
for (let i = 0; i < arms.length; i++) this.code[head + 6 + i] = arms[i]!
|
|
1186
|
+
return head
|
|
1187
|
+
}
|
|
1188
|
+
case 'field':
|
|
1189
|
+
return this.emit(OP_FIELD, this.constant(d.name), this.node(d.parser).ip)
|
|
1190
|
+
case 'lazy': {
|
|
1191
|
+
// A named reference is not a hop: it resolves to the target's row.
|
|
1192
|
+
// Emitting a trampoline here cost one dispatch per reference for nothing.
|
|
1193
|
+
//
|
|
1194
|
+
// AN UNDEFINED `ref()` IS TOLERATED, not refused. `ref<T>()` throws from
|
|
1195
|
+
// its thunk until `.define()` runs, and codegen catches exactly this and
|
|
1196
|
+
// falls back to running the ref live (`emitLazy`'s two `catch` arms ->
|
|
1197
|
+
// `emitRuntimeFallback`), so a grammar assembled before every slot is
|
|
1198
|
+
// filled still COMPILES and throws only if that slot is actually parsed
|
|
1199
|
+
// through. The encoder threw at build time instead, which made the table
|
|
1200
|
+
// lowering refuse a grammar the source lowering accepts. Deferring to the
|
|
1201
|
+
// ref's own `.parse` reproduces codegen's timing and its message.
|
|
1202
|
+
// BY NAME first, when this reference names a rule of the map being
|
|
1203
|
+
// encoded — see `winners` for why a thunk is the wrong resolver for a
|
|
1204
|
+
// merged (composed) map, and why the identity guard is required.
|
|
1205
|
+
const refName = (p as unknown as { _ruleName?: string })._ruleName
|
|
1206
|
+
const winner = refName === undefined ? undefined : this.winners?.[refName]
|
|
1207
|
+
if (winner !== undefined && !wrapsRef(winner, p)) return this.scopedRef(p, winner)
|
|
1208
|
+
let resolved: Combinator<unknown>
|
|
1209
|
+
try { resolved = d.thunk() }
|
|
1210
|
+
catch {
|
|
1211
|
+
this.runtimeOnly.add('ref() used before .define() — the slot is run live')
|
|
1212
|
+
return this.emit(OP_LIVE, this.fn(p, null))
|
|
1213
|
+
}
|
|
1214
|
+
return this.scopedRef(p, resolved)
|
|
1215
|
+
}
|
|
1216
|
+
// `not.ts:50` fails with EXACTLY `not(<child tag>)`, at the assertion's own
|
|
1217
|
+
// position — the same shape `OP_PEEK` below carries. The table emitted no
|
|
1218
|
+
// expected operand at all, so a `not()` failure reported `[]` where every
|
|
1219
|
+
// other engine reported `['not(literal)']`. It hid because `expected` is a
|
|
1220
|
+
// TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
|
|
1221
|
+
case 'not':
|
|
1222
|
+
return this.emit(OP_NOT, this.node(d.parser).ip, this.expected([`not(${d.parser._tag})`]))
|
|
1223
|
+
case 'guard':
|
|
1224
|
+
// `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
|
|
1225
|
+
return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
|
|
1226
|
+
case 'adjacency':
|
|
1227
|
+
// A `kinds` LIST IS VALIDATED HERE, at encode time, against the trivia
|
|
1228
|
+
// table in scope — the same `resolveAdjacencyKindMask` the interpreter
|
|
1229
|
+
// calls on first reach and codegen calls while emitting the `_akN` probe
|
|
1230
|
+
// (`codegen.ts:1034`). Deferring it to the driver's parse-time resolution
|
|
1231
|
+
// left `compile()` accepting a grammar whose kind name no active table
|
|
1232
|
+
// declares: an unlabelled table or a typo'd category compiled clean and
|
|
1233
|
+
// threw only when an input happened to reach the assertion.
|
|
1234
|
+
if (d.kinds !== undefined) resolveAdjacencyKindMask(this.activeTrivia, d.kinds)
|
|
1235
|
+
// The expected label is `adjacencyExpected`'s, not `deriveExpected`'s:
|
|
1236
|
+
// the interpreter fails with exactly `adjacent` / `notAdjacent` /
|
|
1237
|
+
// `notAdjacent(a|b)` (adjacency.ts `adjacencyFail`) and codegen emits the
|
|
1238
|
+
// same string, so this is the one the identity sweep compares.
|
|
1239
|
+
return this.emit(
|
|
1240
|
+
OP_ADJ,
|
|
1241
|
+
d.polarity === 'notAdjacent' ? 1 : 0,
|
|
1242
|
+
d.kinds === undefined ? -1 : this.kindsSlot(d.kinds),
|
|
1243
|
+
this.expected([adjacencyExpected(d)]),
|
|
1244
|
+
)
|
|
1245
|
+
case 'withCtx': {
|
|
1246
|
+
// `extra` is arbitrary user data in the const pool. `emitConst` takes
|
|
1247
|
+
// scalars, arrays of scalars, and plain objects of those — anything
|
|
1248
|
+
// richer (a class instance, a function value) RUNS fine but cannot be
|
|
1249
|
+
// printed into a module. Record it as a runtime-only reason so emit
|
|
1250
|
+
// refuses BY NAME rather than throwing a raw TypeError from the printer.
|
|
1251
|
+
if (!emittableConst(d.extra)) {
|
|
1252
|
+
this.runtimeOnly.add('withCtx(extra) — the state object is not serialisable')
|
|
1253
|
+
}
|
|
1254
|
+
return this.emit(OP_WITHCTX, this.constant(d.extra), this.node(d.parser).ip)
|
|
1255
|
+
}
|
|
1256
|
+
case 'peek':
|
|
1257
|
+
// The expected set is the ASSERTION's, not the body's: `peek.ts:60`
|
|
1258
|
+
// reports `peek(<child tag>)` and DISCARDS what the body wanted — a
|
|
1259
|
+
// lookahead's failure is "the guard did not hold", and naming the body's
|
|
1260
|
+
// internals offers a token the parse never asked the author for. The
|
|
1261
|
+
// table propagated the body's set instead.
|
|
1262
|
+
return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected([`peek(${d.parser._tag})`]))
|
|
1263
|
+
// A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
|
|
1264
|
+
// lowering was correct only for a choice arm.
|
|
1265
|
+
case 'attempt': {
|
|
1266
|
+
const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip)
|
|
1267
|
+
// THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
|
|
1268
|
+
// case already uses — it is the same guard, written twice in the
|
|
1269
|
+
// interpreter (`attempt.ts:22`, `node.ts:239`). It is NOT merely an
|
|
1270
|
+
// optimisation: when it fires it reports the STATIC `deriveExpected` of
|
|
1271
|
+
// the inner, which for a body with nullable leading terms names tokens the
|
|
1272
|
+
// run itself would have skipped past. Running the inner instead reported a
|
|
1273
|
+
// strictly smaller set, so omitting the guard was an observable divergence,
|
|
1274
|
+
// not a slower equivalent.
|
|
1275
|
+
const fs = d.parser._meta.firstSet
|
|
1276
|
+
if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
|
|
1277
|
+
const cls = this.charClass(fs)
|
|
1278
|
+
if (cls < 0) return inner
|
|
1279
|
+
const e = deriveExpected(d.parser)
|
|
1280
|
+
return this.emit(OP_GATE, cls, inner, this.expected(e.length > 0 ? e : [d.parser._tag]))
|
|
1281
|
+
}
|
|
1282
|
+
// A LABEL IS A ROW — see `OP_LABEL`. It replaces the child's expected set,
|
|
1283
|
+
// which is the entire combinator.
|
|
1284
|
+
// A LABEL IS CREDITED ON SUCCESS, as `codegen.ts:4530` credits it: its hook
|
|
1285
|
+
// is emitted AFTER the child's statements, where a failed child has already
|
|
1286
|
+
// broken out of the labelled block.
|
|
1287
|
+
case 'label':
|
|
1288
|
+
return this.covWrap(
|
|
1289
|
+
this.emit(OP_LABEL, this.node(d.parser).ip, this.expected([d.label])),
|
|
1290
|
+
this.plan?.labels.get(p)?.[0],
|
|
1291
|
+
1,
|
|
1292
|
+
)
|
|
1293
|
+
// Transparent wrapper: no row of its own, no dispatch at run time.
|
|
1294
|
+
case 'trivia':
|
|
1295
|
+
return this.node(d.parser).ip
|
|
1296
|
+
case 'grammar': {
|
|
1297
|
+
// Fail closed on the scope switches this encoder does not carry. `trackLines`
|
|
1298
|
+
// is the one field here that is RECONCILED rather than refused: the driver
|
|
1299
|
+
// takes it from TableSettings, so a scope asking for something different is a
|
|
1300
|
+
// silent disagreement between the grammar and the artifact.
|
|
1301
|
+
// `rootCapture: 'opaque'` is INERT for parsing. grammar.ts reads it only
|
|
1302
|
+
// inside a `_rootTriviaStrictScopes` validation throw; it declares that
|
|
1303
|
+
// this scope's trivia is opaque to root capture, which `run()` consumes.
|
|
1304
|
+
// Nothing in the parse changes, so there is nothing for the table to
|
|
1305
|
+
// lower — accepting it is not a gap, it is the correct lowering.
|
|
1306
|
+
// ONLY `true` IS MEANINGFUL HERE. `grammar.ts:94` stores
|
|
1307
|
+
// `opts.trackLines ?? false`, so an UNSET scope and an explicitly-false
|
|
1308
|
+
// one are the same value on the def — the distinction is lost. At parse
|
|
1309
|
+
// time `:103` reads `opts.trackLines ?? _ctx?.trackLines ?? false`, i.e.
|
|
1310
|
+
// unset INHERITS. So stored-false cannot be read as "force off", and
|
|
1311
|
+
// comparing it against the setting refused every inner scope of a
|
|
1312
|
+
// trackLines:true grammar — all four `*PositionsGrammar`s.
|
|
1313
|
+
//
|
|
1314
|
+
// A SCOPE THAT ASKS FOR TRACKING NO LONGER REFUSES. `encodeTable` folds
|
|
1315
|
+
// every such scope into the table's own `track` before encoding begins
|
|
1316
|
+
// (see `hasScopedTrackLines`), exactly as codegen folds them into one
|
|
1317
|
+
// `ctx.lineTracking` for the whole compile. The refusal here was reachable
|
|
1318
|
+
// only because the table read the SETTING alone, and it made the table
|
|
1319
|
+
// lowering reject `parser({ trackLines: true }, …)` — a grammar
|
|
1320
|
+
// `compile()` accepts and annotates.
|
|
1321
|
+
// A trivia scope is a ROW, not a lowering decision: the scope's trivia
|
|
1322
|
+
// combinator goes in the const pool and the driver installs it.
|
|
1323
|
+
// `captureTrivia: true` picks the SCOPE_CAP piece. It is an OR with the
|
|
1324
|
+
// inherited context in the interpreter (`grammar.ts:129`), never a
|
|
1325
|
+
// switch-off, so a scope without it emits plain SCOPE and leaves an
|
|
1326
|
+
// outer capture alone. A scope that ONLY asks for capture still needs a
|
|
1327
|
+
// row — hence the `cap` branch before the `triviaParser === undefined`
|
|
1328
|
+
// shortcut, which would otherwise drop the request on the floor.
|
|
1329
|
+
const cap = d.captureTrivia === true
|
|
1330
|
+
const op = cap ? OP_SCOPE_CAP : OP_SCOPE
|
|
1331
|
+
// ROOT-CAPTURE POLICY, as two bits on the row (ip + 3).
|
|
1332
|
+
//
|
|
1333
|
+
// `rootCapture: 'opaque'` is NOT inert — that claim was wrong, and it is
|
|
1334
|
+
// what let a table parse hand back root markers from a region the grammar
|
|
1335
|
+
// declared opaque. `grammar.ts:141` sets `ctx._rootTriviaCapture = false`
|
|
1336
|
+
// for the scope, and the scope's ctx is a COPY there, so the flag reverts
|
|
1337
|
+
// by construction; the table shares one ctx, so the piece has to restore it.
|
|
1338
|
+
//
|
|
1339
|
+
// Bit 1 is the STRICT-SCOPE refusal `grammar.ts:98` raises: with selected
|
|
1340
|
+
// root trivia active, a local scope whose trivia is not classified and is
|
|
1341
|
+
// not declared opaque cannot be reported faithfully, so it throws rather
|
|
1342
|
+
// than silently dropping markers. Codegen emits the same throw
|
|
1343
|
+
// (codegen.ts:4705); the table emitted nothing at all.
|
|
1344
|
+
const flags = (d.rootCapture === 'opaque' ? 1 : 0)
|
|
1345
|
+
| (d.triviaParser !== undefined
|
|
1346
|
+
&& d.triviaParser._meta.rootTriviaClassified !== true
|
|
1347
|
+
&& d.rootCapture !== 'opaque' ? 2 : 0)
|
|
1348
|
+
// THE SCOPE'S TRIVIA IS IN SCOPE FOR ITS BODY. Tracked across the child
|
|
1349
|
+
// descent so `case 'adjacency'` can resolve a `kinds` list against the
|
|
1350
|
+
// table that will actually be active there, exactly as codegen's
|
|
1351
|
+
// `ctx.activeTrivia` does. Saved and restored rather than assigned,
|
|
1352
|
+
// because a scope is nestable and an inner one does not leak outward.
|
|
1353
|
+
const savedTrivia = this.activeTrivia
|
|
1354
|
+
if (d.clearTrivia === true) this.activeTrivia = undefined
|
|
1355
|
+
else if (d.triviaParser !== undefined) this.activeTrivia = d.triviaParser
|
|
1356
|
+
const bodyIp = this.node(d.parser).ip
|
|
1357
|
+
this.activeTrivia = savedTrivia
|
|
1358
|
+
if (d.clearTrivia === true) return this.emit(op, -1, bodyIp, flags)
|
|
1359
|
+
if (d.triviaParser === undefined) {
|
|
1360
|
+
return cap || flags !== 0 ? this.emit(op, -1, bodyIp, flags) : bodyIp
|
|
1361
|
+
}
|
|
1362
|
+
return this.emit(op, this.triviaSlot(d.triviaParser), bodyIp, flags)
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* A HAND-WRITTEN COMBINATOR. `_def: { tag: 'unknown' }` is the designated
|
|
1366
|
+
* escape for a parser built outside the library, so there is no structure
|
|
1367
|
+
* to lower — the behaviour is a closure. Codegen accepts one and delegates
|
|
1368
|
+
* to `.parse` at run time (`emitRuntimeFallback`); refusing it here made
|
|
1369
|
+
* the table lowering reject a grammar the source lowering compiles.
|
|
1370
|
+
*
|
|
1371
|
+
* Runtime-only BY NAME, because a live combinator cannot be printed —
|
|
1372
|
+
* codegen degrades the same way (`runtimeParsers` non-empty ⇒ no
|
|
1373
|
+
* `inlineExpression`).
|
|
1374
|
+
*/
|
|
1375
|
+
case 'unknown':
|
|
1376
|
+
this.runtimeOnly.add(`${p._tag} — a hand-written combinator, run live through its own .parse`)
|
|
1377
|
+
return this.emit(OP_LIVE, this.fn(p, null))
|
|
1378
|
+
default:
|
|
1379
|
+
// NOT widened to `OP_LIVE`. A LIBRARY tag reaching here is a missing
|
|
1380
|
+
// lowering, and silently running it live would trade a build error for a
|
|
1381
|
+
// permanent slow path nobody would ever find.
|
|
1382
|
+
throw new UnsupportedConstruct(d.tag)
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Peephole: an `OP_RULE` row is pure indirection — a switch dispatch and a
|
|
1388
|
+
* call frame to do nothing but jump. Rewrite every child slot that points at
|
|
1389
|
+
* one to point at its target instead. Transitive, and cycle-safe by bounding
|
|
1390
|
+
* the walk at the number of rows.
|
|
1391
|
+
*/
|
|
1392
|
+
private collapseIndirection(): void {
|
|
1393
|
+
const resolve = (ip: number): number => {
|
|
1394
|
+
let t = ip
|
|
1395
|
+
for (let n = 0; n < this.code.length && this.code[t] === OP_RULE; n++) t = this.code[t + 1]!
|
|
1396
|
+
return t
|
|
1397
|
+
}
|
|
1398
|
+
const slots = (ip: number): number[] => {
|
|
1399
|
+
switch (this.code[ip]) {
|
|
1400
|
+
case OP_GATE: return [ip + 2]
|
|
1401
|
+
case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: return [ip + 1]
|
|
1402
|
+
case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
|
|
1403
|
+
case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
|
|
1404
|
+
case OP_SEQX: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 3 + i)
|
|
1405
|
+
// ARMS START AT ip+4. `ip+3` is the choice's own EXPECTED-SET index, and
|
|
1406
|
+
// rewriting it here treated an `fx` index as a code offset: when the row
|
|
1407
|
+
// at that offset happened to be `OP_RULE` the index was replaced by that
|
|
1408
|
+
// row's target, so a failing choice reported a different expected set —
|
|
1409
|
+
// and the bogus "target" was then pushed onto the walk and its operands
|
|
1410
|
+
// rewritten as if they were child slots. It also left the LAST arm
|
|
1411
|
+
// uncollapsed, which is the only part of this that was merely slow.
|
|
1412
|
+
case OP_CHOICE: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 4 + i)
|
|
1413
|
+
// GREEDY's operands INTERLEAVE a const index with an arm offset, so only
|
|
1414
|
+
// the odd slots are children — rewriting a const-pool index as if it were
|
|
1415
|
+
// a code offset is the same class of bug the OP_CHOICE note above records.
|
|
1416
|
+
case OP_GREEDY: return [ip + 1, ...Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 4 + 2 * i)]
|
|
1417
|
+
// REJECT's check operands are const/class indices, never offsets.
|
|
1418
|
+
case OP_REJECT: return [ip + 1]
|
|
1419
|
+
case OP_REP: case OP_REPV: return this.code[ip + 4]! >= 0 ? [ip + 1, ip + 4] : [ip + 1]
|
|
1420
|
+
default: return []
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const seen = new Set<number>()
|
|
1424
|
+
// Scan subtrees are ROOTS too: a sentinel or skipper is reached through
|
|
1425
|
+
// `prog.scans`, not through any code slot, so leaving them out left their
|
|
1426
|
+
// interiors uncollapsed and — worse — left the spec's own offset pointing at
|
|
1427
|
+
// a trampoline whose target had been rewritten around it.
|
|
1428
|
+
const refs: SubtreeRef[] = []
|
|
1429
|
+
for (const s of this.scans) {
|
|
1430
|
+
if (s.sentinel !== undefined) refs.push(s.sentinel)
|
|
1431
|
+
refs.push(...s.skip)
|
|
1432
|
+
}
|
|
1433
|
+
for (const set of this.scanSkipSets) refs.push(...set)
|
|
1434
|
+
const stack = [...Object.values(this.rules), ...refs.map(r => r[0])]
|
|
1435
|
+
while (stack.length > 0) {
|
|
1436
|
+
const ip = stack.pop()!
|
|
1437
|
+
if (seen.has(ip)) continue
|
|
1438
|
+
seen.add(ip)
|
|
1439
|
+
for (const slot of slots(ip)) {
|
|
1440
|
+
const target = resolve(this.code[slot]!)
|
|
1441
|
+
this.code[slot] = target
|
|
1442
|
+
stack.push(target)
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
for (const name of Object.keys(this.rules)) this.rules[name] = resolve(this.rules[name]!)
|
|
1446
|
+
const res = (r: SubtreeRef): SubtreeRef => [resolve(r[0]), r[1]]
|
|
1447
|
+
this.scans = this.scans.map(s => ({
|
|
1448
|
+
...s,
|
|
1449
|
+
skip: s.skip.map(res),
|
|
1450
|
+
...(s.sentinel === undefined ? {} : { sentinel: res(s.sentinel) }),
|
|
1451
|
+
}))
|
|
1452
|
+
this.scanSkipSets = this.scanSkipSets.map(set => set.map(res))
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
finish(): TableProgram {
|
|
1456
|
+
if (this.code.length === 0) this.emit(OP_EMPTY)
|
|
1457
|
+
this.collapseIndirection()
|
|
1458
|
+
return ownTableProgram({
|
|
1459
|
+
code: this.code, k: this.k, fns: this.fns, cc: this.cc,
|
|
1460
|
+
fx: this.fx, disp: this.disp, dsp: this.dsp, rules: this.rules,
|
|
1461
|
+
...(this.labels === undefined ? {} : { labels: this.labels }),
|
|
1462
|
+
...(this.classified ? { classified: 1 as const } : {}),
|
|
1463
|
+
...(this.scanSkipSets.length === 0
|
|
1464
|
+
? {}
|
|
1465
|
+
: { scanSkip: this.scanSkipSets, scanSkipOf: this.scanSkipOf }),
|
|
1466
|
+
...(this.scans.length === 0 ? {} : { scans: this.scans }),
|
|
1467
|
+
...(this.settings.hostMode === undefined ? {} : { hostMode: this.settings.hostMode }),
|
|
1468
|
+
...(this.runtimeOnly.size === 0 ? {} : { runtimeOnly: [...this.runtimeOnly].sort() }),
|
|
1469
|
+
...(this.triviaSpecs.length === 0 ? {} : { triviaSpecs: this.triviaSpecs }),
|
|
1470
|
+
...(this.rec ? { rec: 1 as const } : {}),
|
|
1471
|
+
...(this.cov === undefined ? {} : { cov: this.cov }),
|
|
1472
|
+
lines: this.track ? 1 : 0,
|
|
1473
|
+
})
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Encode a rule map into ONE table for ONE settings pair.
|
|
1479
|
+
*
|
|
1480
|
+
* Two settings pairs give two programs and therefore two cached reference
|
|
1481
|
+
* tables; the driver that reads them is the same function in both cases and
|
|
1482
|
+
* never sees `settings`.
|
|
1483
|
+
*/
|
|
1484
|
+
/**
|
|
1485
|
+
* Does this grammar ask for line tracking anywhere in its own structure?
|
|
1486
|
+
*
|
|
1487
|
+
* TRACKING IS A WHOLE-TABLE DECISION, as it is a whole-compile decision for the
|
|
1488
|
+
* source lowering: `ctx.lineTracking` is `opts.trackLines || grammarTrackLines ||
|
|
1489
|
+
* hasLineTrackingDef(combinator)` (`compiler/codegen.ts`). The table read the
|
|
1490
|
+
* SETTING alone and refused any `parser({ trackLines: true }, …)` scope it found
|
|
1491
|
+
* underneath, so a grammar that carries its own tracking compiled through
|
|
1492
|
+
* codegen and was rejected by the encoder. Same question, same answer.
|
|
1493
|
+
*
|
|
1494
|
+
* A `lazy` is followed, because a rule reference is how a grammar reaches most of
|
|
1495
|
+
* itself; an undefined one answers `false` rather than throwing, as codegen's does.
|
|
1496
|
+
*/
|
|
1497
|
+
function hasScopedTrackLines(p: Combinator<unknown>, seen: Set<Combinator<unknown>>): boolean {
|
|
1498
|
+
if (seen.has(p)) return false
|
|
1499
|
+
seen.add(p)
|
|
1500
|
+
if (p._meta.grammarTrackLines === true) return true
|
|
1501
|
+
const d = p._def
|
|
1502
|
+
switch (d.tag) {
|
|
1503
|
+
case 'grammar':
|
|
1504
|
+
if (d.trackLines === true) return true
|
|
1505
|
+
break
|
|
1506
|
+
case 'lazy':
|
|
1507
|
+
try { return hasScopedTrackLines(d.thunk(), seen) }
|
|
1508
|
+
catch { return false }
|
|
1509
|
+
// Neither is covered by the shared `childrenOf`: it drops `dispatch` matcher
|
|
1510
|
+
// arms and treats `routed()` as a leaf.
|
|
1511
|
+
case 'dispatch':
|
|
1512
|
+
for (const m of d.matchers ?? []) if (hasScopedTrackLines(m.parser, seen)) return true
|
|
1513
|
+
break
|
|
1514
|
+
case 'routed':
|
|
1515
|
+
return d.fallback === undefined ? false : hasScopedTrackLines(d.fallback, seen)
|
|
1516
|
+
default:
|
|
1517
|
+
break
|
|
1518
|
+
}
|
|
1519
|
+
for (const c of childrenOf(d)) if (hasScopedTrackLines(c, seen)) return true
|
|
1520
|
+
return false
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
export function encodeTable(
|
|
1524
|
+
ruleMap: Record<string, Combinator<unknown>>,
|
|
1525
|
+
settings: TableSettings = {},
|
|
1526
|
+
): TableProgram {
|
|
1527
|
+
return encodeTableProgram(ruleMap, settings).prog
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* `encodeTable` PLUS the reducer sources, for a caller that must PRINT the table.
|
|
1532
|
+
*
|
|
1533
|
+
* The sources are not on `TableProgram` because they are not data the driver
|
|
1534
|
+
* reads and must never travel in an emitted artifact; they are the printer's
|
|
1535
|
+
* input, in `prog.fns` order. A build that lowers macro-evaluated combinators
|
|
1536
|
+
* gets a real source per entry (`fnSrc` / `buildSrc` / `predSrc` / `gateSrcs`,
|
|
1537
|
+
* set by the macro evaluator); a runtime caller gets `null`s and must therefore
|
|
1538
|
+
* refuse to print rather than emit `() => {}` placeholders.
|
|
1539
|
+
*/
|
|
1540
|
+
export function encodeTableProgram(
|
|
1541
|
+
ruleMap: Record<string, Combinator<unknown>>,
|
|
1542
|
+
settings: TableSettings = {},
|
|
1543
|
+
): { prog: TableProgram; fnSrcs: (string | null)[] } {
|
|
1544
|
+
// Line tracking is decided ONCE for the whole artifact, as codegen does
|
|
1545
|
+
// (`opts.trackLines || grammarTrackLines || hasLineTrackingDef(combinator)`).
|
|
1546
|
+
// Reading only the SETTING made the table narrower than codegen and refused a
|
|
1547
|
+
// `parser({ trackLines: true })` scope that codegen simply accepts.
|
|
1548
|
+
const names = Object.keys(ruleMap)
|
|
1549
|
+
const seen = new Set<Combinator<unknown>>()
|
|
1550
|
+
const track = settings.trackLines === true
|
|
1551
|
+
|| names.some(n => hasScopedTrackLines(ruleMap[n]!, seen))
|
|
1552
|
+
// Same precedence as compileRuleMap/compose: an explicit lowering option wins;
|
|
1553
|
+
// otherwise a `rules({ hostMode: 'cst' })` declaration stamped on its entries
|
|
1554
|
+
// selects CST. (`'ast'` is the unstamped default.) Reading only `settings`
|
|
1555
|
+
// produced two opposite contracts for one grammar: compose refused a hostless
|
|
1556
|
+
// run while public encodeTable silently emitted and stamped an AST table.
|
|
1557
|
+
const hostMode = settings.hostMode
|
|
1558
|
+
?? names.map(n => ruleMap[n]!._meta.grammarHostMode).find(Boolean)
|
|
1559
|
+
const resolvedSettings = {
|
|
1560
|
+
...settings,
|
|
1561
|
+
...(hostMode === undefined ? {} : { hostMode }),
|
|
1562
|
+
...(track ? { trackLines: true } : {}),
|
|
1563
|
+
}
|
|
1564
|
+
const enc = new Encoder(resolvedSettings)
|
|
1565
|
+
enc.winners = ruleMap
|
|
1566
|
+
for (const name of names) enc.encodeRule(name, ruleMap[name]!)
|
|
1567
|
+
const prog = enc.finish()
|
|
1568
|
+
return { prog, fnSrcs: enc.fnSrcs }
|
|
1569
|
+
}
|