parseman 0.59.2 → 0.60.0

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