parseman 0.59.3 → 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.
Files changed (84) hide show
  1. package/dist/analysis/diagnostics.js +874 -188
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +874 -188
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts +13 -1
  6. package/dist/combinators/dispatch.d.ts.map +1 -1
  7. package/dist/combinators/first-set.d.ts +35 -2
  8. package/dist/combinators/first-set.d.ts.map +1 -1
  9. package/dist/combinators/regex.d.ts.map +1 -1
  10. package/dist/combinators/trivia-boundary.d.ts +11 -0
  11. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  12. package/dist/combinators/trivia-skip.d.ts +1 -1
  13. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  14. package/dist/compiler/token-alphabet.d.ts +2 -0
  15. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  16. package/dist/compiler/token-capability.js +65 -53
  17. package/dist/compiler/token-capability.js.map +2 -2
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +894 -197
  21. package/dist/index.js.map +3 -3
  22. package/dist/plugin/evaluator.d.ts.map +1 -1
  23. package/dist/plugin/index.js +901 -199
  24. package/dist/plugin/index.js.map +3 -3
  25. package/dist/regex/first-set.d.ts +19 -0
  26. package/dist/regex/first-set.d.ts.map +1 -1
  27. package/dist/table/assemble.d.ts +14 -0
  28. package/dist/table/assemble.d.ts.map +1 -1
  29. package/dist/table/child-slots.d.ts.map +1 -1
  30. package/dist/table/compile-rule-map.d.ts.map +1 -1
  31. package/dist/table/emit-assembly.d.ts +8 -2
  32. package/dist/table/emit-assembly.d.ts.map +1 -1
  33. package/dist/table/emit.d.ts +4 -3
  34. package/dist/table/emit.d.ts.map +1 -1
  35. package/dist/table/encode.d.ts.map +1 -1
  36. package/dist/table/exec.d.ts.map +1 -1
  37. package/dist/table/factor-choice.d.ts +27 -0
  38. package/dist/table/factor-choice.d.ts.map +1 -1
  39. package/dist/table/gap-gate.d.ts +11 -0
  40. package/dist/table/gap-gate.d.ts.map +1 -0
  41. package/dist/table/index.js +924 -199
  42. package/dist/table/index.js.map +3 -3
  43. package/dist/table/lex-program.d.ts.map +1 -1
  44. package/dist/table/ops.d.ts +34 -6
  45. package/dist/table/ops.d.ts.map +1 -1
  46. package/dist/table/program.d.ts +6 -1
  47. package/dist/table/program.d.ts.map +1 -1
  48. package/dist/types.d.ts +16 -4
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/version.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/analysis/duplication.ts +1 -1
  53. package/src/combinators/attempt.ts +1 -1
  54. package/src/combinators/dispatch.ts +30 -6
  55. package/src/combinators/first-set.ts +55 -12
  56. package/src/combinators/node.ts +1 -1
  57. package/src/combinators/regex.ts +9 -4
  58. package/src/combinators/repeat.ts +6 -6
  59. package/src/combinators/sequence.ts +1 -1
  60. package/src/combinators/trivia-boundary.ts +18 -0
  61. package/src/combinators/trivia-skip.ts +1 -1
  62. package/src/compiler/ir-serialize.ts +1 -1
  63. package/src/compiler/token-alphabet.ts +3 -1
  64. package/src/coverage.ts +1 -1
  65. package/src/index.ts +1 -1
  66. package/src/plugin/evaluator.ts +8 -3
  67. package/src/regex/first-set.ts +37 -3
  68. package/src/table/assemble.ts +171 -25
  69. package/src/table/child-slots.ts +8 -3
  70. package/src/table/compile-rule-map.ts +6 -3
  71. package/src/table/emit-assembly.ts +182 -31
  72. package/src/table/emit.ts +6 -4
  73. package/src/table/encode.ts +468 -73
  74. package/src/table/exec.ts +38 -13
  75. package/src/table/factor-choice.ts +62 -0
  76. package/src/table/fail-clean.ts +2 -2
  77. package/src/table/gap-gate.ts +55 -0
  78. package/src/table/hold-gap.ts +2 -2
  79. package/src/table/lex-program.ts +15 -6
  80. package/src/table/ops.ts +37 -6
  81. package/src/table/program.ts +7 -2
  82. package/src/table/select-hot.ts +7 -2
  83. package/src/types.ts +12 -5
  84. package/src/version.ts +1 -1
@@ -1,7 +1,8 @@
1
1
  import { scanRefs } from './program.ts'
2
- import { factorSharedEnds } from './factor-choice.ts'
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 { hasOwnTriviaBoundary } from '../combinators/trivia-boundary.ts'
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 ? undefined : this.activeTrivia,
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
- * Memo for `lazy` REFERENCES, keyed by the trivia scope the reference is made
506
- * from. A cross-rule reference's row depends on that scope (`scopedRef`), so the
507
- * single `memo` slot every other construct uses would freeze the FIRST
508
- * reference's scope onto every later one and undo the fix.
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
- refMemo = new Map<Combinator<unknown>, Map<Combinator<unknown> | undefined, number>>()
511
- pending = new Map<Combinator<unknown>, number[]>()
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
- fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr, this.activeTrivia))
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
  }
@@ -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
- private gateFirstSet(p: Combinator<unknown>): FirstSet {
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
- return p._def.tag === 'lazy' || cached.kind === 'any'
762
- ? firstSetOf(p, new Set(), this.refResolver(), this.activeTrivia)
763
- : cached
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 amb = p._meta.grammarTrivia ?? target._meta.grammarTrivia
937
- if (amb === undefined || this.activeTrivia !== undefined) return ip
938
- if (!hasOwnTriviaBoundary(target)) return ip
939
- return this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), ip)
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
- const first = firstSetOf(next, new Set(), rr, boundary)
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 miss = nextDef.tag === 'label' ? deriveExpected(next) : directTerminalFailureExpected(nextDef)
990
- return { cls, fx: this.expected(miss) }
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 `refMemo`: a reference is memoised per REFERENCING trivia scope, every
995
- // other construct once for the program.
996
- const lazyRef = (p._def as ParserDef).tag === 'lazy'
997
- const scopeKey = lazyRef ? this.activeTrivia : undefined
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
- const inFlight = this.pending.get(p)
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, patches)
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
- this.pending.delete(p)
1018
- if (lazyRef) {
1019
- let byScope = refMemo.get(p)
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 factored = factorSharedEnds(d)
1154
- if (factored !== undefined) return this.node(factored).ip
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 : this.emit(OP_RGATE, routedGate.cls, body, routedGate.fx)
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
- const finalChoice = classifyFinalChoice(arms, rr, this.activeTrivia)
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(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
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, which is the SEPARATOR. Only `min >= 2` can
1375
- // reach that end under `min` with items already taken — at `min === 1`
1376
- // it means the FIRST item failed, and that failure already set the
1377
- // item's own set. So no committed grammar grows by a word.
1378
- const flags = (d.trailing === 'allow' ? 1 : 0) | (d.keepSeparators === true ? 2 : 0) | (d.min >= 2 ? 4 : 0)
1379
- // Every list row carries ip + 6 (the ITEM's expected set, which
1380
- // the resync error reports) and ip + 7 (the SEPARATOR's sentinel class, so
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
- return this.emit(
1385
- OP_REP, child, d.min, d.max ?? -1, sep, flags,
1386
- this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
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
- const fs = this.gateFirstSet(d.parser)
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
- return this.emit(OP_TOKEN, this.node(d.parser).ip)
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
- this.code[head + 4] = otherRouted ? 1 : 0
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
- if (d.clearTrivia === true) this.activeTrivia = undefined
1797
- else if (d.triviaParser !== undefined) this.activeTrivia = d.triviaParser
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
  }