parseman 0.59.2 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/commitment.d.ts +1 -1
- package/dist/analysis/diagnostics.js +877 -191
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +877 -191
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/completions.d.ts +3 -3
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +13 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +35 -2
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts +1 -1
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +11 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +2 -3
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +2 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +65 -53
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/cst/host-mode.d.ts +1 -1
- package/dist/cst/root-trivia-scope.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +897 -200
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.d.ts +1 -1
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +3 -7
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +904 -202
- package/dist/plugin/index.js.map +3 -3
- package/dist/regex/first-set.d.ts +19 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/spec/index.js.map +1 -1
- package/dist/table/assemble.d.ts +16 -3
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +3 -2
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/compile.d.ts +2 -2
- package/dist/table/emit-assembly.d.ts +8 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +8 -8
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts +6 -7
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/factor-choice.d.ts +27 -0
- package/dist/table/factor-choice.d.ts.map +1 -1
- package/dist/table/gap-gate.d.ts +11 -0
- package/dist/table/gap-gate.d.ts.map +1 -0
- package/dist/table/index.js +927 -202
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +48 -20
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +13 -8
- package/dist/table/program.d.ts.map +1 -1
- package/dist/types.d.ts +17 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +6 -6
- package/package.json +1 -1
- package/src/analysis/commitment.ts +1 -1
- package/src/analysis/duplication.ts +1 -1
- package/src/analysis/terminal.ts +1 -1
- package/src/combinators/attempt.ts +1 -1
- package/src/combinators/choice.ts +1 -1
- package/src/combinators/completions.ts +3 -3
- package/src/combinators/dispatch.ts +30 -6
- package/src/combinators/first-set.ts +55 -12
- package/src/combinators/gate.ts +1 -1
- package/src/combinators/node.ts +1 -1
- package/src/combinators/regex.ts +9 -4
- package/src/combinators/repeat.ts +6 -6
- package/src/combinators/sequence.ts +1 -1
- package/src/combinators/trivia-boundary.ts +18 -0
- package/src/combinators/trivia-skip.ts +1 -1
- package/src/compiler/compile-linkable-table.ts +2 -3
- package/src/compiler/degradation.ts +2 -2
- package/src/compiler/ir-serialize.ts +1 -1
- package/src/compiler/linker.ts +5 -3
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +1 -1
- package/src/cst/capture-buffer.ts +2 -2
- package/src/cst/host-mode.ts +1 -1
- package/src/cst/root-trivia-scope.ts +1 -1
- package/src/index.ts +1 -1
- package/src/language-service/index.ts +1 -1
- package/src/plugin/entry-supercompiler.ts +2 -1
- package/src/plugin/evaluator.ts +8 -3
- package/src/plugin/index.ts +11 -16
- package/src/regex/first-set.ts +37 -3
- package/src/table/assemble.ts +180 -35
- package/src/table/child-slots.ts +8 -3
- package/src/table/compile-rule-map.ts +10 -6
- package/src/table/compile.ts +3 -3
- package/src/table/emit-assembly.ts +186 -35
- package/src/table/emit.ts +19 -18
- package/src/table/encode.ts +482 -88
- package/src/table/exec.ts +42 -18
- package/src/table/factor-choice.ts +62 -0
- package/src/table/fail-clean.ts +2 -2
- package/src/table/gap-gate.ts +55 -0
- package/src/table/hold-gap.ts +2 -2
- package/src/table/lex-program.ts +15 -6
- package/src/table/ops.ts +51 -20
- package/src/table/program.ts +14 -9
- package/src/table/select-hot.ts +7 -2
- package/src/table/stamp.ts +1 -1
- package/src/types.ts +13 -6
- package/src/version.ts +6 -6
package/src/table/encode.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { scanRefs } from './program.ts'
|
|
2
|
-
import {
|
|
2
|
+
import { regexFirstTwo } from '../regex/first-set.ts'
|
|
3
|
+
import { factorPrefixArms, factorSharedEnds, factoredChoices } from './factor-choice.ts'
|
|
3
4
|
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
4
|
-
import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
5
|
+
import { classifyFinalChoice, empty, firstSetOf, intersects, matchesEmpty, union, winnerForRef, type RefResolver } from '../combinators/first-set.ts'
|
|
5
6
|
import { childrenOf } from '../analysis/gating.ts'
|
|
6
7
|
import { capturesLeaf, mayCommitFailure, mayLeavePartialCapture, reachesMarked } from '../analysis/commitment.ts'
|
|
7
8
|
import { getCoreLiteralValue } from '../combinators/choice.ts'
|
|
@@ -17,13 +18,13 @@ import {
|
|
|
17
18
|
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
18
19
|
OP_LIVE, OP_ATTEMPT, OP_LABEL,
|
|
19
20
|
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
|
|
20
|
-
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_COV,
|
|
21
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_COV, OP_GAPGATE, OP_RGATE2, OP_STRICT,
|
|
21
22
|
OP_LEX_BODY, OP_LEX_PROGRAM,
|
|
22
23
|
} from './ops.ts'
|
|
23
24
|
import { adjacencyExpected } from '../combinators/adjacency.ts'
|
|
24
25
|
import { resolveAdjacencyKindMask } from '../cst/trivia-kinds.ts'
|
|
25
26
|
import { missingInferredType } from '../combinators/node.ts'
|
|
26
|
-
import {
|
|
27
|
+
import { triviaThroughRef } from '../combinators/trivia-boundary.ts'
|
|
27
28
|
import type { BalancedSpec } from '../combinators/scanTo.ts'
|
|
28
29
|
import type { DispatchSpec, LexBodySpec, LexProgramSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
|
|
29
30
|
import {
|
|
@@ -86,15 +87,14 @@ export type TableSettings = {
|
|
|
86
87
|
* smaller, so the size argument for making it optional no longer holds.
|
|
87
88
|
*
|
|
88
89
|
* LOWERING RECOVERY IS NOT RUNNING IT. Every recovery path stays gated on
|
|
89
|
-
* `ctx._tolerant` — the same dormancy the source lowering
|
|
90
|
-
*
|
|
90
|
+
* `ctx._tolerant` — the same dormancy the source lowering (removed in 0.47.0)
|
|
91
|
+
* had and the interpreter tests (`repeat.ts:163`) — so a strict
|
|
91
92
|
* parse is strict, and `test/parity/table-recovery-always.test.ts` pins that.
|
|
92
93
|
*
|
|
93
|
-
* The field is kept
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* project keeps finding.
|
|
94
|
+
* @deprecated The field is kept only so existing callers still type-check, and
|
|
95
|
+
* is removed in the next minor release. Passing `false` does NOT switch recovery
|
|
96
|
+
* off, and saying so here is the point — a setting that silently means nothing is
|
|
97
|
+
* the defect class this project keeps finding.
|
|
98
98
|
*/
|
|
99
99
|
readonly recovery?: boolean
|
|
100
100
|
/**
|
|
@@ -117,6 +117,48 @@ export type TableSettings = {
|
|
|
117
117
|
type Emitted = { readonly ip: number }
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The first two characters a term reads, where it says them outright: a
|
|
122
|
+
* literal of two or more characters, or a case-sensitive regex
|
|
123
|
+
* (`regexFirstTwo`), through a label or a trivia scope. The first must be BMP
|
|
124
|
+
* and not a surrogate, so the second starts at the next code unit. The gate
|
|
125
|
+
* reads that second character as a code point (`lead`); a class holding a high
|
|
126
|
+
* surrogate admits the astral points it begins (`leadRanges`), so a code-unit
|
|
127
|
+
* second set reads them too.
|
|
128
|
+
*/
|
|
129
|
+
function firstTwo(p: Combinator<unknown>): { first: FirstSet; second: FirstSet } | undefined {
|
|
130
|
+
const d = p._def as ParserDef
|
|
131
|
+
if (d.tag === 'label' || d.tag === 'grammar') return firstTwo(d.parser)
|
|
132
|
+
let two: { first: FirstSet; second: FirstSet } | undefined
|
|
133
|
+
if (d.tag === 'literal' && d.value.length >= 2 && d.caseInsensitive !== true) {
|
|
134
|
+
const second = d.value.codePointAt(1)!
|
|
135
|
+
two = { first: { kind: 'ranges', ranges: [{ lo: d.value.charCodeAt(0), hi: d.value.charCodeAt(0) }] }, second: { kind: 'ranges', ranges: [{ lo: second, hi: second }] } }
|
|
136
|
+
} else if (d.tag === 'regex' && !d.flags.includes('i')) {
|
|
137
|
+
const units = regexFirstTwo(d.source, d.flags)
|
|
138
|
+
if (units !== undefined) two = units
|
|
139
|
+
}
|
|
140
|
+
if (two === undefined || two.first.kind !== 'ranges') return undefined
|
|
141
|
+
return two.first.ranges.every(r => r.hi < 0xd800 || (r.lo > 0xdfff && r.hi <= 0xffff)) ? two : undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** ASCII whitespace and the `/` a comment opens with: what a gap starts with. */
|
|
145
|
+
const GAP_CHARS = new Set([9, 10, 12, 13, 32, 47])
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A regex that reads a GAP: it may match empty, or every non-empty match starts
|
|
149
|
+
* with whitespace or `/`. Only such a regex leads an item the repeat gap-gates;
|
|
150
|
+
* a word or a number before the deciding character is the item's own class
|
|
151
|
+
* test's business, not a gap's.
|
|
152
|
+
*/
|
|
153
|
+
function isGap(p: Combinator<unknown>): boolean {
|
|
154
|
+
if (matchesEmpty(p)) return true
|
|
155
|
+
const fs = p._meta.firstSet
|
|
156
|
+
return fs.kind === 'ranges' && fs.ranges.every(r => {
|
|
157
|
+
for (let c = r.lo; c <= r.hi; c++) if (!GAP_CHARS.has(c)) return false
|
|
158
|
+
return true
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
120
162
|
class Encoder {
|
|
121
163
|
code: number[] = []
|
|
122
164
|
k: unknown[] = []
|
|
@@ -164,8 +206,33 @@ class Encoder {
|
|
|
164
206
|
*/
|
|
165
207
|
private commitFailureMemo = new Map<Combinator<unknown>, boolean>()
|
|
166
208
|
labels: readonly string[] | undefined = undefined
|
|
209
|
+
/** The choice being encoded as written, for `OP_STRICT`'s tolerant child. */
|
|
210
|
+
private unfactoredChoice: Combinator<unknown> | undefined = undefined
|
|
167
211
|
/** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
|
|
168
212
|
activeTrivia: Combinator<unknown> | undefined = undefined
|
|
213
|
+
/**
|
|
214
|
+
* Is the trivia live at this row known? It is not in the body of an entry
|
|
215
|
+
* that installs none of its own: that body runs under whatever trivia its
|
|
216
|
+
* caller has (a compiled rule inside another grammar, `run({ trivia })`).
|
|
217
|
+
* Every scope (`parser({ trivia })`, `noTrivia`, `token()`, a rule's install)
|
|
218
|
+
* makes it known again.
|
|
219
|
+
*/
|
|
220
|
+
triviaKnown = true
|
|
221
|
+
/**
|
|
222
|
+
* The trivia a gate's first set is derived under: the live table, `null` for a
|
|
223
|
+
* scope known to have none, or `undefined` where it is not known.
|
|
224
|
+
*/
|
|
225
|
+
private gateTrivia(): Combinator<unknown> | null | undefined {
|
|
226
|
+
return this.activeTrivia ?? (this.triviaKnown ? null : undefined)
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Whether a gate may read a nullable pad through its non-empty matches
|
|
230
|
+
* (`firstSetOf`'s `consuming`): only where the trivia that may follow the pad
|
|
231
|
+
* is known. Elsewhere a gate reads first sets as 0.58.0 did.
|
|
232
|
+
*/
|
|
233
|
+
private gateConsuming(): boolean {
|
|
234
|
+
return this.activeTrivia !== undefined || this.triviaKnown
|
|
235
|
+
}
|
|
169
236
|
classified = false
|
|
170
237
|
scans: ScanSpec[] = []
|
|
171
238
|
/** Ambient `scanSkip` sets, pooled by the ARRAY's identity (stable per grammar). */
|
|
@@ -304,10 +371,11 @@ class Encoder {
|
|
|
304
371
|
// encoder would hold at that row, so both lowerings rank the same
|
|
305
372
|
// failures.
|
|
306
373
|
const final = classifyFinalChoice(
|
|
307
|
-
node.parsers, this.refResolver(), node.cleared ?
|
|
374
|
+
node.parsers, this.refResolver(), node.cleared ? null : this.gateTrivia(), false, node.disjoint,
|
|
308
375
|
)
|
|
309
376
|
const classes = final.firstSets.map((first, i) => final.nullable[i] ? -1 : this.charClass(first))
|
|
310
|
-
const exclusive = final.exclusive && classes.every(cls => cls >= 0)
|
|
377
|
+
const exclusive = final.exclusive && classes.every(cls => cls >= 0) &&
|
|
378
|
+
this.wideArms(node.parsers, final.firstSets, node.disjoint, node.cleared ? null : this.gateTrivia(), node.cleared ? undefined : this.activeTrivia).length === 0
|
|
311
379
|
words.push(LEX_NODE_CHOICE, node.arms.length, exclusive ? 1 : 0, this.expected(node.expected))
|
|
312
380
|
for (let i = 0; i < node.arms.length; i++) {
|
|
313
381
|
words.push(classes[i]!, this.expected(node.armExpected[i]!))
|
|
@@ -500,16 +568,104 @@ class Encoder {
|
|
|
500
568
|
private kIndex = new Map<unknown, number>()
|
|
501
569
|
private ccIndex = new Map<string, number>()
|
|
502
570
|
private fxIndex = new Map<string, number>()
|
|
503
|
-
/** Memoized by combinator identity: a shared sub-combinator is ONE row. */
|
|
504
|
-
memo = new Map<Combinator<unknown>, number>()
|
|
505
571
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
572
|
+
* Memoized by combinator identity AND the trivia scope it is encoded under
|
|
573
|
+
* (`scopeKey`): a shared sub-combinator is ONE row per scope it runs in.
|
|
574
|
+
*
|
|
575
|
+
* A construct's rows depend on that scope. A first set derived through a
|
|
576
|
+
* nullable term takes in the trivia that may follow it, a gap gate holds only
|
|
577
|
+
* where no trivia runs, a routed gate reads what may follow its token, and a
|
|
578
|
+
* cross-rule reference installs its rule's trivia only from a cleared region
|
|
579
|
+
* (`scopedRef`). One memo slot per combinator froze the FIRST scope it was
|
|
580
|
+
* reached under onto every later one: a rule body encoded as an entry with no
|
|
581
|
+
* trivia, then run under a caller's comment trivia, gated its repeats as if no
|
|
582
|
+
* comment could come between their terms.
|
|
583
|
+
*/
|
|
584
|
+
memo = new Map<Combinator<unknown>, Map<string, number>>()
|
|
585
|
+
pending = new Map<Combinator<unknown>, Map<string, number[]>>()
|
|
586
|
+
private triviaSignatures = new Map<Combinator<unknown>, string>()
|
|
587
|
+
/**
|
|
588
|
+
* What of the live trivia an encoding can depend on: none, or its first set.
|
|
589
|
+
* Every derivation reads trivia only through `firstSetOf(trivia)` or whether it
|
|
590
|
+
* is live, so two scopes whose trivia opens on the same characters encode the
|
|
591
|
+
* same rows and share them. A construct that reads no scope (`readsScope`)
|
|
592
|
+
* has one encoding.
|
|
593
|
+
*/
|
|
594
|
+
private scopeKey(p: Combinator<unknown>): string {
|
|
595
|
+
const t = this.activeTrivia
|
|
596
|
+
if (!this.readsScope(p)) return ''
|
|
597
|
+
if (t === undefined) return this.triviaKnown ? '' : '?'
|
|
598
|
+
let sig = this.triviaSignatures.get(t)
|
|
599
|
+
if (sig === undefined) {
|
|
600
|
+
sig = `t${JSON.stringify(firstSetOf(t, new Set(), this.refResolver()))}`
|
|
601
|
+
this.triviaSignatures.set(t, sig)
|
|
602
|
+
}
|
|
603
|
+
return sig
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private readsScopeMemo = new Map<Combinator<unknown>, boolean>()
|
|
607
|
+
/**
|
|
608
|
+
* Can `p`'s rows depend on the trivia scope it is encoded under? No for a
|
|
609
|
+
* terminal, a scope of its own (`parser({ trivia })`, `noTrivia`, `token()`),
|
|
610
|
+
* and a wrapper over a construct that reads none; yes wherever a first set, a gate or
|
|
611
|
+
* a reference's install is derived with the scope: a sequence with a nullable
|
|
612
|
+
* term before another (the trivia between them), a routed arm (the trivia after
|
|
613
|
+
* its token), a reference that installs its rule's trivia from a cleared
|
|
614
|
+
* region, and anything not modelled here — a cycle included.
|
|
510
615
|
*/
|
|
511
|
-
|
|
512
|
-
|
|
616
|
+
private readsScope(p: Combinator<unknown>): boolean {
|
|
617
|
+
const hit = this.readsScopeMemo.get(p)
|
|
618
|
+
if (hit !== undefined) return hit
|
|
619
|
+
this.readsScopeMemo.set(p, true)
|
|
620
|
+
const d = p._def as ParserDef
|
|
621
|
+
const rr = this.refResolver()
|
|
622
|
+
let out: boolean
|
|
623
|
+
switch (d.tag) {
|
|
624
|
+
case 'literal': case 'regex': case 'keywords': case 'token':
|
|
625
|
+
out = false
|
|
626
|
+
break
|
|
627
|
+
// `routed()` encodes its fallback under the scope that reaches it.
|
|
628
|
+
case 'routed':
|
|
629
|
+
out = d.fallback !== undefined && this.readsScope(d.fallback)
|
|
630
|
+
break
|
|
631
|
+
case 'grammar':
|
|
632
|
+
out = d.clearTrivia !== true && d.triviaParser === undefined && this.readsScope(d.parser)
|
|
633
|
+
break
|
|
634
|
+
case 'node': case 'label': case 'leaf': case 'transform': case 'attempt': case 'expect':
|
|
635
|
+
case 'not': case 'peek': case 'optional': case 'many': case 'oneOrMore': case 'field':
|
|
636
|
+
out = this.readsScope(d.parser)
|
|
637
|
+
break
|
|
638
|
+
case 'sepBy':
|
|
639
|
+
out = this.readsScope(d.parser) || this.readsScope(d.separator)
|
|
640
|
+
break
|
|
641
|
+
case 'choice':
|
|
642
|
+
out = d.parsers.some(a => this.readsScope(a))
|
|
643
|
+
break
|
|
644
|
+
case 'dispatch':
|
|
645
|
+
out = this.readsScope(d.selector) || d.cases.some(c => this.readsScope(c.parser))
|
|
646
|
+
|| (d.matchers?.some(m => this.readsScope(m.parser)) ?? false)
|
|
647
|
+
|| (d.otherwise !== undefined && this.readsScope(d.otherwise))
|
|
648
|
+
break
|
|
649
|
+
case 'sequence': {
|
|
650
|
+
const ps = d.parsers
|
|
651
|
+
// A gap-led sequence: the gap walk holds only where no trivia runs.
|
|
652
|
+
out = ps.some(t => this.readsScope(t))
|
|
653
|
+
|| (ps[0] !== undefined && ((ps[0]._def as ParserDef).tag === 'routed' || ((ps[0]._def as ParserDef).tag === 'regex' && isGap(ps[0]))))
|
|
654
|
+
|| ps.some((t, i) => i + 1 < ps.length && matchesEmpty(t, new Set(), rr))
|
|
655
|
+
break
|
|
656
|
+
}
|
|
657
|
+
case 'lazy': {
|
|
658
|
+
let target: Combinator<unknown> | undefined = winnerForRef(p, rr)
|
|
659
|
+
if (target === undefined) { try { target = d.thunk() } catch { target = undefined } }
|
|
660
|
+
out = target === undefined || triviaThroughRef(p, target, null) !== null || this.readsScope(target)
|
|
661
|
+
break
|
|
662
|
+
}
|
|
663
|
+
default:
|
|
664
|
+
out = true
|
|
665
|
+
}
|
|
666
|
+
this.readsScopeMemo.set(p, out)
|
|
667
|
+
return out
|
|
668
|
+
}
|
|
513
669
|
|
|
514
670
|
readonly settings: TableSettings
|
|
515
671
|
/** Resolved once, HERE, at table-build time — never consulted at run time. */
|
|
@@ -531,8 +687,8 @@ class Encoder {
|
|
|
531
687
|
/**
|
|
532
688
|
* The class of the follow set of term `i` in a sequence — the sync sentinel a
|
|
533
689
|
* list nested in that term resyncs to, INFERRED from structure exactly as
|
|
534
|
-
* `sequence()` (combinators/sequence.ts:58) and
|
|
535
|
-
*
|
|
690
|
+
* `sequence()` (combinators/sequence.ts:58) and the source
|
|
691
|
+
* lowering's `emitSeqValues` infer it: the union of every LATER term's first
|
|
536
692
|
* set, not merely up to the first non-nullable one, so a mandatory middle term
|
|
537
693
|
* cannot hide a later close. −1 where there is no usable sentinel, which is the
|
|
538
694
|
* same set of cases `firstSetSentinel` answers `null` for.
|
|
@@ -541,7 +697,9 @@ class Encoder {
|
|
|
541
697
|
let fs: FirstSet = { kind: 'empty' }
|
|
542
698
|
const rr = this.refResolver()
|
|
543
699
|
for (let j = i + 1; j < parsers.length; j++) {
|
|
544
|
-
|
|
700
|
+
// As the interpreter's `followSentinels` derive them (`sequence.ts`): with no
|
|
701
|
+
// trivia, whatever scope the sequence runs in.
|
|
702
|
+
fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr, undefined, false))
|
|
545
703
|
}
|
|
546
704
|
return this.charClass(fs)
|
|
547
705
|
}
|
|
@@ -725,7 +883,7 @@ class Encoder {
|
|
|
725
883
|
|| op === OP_RX || op === OP_RX_TRACK) return child
|
|
726
884
|
const rr = this.refResolver()
|
|
727
885
|
if (matchesEmpty(p, new Set(), rr)) return child
|
|
728
|
-
const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
|
|
886
|
+
const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia, false))
|
|
729
887
|
if (cls < 0) return child
|
|
730
888
|
const e = deriveExpected(p)
|
|
731
889
|
return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
|
|
@@ -757,11 +915,39 @@ class Encoder {
|
|
|
757
915
|
* `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
|
|
758
916
|
* over the final merged map.
|
|
759
917
|
*/
|
|
760
|
-
|
|
918
|
+
/**
|
|
919
|
+
* The arms whose first set under `gateTrivia()` is wider than under the
|
|
920
|
+
* active trivia alone — a reference's installed trivia read through from a
|
|
921
|
+
* region with none. Empty for construction-set (disjoint) choices, whose
|
|
922
|
+
* sets do not depend on trivia.
|
|
923
|
+
*/
|
|
924
|
+
private wideArms(
|
|
925
|
+
arms: readonly Combinator<unknown>[], wide: readonly FirstSet[], constructionSets: boolean,
|
|
926
|
+
wideTrivia: Combinator<unknown> | null | undefined = this.gateTrivia(),
|
|
927
|
+
base: Combinator<unknown> | undefined = this.activeTrivia,
|
|
928
|
+
): number[] {
|
|
929
|
+
if (constructionSets || wideTrivia === base) return []
|
|
930
|
+
const narrow = classifyFinalChoice(arms, this.refResolver(), base, false, false).firstSets
|
|
931
|
+
const out: number[] = []
|
|
932
|
+
for (let i = 0; i < arms.length; i++) {
|
|
933
|
+
if (this.charClass(wide[i]!) !== this.charClass(narrow[i]!)) out.push(i)
|
|
934
|
+
}
|
|
935
|
+
return out
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private gateFirstSet(p: Combinator<unknown>, guarded: boolean, narrowed: boolean): FirstSet {
|
|
761
939
|
const cached = p._meta.firstSet
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
940
|
+
if (p._def.tag !== 'lazy' && cached.kind !== 'any') return cached
|
|
941
|
+
const rr = this.refResolver()
|
|
942
|
+
// GUARDED: the interpreter tests this child's set before entering it (its set
|
|
943
|
+
// was finite when the wrapper was built), so the table tests the same set: a
|
|
944
|
+
// reference's resolved target, as its construction-time set reads it, without
|
|
945
|
+
// the trivia it installs. Otherwise the interpreter enters the child, and the
|
|
946
|
+
// gate is the set the child can start with where it runs. Only a node's and a
|
|
947
|
+
// repeat item's gate read a nullable pad through its non-empty matches (the
|
|
948
|
+
// operator of `A (pad op pad A)*`); an attempt's reads first sets as 0.58.0 did.
|
|
949
|
+
if (guarded) return firstSetOf(p, new Set(), rr, undefined, false)
|
|
950
|
+
return firstSetOf(p, new Set(), rr, this.gateTrivia(), narrowed && this.gateConsuming())
|
|
765
951
|
}
|
|
766
952
|
|
|
767
953
|
/**
|
|
@@ -793,6 +979,133 @@ class Encoder {
|
|
|
793
979
|
return ip
|
|
794
980
|
}
|
|
795
981
|
|
|
982
|
+
/**
|
|
983
|
+
* An optional repeat item tests the item's class before it runs it (`OP_REP`
|
|
984
|
+
* ip+7), wherever the item's own first-set gate would also run. So a sequence
|
|
985
|
+
* item whose first term is a gate on that same class gets its own copy of the
|
|
986
|
+
* sequence row with the gate's child as that term: the test runs once. The
|
|
987
|
+
* shared row is left as it is for its other references.
|
|
988
|
+
*/
|
|
989
|
+
private withoutLeadGate(seq: number, cls: number): number {
|
|
990
|
+
const op = this.code[seq]
|
|
991
|
+
if (op !== OP_SEQ && op !== OP_SEQV) return seq
|
|
992
|
+
const first = this.code[seq + 2]!
|
|
993
|
+
if (this.code[first] !== OP_GATE || this.code[first + 1] !== cls) return seq
|
|
994
|
+
const n = this.code[seq + 1]!
|
|
995
|
+
const width = 1 + 2 * n
|
|
996
|
+
const head = this.emitHead(op, width)
|
|
997
|
+
for (let i = 1; i <= width; i++) this.code[head + i] = this.code[seq + i]!
|
|
998
|
+
this.code[head + 2] = this.code[first + 2]!
|
|
999
|
+
return head
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* An optional repeat item that opens with a GAP — a regex that may match
|
|
1004
|
+
* empty, or one that starts only with whitespace or a comment's `/` — and
|
|
1005
|
+
* then a term with a finite first set gets an `OP_GAPGATE` row
|
|
1006
|
+
* (`gap-gate.ts`): the repeat reads the gap once, ends where the character
|
|
1007
|
+
* after it opens neither that term nor an arm without the gap, and hands the
|
|
1008
|
+
* gap's end to the regex row. Anything the walk cannot prove keeps the item as
|
|
1009
|
+
* written.
|
|
1010
|
+
*/
|
|
1011
|
+
private gapGated(item: Combinator<unknown>, ip: number): number {
|
|
1012
|
+
// The gate holds only where no trivia runs between the gap and its term, so
|
|
1013
|
+
// not where the live trivia is unknown.
|
|
1014
|
+
if (this.activeTrivia === undefined && !this.triviaKnown) return ip
|
|
1015
|
+
const lead = this.gapLead(item, this.activeTrivia, 0)
|
|
1016
|
+
if (lead === undefined) return ip
|
|
1017
|
+
const cls = this.charClass(lead.follow)
|
|
1018
|
+
if (cls < 0) return ip
|
|
1019
|
+
const other = lead.other.kind === 'empty' ? -1 : this.charClass(lead.other)
|
|
1020
|
+
if (lead.other.kind !== 'empty' && other < 0) return ip
|
|
1021
|
+
const p = this.node(lead.p).ip
|
|
1022
|
+
if (this.code[p] !== OP_RX && this.code[p] !== OP_RX_TRACK) return ip
|
|
1023
|
+
return this.emit(OP_GAPGATE, ip, p, cls, other)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* The gap an item opens with: the regex `p`, the first set `follow` of what
|
|
1028
|
+
* must come straight after it, and the first set `other` of choice arms that
|
|
1029
|
+
* do not open with `p`. Only through wrappers that consume nothing before
|
|
1030
|
+
* their body, a sequence's first term, and a choice's arms; the gap must sit
|
|
1031
|
+
* in a sequence with no trivia between its terms.
|
|
1032
|
+
*/
|
|
1033
|
+
private gapLead(
|
|
1034
|
+
c: Combinator<unknown>, trivia: Combinator<unknown> | null | undefined, depth: number,
|
|
1035
|
+
): { p: Combinator<unknown>; follow: FirstSet; other: FirstSet } | undefined {
|
|
1036
|
+
if (depth > 24) return undefined
|
|
1037
|
+
const rr = this.refResolver()
|
|
1038
|
+
const d = c._def as ParserDef
|
|
1039
|
+
switch (d.tag) {
|
|
1040
|
+
case 'lazy': {
|
|
1041
|
+
let target: Combinator<unknown> | undefined = winnerForRef(c, rr)
|
|
1042
|
+
if (target === undefined) { try { target = d.thunk() } catch { return undefined } }
|
|
1043
|
+
return this.gapLead(target, triviaThroughRef(c, target, trivia), depth + 1)
|
|
1044
|
+
}
|
|
1045
|
+
case 'node': case 'leaf': case 'label': case 'attempt':
|
|
1046
|
+
return this.gapLead(d.parser, trivia, depth + 1)
|
|
1047
|
+
case 'field':
|
|
1048
|
+
return d.leadingTrivia === true ? undefined : this.gapLead(d.parser, trivia, depth + 1)
|
|
1049
|
+
case 'grammar':
|
|
1050
|
+
return this.gapLead(d.parser, d.clearTrivia === true ? null : (d.triviaParser ?? trivia), depth + 1)
|
|
1051
|
+
case 'sequence': {
|
|
1052
|
+
const [first, ...rest] = d.parsers
|
|
1053
|
+
if (first === undefined) return undefined
|
|
1054
|
+
if ((first._def as ParserDef).tag !== 'regex' || !isGap(first)) {
|
|
1055
|
+
return this.gapLead(first, trivia, depth + 1)
|
|
1056
|
+
}
|
|
1057
|
+
// No trivia between the gap and the term after it (`undefined`: none).
|
|
1058
|
+
if ((trivia !== undefined && trivia !== null) || rest.length === 0) return undefined
|
|
1059
|
+
let follow: FirstSet = empty()
|
|
1060
|
+
let closed = false
|
|
1061
|
+
for (const t of rest) {
|
|
1062
|
+
const fs = firstSetOf(t, new Set(), rr, null)
|
|
1063
|
+
if (fs.kind === 'any') return undefined
|
|
1064
|
+
follow = union(follow, fs)
|
|
1065
|
+
if (!matchesEmpty(t, new Set(), rr)) { closed = true; break }
|
|
1066
|
+
}
|
|
1067
|
+
return closed ? { p: first, follow, other: empty() } : undefined
|
|
1068
|
+
}
|
|
1069
|
+
case 'choice': {
|
|
1070
|
+
let p: Combinator<unknown> | undefined
|
|
1071
|
+
let follow: FirstSet = empty()
|
|
1072
|
+
let other: FirstSet = empty()
|
|
1073
|
+
for (const arm of d.parsers) {
|
|
1074
|
+
const lead = this.gapLead(arm, trivia, depth + 1)
|
|
1075
|
+
if (lead !== undefined && (p === undefined || lead.p === p)) {
|
|
1076
|
+
p = lead.p
|
|
1077
|
+
follow = union(follow, lead.follow)
|
|
1078
|
+
other = union(other, lead.other)
|
|
1079
|
+
continue
|
|
1080
|
+
}
|
|
1081
|
+
if (matchesEmpty(arm, new Set(), rr)) return undefined
|
|
1082
|
+
// Tested where the gate runs, which is only where no trivia is live.
|
|
1083
|
+
const fs = firstSetOf(arm, new Set(), rr, trivia ?? null)
|
|
1084
|
+
if (fs.kind === 'any') return undefined
|
|
1085
|
+
other = union(other, fs)
|
|
1086
|
+
}
|
|
1087
|
+
return p === undefined ? undefined : { p, follow, other }
|
|
1088
|
+
}
|
|
1089
|
+
default:
|
|
1090
|
+
return undefined
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* The class a strict list may end on — a character its separator `p` (row
|
|
1096
|
+
* `ip`) cannot start — without entering the separator, or −1.
|
|
1097
|
+
*
|
|
1098
|
+
* The test runs before every separator, to save the one that fails at the
|
|
1099
|
+
* list's end, so it is laid down only where entering the separator costs more
|
|
1100
|
+
* than the test: not on a terminal row, which is itself one character test.
|
|
1101
|
+
*/
|
|
1102
|
+
private sepGate(p: Combinator<unknown>, ip: number): number {
|
|
1103
|
+
const op = this.code[ip]
|
|
1104
|
+
if (op === OP_LIT || op === OP_LIT_CI || op === OP_RX || op === OP_LIT_TRACK || op === OP_RX_TRACK || op === OP_LIT_CI_TRACK) return -1
|
|
1105
|
+
if (matchesEmpty(p, new Set(), this.refResolver())) return -1
|
|
1106
|
+
return this.charClass(firstSetOf(p, new Set(), this.refResolver(), this.gateTrivia(), this.gateConsuming()))
|
|
1107
|
+
}
|
|
1108
|
+
|
|
796
1109
|
/** Reserve `n` operand slots to patch once children are laid out. */
|
|
797
1110
|
private emitHead(op: number, n: number): number {
|
|
798
1111
|
const ip = this.code.length
|
|
@@ -846,8 +1159,10 @@ class Encoder {
|
|
|
846
1159
|
// scope for a `notAdjacent({ kinds })` anywhere inside it — see the same
|
|
847
1160
|
// save/restore in `case 'grammar'`.
|
|
848
1161
|
this.activeTrivia = p._meta.grammarTrivia
|
|
1162
|
+
this.triviaKnown = p._meta.grammarTrivia !== undefined
|
|
849
1163
|
const body = this.node(p).ip
|
|
850
1164
|
this.activeTrivia = undefined
|
|
1165
|
+
this.triviaKnown = true
|
|
851
1166
|
// GRAMMAR-LEVEL AMBIENT TRIVIA (`rules({ trivia }, …)`).
|
|
852
1167
|
//
|
|
853
1168
|
// `run()` installs this from `entry._meta.grammarTrivia`, but ONLY for a
|
|
@@ -931,13 +1246,19 @@ class Encoder {
|
|
|
931
1246
|
* silently produced the ONE keyword `bc`.
|
|
932
1247
|
*/
|
|
933
1248
|
private scopedRef(p: Combinator<unknown>, target: Combinator<unknown>): number {
|
|
934
|
-
const ip = this.node(target).ip
|
|
935
1249
|
// `rules()` stamps `grammarTrivia` on the map entry, which for a proxied rule
|
|
936
1250
|
// is the REF itself; a composed `winners` entry carries it on the target.
|
|
937
|
-
const
|
|
938
|
-
if (
|
|
939
|
-
|
|
940
|
-
|
|
1251
|
+
const runsUnder = triviaThroughRef(p, target, this.activeTrivia)
|
|
1252
|
+
if (this.activeTrivia !== undefined || runsUnder === undefined || runsUnder === null) return this.node(target).ip
|
|
1253
|
+
// The body is encoded under the trivia this row installs for it, so its own
|
|
1254
|
+
// gates are derived under the scope they run in.
|
|
1255
|
+
const savedKnown = this.triviaKnown
|
|
1256
|
+
this.activeTrivia = runsUnder
|
|
1257
|
+
this.triviaKnown = true
|
|
1258
|
+
const ip = this.node(target).ip
|
|
1259
|
+
this.activeTrivia = undefined
|
|
1260
|
+
this.triviaKnown = savedKnown
|
|
1261
|
+
return this.emit(OP_SCOPE_PLAIN, this.triviaSlot(runsUnder), ip)
|
|
941
1262
|
}
|
|
942
1263
|
|
|
943
1264
|
/**
|
|
@@ -949,7 +1270,7 @@ class Encoder {
|
|
|
949
1270
|
* unable to match empty or to commit, so a miss is exactly that term failing
|
|
950
1271
|
* at the token's end with its own set.
|
|
951
1272
|
*/
|
|
952
|
-
private routedArmGate(arm: Combinator<unknown>): { cls: number; fx: number } | undefined {
|
|
1273
|
+
private routedArmGate(arm: Combinator<unknown>): { cls: number; fx: number; two?: readonly [number, number] } | undefined {
|
|
953
1274
|
type Follow = 'end' | { next: Combinator<unknown>; boundary: Combinator<unknown> | undefined } | undefined
|
|
954
1275
|
const walk = (p: Combinator<unknown>, trivia: Combinator<unknown> | undefined, depth: number): Follow => {
|
|
955
1276
|
if (depth > 16) return undefined
|
|
@@ -984,45 +1305,51 @@ class Encoder {
|
|
|
984
1305
|
// reports that opener once statically, once per arm when it runs.
|
|
985
1306
|
const nextDef = next._def as ParserDef
|
|
986
1307
|
if (nextDef.tag !== 'literal' && nextDef.tag !== 'regex' && nextDef.tag !== 'keywords' && nextDef.tag !== 'label') return undefined
|
|
987
|
-
|
|
1308
|
+
// Where the trivia after the token is not known, the gate cannot say what may
|
|
1309
|
+
// follow it.
|
|
1310
|
+
if (boundary === undefined && !this.triviaKnown) return undefined
|
|
1311
|
+
const first = firstSetOf(next, new Set(), rr, boundary ?? null)
|
|
988
1312
|
const cls = this.charClass(boundary === undefined ? first : union(first, firstSetOf(boundary, new Set(), rr)))
|
|
989
1313
|
if (cls < 0) return undefined
|
|
990
|
-
const
|
|
991
|
-
|
|
1314
|
+
const fx = this.expected(nextDef.tag === 'label' ? deriveExpected(next) : directTerminalFailureExpected(nextDef))
|
|
1315
|
+
// THE SECOND CHARACTER. Where the next term starts with a character of
|
|
1316
|
+
// `first` — which no gap before it can start with, so the term starts at the
|
|
1317
|
+
// token's end — it also reads the one after: test both, still with no scan.
|
|
1318
|
+
const two = firstTwo(next)
|
|
1319
|
+
if (two === undefined) return { cls, fx }
|
|
1320
|
+
if (boundary !== undefined && intersects(two.first, firstSetOf(boundary, new Set(), rr))) return { cls, fx }
|
|
1321
|
+
const a = this.charClass(two.first)
|
|
1322
|
+
const b = this.charClass(two.second)
|
|
1323
|
+
return a < 0 || b < 0 ? { cls, fx } : { cls, fx, two: [a, b] }
|
|
992
1324
|
}
|
|
993
1325
|
|
|
994
1326
|
node(p: Combinator<unknown>): Emitted {
|
|
995
|
-
// See `
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
const refMemo = this.refMemo
|
|
1000
|
-
const hit = lazyRef ? refMemo.get(p)?.get(scopeKey) : this.memo.get(p)
|
|
1327
|
+
// See `memo`: one encoding per combinator per trivia scope.
|
|
1328
|
+
const key = this.scopeKey(p)
|
|
1329
|
+
let byScope = this.memo.get(p)
|
|
1330
|
+
const hit = byScope?.get(key)
|
|
1001
1331
|
if (hit !== undefined) return { ip: hit }
|
|
1002
1332
|
// Recursion: reserve a trampoline row now, patch its target when the real
|
|
1003
1333
|
// body lands. One extra indirection per recursive rule, zero per call site.
|
|
1004
|
-
|
|
1334
|
+
let inFlightByScope = this.pending.get(p)
|
|
1335
|
+
const inFlight = inFlightByScope?.get(key)
|
|
1005
1336
|
if (inFlight !== undefined) {
|
|
1006
1337
|
const ip = this.emitHead(OP_RULE, 1)
|
|
1007
1338
|
inFlight.push(ip + 1)
|
|
1008
1339
|
return { ip }
|
|
1009
1340
|
}
|
|
1010
1341
|
const patches: number[] = []
|
|
1011
|
-
this.pending.set(p,
|
|
1012
|
-
|
|
1342
|
+
if (inFlightByScope === undefined) { inFlightByScope = new Map(); this.pending.set(p, inFlightByScope) }
|
|
1343
|
+
inFlightByScope.set(key, patches)
|
|
1344
|
+
// A RULE IS CREDITED ON ENTRY, not on success — the source lowering emitted the
|
|
1013
1345
|
// hook as the first statement of the named function body, so a rule that is
|
|
1014
1346
|
// reached and then fails still counts as exercised. The counter goes on the
|
|
1015
1347
|
// MEMOISED offset, which is the one every reference to this combinator lands
|
|
1016
1348
|
// on, and the recursion trampolines below are patched with it too.
|
|
1017
1349
|
const ip = this.covWrap(this.encodeDef(p), this.plan?.rules.get(p), 0)
|
|
1018
|
-
|
|
1019
|
-
if (
|
|
1020
|
-
|
|
1021
|
-
if (byScope === undefined) { byScope = new Map(); refMemo.set(p, byScope) }
|
|
1022
|
-
byScope.set(scopeKey, ip)
|
|
1023
|
-
} else {
|
|
1024
|
-
this.memo.set(p, ip)
|
|
1025
|
-
}
|
|
1350
|
+
inFlightByScope.delete(key)
|
|
1351
|
+
if (byScope === undefined) { byScope = new Map(); this.memo.set(p, byScope) }
|
|
1352
|
+
byScope.set(key, ip)
|
|
1026
1353
|
for (const slot of patches) {
|
|
1027
1354
|
// A TRAMPOLINE PATCHED TO ITSELF IS NOT A TABLE. `OP_RULE ip → ip` makes no
|
|
1028
1355
|
// progress in either driver: `exec.ts`'s `case OP_RULE` recurses on the same
|
|
@@ -1151,8 +1478,24 @@ class Encoder {
|
|
|
1151
1478
|
// Arms that differ in one term are that term's choice (`factor-choice.ts`).
|
|
1152
1479
|
// Not under a coverage plan, which credits the arms as written.
|
|
1153
1480
|
if (this.plan === undefined) {
|
|
1154
|
-
const
|
|
1155
|
-
if (
|
|
1481
|
+
const shared = factorSharedEnds(d)
|
|
1482
|
+
if (shared !== undefined) return this.node(shared).ip
|
|
1483
|
+
// The prefix arm reads `X` once only in a strict parse. Tolerant
|
|
1484
|
+
// recovery and a completions probe run the choice as written: inside
|
|
1485
|
+
// `sequence(X, optional(a))`, `X`'s own recovery would see `a`'s first
|
|
1486
|
+
// set as all that may follow it, where the next arm's `X` sees the
|
|
1487
|
+
// choice's.
|
|
1488
|
+
if (this.unfactoredChoice !== p) {
|
|
1489
|
+
const prefix = factorPrefixArms(d)
|
|
1490
|
+
if (prefix !== undefined) {
|
|
1491
|
+
const strict = this.node(prefix).ip
|
|
1492
|
+
const saved = this.unfactoredChoice
|
|
1493
|
+
this.unfactoredChoice = p
|
|
1494
|
+
const tolerant = this.encodeDef(p)
|
|
1495
|
+
this.unfactoredChoice = saved
|
|
1496
|
+
return this.emit(OP_STRICT, strict, tolerant)
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1156
1499
|
}
|
|
1157
1500
|
// `greedyClassify` is NOT a choice at all — one arm runs and another arm
|
|
1158
1501
|
// is credited — so it gets its own row rather than an arm ordering.
|
|
@@ -1227,7 +1570,9 @@ class Encoder {
|
|
|
1227
1570
|
&& (gate === null || gate === undefined)
|
|
1228
1571
|
const routedGate = plain ? this.routedArmGate(c) : undefined
|
|
1229
1572
|
const body = this.node(c).ip
|
|
1230
|
-
const inner = routedGate === undefined ? body
|
|
1573
|
+
const inner = routedGate === undefined ? body
|
|
1574
|
+
: routedGate.two === undefined ? this.emit(OP_RGATE, routedGate.cls, body, routedGate.fx)
|
|
1575
|
+
: this.emit(OP_RGATE2, routedGate.cls, body, routedGate.fx, routedGate.two[0], routedGate.two[1])
|
|
1231
1576
|
const ip = checks === null || checks === undefined || checks.length === 0
|
|
1232
1577
|
? inner
|
|
1233
1578
|
: this.reject(inner, checks)
|
|
@@ -1270,7 +1615,13 @@ class Encoder {
|
|
|
1270
1615
|
// the O(1) table (`exclusive`) or falls to the ordered per-arm path.
|
|
1271
1616
|
// Arm ORDER is preserved on both, which is what makes this a PEG-safe
|
|
1272
1617
|
// change rather than a reordering.
|
|
1273
|
-
|
|
1618
|
+
// The arms' classes let in what the interpreter's `choice()` lets in. A
|
|
1619
|
+
// disjoint one dispatches on its arms' construction-time sets (no trivia,
|
|
1620
|
+
// a nullable pad as `any`), and the table reads those same sets. Any other
|
|
1621
|
+
// tries each arm under the trivia that runs there; see `wideArms` below.
|
|
1622
|
+
const constructionSets = d.disjoint && !factoredChoices.has(p)
|
|
1623
|
+
const finalChoice = classifyFinalChoice(arms, rr, this.gateTrivia(), false, constructionSets)
|
|
1624
|
+
const wideArms = this.wideArms(arms, finalChoice.firstSets, constructionSets)
|
|
1274
1625
|
// attempt() preserves its child's success language, but the broad
|
|
1275
1626
|
// classifier deliberately treats the transaction as nullable/unknown so
|
|
1276
1627
|
// it can never by itself authorize exclusive one-arm selection. Recover
|
|
@@ -1282,7 +1633,7 @@ class Encoder {
|
|
|
1282
1633
|
if (!finalChoice.nullable[i]) return undefined
|
|
1283
1634
|
const def = arm._def as ParserDef
|
|
1284
1635
|
if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
|
|
1285
|
-
return firstSetOf(def.parser, new Set(), rr, this.activeTrivia)
|
|
1636
|
+
return firstSetOf(def.parser, new Set(), rr, this.activeTrivia, false)
|
|
1286
1637
|
})
|
|
1287
1638
|
const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
|
|
1288
1639
|
const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
|
|
@@ -1305,6 +1656,13 @@ class Encoder {
|
|
|
1305
1656
|
// ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
|
|
1306
1657
|
// its classes: there the dispatched arm's gate failing IS the choice
|
|
1307
1658
|
// failing, with that arm's set, as in the interpreter's disjoint path.
|
|
1659
|
+
// A class widened by a reference's installed trivia can make arms look
|
|
1660
|
+
// disjoint where the interpreter still tries each one and merges their
|
|
1661
|
+
// failures at the choice position. Such a site stays ordered: the widened
|
|
1662
|
+
// arm is always entered, so the arms beside it keep their opener sets.
|
|
1663
|
+
if (wideArms.length > 0 && dispatchIsExclusive(classes, this.cc)) {
|
|
1664
|
+
for (const i of wideArms) classes[i] = -1
|
|
1665
|
+
}
|
|
1308
1666
|
if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
|
|
1309
1667
|
for (let k = 0; k < classes.length; k++) {
|
|
1310
1668
|
const gate = d.gates[order[k]!]
|
|
@@ -1350,13 +1708,18 @@ class Encoder {
|
|
|
1350
1708
|
}
|
|
1351
1709
|
case 'many':
|
|
1352
1710
|
case 'oneOrMore': {
|
|
1353
|
-
const child = this.node(d.parser).ip
|
|
1354
1711
|
const rr = this.refResolver()
|
|
1712
|
+
// The item's gate tests what the interpreter's repeat guard tests where it
|
|
1713
|
+
// has one (`repeat.ts`), and otherwise what the item can start with.
|
|
1355
1714
|
const itemClass = matchesEmpty(d.parser, new Set(), rr)
|
|
1356
1715
|
? -1
|
|
1357
|
-
: this.charClass(
|
|
1716
|
+
: this.charClass(this.gateFirstSet(d.parser, d.guarded === true, true))
|
|
1358
1717
|
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
1359
1718
|
const min = d.tag === 'many' ? 0 : d.min
|
|
1719
|
+
const item = min === 0 && itemClass >= 0
|
|
1720
|
+
? this.withoutLeadGate(this.node(d.parser).ip, itemClass)
|
|
1721
|
+
: this.node(d.parser).ip
|
|
1722
|
+
const child = min === 0 && itemClass >= 0 ? this.gapGated(d.parser, item) : item
|
|
1360
1723
|
// ip + 6 is the ITEM's expected set and ip + 7 the item's sentinel class:
|
|
1361
1724
|
// the resync error's payload is the ITEM's set (`deriveExpected` in
|
|
1362
1725
|
// `combinators/repeat.ts`).
|
|
@@ -1372,20 +1735,33 @@ class Encoder {
|
|
|
1372
1735
|
// under `min` reports what would have let it CONTINUE — the ITEM, which
|
|
1373
1736
|
// is what `failAt` (repeat.ts) and `deriveExpectedArr([def.parser])`
|
|
1374
1737
|
// (codegen emitSepBy) both use; the driver otherwise reports whatever
|
|
1375
|
-
// sub-parse failed last
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
const
|
|
1380
|
-
//
|
|
1381
|
-
// the
|
|
1738
|
+
// sub-parse failed last. At `min === 1` that end is the FIRST item
|
|
1739
|
+
// failing, and `failAt` reports the item's derived set at the list's
|
|
1740
|
+
// start there too, not whatever the item's own failure left (a deeper
|
|
1741
|
+
// set the interpreter discards, `repeat.ts` `failAt(pos)`).
|
|
1742
|
+
const gate = this.sepGate(d.separator, sep)
|
|
1743
|
+
// The ITEM's set, reported where the list ends under `min`, falls back to
|
|
1744
|
+
// the item's tag where it derives empty (`repeat.ts` `failAt`); a resync
|
|
1745
|
+
// error carries the set as derived. Bit 16 says that set is empty.
|
|
1746
|
+
const itemSet = deriveExpected(d.parser)
|
|
1747
|
+
const itemFx = this.expected(itemSet.length > 0 ? itemSet : [d.parser._tag])
|
|
1748
|
+
const flags = (d.trailing === 'allow' ? 1 : 0) | (d.keepSeparators === true ? 2 : 0) | (d.min >= 1 ? 4 : 0) | (gate >= 0 ? 8 : 0) | (itemSet.length === 0 ? 16 : 0)
|
|
1749
|
+
// Every list row carries ip + 6 (the ITEM's expected set, which the
|
|
1750
|
+
// resync error reports) and ip + 7 (the SEPARATOR's sentinel class, so
|
|
1382
1751
|
// a tolerant list resyncs to its own separator OR the enclosing
|
|
1383
1752
|
// delimiter — `orSentinel(separator, term)` in repeat.ts:454 and
|
|
1384
|
-
// `_rec.or(sepSent, mySync)` in codegen's emitSepBy).
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1753
|
+
// `_rec.or(sepSent, mySync)` in codegen's emitSepBy). With flag bit 8,
|
|
1754
|
+
// ip + 8 is the separator's first set for a strict list to end on
|
|
1755
|
+
// (`sepGate`).
|
|
1756
|
+
const ops = [
|
|
1757
|
+
child, d.min, d.max ?? -1, sep, flags,
|
|
1758
|
+
itemFx,
|
|
1759
|
+
// As 0.58.0: the interpreter resyncs on the separator itself
|
|
1760
|
+
// (`orSentinel`), which runs under the live trivia.
|
|
1761
|
+
this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia, false)),
|
|
1762
|
+
]
|
|
1763
|
+
if (gate >= 0) ops.push(gate)
|
|
1764
|
+
return this.emit(OP_REP, ...ops)
|
|
1389
1765
|
}
|
|
1390
1766
|
case 'optional': {
|
|
1391
1767
|
const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
|
|
@@ -1401,7 +1777,7 @@ class Encoder {
|
|
|
1401
1777
|
// rule invocation. The inner sequence must not be shared with anything
|
|
1402
1778
|
// else, or fusing it here would steal it from the other reference.
|
|
1403
1779
|
const inner = d.parser._def as ParserDef
|
|
1404
|
-
if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
|
|
1780
|
+
if (inner.tag === 'sequence' && !this.memo.get(d.parser)?.has(this.scopeKey(d.parser)) && !this.pending.get(d.parser)?.has(this.scopeKey(d.parser))) {
|
|
1405
1781
|
const kids = inner.parsers.map(c => this.node(c).ip)
|
|
1406
1782
|
const head = this.emitHead(OP_SEQX, 2 + 2 * kids.length)
|
|
1407
1783
|
const projection = directArrayProjection(d.fn, d.fnSrc, kids.length)
|
|
@@ -1514,7 +1890,11 @@ class Encoder {
|
|
|
1514
1890
|
// accepts (the reducer reroutes via `winners`, but this recognizer gate did
|
|
1515
1891
|
// not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
|
|
1516
1892
|
// resolves the call. Non-ref bodies keep the cached set (byte-identical).
|
|
1517
|
-
|
|
1893
|
+
// Not narrowed through a nullable pad: a node's gate miss reports a static
|
|
1894
|
+
// set, and only where the body's first set is finite without the pad does
|
|
1895
|
+
// that set match what entering the body reports (a `keywords()` term, a
|
|
1896
|
+
// list's item set, an optional lead are only known by running it).
|
|
1897
|
+
const fs = this.gateFirstSet(d.parser, d.guarded === true, false)
|
|
1518
1898
|
if (fs.kind === 'any') return body
|
|
1519
1899
|
const cls = this.charClass(fs)
|
|
1520
1900
|
if (cls < 0) return body
|
|
@@ -1522,7 +1902,16 @@ class Encoder {
|
|
|
1522
1902
|
return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
|
|
1523
1903
|
}
|
|
1524
1904
|
case 'token': {
|
|
1525
|
-
|
|
1905
|
+
// A token runs its body with trivia cleared (`token.ts`), so its body is
|
|
1906
|
+
// encoded under no trivia.
|
|
1907
|
+
const saved = this.activeTrivia
|
|
1908
|
+
const savedKnown = this.triviaKnown
|
|
1909
|
+
this.activeTrivia = undefined
|
|
1910
|
+
this.triviaKnown = true
|
|
1911
|
+
const body = this.node(d.parser).ip
|
|
1912
|
+
this.activeTrivia = saved
|
|
1913
|
+
this.triviaKnown = savedKnown
|
|
1914
|
+
return this.emit(OP_TOKEN, body)
|
|
1526
1915
|
}
|
|
1527
1916
|
case 'routed':
|
|
1528
1917
|
return this.emit(OP_ROUTED, d.fallback === undefined ? -1 : this.node(d.fallback).ip)
|
|
@@ -1611,7 +2000,9 @@ class Encoder {
|
|
|
1611
2000
|
const otherRouted = d.otherwise === undefined
|
|
1612
2001
|
? false
|
|
1613
2002
|
: branchUsesRouted({ parser: d.otherwise as Combinator<unknown>, usesRouted: d.otherwiseUsesRouted === true })
|
|
1614
|
-
|
|
2003
|
+
// Bit 0: the fallback reads the routed token. Bit 1: its failure is not
|
|
2004
|
+
// committed (`otherwise(p, { commit: false })`).
|
|
2005
|
+
this.code[head + 4] = (otherRouted ? 1 : 0) | (d.otherwiseCommit === false ? 2 : 0)
|
|
1615
2006
|
this.code[head + 5] = arms.length
|
|
1616
2007
|
for (let i = 0; i < arms.length; i++) this.code[head + 6 + i] = arms[i]!
|
|
1617
2008
|
return head
|
|
@@ -1662,7 +2053,7 @@ class Encoder {
|
|
|
1662
2053
|
// A `kinds` LIST IS VALIDATED HERE, at encode time, against the trivia
|
|
1663
2054
|
// table in scope — the same `resolveAdjacencyKindMask` the interpreter
|
|
1664
2055
|
// calls on first reach and codegen calls while emitting the `_akN` probe
|
|
1665
|
-
// (
|
|
2056
|
+
// (in the source lowering). Deferring it to the driver's parse-time resolution
|
|
1666
2057
|
// left `compile()` accepting a grammar whose kind name no active table
|
|
1667
2058
|
// declares: an unlabelled table or a typo'd category compiled clean and
|
|
1668
2059
|
// threw only when an input happened to reach the assertion.
|
|
@@ -1715,7 +2106,7 @@ class Encoder {
|
|
|
1715
2106
|
// not a slower equivalent.
|
|
1716
2107
|
// Same override-aware resolution as the node gate: a bare-ref inner resolves
|
|
1717
2108
|
// its gate through `winners` so a widened winner is not gated out.
|
|
1718
|
-
const fs = this.gateFirstSet(d.parser)
|
|
2109
|
+
const fs = this.gateFirstSet(d.parser, d.guarded === true, false)
|
|
1719
2110
|
if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
|
|
1720
2111
|
const cls = this.charClass(fs)
|
|
1721
2112
|
if (cls < 0) return inner
|
|
@@ -1724,7 +2115,7 @@ class Encoder {
|
|
|
1724
2115
|
}
|
|
1725
2116
|
// A LABEL IS A ROW — see `OP_LABEL`. It replaces the child's expected set,
|
|
1726
2117
|
// which is the entire combinator.
|
|
1727
|
-
// A LABEL IS CREDITED ON SUCCESS, as
|
|
2118
|
+
// A LABEL IS CREDITED ON SUCCESS, as the source lowering credited it: its hook
|
|
1728
2119
|
// is emitted AFTER the child's statements, where a failed child has already
|
|
1729
2120
|
// broken out of the labelled block.
|
|
1730
2121
|
case 'label':
|
|
@@ -1782,8 +2173,8 @@ class Encoder {
|
|
|
1782
2173
|
// Bit 1 is the STRICT-SCOPE refusal `grammar.ts:98` raises: with selected
|
|
1783
2174
|
// root trivia active, a local scope whose trivia is not classified and is
|
|
1784
2175
|
// not declared opaque cannot be reported faithfully, so it throws rather
|
|
1785
|
-
// than silently dropping markers.
|
|
1786
|
-
//
|
|
2176
|
+
// than silently dropping markers. The source lowering emitted the
|
|
2177
|
+
// same throw; the table emitted nothing at all.
|
|
1787
2178
|
const flags = (d.rootCapture === 'opaque' ? 1 : 0)
|
|
1788
2179
|
| (d.triviaParser !== undefined
|
|
1789
2180
|
&& d.triviaParser._meta.rootTriviaClassified !== true
|
|
@@ -1794,10 +2185,12 @@ class Encoder {
|
|
|
1794
2185
|
// `ctx.activeTrivia` does. Saved and restored rather than assigned,
|
|
1795
2186
|
// because a scope is nestable and an inner one does not leak outward.
|
|
1796
2187
|
const savedTrivia = this.activeTrivia
|
|
1797
|
-
|
|
1798
|
-
|
|
2188
|
+
const savedKnown = this.triviaKnown
|
|
2189
|
+
if (d.clearTrivia === true) { this.activeTrivia = undefined; this.triviaKnown = true }
|
|
2190
|
+
else if (d.triviaParser !== undefined) { this.activeTrivia = d.triviaParser; this.triviaKnown = true }
|
|
1799
2191
|
const bodyIp = this.node(d.parser).ip
|
|
1800
2192
|
this.activeTrivia = savedTrivia
|
|
2193
|
+
this.triviaKnown = savedKnown
|
|
1801
2194
|
if (d.clearTrivia === true) return this.emit(op, -1, bodyIp, flags)
|
|
1802
2195
|
if (d.triviaParser === undefined) {
|
|
1803
2196
|
return cap || flags !== 0 ? this.emit(op, -1, bodyIp, flags) : bodyIp
|
|
@@ -1840,8 +2233,8 @@ class Encoder {
|
|
|
1840
2233
|
}
|
|
1841
2234
|
const slots = (ip: number): number[] => {
|
|
1842
2235
|
switch (this.code[ip]) {
|
|
1843
|
-
case OP_GATE: case OP_RGATE: return [ip + 2]
|
|
1844
|
-
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]
|
|
2236
|
+
case OP_GATE: case OP_RGATE: case OP_RGATE2: return [ip + 2]
|
|
2237
|
+
case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: case OP_GAPGATE: return [ip + 1]
|
|
1845
2238
|
case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_SOURCE_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
|
|
1846
2239
|
case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
|
|
1847
2240
|
case OP_SEQX: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 3 + i)
|
|
@@ -1860,6 +2253,7 @@ class Encoder {
|
|
|
1860
2253
|
// REJECT's check operands are const/class indices, never offsets.
|
|
1861
2254
|
case OP_REJECT: return [ip + 1]
|
|
1862
2255
|
case OP_REP: case OP_REPV: return this.code[ip + 4]! >= 0 ? [ip + 1, ip + 4] : [ip + 1]
|
|
2256
|
+
case OP_STRICT: return [ip + 1, ip + 2]
|
|
1863
2257
|
default: return []
|
|
1864
2258
|
}
|
|
1865
2259
|
}
|
|
@@ -1938,7 +2332,7 @@ class Encoder {
|
|
|
1938
2332
|
*
|
|
1939
2333
|
* TRACKING IS A WHOLE-TABLE DECISION, as it is a whole-compile decision for the
|
|
1940
2334
|
* source lowering: `ctx.lineTracking` is `opts.trackLines || grammarTrackLines ||
|
|
1941
|
-
* hasLineTrackingDef(combinator)` (
|
|
2335
|
+
* hasLineTrackingDef(combinator)` (the source lowering). The table read the
|
|
1942
2336
|
* SETTING alone and refused any `parser({ trackLines: true }, …)` scope it found
|
|
1943
2337
|
* underneath, so a grammar that carries its own tracking compiled through
|
|
1944
2338
|
* codegen and was rejected by the encoder. Same question, same answer.
|