parseman 0.59.3 → 0.60.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/dist/analysis/diagnostics.js +876 -190
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +876 -190
- package/dist/cli/index.js.map +3 -3
- 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/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/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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +896 -199
- package/dist/index.js.map +3 -3
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.js +903 -201
- 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/table/assemble.d.ts +15 -1
- 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 +1 -1
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/compile.d.ts +1 -1
- 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 +4 -3
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts +0 -35
- 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 +926 -201
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +35 -16
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +6 -1
- package/dist/table/program.d.ts.map +1 -1
- package/dist/types.d.ts +16 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -4
- package/src/analysis/duplication.ts +1 -1
- package/src/combinators/attempt.ts +1 -1
- package/src/combinators/dispatch.ts +30 -6
- package/src/combinators/first-set.ts +55 -12
- 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/ir-serialize.ts +1 -1
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +1 -1
- package/src/index.ts +1 -1
- package/src/plugin/evaluator.ts +8 -3
- package/src/regex/first-set.ts +37 -3
- package/src/table/assemble.ts +176 -30
- package/src/table/child-slots.ts +8 -3
- package/src/table/compile-rule-map.ts +7 -4
- package/src/table/compile.ts +1 -1
- package/src/table/emit-assembly.ts +184 -33
- package/src/table/emit.ts +6 -4
- package/src/table/encode.ts +469 -74
- package/src/table/exec.ts +38 -82
- 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 +38 -16
- package/src/table/program.ts +7 -2
- package/src/table/select-hot.ts +7 -2
- package/src/types.ts +12 -5
- package/src/version.ts +1 -1
- package/dist/table/encode-baseline.d.ts +0 -16
- package/dist/table/encode-baseline.d.ts.map +0 -1
- package/dist/table/exec-baseline.d.ts +0 -9
- package/dist/table/exec-baseline.d.ts.map +0 -1
- package/src/table/encode-baseline.ts +0 -346
- package/src/table/exec-baseline.ts +0 -557
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 {
|
|
@@ -116,6 +117,48 @@ export type TableSettings = {
|
|
|
116
117
|
type Emitted = { readonly ip: number }
|
|
117
118
|
|
|
118
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
|
+
|
|
119
162
|
class Encoder {
|
|
120
163
|
code: number[] = []
|
|
121
164
|
k: unknown[] = []
|
|
@@ -163,8 +206,33 @@ class Encoder {
|
|
|
163
206
|
*/
|
|
164
207
|
private commitFailureMemo = new Map<Combinator<unknown>, boolean>()
|
|
165
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
|
|
166
211
|
/** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
|
|
167
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
|
+
}
|
|
168
236
|
classified = false
|
|
169
237
|
scans: ScanSpec[] = []
|
|
170
238
|
/** Ambient `scanSkip` sets, pooled by the ARRAY's identity (stable per grammar). */
|
|
@@ -303,10 +371,11 @@ class Encoder {
|
|
|
303
371
|
// encoder would hold at that row, so both lowerings rank the same
|
|
304
372
|
// failures.
|
|
305
373
|
const final = classifyFinalChoice(
|
|
306
|
-
node.parsers, this.refResolver(), node.cleared ?
|
|
374
|
+
node.parsers, this.refResolver(), node.cleared ? null : this.gateTrivia(), false, node.disjoint,
|
|
307
375
|
)
|
|
308
376
|
const classes = final.firstSets.map((first, i) => final.nullable[i] ? -1 : this.charClass(first))
|
|
309
|
-
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
|
|
310
379
|
words.push(LEX_NODE_CHOICE, node.arms.length, exclusive ? 1 : 0, this.expected(node.expected))
|
|
311
380
|
for (let i = 0; i < node.arms.length; i++) {
|
|
312
381
|
words.push(classes[i]!, this.expected(node.armExpected[i]!))
|
|
@@ -499,16 +568,104 @@ class Encoder {
|
|
|
499
568
|
private kIndex = new Map<unknown, number>()
|
|
500
569
|
private ccIndex = new Map<string, number>()
|
|
501
570
|
private fxIndex = new Map<string, number>()
|
|
502
|
-
/** Memoized by combinator identity: a shared sub-combinator is ONE row. */
|
|
503
|
-
memo = new Map<Combinator<unknown>, number>()
|
|
504
571
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
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.
|
|
509
615
|
*/
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
}
|
|
512
669
|
|
|
513
670
|
readonly settings: TableSettings
|
|
514
671
|
/** Resolved once, HERE, at table-build time — never consulted at run time. */
|
|
@@ -540,7 +697,9 @@ class Encoder {
|
|
|
540
697
|
let fs: FirstSet = { kind: 'empty' }
|
|
541
698
|
const rr = this.refResolver()
|
|
542
699
|
for (let j = i + 1; j < parsers.length; j++) {
|
|
543
|
-
|
|
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))
|
|
544
703
|
}
|
|
545
704
|
return this.charClass(fs)
|
|
546
705
|
}
|
|
@@ -624,7 +783,7 @@ class Encoder {
|
|
|
624
783
|
* simply parses it with the gate switched off at every choice arm that reaches
|
|
625
784
|
* a hole.
|
|
626
785
|
*
|
|
627
|
-
* Measured on jess's less grammar (`
|
|
786
|
+
* Measured on jess's less grammar (with the one-off `macro-program-diff` bench, last at `4a5d0341`): 195 of
|
|
628
787
|
* 562 reachable choice arms carried NO first set in the shipped artifact
|
|
629
788
|
* against 103 of 540 in the runtime encode, and 10 dispatch sites lost the O(1)
|
|
630
789
|
* `exclusive` piece (`assemble.ts:1758`) for the candidate-mask loop. With this
|
|
@@ -724,7 +883,7 @@ class Encoder {
|
|
|
724
883
|
|| op === OP_RX || op === OP_RX_TRACK) return child
|
|
725
884
|
const rr = this.refResolver()
|
|
726
885
|
if (matchesEmpty(p, new Set(), rr)) return child
|
|
727
|
-
const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
|
|
886
|
+
const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia, false))
|
|
728
887
|
if (cls < 0) return child
|
|
729
888
|
const e = deriveExpected(p)
|
|
730
889
|
return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
|
|
@@ -756,11 +915,39 @@ class Encoder {
|
|
|
756
915
|
* `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
|
|
757
916
|
* over the final merged map.
|
|
758
917
|
*/
|
|
759
|
-
|
|
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 {
|
|
760
939
|
const cached = p._meta.firstSet
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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())
|
|
764
951
|
}
|
|
765
952
|
|
|
766
953
|
/**
|
|
@@ -792,6 +979,133 @@ class Encoder {
|
|
|
792
979
|
return ip
|
|
793
980
|
}
|
|
794
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
|
+
|
|
795
1109
|
/** Reserve `n` operand slots to patch once children are laid out. */
|
|
796
1110
|
private emitHead(op: number, n: number): number {
|
|
797
1111
|
const ip = this.code.length
|
|
@@ -845,8 +1159,10 @@ class Encoder {
|
|
|
845
1159
|
// scope for a `notAdjacent({ kinds })` anywhere inside it — see the same
|
|
846
1160
|
// save/restore in `case 'grammar'`.
|
|
847
1161
|
this.activeTrivia = p._meta.grammarTrivia
|
|
1162
|
+
this.triviaKnown = p._meta.grammarTrivia !== undefined
|
|
848
1163
|
const body = this.node(p).ip
|
|
849
1164
|
this.activeTrivia = undefined
|
|
1165
|
+
this.triviaKnown = true
|
|
850
1166
|
// GRAMMAR-LEVEL AMBIENT TRIVIA (`rules({ trivia }, …)`).
|
|
851
1167
|
//
|
|
852
1168
|
// `run()` installs this from `entry._meta.grammarTrivia`, but ONLY for a
|
|
@@ -930,13 +1246,19 @@ class Encoder {
|
|
|
930
1246
|
* silently produced the ONE keyword `bc`.
|
|
931
1247
|
*/
|
|
932
1248
|
private scopedRef(p: Combinator<unknown>, target: Combinator<unknown>): number {
|
|
933
|
-
const ip = this.node(target).ip
|
|
934
1249
|
// `rules()` stamps `grammarTrivia` on the map entry, which for a proxied rule
|
|
935
1250
|
// is the REF itself; a composed `winners` entry carries it on the target.
|
|
936
|
-
const
|
|
937
|
-
if (
|
|
938
|
-
|
|
939
|
-
|
|
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)
|
|
940
1262
|
}
|
|
941
1263
|
|
|
942
1264
|
/**
|
|
@@ -948,7 +1270,7 @@ class Encoder {
|
|
|
948
1270
|
* unable to match empty or to commit, so a miss is exactly that term failing
|
|
949
1271
|
* at the token's end with its own set.
|
|
950
1272
|
*/
|
|
951
|
-
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 {
|
|
952
1274
|
type Follow = 'end' | { next: Combinator<unknown>; boundary: Combinator<unknown> | undefined } | undefined
|
|
953
1275
|
const walk = (p: Combinator<unknown>, trivia: Combinator<unknown> | undefined, depth: number): Follow => {
|
|
954
1276
|
if (depth > 16) return undefined
|
|
@@ -983,45 +1305,51 @@ class Encoder {
|
|
|
983
1305
|
// reports that opener once statically, once per arm when it runs.
|
|
984
1306
|
const nextDef = next._def as ParserDef
|
|
985
1307
|
if (nextDef.tag !== 'literal' && nextDef.tag !== 'regex' && nextDef.tag !== 'keywords' && nextDef.tag !== 'label') return undefined
|
|
986
|
-
|
|
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)
|
|
987
1312
|
const cls = this.charClass(boundary === undefined ? first : union(first, firstSetOf(boundary, new Set(), rr)))
|
|
988
1313
|
if (cls < 0) return undefined
|
|
989
|
-
const
|
|
990
|
-
|
|
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] }
|
|
991
1324
|
}
|
|
992
1325
|
|
|
993
1326
|
node(p: Combinator<unknown>): Emitted {
|
|
994
|
-
// See `
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const
|
|
998
|
-
const refMemo = this.refMemo
|
|
999
|
-
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)
|
|
1000
1331
|
if (hit !== undefined) return { ip: hit }
|
|
1001
1332
|
// Recursion: reserve a trampoline row now, patch its target when the real
|
|
1002
1333
|
// body lands. One extra indirection per recursive rule, zero per call site.
|
|
1003
|
-
|
|
1334
|
+
let inFlightByScope = this.pending.get(p)
|
|
1335
|
+
const inFlight = inFlightByScope?.get(key)
|
|
1004
1336
|
if (inFlight !== undefined) {
|
|
1005
1337
|
const ip = this.emitHead(OP_RULE, 1)
|
|
1006
1338
|
inFlight.push(ip + 1)
|
|
1007
1339
|
return { ip }
|
|
1008
1340
|
}
|
|
1009
1341
|
const patches: number[] = []
|
|
1010
|
-
this.pending.set(p,
|
|
1342
|
+
if (inFlightByScope === undefined) { inFlightByScope = new Map(); this.pending.set(p, inFlightByScope) }
|
|
1343
|
+
inFlightByScope.set(key, patches)
|
|
1011
1344
|
// A RULE IS CREDITED ON ENTRY, not on success — the source lowering emitted the
|
|
1012
1345
|
// hook as the first statement of the named function body, so a rule that is
|
|
1013
1346
|
// reached and then fails still counts as exercised. The counter goes on the
|
|
1014
1347
|
// MEMOISED offset, which is the one every reference to this combinator lands
|
|
1015
1348
|
// on, and the recursion trampolines below are patched with it too.
|
|
1016
1349
|
const ip = this.covWrap(this.encodeDef(p), this.plan?.rules.get(p), 0)
|
|
1017
|
-
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1020
|
-
if (byScope === undefined) { byScope = new Map(); refMemo.set(p, byScope) }
|
|
1021
|
-
byScope.set(scopeKey, ip)
|
|
1022
|
-
} else {
|
|
1023
|
-
this.memo.set(p, ip)
|
|
1024
|
-
}
|
|
1350
|
+
inFlightByScope.delete(key)
|
|
1351
|
+
if (byScope === undefined) { byScope = new Map(); this.memo.set(p, byScope) }
|
|
1352
|
+
byScope.set(key, ip)
|
|
1025
1353
|
for (const slot of patches) {
|
|
1026
1354
|
// A TRAMPOLINE PATCHED TO ITSELF IS NOT A TABLE. `OP_RULE ip → ip` makes no
|
|
1027
1355
|
// progress in either driver: `exec.ts`'s `case OP_RULE` recurses on the same
|
|
@@ -1150,8 +1478,24 @@ class Encoder {
|
|
|
1150
1478
|
// Arms that differ in one term are that term's choice (`factor-choice.ts`).
|
|
1151
1479
|
// Not under a coverage plan, which credits the arms as written.
|
|
1152
1480
|
if (this.plan === undefined) {
|
|
1153
|
-
const
|
|
1154
|
-
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
|
+
}
|
|
1155
1499
|
}
|
|
1156
1500
|
// `greedyClassify` is NOT a choice at all — one arm runs and another arm
|
|
1157
1501
|
// is credited — so it gets its own row rather than an arm ordering.
|
|
@@ -1226,7 +1570,9 @@ class Encoder {
|
|
|
1226
1570
|
&& (gate === null || gate === undefined)
|
|
1227
1571
|
const routedGate = plain ? this.routedArmGate(c) : undefined
|
|
1228
1572
|
const body = this.node(c).ip
|
|
1229
|
-
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])
|
|
1230
1576
|
const ip = checks === null || checks === undefined || checks.length === 0
|
|
1231
1577
|
? inner
|
|
1232
1578
|
: this.reject(inner, checks)
|
|
@@ -1269,7 +1615,13 @@ class Encoder {
|
|
|
1269
1615
|
// the O(1) table (`exclusive`) or falls to the ordered per-arm path.
|
|
1270
1616
|
// Arm ORDER is preserved on both, which is what makes this a PEG-safe
|
|
1271
1617
|
// change rather than a reordering.
|
|
1272
|
-
|
|
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)
|
|
1273
1625
|
// attempt() preserves its child's success language, but the broad
|
|
1274
1626
|
// classifier deliberately treats the transaction as nullable/unknown so
|
|
1275
1627
|
// it can never by itself authorize exclusive one-arm selection. Recover
|
|
@@ -1281,7 +1633,7 @@ class Encoder {
|
|
|
1281
1633
|
if (!finalChoice.nullable[i]) return undefined
|
|
1282
1634
|
const def = arm._def as ParserDef
|
|
1283
1635
|
if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
|
|
1284
|
-
return firstSetOf(def.parser, new Set(), rr, this.activeTrivia)
|
|
1636
|
+
return firstSetOf(def.parser, new Set(), rr, this.activeTrivia, false)
|
|
1285
1637
|
})
|
|
1286
1638
|
const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
|
|
1287
1639
|
const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
|
|
@@ -1304,6 +1656,13 @@ class Encoder {
|
|
|
1304
1656
|
// ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
|
|
1305
1657
|
// its classes: there the dispatched arm's gate failing IS the choice
|
|
1306
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
|
+
}
|
|
1307
1666
|
if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
|
|
1308
1667
|
for (let k = 0; k < classes.length; k++) {
|
|
1309
1668
|
const gate = d.gates[order[k]!]
|
|
@@ -1349,13 +1708,18 @@ class Encoder {
|
|
|
1349
1708
|
}
|
|
1350
1709
|
case 'many':
|
|
1351
1710
|
case 'oneOrMore': {
|
|
1352
|
-
const child = this.node(d.parser).ip
|
|
1353
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.
|
|
1354
1714
|
const itemClass = matchesEmpty(d.parser, new Set(), rr)
|
|
1355
1715
|
? -1
|
|
1356
|
-
: this.charClass(
|
|
1716
|
+
: this.charClass(this.gateFirstSet(d.parser, d.guarded === true, true))
|
|
1357
1717
|
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
1358
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
|
|
1359
1723
|
// ip + 6 is the ITEM's expected set and ip + 7 the item's sentinel class:
|
|
1360
1724
|
// the resync error's payload is the ITEM's set (`deriveExpected` in
|
|
1361
1725
|
// `combinators/repeat.ts`).
|
|
@@ -1371,20 +1735,33 @@ class Encoder {
|
|
|
1371
1735
|
// under `min` reports what would have let it CONTINUE — the ITEM, which
|
|
1372
1736
|
// is what `failAt` (repeat.ts) and `deriveExpectedArr([def.parser])`
|
|
1373
1737
|
// (codegen emitSepBy) both use; the driver otherwise reports whatever
|
|
1374
|
-
// sub-parse failed last
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
const
|
|
1379
|
-
//
|
|
1380
|
-
// 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
|
|
1381
1751
|
// a tolerant list resyncs to its own separator OR the enclosing
|
|
1382
1752
|
// delimiter — `orSentinel(separator, term)` in repeat.ts:454 and
|
|
1383
|
-
// `_rec.or(sepSent, mySync)` in codegen's emitSepBy).
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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)
|
|
1388
1765
|
}
|
|
1389
1766
|
case 'optional': {
|
|
1390
1767
|
const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
|
|
@@ -1400,7 +1777,7 @@ class Encoder {
|
|
|
1400
1777
|
// rule invocation. The inner sequence must not be shared with anything
|
|
1401
1778
|
// else, or fusing it here would steal it from the other reference.
|
|
1402
1779
|
const inner = d.parser._def as ParserDef
|
|
1403
|
-
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))) {
|
|
1404
1781
|
const kids = inner.parsers.map(c => this.node(c).ip)
|
|
1405
1782
|
const head = this.emitHead(OP_SEQX, 2 + 2 * kids.length)
|
|
1406
1783
|
const projection = directArrayProjection(d.fn, d.fnSrc, kids.length)
|
|
@@ -1513,7 +1890,11 @@ class Encoder {
|
|
|
1513
1890
|
// accepts (the reducer reroutes via `winners`, but this recognizer gate did
|
|
1514
1891
|
// not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
|
|
1515
1892
|
// resolves the call. Non-ref bodies keep the cached set (byte-identical).
|
|
1516
|
-
|
|
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)
|
|
1517
1898
|
if (fs.kind === 'any') return body
|
|
1518
1899
|
const cls = this.charClass(fs)
|
|
1519
1900
|
if (cls < 0) return body
|
|
@@ -1521,7 +1902,16 @@ class Encoder {
|
|
|
1521
1902
|
return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
|
|
1522
1903
|
}
|
|
1523
1904
|
case 'token': {
|
|
1524
|
-
|
|
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)
|
|
1525
1915
|
}
|
|
1526
1916
|
case 'routed':
|
|
1527
1917
|
return this.emit(OP_ROUTED, d.fallback === undefined ? -1 : this.node(d.fallback).ip)
|
|
@@ -1610,7 +2000,9 @@ class Encoder {
|
|
|
1610
2000
|
const otherRouted = d.otherwise === undefined
|
|
1611
2001
|
? false
|
|
1612
2002
|
: branchUsesRouted({ parser: d.otherwise as Combinator<unknown>, usesRouted: d.otherwiseUsesRouted === true })
|
|
1613
|
-
|
|
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)
|
|
1614
2006
|
this.code[head + 5] = arms.length
|
|
1615
2007
|
for (let i = 0; i < arms.length; i++) this.code[head + 6 + i] = arms[i]!
|
|
1616
2008
|
return head
|
|
@@ -1714,7 +2106,7 @@ class Encoder {
|
|
|
1714
2106
|
// not a slower equivalent.
|
|
1715
2107
|
// Same override-aware resolution as the node gate: a bare-ref inner resolves
|
|
1716
2108
|
// its gate through `winners` so a widened winner is not gated out.
|
|
1717
|
-
const fs = this.gateFirstSet(d.parser)
|
|
2109
|
+
const fs = this.gateFirstSet(d.parser, d.guarded === true, false)
|
|
1718
2110
|
if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
|
|
1719
2111
|
const cls = this.charClass(fs)
|
|
1720
2112
|
if (cls < 0) return inner
|
|
@@ -1793,10 +2185,12 @@ class Encoder {
|
|
|
1793
2185
|
// `ctx.activeTrivia` does. Saved and restored rather than assigned,
|
|
1794
2186
|
// because a scope is nestable and an inner one does not leak outward.
|
|
1795
2187
|
const savedTrivia = this.activeTrivia
|
|
1796
|
-
|
|
1797
|
-
|
|
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 }
|
|
1798
2191
|
const bodyIp = this.node(d.parser).ip
|
|
1799
2192
|
this.activeTrivia = savedTrivia
|
|
2193
|
+
this.triviaKnown = savedKnown
|
|
1800
2194
|
if (d.clearTrivia === true) return this.emit(op, -1, bodyIp, flags)
|
|
1801
2195
|
if (d.triviaParser === undefined) {
|
|
1802
2196
|
return cap || flags !== 0 ? this.emit(op, -1, bodyIp, flags) : bodyIp
|
|
@@ -1839,8 +2233,8 @@ class Encoder {
|
|
|
1839
2233
|
}
|
|
1840
2234
|
const slots = (ip: number): number[] => {
|
|
1841
2235
|
switch (this.code[ip]) {
|
|
1842
|
-
case OP_GATE: case OP_RGATE: return [ip + 2]
|
|
1843
|
-
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]
|
|
1844
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]
|
|
1845
2239
|
case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
|
|
1846
2240
|
case OP_SEQX: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 3 + i)
|
|
@@ -1859,6 +2253,7 @@ class Encoder {
|
|
|
1859
2253
|
// REJECT's check operands are const/class indices, never offsets.
|
|
1860
2254
|
case OP_REJECT: return [ip + 1]
|
|
1861
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]
|
|
1862
2257
|
default: return []
|
|
1863
2258
|
}
|
|
1864
2259
|
}
|