parseman 0.47.1 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +27 -21
  2. package/dist/analysis/diagnostics.js +2964 -347
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2960 -343
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/choice.d.ts.map +1 -1
  7. package/dist/combinators/expect.d.ts +1 -0
  8. package/dist/combinators/expect.d.ts.map +1 -1
  9. package/dist/combinators/expected.d.ts +8 -0
  10. package/dist/combinators/expected.d.ts.map +1 -0
  11. package/dist/combinators/first-set.d.ts +8 -0
  12. package/dist/combinators/first-set.d.ts.map +1 -1
  13. package/dist/combinators/grammar.d.ts.map +1 -1
  14. package/dist/combinators/keywords.d.ts +4 -0
  15. package/dist/combinators/keywords.d.ts.map +1 -1
  16. package/dist/combinators/literal.d.ts.map +1 -1
  17. package/dist/combinators/not.d.ts.map +1 -1
  18. package/dist/combinators/peek.d.ts.map +1 -1
  19. package/dist/combinators/regex.d.ts.map +1 -1
  20. package/dist/combinators/repeat.d.ts.map +1 -1
  21. package/dist/combinators/sequence.d.ts.map +1 -1
  22. package/dist/combinators/trivia-skip.d.ts +16 -3
  23. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  24. package/dist/compiler/direct-projection.d.ts +12 -0
  25. package/dist/compiler/direct-projection.d.ts.map +1 -0
  26. package/dist/compiler/token-alphabet.d.ts +845 -5
  27. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  28. package/dist/compiler/token-capability.d.ts +3 -0
  29. package/dist/compiler/token-capability.d.ts.map +1 -0
  30. package/dist/compiler/token-capability.js +3192 -0
  31. package/dist/compiler/token-capability.js.map +6 -0
  32. package/dist/cst/trivia-charscan.d.ts +3 -0
  33. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  34. package/dist/cst/trivia-css-scanner.d.ts +5 -0
  35. package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
  36. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  37. package/dist/index.js +2973 -343
  38. package/dist/index.js.map +3 -3
  39. package/dist/language-service/index.js +5 -1
  40. package/dist/language-service/index.js.map +1 -1
  41. package/dist/plugin/index.js +2982 -352
  42. package/dist/plugin/index.js.map +3 -3
  43. package/dist/spec/index.js +325 -47
  44. package/dist/spec/index.js.map +3 -3
  45. package/dist/table/assemble.d.ts.map +1 -1
  46. package/dist/table/child-slots.d.ts.map +1 -1
  47. package/dist/table/compile-rule-map.d.ts +3 -1
  48. package/dist/table/compile-rule-map.d.ts.map +1 -1
  49. package/dist/table/emit-assembly.d.ts +12 -13
  50. package/dist/table/emit-assembly.d.ts.map +1 -1
  51. package/dist/table/emit.d.ts.map +1 -1
  52. package/dist/table/encode.d.ts.map +1 -1
  53. package/dist/table/exec-baseline.d.ts.map +1 -1
  54. package/dist/table/exec.d.ts.map +1 -1
  55. package/dist/table/index.js +2937 -311
  56. package/dist/table/index.js.map +3 -3
  57. package/dist/table/lex-body.d.ts +9 -0
  58. package/dist/table/lex-body.d.ts.map +1 -0
  59. package/dist/table/lex-program.d.ts +25 -0
  60. package/dist/table/lex-program.d.ts.map +1 -0
  61. package/dist/table/ops.d.ts +47 -11
  62. package/dist/table/ops.d.ts.map +1 -1
  63. package/dist/table/program.d.ts +50 -2
  64. package/dist/table/program.d.ts.map +1 -1
  65. package/dist/table/scalar-terminal.d.ts +13 -0
  66. package/dist/table/scalar-terminal.d.ts.map +1 -0
  67. package/dist/table/site-labels.d.ts.map +1 -1
  68. package/dist/types.d.ts +2 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/version.d.ts +1 -1
  71. package/package.json +11 -11
  72. package/src/combinators/choice.ts +10 -1
  73. package/src/combinators/expect.ts +1 -0
  74. package/src/combinators/expected.ts +17 -0
  75. package/src/combinators/first-set.ts +58 -5
  76. package/src/combinators/grammar.ts +8 -3
  77. package/src/combinators/keywords.ts +22 -6
  78. package/src/combinators/literal.ts +2 -1
  79. package/src/combinators/not.ts +5 -1
  80. package/src/combinators/peek.ts +5 -1
  81. package/src/combinators/regex.ts +5 -3
  82. package/src/combinators/repeat.ts +4 -10
  83. package/src/combinators/sequence.ts +17 -14
  84. package/src/combinators/trivia-skip.ts +46 -13
  85. package/src/compiler/direct-projection.ts +43 -0
  86. package/src/compiler/token-alphabet.ts +4316 -12
  87. package/src/compiler/token-capability.ts +6 -0
  88. package/src/cst/trivia-charscan.ts +34 -0
  89. package/src/cst/trivia-css-scanner.ts +142 -0
  90. package/src/cst/trivia-kinds.ts +3 -1
  91. package/src/plugin/index.ts +4 -4
  92. package/src/table/assemble.ts +1253 -240
  93. package/src/table/child-slots.ts +2 -1
  94. package/src/table/compile-rule-map.ts +21 -3
  95. package/src/table/emit-assembly.ts +335 -87
  96. package/src/table/emit.ts +16 -2
  97. package/src/table/encode.ts +248 -60
  98. package/src/table/exec-baseline.ts +3 -7
  99. package/src/table/exec.ts +134 -25
  100. package/src/table/lex-body.ts +30 -0
  101. package/src/table/lex-program.ts +431 -0
  102. package/src/table/ops.ts +49 -11
  103. package/src/table/program.ts +46 -2
  104. package/src/table/scalar-terminal.ts +51 -0
  105. package/src/table/site-labels.ts +11 -1
  106. package/src/table/stamp.ts +1 -1
  107. package/src/types.ts +1 -1
  108. package/src/version.ts +1 -1
  109. package/dist/analysis/diagnostics.cjs +0 -13824
  110. package/dist/analysis/diagnostics.cjs.map +0 -6
  111. package/dist/cli/index.cjs +0 -14925
  112. package/dist/cli/index.cjs.map +0 -6
  113. package/dist/index.cjs +0 -16391
  114. package/dist/index.cjs.map +0 -6
  115. package/dist/language-service/index.cjs +0 -1304
  116. package/dist/language-service/index.cjs.map +0 -6
  117. package/dist/oracle/index.cjs +0 -254
  118. package/dist/oracle/index.cjs.map +0 -6
  119. package/dist/plugin/index.cjs +0 -16097
  120. package/dist/plugin/index.cjs.map +0 -6
  121. package/dist/run/index.cjs +0 -577
  122. package/dist/run/index.cjs.map +0 -6
  123. package/dist/spec/index.cjs +0 -6236
  124. package/dist/spec/index.cjs.map +0 -6
  125. package/dist/table/index.cjs +0 -11463
  126. package/dist/table/index.cjs.map +0 -6
@@ -71,8 +71,8 @@ import { projectChild, unwrapChild } from '../combinators/node.ts'
71
71
  import { cstOutputHost } from '../compiler/build-arity.ts'
72
72
  import { consumeTrivia } from '../combinators/trivia-skip.ts'
73
73
  import {
74
- advanceTrivia, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTrivia, rollbackTriviaAt,
75
- saveTriviaMark, scanTrivia, skipTriviaScanned, type FastTriviaScanner,
74
+ advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTrivia, rollbackTriviaAt,
75
+ saveTriviaMark, scanTrivia, scanTriviaCompact, skipTriviaScanned, type FastTriviaScanner,
76
76
  } from '../combinators/trivia-skip.ts'
77
77
  import {
78
78
  cstCaptureActive, cstLeavesLen, cstTlLen,
@@ -87,6 +87,7 @@ import {
87
87
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
88
88
  OP_COV,
89
89
  OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
90
+ OP_LEX_BODY, OP_LEX_PROGRAM,
90
91
  } from './ops.ts'
91
92
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
92
93
  import { failAt } from '../combinators/probe.ts'
@@ -118,14 +119,18 @@ import { lead, rawEntry, spanLines } from './run-support.ts'
118
119
  * property read per terminal miss — the same price codegen pays.
119
120
  */
120
121
  import {
121
- classHas, decodeClassSpec, expandCompact, resolveTable, validateDispatchSpec,
122
+ classHas, decodeClassSpec, expandCompact, resolveTable,
123
+ validateDispatchSpec,
122
124
  type CompactProgram, type ResolvedClass, type ResolvedTable,
123
125
  type SubtreeRef, type TableProgram, type TableRule,
124
126
  } from './program.ts'
125
- import { reachableSites } from './site-labels.ts'
126
127
  import { stampRuleMap } from './stamp.ts'
128
+ import { reachableSites } from './site-labels.ts'
127
129
  import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
128
130
  import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
131
+ import {
132
+ makeScalarRecognizer, scalarTerminalNodeChild, scalarTerminalNotChild, type ScalarRecognizer,
133
+ } from './scalar-terminal.ts'
129
134
 
130
135
  /**
131
136
  * Is the EMITTED engine (`emit-assembly.ts`) enabled for this process?
@@ -139,7 +144,7 @@ const EMIT_ENABLED = (globalThis as { process?: { env?: Record<string, string |
139
144
  .process?.env?.PM_TABLE_EMIT !== '0'
140
145
 
141
146
  /** Failure sentinel — SHARED with the other two engines, see `cell.ts`. */
142
- import { FAIL, newEndCell, type EndCell } from './cell.ts'
147
+ import { FAIL, newEndCell } from './cell.ts'
143
148
 
144
149
  const EMPTY_TL: readonly number[] = Object.freeze([])
145
150
  /** `finishCstBuf`'s two empty sentinels, held here so the node piece can inline it. */
@@ -167,10 +172,20 @@ function linkMatcher(m: readonly [number, string, string, number]): (key: string
167
172
  case 1: return key => key.endsWith(value)
168
173
  case 3: return key => asciiFoldKey(key).startsWith(value)
169
174
  case 4: return key => asciiFoldKey(key).endsWith(value)
170
- // A fresh RegExp per test, as `matchesDispatchMatcher` builds — a cached one
171
- // would carry `lastIndex` across parses whenever the author's pattern was
172
- // sticky or global.
173
- default: { const flags = m[2]; return key => new RegExp(value, flags).test(key) }
175
+ default: {
176
+ const flags = m[2]
177
+ // `matches()` refuses global/sticky patterns, so every compiler-owned
178
+ // matcher has stable `lastIndex` and can be compiled once per assembly.
179
+ // Preserve the old fresh-per-test behavior for hand-built low-level rows
180
+ // that contain g/y or invalid flags rather than broadening the wire ABI.
181
+ if (!flags.includes('g') && !flags.includes('y')) {
182
+ try {
183
+ const pattern = new RegExp(value, flags)
184
+ return key => pattern.test(key)
185
+ } catch {}
186
+ }
187
+ return key => new RegExp(value, flags).test(key)
188
+ }
174
189
  }
175
190
  }
176
191
 
@@ -198,6 +213,36 @@ const NO_COVERAGE = (_id: string): void => {}
198
213
  * `{ value, end }` pair would be an allocation per row.
199
214
  */
200
215
  type Piece = (input: string, pos: number, ctx: ParseContext) => unknown
216
+ type NodeBuilder = (
217
+ children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
218
+ rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
219
+ ) => unknown
220
+ type ChoiceExpectedPrefix = (
221
+ target: number, prev: number, acc: string[] | undefined,
222
+ ) => string[] | undefined
223
+ type MaskedChoiceBlock = (
224
+ input: string, pos: number, ctx: ParseContext, bits: number,
225
+ need: boolean, mRaw: number, mTl: number, mLv: number, mFl: number,
226
+ mEr: number, mLog: number, mRoot: number,
227
+ acc: string[] | undefined, prev: number, best: number,
228
+ ) => unknown
229
+ type GeneralChoiceBlock = (
230
+ input: string, pos: number, ctx: ParseContext, c: number,
231
+ need: boolean, mRaw: number, mTl: number, mLv: number, mFl: number,
232
+ mEr: number, mLog: number, mRoot: number,
233
+ acc: string[] | undefined, prev: number, best: number,
234
+ ) => unknown
235
+ type ExclusiveChoiceBlock = (arm: number, input: string, pos: number, ctx: ParseContext) => unknown
236
+ type DispatchMatcher = (key: string) => boolean
237
+ type DispatchMatcherBlock = (key: string) => number | undefined
238
+ type DispatchBranch = (
239
+ input: string, pos: number, selEnd: number, key: string,
240
+ selectorMark: ReturnType<typeof saveTriviaMark>, ctx: ParseContext,
241
+ ) => unknown
242
+ type DispatchArmBlock = (
243
+ arm: number, input: string, pos: number, selEnd: number, key: string,
244
+ selectorMark: ReturnType<typeof saveTriviaMark>, ctx: ParseContext,
245
+ ) => unknown
201
246
 
202
247
  /**
203
248
  * ONE NON-FIRST SEQUENCE TERM, in a sequence that carries an adjacency
@@ -205,7 +250,14 @@ type Piece = (input: string, pos: number, ctx: ParseContext) => unknown
205
250
  * `nextTerm`'s protocol, so a bound term and a bound assertion are
206
251
  * interchangeable in the loop and the loop tests neither.
207
252
  */
208
- type TermRunner = (input: string, cur: number, ctx: ParseContext) => number
253
+ type TermRunner = (
254
+ input: string, cur: number, ctx: ParseContext,
255
+ inheritedSync?: Combinator<unknown>,
256
+ ) => number
257
+ type SequenceTermBlock = (
258
+ input: string, cur: number, ctx: ParseContext, values: unknown[] | undefined,
259
+ inheritedSync?: Combinator<unknown>,
260
+ ) => number
209
261
 
210
262
  /**
211
263
  * The option set an assembly is specialised for.
@@ -415,6 +467,21 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
415
467
  */
416
468
  const EC = newEndCell()
417
469
 
470
+ // Indexed by the existing constant-pool operand: the same scalar recognizer
471
+ // serves an ordinary terminal piece and a direct terminal-node materializer.
472
+ // Value/CST/failure/cursor effects stay in those consumers.
473
+ const scalarRecognizers: Array<ScalarRecognizer | undefined> = []
474
+ const rawScalarSpecs = new Set<number>()
475
+ function scalarFor(child: number): ScalarRecognizer | undefined {
476
+ const spec = code[child + 1]!
477
+ let recognize = scalarRecognizers[spec]
478
+ if (recognize === undefined) {
479
+ recognize = makeScalarRecognizer(code[child]!, k[spec])
480
+ scalarRecognizers[spec] = recognize
481
+ }
482
+ return recognize
483
+ }
484
+
418
485
  /**
419
486
  * The INSTALLED trivia scanner for the scope currently running.
420
487
  *
@@ -433,6 +500,263 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
433
500
  pushCstLeaf(ctx, lf)
434
501
  }
435
502
 
503
+ /**
504
+ * The three dominant AST node shapes, selected once while linking.
505
+ *
506
+ * The generic node body is necessarily broad: CST hosts, tracked spans,
507
+ * fields, trivia/state reads, structural hosts, projections, collapse and
508
+ * unwrap all share it. That made its one FunctionLiteral 1,100+ V8 bytecodes
509
+ * even for the ordinary AST builder that needs none of those branches. Jess's
510
+ * exact AST workloads execute the three shapes below 42k/66k/205k times per
511
+ * CSS/Less/generated parse. Keep their bodies scalar and keep every other
512
+ * shape on the generic oracle below.
513
+ */
514
+ function plainBuildNode(child: Piece, build: NodeBuilder): Piece {
515
+ return (input, pos, ctx) => {
516
+ const sCh = ctx._cstChildren
517
+ const sLv = ctx._cstLeaves
518
+ const sRaw = ctx._cstRawChildren
519
+ const sTl = ctx._cstTriviaLog
520
+ const sCap = ctx.captureTrivia
521
+ const sBuf = ctx._cstBuf
522
+ const savedFields = ctx._fields
523
+ const buf: CstCaptureBuf = {}
524
+ ctx._cstBuf = buf
525
+ ctx._cstChildren = undefined
526
+ ctx._cstLeaves = undefined
527
+ ctx._cstRawChildren = undefined
528
+ ctx._cstTriviaLog = undefined
529
+ ctx.captureTrivia = false
530
+ ctx._fields = undefined
531
+ const value = child(input, pos, ctx)
532
+ const kids = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY_CH)
533
+ const rawKids = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY_CH)
534
+ ctx._fields = savedFields
535
+ ctx._cstBuf = sBuf
536
+ ctx._cstChildren = sCh
537
+ ctx._cstLeaves = sLv
538
+ ctx._cstRawChildren = sRaw
539
+ ctx._cstTriviaLog = sTl
540
+ ctx.captureTrivia = sCap
541
+ if (value === FAIL) return FAIL
542
+ const end = EC.e
543
+ const span = { start: pos, end }
544
+ const node = build(kids, undefined, span, rawKids, EMPTY_TL, undefined)
545
+ if (sBuf !== undefined || sCh !== undefined) {
546
+ pushCstChild(ctx, node, rawEntry(node, input, pos, end))
547
+ }
548
+ EC.e = end
549
+ return node
550
+ }
551
+ }
552
+
553
+ /** The lazy capture buffer uses `single === undefined` as its empty sentinel:
554
+ * leading undefined semantic children are therefore absent, while undefined
555
+ * values after the first present child are retained in `ch`. A flat collector
556
+ * normalizes only that cold prefix so the builder sees the identical list. */
557
+ function capturedFlatChildren(children: unknown[]): readonly unknown[] {
558
+ if (children.length === 0) return EMPTY_CH
559
+ if (children[0] !== undefined) return children
560
+ let first = 1
561
+ while (first < children.length && children[first] === undefined) first++
562
+ return first === children.length ? EMPTY_CH : children.slice(first)
563
+ }
564
+
565
+ /** Direct reducer whose declared arity proves `rawChildren` is unobservable.
566
+ * Use the existing split children/leaves collector instead of opening a
567
+ * duplicate raw capture buffer. The body shape is selected while linking. */
568
+ function childrenOnlyBuildNode(child: Piece, build: NodeBuilder): Piece {
569
+ return (input, pos, ctx) => {
570
+ const sCh = ctx._cstChildren
571
+ const sLv = ctx._cstLeaves
572
+ const sRaw = ctx._cstRawChildren
573
+ const sTl = ctx._cstTriviaLog
574
+ const sCap = ctx.captureTrivia
575
+ const sBuf = ctx._cstBuf
576
+ const savedFields = ctx._fields
577
+ const kids: unknown[] = []
578
+ ctx._cstBuf = undefined
579
+ ctx._cstChildren = kids
580
+ ctx._cstLeaves = kids
581
+ ctx._cstRawChildren = undefined
582
+ ctx._cstTriviaLog = undefined
583
+ ctx.captureTrivia = false
584
+ ctx._fields = undefined
585
+ const value = child(input, pos, ctx)
586
+ const captured = capturedFlatChildren(kids)
587
+ ctx._fields = savedFields
588
+ ctx._cstBuf = sBuf
589
+ ctx._cstChildren = sCh
590
+ ctx._cstLeaves = sLv
591
+ ctx._cstRawChildren = sRaw
592
+ ctx._cstTriviaLog = sTl
593
+ ctx.captureTrivia = sCap
594
+ if (value === FAIL) return FAIL
595
+ const end = EC.e
596
+ const node = build(captured, undefined, { start: pos, end }, EMPTY_CH, EMPTY_TL, undefined)
597
+ if (sBuf !== undefined || sCh !== undefined) {
598
+ pushCstChild(ctx, node, sBuf !== undefined || sRaw !== undefined ? rawEntry(node, input, pos, end) : undefined)
599
+ }
600
+ EC.e = end
601
+ return node
602
+ }
603
+ }
604
+
605
+ function childrenOnlyFieldsBuildNode(child: Piece, build: NodeBuilder): Piece {
606
+ return (input, pos, ctx) => {
607
+ const sCh = ctx._cstChildren
608
+ const sLv = ctx._cstLeaves
609
+ const sRaw = ctx._cstRawChildren
610
+ const sTl = ctx._cstTriviaLog
611
+ const sCap = ctx.captureTrivia
612
+ const sBuf = ctx._cstBuf
613
+ const savedFields = ctx._fields
614
+ const kids: unknown[] = []
615
+ ctx._cstBuf = undefined
616
+ ctx._cstChildren = kids
617
+ ctx._cstLeaves = kids
618
+ ctx._cstRawChildren = undefined
619
+ ctx._cstTriviaLog = undefined
620
+ ctx.captureTrivia = false
621
+ ctx._fields = []
622
+ const value = child(input, pos, ctx)
623
+ const captured = capturedFlatChildren(kids)
624
+ const fieldMap = buildFieldMap(ctx._fields)
625
+ ctx._fields = savedFields
626
+ ctx._cstBuf = sBuf
627
+ ctx._cstChildren = sCh
628
+ ctx._cstLeaves = sLv
629
+ ctx._cstRawChildren = sRaw
630
+ ctx._cstTriviaLog = sTl
631
+ ctx.captureTrivia = sCap
632
+ if (value === FAIL) return FAIL
633
+ const end = EC.e
634
+ const node = build(captured, fieldMap, { start: pos, end }, EMPTY_CH, EMPTY_TL, undefined)
635
+ if (sBuf !== undefined || sCh !== undefined) {
636
+ pushCstChild(ctx, node, sBuf !== undefined || sRaw !== undefined ? rawEntry(node, input, pos, end) : undefined)
637
+ }
638
+ EC.e = end
639
+ return node
640
+ }
641
+ }
642
+
643
+ function collapseBuildNode(child: Piece, build: NodeBuilder): Piece {
644
+ return (input, pos, ctx) => {
645
+ const sCh = ctx._cstChildren
646
+ const sLv = ctx._cstLeaves
647
+ const sRaw = ctx._cstRawChildren
648
+ const sTl = ctx._cstTriviaLog
649
+ const sCap = ctx.captureTrivia
650
+ const sBuf = ctx._cstBuf
651
+ const savedFields = ctx._fields
652
+ const buf: CstCaptureBuf = {}
653
+ ctx._cstBuf = buf
654
+ ctx._cstChildren = undefined
655
+ ctx._cstLeaves = undefined
656
+ ctx._cstRawChildren = undefined
657
+ ctx._cstTriviaLog = undefined
658
+ ctx.captureTrivia = false
659
+ ctx._fields = undefined
660
+ const value = child(input, pos, ctx)
661
+ const kids = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY_CH)
662
+ const rawKids = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY_CH)
663
+ ctx._fields = savedFields
664
+ ctx._cstBuf = sBuf
665
+ ctx._cstChildren = sCh
666
+ ctx._cstLeaves = sLv
667
+ ctx._cstRawChildren = sRaw
668
+ ctx._cstTriviaLog = sTl
669
+ ctx.captureTrivia = sCap
670
+ if (value === FAIL) return FAIL
671
+ const end = EC.e
672
+ const span = { start: pos, end }
673
+ const node = kids.length === 1
674
+ ? kids[0]
675
+ : build(kids, undefined, span, rawKids, EMPTY_TL, undefined)
676
+ if (sBuf !== undefined || sCh !== undefined) {
677
+ pushCstChild(ctx, node, rawEntry(node, input, pos, end))
678
+ }
679
+ EC.e = end
680
+ return node
681
+ }
682
+ }
683
+
684
+ function childrenOnlyCollapseBuildNode(child: Piece, build: NodeBuilder): Piece {
685
+ return (input, pos, ctx) => {
686
+ const sCh = ctx._cstChildren
687
+ const sLv = ctx._cstLeaves
688
+ const sRaw = ctx._cstRawChildren
689
+ const sTl = ctx._cstTriviaLog
690
+ const sCap = ctx.captureTrivia
691
+ const sBuf = ctx._cstBuf
692
+ const savedFields = ctx._fields
693
+ const kids: unknown[] = []
694
+ ctx._cstBuf = undefined
695
+ ctx._cstChildren = kids
696
+ ctx._cstLeaves = kids
697
+ ctx._cstRawChildren = undefined
698
+ ctx._cstTriviaLog = undefined
699
+ ctx.captureTrivia = false
700
+ ctx._fields = undefined
701
+ const value = child(input, pos, ctx)
702
+ const captured = capturedFlatChildren(kids)
703
+ ctx._fields = savedFields
704
+ ctx._cstBuf = sBuf
705
+ ctx._cstChildren = sCh
706
+ ctx._cstLeaves = sLv
707
+ ctx._cstRawChildren = sRaw
708
+ ctx._cstTriviaLog = sTl
709
+ ctx.captureTrivia = sCap
710
+ if (value === FAIL) return FAIL
711
+ const end = EC.e
712
+ const node = captured.length === 1
713
+ ? captured[0]
714
+ : build(captured, undefined, { start: pos, end }, EMPTY_CH, EMPTY_TL, undefined)
715
+ if (sBuf !== undefined || sCh !== undefined) {
716
+ pushCstChild(ctx, node, sBuf !== undefined || sRaw !== undefined ? rawEntry(node, input, pos, end) : undefined)
717
+ }
718
+ EC.e = end
719
+ return node
720
+ }
721
+ }
722
+
723
+ function plainProjectNode(child: Piece, projection: number, type: string): Piece {
724
+ return (input, pos, ctx) => {
725
+ const sCh = ctx._cstChildren
726
+ const sLv = ctx._cstLeaves
727
+ const sRaw = ctx._cstRawChildren
728
+ const sTl = ctx._cstTriviaLog
729
+ const sCap = ctx.captureTrivia
730
+ const sBuf = ctx._cstBuf
731
+ const savedFields = ctx._fields
732
+ const buf: CstCaptureBuf = {}
733
+ ctx._cstBuf = buf
734
+ ctx._cstChildren = undefined
735
+ ctx._cstLeaves = undefined
736
+ ctx._cstRawChildren = undefined
737
+ ctx._cstTriviaLog = undefined
738
+ ctx.captureTrivia = false
739
+ ctx._fields = undefined
740
+ const value = child(input, pos, ctx)
741
+ const kids = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY_CH)
742
+ ctx._fields = savedFields
743
+ ctx._cstBuf = sBuf
744
+ ctx._cstChildren = sCh
745
+ ctx._cstLeaves = sLv
746
+ ctx._cstRawChildren = sRaw
747
+ ctx._cstTriviaLog = sTl
748
+ ctx.captureTrivia = sCap
749
+ if (value === FAIL) return FAIL
750
+ const end = EC.e
751
+ const node = projectChild(kids, projection, type)
752
+ if (sBuf !== undefined || sCh !== undefined) {
753
+ pushCstChild(ctx, node, rawEntry(node, input, pos, end))
754
+ }
755
+ EC.e = end
756
+ return node
757
+ }
758
+ }
759
+
436
760
  /** Mirrors `exec.ts`'s `rollbackNeeded` — a runtime question about live sinks. */
437
761
  function rollbackNeeded(ctx: ParseContext): boolean {
438
762
  return ctx._cstBuf !== undefined
@@ -562,9 +886,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
562
886
  // keeps the exact branch it had.
563
887
  if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
564
888
  if (needsDeferredTriviaCommit(ctx)) {
565
- const scan = scanTrivia(input, cur, ctx)
566
- scan.commit()
567
- return scan.end
889
+ return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
568
890
  }
569
891
  return advanceTrivia(input, cur, ctx)
570
892
  }
@@ -662,7 +984,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
662
984
  TERMV = v
663
985
  return EC.e > scanEnd ? EC.e : cur
664
986
  }
665
- // SCALAR MARKS — no per-term mark object, as `exec.ts` established.
987
+ // SCALAR MARKS — only the three trivia sinks can receive ambient-scan rows.
988
+ // Child-owned nodes, fields and errors are deliberately outside this range.
666
989
  const need = rollbackNeeded(ctx)
667
990
  const mTl = need ? cstTlLen(ctx) : 0
668
991
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
@@ -685,19 +1008,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
685
1008
  return c._fc === true
686
1009
  }
687
1010
 
688
- /**
689
- * Accumulate one failed choice arm's expected set, in arm order.
690
- *
691
- * `choice.ts:167` is `expected.push(...result.expected)` per failed arm, and the
692
- * accumulation — not the static union of the arms' OPENERS — is what a failing
693
- * ordered choice reports. A left-factored or shared-prefix choice whose arms all
694
- * failed PAST their common opener reported that opener alone: `['/::?/']` where
695
- * the interpreter says `['"before"', '"hover"']`.
696
- *
697
- * Returns the accumulator so the common one-failing-arm case borrows the arm's
698
- * own array (`slice` once) instead of building one per choice execution the way
699
- * the interpreter's eager `const expected: string[] = []` does.
700
- */
1011
+ /** Append an exact-depth failed arm set in source order. Callers own depth ranking. */
701
1012
  function accSet(ax: readonly string[] | undefined, acc: string[] | undefined): string[] | undefined {
702
1013
  if (ax === undefined || ax.length === 0) return acc
703
1014
  if (acc === undefined) return ax.slice()
@@ -705,6 +1016,242 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
705
1016
  return acc
706
1017
  }
707
1018
 
1019
+ /** No fixed child array is permitted to survive into a choice piece. */
1020
+ const NEVER_PIECE: Piece = () => FAIL
1021
+ const NEVER_CLASS: ResolvedClass = { ascii: new Uint8Array(128), hi: [] }
1022
+ const EMPTY_EXPECTED_PREFIX: ChoiceExpectedPrefix = (_target, _prev, acc) => acc
1023
+
1024
+ /**
1025
+ * Four directly captured static expected sets. The chain keeps skipped-arm
1026
+ * diagnostics lazy across block boundaries: a later successful arm never
1027
+ * materialises expectations for earlier char-excluded arms.
1028
+ */
1029
+ function expectedBlock(
1030
+ start: number,
1031
+ e0: readonly string[], e1: readonly string[], e2: readonly string[], e3: readonly string[],
1032
+ prior: ChoiceExpectedPrefix,
1033
+ ): ChoiceExpectedPrefix {
1034
+ return (target, prev, acc) => {
1035
+ if (prev < start) { acc = prior(start, prev, acc); prev = start }
1036
+ if (target > start && prev < start + 1) acc = accSet(e0, acc)
1037
+ if (target > start + 1 && prev < start + 2) acc = accSet(e1, acc)
1038
+ if (target > start + 2 && prev < start + 3) acc = accSet(e2, acc)
1039
+ if (target > start + 3 && prev < start + 4) acc = accSet(e3, acc)
1040
+ return acc
1041
+ }
1042
+ }
1043
+
1044
+ /** One four-arm ASCII-mask block, selected once when the arity is maskable. */
1045
+ function maskedChoiceBlock(
1046
+ start: number,
1047
+ a0: Piece, a1: Piece, a2: Piece, a3: Piece,
1048
+ through: ChoiceExpectedPrefix,
1049
+ next: MaskedChoiceBlock | undefined,
1050
+ total: number,
1051
+ choiceFx: string[],
1052
+ ): MaskedChoiceBlock {
1053
+ return (input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best) => {
1054
+ if ((bits & (1 << start)) !== 0) {
1055
+ ctx._fc = false
1056
+ const v = a0(input, pos, ctx)
1057
+ if (v !== FAIL) return v
1058
+ if (best === pos) acc = through(start, prev, acc)
1059
+ prev = start + 1
1060
+ const at = ctx._fe ?? pos
1061
+ if (at > best) { best = at; acc = undefined }
1062
+ if (at === best) acc = accSet(ctx._fx, acc)
1063
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1064
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1065
+ }
1066
+ if ((bits & (1 << (start + 1))) !== 0) {
1067
+ ctx._fc = false
1068
+ const v = a1(input, pos, ctx)
1069
+ if (v !== FAIL) return v
1070
+ if (best === pos) acc = through(start + 1, prev, acc)
1071
+ prev = start + 2
1072
+ const at = ctx._fe ?? pos
1073
+ if (at > best) { best = at; acc = undefined }
1074
+ if (at === best) acc = accSet(ctx._fx, acc)
1075
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1076
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1077
+ }
1078
+ if ((bits & (1 << (start + 2))) !== 0) {
1079
+ ctx._fc = false
1080
+ const v = a2(input, pos, ctx)
1081
+ if (v !== FAIL) return v
1082
+ if (best === pos) acc = through(start + 2, prev, acc)
1083
+ prev = start + 3
1084
+ const at = ctx._fe ?? pos
1085
+ if (at > best) { best = at; acc = undefined }
1086
+ if (at === best) acc = accSet(ctx._fx, acc)
1087
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1088
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1089
+ }
1090
+ if ((bits & (1 << (start + 3))) !== 0) {
1091
+ ctx._fc = false
1092
+ const v = a3(input, pos, ctx)
1093
+ if (v !== FAIL) return v
1094
+ if (best === pos) acc = through(start + 3, prev, acc)
1095
+ prev = start + 4
1096
+ const at = ctx._fe ?? pos
1097
+ if (at > best) { best = at; acc = undefined }
1098
+ if (at === best) acc = accSet(ctx._fx, acc)
1099
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1100
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1101
+ }
1102
+ if (next !== undefined) {
1103
+ return next(input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
1104
+ }
1105
+ if (best === pos) acc = through(total, prev, acc)
1106
+ ctx._fe = pos; ctx._fx = acc ?? choiceFx
1107
+ return FAIL
1108
+ }
1109
+ }
1110
+
1111
+ /** One four-arm non-ASCII/general block. No mask state exists in this shape. */
1112
+ function generalChoiceBlock(
1113
+ start: number,
1114
+ a0: Piece, a1: Piece, a2: Piece, a3: Piece,
1115
+ g0: ResolvedClass | null, g1: ResolvedClass | null,
1116
+ g2: ResolvedClass | null, g3: ResolvedClass | null,
1117
+ through: ChoiceExpectedPrefix,
1118
+ next: GeneralChoiceBlock | undefined,
1119
+ total: number,
1120
+ choiceFx: string[],
1121
+ ): GeneralChoiceBlock {
1122
+ return (input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best) => {
1123
+ if (g0 === null || classHas(g0, c)) {
1124
+ ctx._fc = false
1125
+ const v = a0(input, pos, ctx)
1126
+ if (v !== FAIL) return v
1127
+ if (best === pos) acc = through(start, prev, acc)
1128
+ prev = start + 1
1129
+ const at = ctx._fe ?? pos
1130
+ if (at > best) { best = at; acc = undefined }
1131
+ if (at === best) acc = accSet(ctx._fx, acc)
1132
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1133
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1134
+ }
1135
+ if (g1 === null || classHas(g1, c)) {
1136
+ ctx._fc = false
1137
+ const v = a1(input, pos, ctx)
1138
+ if (v !== FAIL) return v
1139
+ if (best === pos) acc = through(start + 1, prev, acc)
1140
+ prev = start + 2
1141
+ const at = ctx._fe ?? pos
1142
+ if (at > best) { best = at; acc = undefined }
1143
+ if (at === best) acc = accSet(ctx._fx, acc)
1144
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1145
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1146
+ }
1147
+ if (g2 === null || classHas(g2, c)) {
1148
+ ctx._fc = false
1149
+ const v = a2(input, pos, ctx)
1150
+ if (v !== FAIL) return v
1151
+ if (best === pos) acc = through(start + 2, prev, acc)
1152
+ prev = start + 3
1153
+ const at = ctx._fe ?? pos
1154
+ if (at > best) { best = at; acc = undefined }
1155
+ if (at === best) acc = accSet(ctx._fx, acc)
1156
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1157
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1158
+ }
1159
+ if (g3 === null || classHas(g3, c)) {
1160
+ ctx._fc = false
1161
+ const v = a3(input, pos, ctx)
1162
+ if (v !== FAIL) return v
1163
+ if (best === pos) acc = through(start + 3, prev, acc)
1164
+ prev = start + 4
1165
+ const at = ctx._fe ?? pos
1166
+ if (at > best) { best = at; acc = undefined }
1167
+ if (at === best) acc = accSet(ctx._fx, acc)
1168
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1169
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1170
+ }
1171
+ if (next !== undefined) {
1172
+ return next(input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
1173
+ }
1174
+ if (best === pos) acc = through(total, prev, acc)
1175
+ ctx._fe = pos; ctx._fx = acc ?? choiceFx
1176
+ return FAIL
1177
+ }
1178
+ }
1179
+
1180
+ /** Four direct exclusive arms, chained for every arity above three. */
1181
+ function exclusiveChoiceBlock(
1182
+ start: number, a0: Piece, a1: Piece, a2: Piece, a3: Piece,
1183
+ next: ExclusiveChoiceBlock | undefined,
1184
+ ): ExclusiveChoiceBlock {
1185
+ return (arm, input, pos, ctx) => {
1186
+ if (arm === start) return a0(input, pos, ctx)
1187
+ if (arm === start + 1) return a1(input, pos, ctx)
1188
+ if (arm === start + 2) return a2(input, pos, ctx)
1189
+ if (arm === start + 3) return a3(input, pos, ctx)
1190
+ return next === undefined ? FAIL : next(arm, input, pos, ctx)
1191
+ }
1192
+ }
1193
+
1194
+ function dispatchMatcherBlock(
1195
+ m0: DispatchMatcher, a0: number, m1: DispatchMatcher, a1: number,
1196
+ m2: DispatchMatcher, a2: number, m3: DispatchMatcher, a3: number,
1197
+ next: DispatchMatcherBlock | undefined,
1198
+ ): DispatchMatcherBlock {
1199
+ return key => {
1200
+ if (m0(key)) return a0
1201
+ if (m1(key)) return a1
1202
+ if (m2(key)) return a2
1203
+ if (m3(key)) return a3
1204
+ return next?.(key)
1205
+ }
1206
+ }
1207
+
1208
+ function dispatchBranch(child: Piece, usesRouted: boolean): DispatchBranch {
1209
+ if (!usesRouted) {
1210
+ return (input, _pos, selEnd, key, _selectorMark, ctx) => {
1211
+ const mark = saveTriviaMark(ctx)
1212
+ const v = child(input, selEnd, ctx)
1213
+ if (v === FAIL) {
1214
+ rollbackTrivia(ctx, mark)
1215
+ ctx._fc = true
1216
+ return FAIL
1217
+ }
1218
+ return [key, v]
1219
+ }
1220
+ }
1221
+ return (input, pos, selEnd, key, selectorMark, ctx) => {
1222
+ const savedRouted = ctx._routed
1223
+ rollbackTrivia(ctx, selectorMark)
1224
+ const mark = saveTriviaMark(ctx)
1225
+ ctx._routed = { value: key, span: { start: pos, end: selEnd } }
1226
+ let v: unknown
1227
+ try {
1228
+ v = child(input, pos, ctx)
1229
+ } finally {
1230
+ ctx._routed = savedRouted
1231
+ }
1232
+ if (v === FAIL) {
1233
+ rollbackTrivia(ctx, mark)
1234
+ ctx._fc = true
1235
+ return FAIL
1236
+ }
1237
+ return [key, v]
1238
+ }
1239
+ }
1240
+
1241
+ function dispatchArmBlock(
1242
+ start: number,
1243
+ a0: DispatchBranch, a1: DispatchBranch, a2: DispatchBranch, a3: DispatchBranch,
1244
+ next: DispatchArmBlock | undefined,
1245
+ ): DispatchArmBlock {
1246
+ return (arm, input, pos, selEnd, key, selectorMark, ctx) => {
1247
+ if (arm === start) return a0(input, pos, selEnd, key, selectorMark, ctx)
1248
+ if (arm === start + 1) return a1(input, pos, selEnd, key, selectorMark, ctx)
1249
+ if (arm === start + 2) return a2(input, pos, selEnd, key, selectorMark, ctx)
1250
+ if (arm === start + 3) return a3(input, pos, selEnd, key, selectorMark, ctx)
1251
+ return next === undefined ? FAIL : next(arm, input, pos, selEnd, key, selectorMark, ctx)
1252
+ }
1253
+ }
1254
+
708
1255
 
709
1256
 
710
1257
  function trackLinesInto(ctx: ParseContext, input: string, end: number): void {
@@ -759,6 +1306,20 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
759
1306
  const s = k[code[ip + 1]!] as string
760
1307
  const len = s.length
761
1308
  const xf = fx[code[ip + 2]!] as string[]
1309
+ const shared = rawScalarSpecs.has(code[ip + 1]!) ? scalarFor(ip) : undefined
1310
+ if (shared !== undefined) {
1311
+ return (input, pos, ctx) => {
1312
+ const e = shared(input, pos)
1313
+ if (e >= 0) {
1314
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
1315
+ EC.e = e
1316
+ return s
1317
+ }
1318
+ ctx._fe = pos; ctx._fx = xf
1319
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
1320
+ return FAIL
1321
+ }
1322
+ }
762
1323
  // LENGTH IS TABLE DATA, so the COMPARE is chosen here rather than
763
1324
  // delegated to a builtin that has to rediscover it. See `litBodyNote`.
764
1325
  if (len === 1) {
@@ -884,6 +1445,21 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
884
1445
  case OP_RX: {
885
1446
  const re = k[code[ip + 1]!] as RegExp
886
1447
  const xf = fx[code[ip + 2]!] as string[]
1448
+ const shared = rawScalarSpecs.has(code[ip + 1]!) ? scalarFor(ip) : undefined
1449
+ if (shared !== undefined) {
1450
+ return (input, pos, ctx) => {
1451
+ const e = shared(input, pos)
1452
+ if (e >= 0) {
1453
+ const v = input.slice(pos, e)
1454
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
1455
+ EC.e = e
1456
+ return v
1457
+ }
1458
+ ctx._fe = pos; ctx._fx = xf
1459
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
1460
+ return FAIL
1461
+ }
1462
+ }
887
1463
  return (input, pos, ctx) => {
888
1464
  re.lastIndex = pos
889
1465
  // `regex()` rows are sticky and expose only their whole match. `exec()`
@@ -1307,6 +1883,21 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1307
1883
  /* ── zero-width ──────────────────────────────────────────────────────── */
1308
1884
 
1309
1885
  case OP_NOT: {
1886
+ const scalarChild = scalarTerminalNotChild(code, ip)
1887
+ if (scalarChild >= 0) {
1888
+ const recognize = scalarFor(scalarChild)!
1889
+ const xf = fx[code[ip + 2]!] as string[]
1890
+ return (input, pos, ctx) => {
1891
+ if (recognize(input, pos) < 0) {
1892
+ EC.e = pos
1893
+ return null
1894
+ }
1895
+ ctx._fe = pos
1896
+ ctx._fx = xf
1897
+ EC.e = pos
1898
+ return FAIL
1899
+ }
1900
+ }
1310
1901
  const child = link(code[ip + 1]!)
1311
1902
  const xf = fx[code[ip + 2]!] as string[]
1312
1903
  return (input, pos, ctx) => {
@@ -1420,6 +2011,93 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1420
2011
  }
1421
2012
  }
1422
2013
 
2014
+ case OP_LEX_BODY: {
2015
+ const recognize = t.lex[code[ip + 1]!]!
2016
+ const expected = fx[code[ip + 2]!] as string[]
2017
+ return (input, pos, ctx) => {
2018
+ const recognized = recognize(input, pos)
2019
+ if (recognized < 0) {
2020
+ ctx._fe = pos
2021
+ ctx._fx = expected as string[]
2022
+ if (ctx._probe !== undefined) failAt(ctx, expected, pos)
2023
+ return FAIL
2024
+ }
2025
+ const lineFlags = code[ip + 4]!
2026
+ const hasSuffix = (lineFlags & 4) !== 0
2027
+ const suffixMatched = hasSuffix && recognized % 2 === 1
2028
+ const end = (recognized - (suffixMatched ? 1 : 0)) / 2
2029
+ if ((lineFlags & 1) !== 0) {
2030
+ trackLinesInto(ctx, input, suffixMatched ? end - 1 : end)
2031
+ }
2032
+ if (hasSuffix) ctx._fc = false
2033
+ if (hasSuffix && suffixMatched && (lineFlags & 2) !== 0) {
2034
+ trackLinesInto(ctx, input, end)
2035
+ }
2036
+ if (hasSuffix && !suffixMatched) {
2037
+ const suffixExpected = fx[code[ip + 3]!] as string[]
2038
+ ctx._fe = end
2039
+ ctx._fx = suffixExpected
2040
+ if (ctx._probe !== undefined) failAt(ctx, suffixExpected, end)
2041
+ }
2042
+ const value = input.slice(pos, end)
2043
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
2044
+ EC.e = end
2045
+ return value
2046
+ }
2047
+ }
2048
+
2049
+ case OP_LEX_PROGRAM: {
2050
+ const run = t.lexPrograms[code[ip + 1]!]!
2051
+ const scanId = run.scan
2052
+ if (scanId !== undefined) {
2053
+ return (input, pos, ctx) => {
2054
+ const sTrivia = ctx.trivia, sKinds = ctx.triviaKindLabels
2055
+ const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
2056
+ const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
2057
+ const sOuterTl = ctx._triviaLog, sRootTl = ctx._rootTriviaLog
2058
+ const wasCapturing = cstCaptureActive(ctx)
2059
+ const sScan = SCAN
2060
+ SCAN = null
2061
+ ctx.trivia = undefined
2062
+ ctx.triviaKindLabels = undefined
2063
+ ctx._cstBuf = undefined
2064
+ ctx._cstChildren = undefined
2065
+ ctx._cstLeaves = undefined
2066
+ ctx._cstRawChildren = undefined
2067
+ ctx._cstTriviaLog = undefined
2068
+ ctx._triviaLog = undefined
2069
+ ctx._rootTriviaLog = undefined
2070
+ let end: number
2071
+ try { end = run(input, pos, ctx, scans[scanId]) }
2072
+ finally {
2073
+ SCAN = sScan
2074
+ ctx.trivia = sTrivia
2075
+ ctx.triviaKindLabels = sKinds
2076
+ ctx._cstBuf = sBuf
2077
+ ctx._cstChildren = sChildren
2078
+ ctx._cstLeaves = sLeaves
2079
+ ctx._cstRawChildren = sRaw
2080
+ ctx._cstTriviaLog = sTl
2081
+ ctx._triviaLog = sOuterTl
2082
+ ctx._rootTriviaLog = sRootTl
2083
+ }
2084
+ if (end < 0) return FAIL
2085
+ const value = input.slice(pos, end)
2086
+ if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
2087
+ EC.e = end
2088
+ return value
2089
+ }
2090
+ }
2091
+ return (input, pos, ctx) => {
2092
+ const end = run(input, pos, ctx)
2093
+ if (end < 0) return FAIL
2094
+ const value = input.slice(pos, end)
2095
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
2096
+ EC.e = end
2097
+ return value
2098
+ }
2099
+ }
2100
+
1423
2101
  case OP_LEAF: {
1424
2102
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1425
2103
  const child = link(code[ip + 2]!)
@@ -1464,36 +2142,60 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1464
2142
  const fused = op === OP_SEQX
1465
2143
  const base = fused ? ip + 3 : ip + 2
1466
2144
  const n = code[fused ? ip + 2 : ip + 1]!
1467
- // Children bound as a plain array of DIRECT references, laid out once.
1468
- const kids: Piece[] = new Array<Piece>(n)
1469
- for (let i = 0; i < n; i++) kids[i] = link(code[base + i]!)
1470
2145
  const wantValues = op !== OP_SEQV
1471
- const fn = fused
1472
- ? fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
2146
+ const reducer = fused ? code[ip + 1]! : -1
2147
+ const projection = reducer < 0 ? ~reducer : -1
2148
+ const fn = fused && projection < 0
2149
+ ? fns[reducer] as (value: unknown, span: { start: number; end: number }) => unknown
1473
2150
  : undefined
1474
2151
 
1475
- /**
1476
- * The generic term loop, for arities the unrolled pieces below do not
1477
- * cover.
1478
- *
1479
- * `exec.ts` open-coded a LIT/RX fast path here to dodge "a JS call frame
1480
- * plus a switch dispatch". Half of that cost is gone by construction — a
1481
- * piece has no switch — and the other half is what TurboFan removes by
1482
- * INLINING a small monomorphic child. Re-emitting the terminal bodies
1483
- * would instead grow this function past the inlining budget, which is the
1484
- * defect being fixed, so they are not re-emitted.
1485
- */
1486
- const runTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1487
- const v0 = kids[0]!(input, pos, ctx)
1488
- if (v0 === FAIL) return -1
1489
- if (values !== undefined) values.push(v0)
1490
- let cur = EC.e
1491
- for (let i = 1; i < n; i++) {
1492
- cur = nextTerm(kids[i]!, input, cur, ctx)
2152
+ const runnerBlock = (
2153
+ count: number,
2154
+ r0: TermRunner, r1: TermRunner, r2: TermRunner, r3: TermRunner,
2155
+ next: SequenceTermBlock | undefined,
2156
+ ): SequenceTermBlock => {
2157
+ if (count === 1) return (input, cur, ctx, values, inherited) => {
2158
+ cur = r0(input, cur, ctx, inherited)
2159
+ if (cur < 0) return -1
2160
+ if (values !== undefined) values.push(TERMV)
2161
+ return next === undefined ? cur : next(input, cur, ctx, values, inherited)
2162
+ }
2163
+ if (count === 2) return (input, cur, ctx, values, inherited) => {
2164
+ cur = r0(input, cur, ctx, inherited)
1493
2165
  if (cur < 0) return -1
1494
2166
  if (values !== undefined) values.push(TERMV)
2167
+ cur = r1(input, cur, ctx, inherited)
2168
+ if (cur < 0) return -1
2169
+ if (values !== undefined) values.push(TERMV)
2170
+ return next === undefined ? cur : next(input, cur, ctx, values, inherited)
2171
+ }
2172
+ if (count === 3) return (input, cur, ctx, values, inherited) => {
2173
+ cur = r0(input, cur, ctx, inherited)
2174
+ if (cur < 0) return -1
2175
+ if (values !== undefined) values.push(TERMV)
2176
+ cur = r1(input, cur, ctx, inherited)
2177
+ if (cur < 0) return -1
2178
+ if (values !== undefined) values.push(TERMV)
2179
+ cur = r2(input, cur, ctx, inherited)
2180
+ if (cur < 0) return -1
2181
+ if (values !== undefined) values.push(TERMV)
2182
+ return next === undefined ? cur : next(input, cur, ctx, values, inherited)
2183
+ }
2184
+ return (input, cur, ctx, values, inherited) => {
2185
+ cur = r0(input, cur, ctx, inherited)
2186
+ if (cur < 0) return -1
2187
+ if (values !== undefined) values.push(TERMV)
2188
+ cur = r1(input, cur, ctx, inherited)
2189
+ if (cur < 0) return -1
2190
+ if (values !== undefined) values.push(TERMV)
2191
+ cur = r2(input, cur, ctx, inherited)
2192
+ if (cur < 0) return -1
2193
+ if (values !== undefined) values.push(TERMV)
2194
+ cur = r3(input, cur, ctx, inherited)
2195
+ if (cur < 0) return -1
2196
+ if (values !== undefined) values.push(TERMV)
2197
+ return next === undefined ? cur : next(input, cur, ctx, values, inherited)
1495
2198
  }
1496
- return cur
1497
2199
  }
1498
2200
 
1499
2201
  /**
@@ -1544,21 +2246,24 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1544
2246
  * sentinel published is the kind of state a later reader will trust.
1545
2247
  */
1546
2248
  if (REC) {
1547
- const syncs: (Combinator<unknown> | undefined)[] = new Array<Combinator<unknown> | undefined>(n)
1548
- for (let i = 0; i < n; i++) syncs[i] = sentinelFor(code[base + n + i]!)
1549
- const runners: TermRunner[] = new Array<TermRunner>(n)
1550
- for (let i = 1; i < n; i++) {
2249
+ const first = link(code[base]!)
2250
+ const firstSync = sentinelFor(code[base + n]!)
2251
+ const runnerAt = (i: number): TermRunner => {
2252
+ const sync = sentinelFor(code[base + n + i]!)
1551
2253
  const kidIp = code[base + i]!
1552
2254
  if (code[kidIp] !== OP_ADJ) {
1553
- const kid = kids[i]!
1554
- runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1555
- continue
2255
+ const kid = link(kidIp)
2256
+ return (input, cur, ctx, inherited) => {
2257
+ ctx._sync = sync ?? inherited
2258
+ return nextTerm(kid, input, cur, ctx)
2259
+ }
1556
2260
  }
1557
2261
  const negated = code[kidIp + 1] === 1
1558
2262
  const ki = code[kidIp + 2]!
1559
2263
  const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1560
2264
  const xf = fx[code[kidIp + 3]!] as string[]
1561
- runners[i] = (input, cur, ctx) => {
2265
+ return (input, cur, ctx, inherited) => {
2266
+ ctx._sync = sync ?? inherited
1562
2267
  if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1563
2268
  ctx._fe = cur; ctx._fx = xf
1564
2269
  return -1
@@ -1567,19 +2272,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1567
2272
  return cur
1568
2273
  }
1569
2274
  }
2275
+ const bindRunners = (start: number): SequenceTermBlock => {
2276
+ const count = Math.min(4, n - start)
2277
+ const at = (offset: number): TermRunner => runnerAt(start + offset)
2278
+ const never: TermRunner = () => -1
2279
+ return runnerBlock(
2280
+ count, at(0), count > 1 ? at(1) : never,
2281
+ count > 2 ? at(2) : never, count > 3 ? at(3) : never,
2282
+ start + count < n ? bindRunners(start + count) : undefined,
2283
+ )
2284
+ }
2285
+ const tail = n > 1 ? bindRunners(1) : undefined
1570
2286
  const runSyncTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1571
2287
  const inherited = ctx._sync
1572
- ctx._sync = syncs[0] ?? inherited
1573
- const v0 = kids[0]!(input, pos, ctx)
2288
+ ctx._sync = firstSync ?? inherited
2289
+ const v0 = first(input, pos, ctx)
1574
2290
  if (v0 === FAIL) { ctx._sync = inherited; return -1 }
1575
2291
  if (values !== undefined) values.push(v0)
1576
- let cur = EC.e
1577
- for (let i = 1; i < n; i++) {
1578
- ctx._sync = syncs[i] ?? inherited
1579
- cur = runners[i]!(input, cur, ctx)
1580
- if (cur < 0) { ctx._sync = inherited; return -1 }
1581
- if (values !== undefined) values.push(TERMV)
1582
- }
2292
+ const cur = tail === undefined ? EC.e : tail(input, EC.e, ctx, values, inherited)
2293
+ if (cur < 0) { ctx._sync = inherited; return -1 }
1583
2294
  ctx._sync = inherited
1584
2295
  return cur
1585
2296
  }
@@ -1589,7 +2300,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1589
2300
  const cur = runSyncTerms(input, pos, ctx, values)
1590
2301
  if (cur < 0) return FAIL
1591
2302
  EC.e = cur
1592
- return fn!(values, { start: pos, end: cur })
2303
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1593
2304
  }
1594
2305
  }
1595
2306
  if (wantValues) {
@@ -1612,13 +2323,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1612
2323
  let hasAdj = false
1613
2324
  for (let i = 1; i < n; i++) if (code[code[base + i]!] === OP_ADJ) { hasAdj = true; break }
1614
2325
  if (hasAdj) {
1615
- const runners: TermRunner[] = new Array<TermRunner>(n)
1616
- for (let i = 1; i < n; i++) {
2326
+ const first = link(code[base]!)
2327
+ const runnerAt = (i: number): TermRunner => {
1617
2328
  const kidIp = code[base + i]!
1618
2329
  if (code[kidIp] !== OP_ADJ) {
1619
- const kid = kids[i]!
1620
- runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1621
- continue
2330
+ const kid = link(kidIp)
2331
+ return (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1622
2332
  }
1623
2333
  const negated = code[kidIp + 1] === 1
1624
2334
  const ki = code[kidIp + 2]!
@@ -1628,7 +2338,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1628
2338
  // the interpreter does.
1629
2339
  const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1630
2340
  const xf = fx[code[kidIp + 3]!] as string[]
1631
- runners[i] = (input, cur, ctx) => {
2341
+ return (input, cur, ctx) => {
1632
2342
  if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1633
2343
  ctx._fe = cur; ctx._fx = xf
1634
2344
  return -1
@@ -1637,17 +2347,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1637
2347
  return cur
1638
2348
  }
1639
2349
  }
2350
+ const bindRunners = (start: number): SequenceTermBlock => {
2351
+ const count = Math.min(4, n - start)
2352
+ const at = (offset: number): TermRunner => runnerAt(start + offset)
2353
+ const never: TermRunner = () => -1
2354
+ return runnerBlock(
2355
+ count, at(0), count > 1 ? at(1) : never,
2356
+ count > 2 ? at(2) : never, count > 3 ? at(3) : never,
2357
+ start + count < n ? bindRunners(start + count) : undefined,
2358
+ )
2359
+ }
2360
+ const tail = bindRunners(1)
1640
2361
  const runAdjTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1641
- const v0 = kids[0]!(input, pos, ctx)
2362
+ const v0 = first(input, pos, ctx)
1642
2363
  if (v0 === FAIL) return -1
1643
2364
  if (values !== undefined) values.push(v0)
1644
- let cur = EC.e
1645
- for (let i = 1; i < n; i++) {
1646
- cur = runners[i]!(input, cur, ctx)
1647
- if (cur < 0) return -1
1648
- if (values !== undefined) values.push(TERMV)
1649
- }
1650
- return cur
2365
+ return tail(input, EC.e, ctx, values)
1651
2366
  }
1652
2367
  if (fused) {
1653
2368
  return (input, pos, ctx) => {
@@ -1655,7 +2370,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1655
2370
  const cur = runAdjTerms(input, pos, ctx, values)
1656
2371
  if (cur < 0) return FAIL
1657
2372
  EC.e = cur
1658
- return fn!(values, { start: pos, end: cur })
2373
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1659
2374
  }
1660
2375
  }
1661
2376
  if (wantValues) {
@@ -1693,12 +2408,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1693
2408
  * runs, so there is no arity this fails to lower.
1694
2409
  */
1695
2410
  if (n === 1) {
1696
- const k0 = kids[0]!
2411
+ const k0 = link(code[base]!)
1697
2412
  if (fused) {
1698
2413
  return (input, pos, ctx) => {
1699
2414
  const v = k0(input, pos, ctx)
1700
2415
  if (v === FAIL) return FAIL
1701
- return fn!([v], { start: pos, end: EC.e })
2416
+ return projection === 0 ? v : fn!([v], { start: pos, end: EC.e })
1702
2417
  }
1703
2418
  }
1704
2419
  if (wantValues) {
@@ -1716,7 +2431,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1716
2431
  }
1717
2432
 
1718
2433
  if (n === 2) {
1719
- const k0 = kids[0]!, k1 = kids[1]!
2434
+ const k0 = link(code[base]!), k1 = link(code[base + 1]!)
1720
2435
  if (fused) {
1721
2436
  return (input, pos, ctx) => {
1722
2437
  const v0 = k0(input, pos, ctx)
@@ -1724,7 +2439,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1724
2439
  const cur = nextTerm(k1, input, EC.e, ctx)
1725
2440
  if (cur < 0) return FAIL
1726
2441
  EC.e = cur
1727
- return fn!([v0, TERMV], { start: pos, end: cur })
2442
+ return projection === 0 ? v0
2443
+ : projection === 1 ? TERMV
2444
+ : fn!([v0, TERMV], { start: pos, end: cur })
1728
2445
  }
1729
2446
  }
1730
2447
  if (wantValues) {
@@ -1748,7 +2465,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1748
2465
  }
1749
2466
 
1750
2467
  if (n === 3) {
1751
- const k0 = kids[0]!, k1 = kids[1]!, k2 = kids[2]!
2468
+ const k0 = link(code[base]!), k1 = link(code[base + 1]!), k2 = link(code[base + 2]!)
1752
2469
  if (fused) {
1753
2470
  return (input, pos, ctx) => {
1754
2471
  const v0 = k0(input, pos, ctx)
@@ -1759,7 +2476,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1759
2476
  cur = nextTerm(k2, input, cur, ctx)
1760
2477
  if (cur < 0) return FAIL
1761
2478
  EC.e = cur
1762
- return fn!([v0, v1, TERMV], { start: pos, end: cur })
2479
+ return projection === 0 ? v0
2480
+ : projection === 1 ? v1
2481
+ : projection === 2 ? TERMV
2482
+ : fn!([v0, v1, TERMV], { start: pos, end: cur })
1763
2483
  }
1764
2484
  }
1765
2485
  if (wantValues) {
@@ -1787,26 +2507,97 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1787
2507
  }
1788
2508
  }
1789
2509
 
2510
+ // Arbitrary strict arities use a bounded scalar block chain. Fixed
2511
+ // children are captured into prewritten four-term blocks at assembly;
2512
+ // the parse path has no `kids[i]`, fixed-child loop, or opcode lookup.
2513
+ const block = (
2514
+ count: number,
2515
+ k0: Piece, k1: Piece, k2: Piece, k3: Piece,
2516
+ next: SequenceTermBlock | undefined,
2517
+ ): SequenceTermBlock => {
2518
+ if (count === 1) return (input, cur, ctx, values) => {
2519
+ cur = nextTerm(k0, input, cur, ctx)
2520
+ if (cur < 0) return -1
2521
+ if (values !== undefined) values.push(TERMV)
2522
+ return next === undefined ? cur : next(input, cur, ctx, values)
2523
+ }
2524
+ if (count === 2) return (input, cur, ctx, values) => {
2525
+ cur = nextTerm(k0, input, cur, ctx)
2526
+ if (cur < 0) return -1
2527
+ if (values !== undefined) values.push(TERMV)
2528
+ cur = nextTerm(k1, input, cur, ctx)
2529
+ if (cur < 0) return -1
2530
+ if (values !== undefined) values.push(TERMV)
2531
+ return next === undefined ? cur : next(input, cur, ctx, values)
2532
+ }
2533
+ if (count === 3) return (input, cur, ctx, values) => {
2534
+ cur = nextTerm(k0, input, cur, ctx)
2535
+ if (cur < 0) return -1
2536
+ if (values !== undefined) values.push(TERMV)
2537
+ cur = nextTerm(k1, input, cur, ctx)
2538
+ if (cur < 0) return -1
2539
+ if (values !== undefined) values.push(TERMV)
2540
+ cur = nextTerm(k2, input, cur, ctx)
2541
+ if (cur < 0) return -1
2542
+ if (values !== undefined) values.push(TERMV)
2543
+ return next === undefined ? cur : next(input, cur, ctx, values)
2544
+ }
2545
+ return (input, cur, ctx, values) => {
2546
+ cur = nextTerm(k0, input, cur, ctx)
2547
+ if (cur < 0) return -1
2548
+ if (values !== undefined) values.push(TERMV)
2549
+ cur = nextTerm(k1, input, cur, ctx)
2550
+ if (cur < 0) return -1
2551
+ if (values !== undefined) values.push(TERMV)
2552
+ cur = nextTerm(k2, input, cur, ctx)
2553
+ if (cur < 0) return -1
2554
+ if (values !== undefined) values.push(TERMV)
2555
+ cur = nextTerm(k3, input, cur, ctx)
2556
+ if (cur < 0) return -1
2557
+ if (values !== undefined) values.push(TERMV)
2558
+ return next === undefined ? cur : next(input, cur, ctx, values)
2559
+ }
2560
+ }
2561
+ const bind = (start: number): SequenceTermBlock => {
2562
+ const count = Math.min(4, n - start)
2563
+ const child = (offset: number): Piece => link(code[base + start + offset]!)
2564
+ return block(
2565
+ count, child(0), count > 1 ? child(1) : NEVER_PIECE,
2566
+ count > 2 ? child(2) : NEVER_PIECE, count > 3 ? child(3) : NEVER_PIECE,
2567
+ start + count < n ? bind(start + count) : undefined,
2568
+ )
2569
+ }
2570
+ const first = link(code[base]!)
2571
+ const tail = bind(1)
2572
+ const runDirectTerms = (
2573
+ input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined,
2574
+ ): number => {
2575
+ const v0 = first(input, pos, ctx)
2576
+ if (v0 === FAIL) return -1
2577
+ if (values !== undefined) values.push(v0)
2578
+ return tail(input, EC.e, ctx, values)
2579
+ }
2580
+
1790
2581
  if (fused) {
1791
2582
  return (input, pos, ctx) => {
1792
2583
  const values: unknown[] = []
1793
- const cur = runTerms(input, pos, ctx, values)
2584
+ const cur = runDirectTerms(input, pos, ctx, values)
1794
2585
  if (cur < 0) return FAIL
1795
2586
  EC.e = cur
1796
- return fn!(values, { start: pos, end: cur })
2587
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1797
2588
  }
1798
2589
  }
1799
2590
  if (wantValues) {
1800
2591
  return (input, pos, ctx) => {
1801
2592
  const values: unknown[] = []
1802
- const cur = runTerms(input, pos, ctx, values)
2593
+ const cur = runDirectTerms(input, pos, ctx, values)
1803
2594
  if (cur < 0) return FAIL
1804
2595
  EC.e = cur
1805
2596
  return values
1806
2597
  }
1807
2598
  }
1808
2599
  return (input, pos, ctx) => {
1809
- const cur = runTerms(input, pos, ctx, undefined)
2600
+ const cur = runDirectTerms(input, pos, ctx, undefined)
1810
2601
  if (cur < 0) return FAIL
1811
2602
  EC.e = cur
1812
2603
  return undefined
@@ -1820,20 +2611,74 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1820
2611
  const n = code[ip + 2]!
1821
2612
  const choiceFx = fx[code[ip + 3]!] as string[]
1822
2613
  const base = ip + 4
1823
- const arms: Piece[] = new Array<Piece>(n)
1824
- for (let i = 0; i < n; i++) arms[i] = link(code[base + i]!)
1825
- // PER-ARM STATIC SETS, resolved once at assembly. An arm the char gate
1826
- // declines to enter still owes its expectation: the interpreter has no
1827
- // such gate, enters the arm, and its opener's failure is what lands in
1828
- // the accumulation.
1829
- const armFx: (string[])[] = new Array<string[]>(n)
1830
- for (let i = 0; i < n; i++) armFx[i] = fx[code[base + n + i]!] as string[]
1831
2614
 
1832
2615
  // THE TWO CHOICE SHAPES ARE TWO PIECES. `exec.ts` tested `table.exclusive`
1833
2616
  // on every choice execution; it is table data, so it selects here.
1834
2617
  if (table.exclusive) {
1835
2618
  const ascii = table.ascii
1836
2619
  const hi = table.hi
2620
+ // THE COMMON ARITIES OWN THEIR TOPOLOGY AS SCALARS. Do not build an
2621
+ // `arms[]` merely to rediscover the same two or three children at
2622
+ // every dispatch. The classifier remains table data; only its result
2623
+ // is turned into a direct captured call.
2624
+ if (n === 2) {
2625
+ const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2626
+ return (input, pos, ctx) => {
2627
+ const c = lead(input, pos)
2628
+ let arm = -1
2629
+ if (c >= 0 && c < 128) {
2630
+ const a = ascii[c]!
2631
+ if (a !== 0) arm = a - 1
2632
+ } else if (c >= 128) {
2633
+ for (let i = 0; i < hi.length; i += 3) {
2634
+ if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
2635
+ }
2636
+ }
2637
+ if (arm >= 0) {
2638
+ ctx._fc = false
2639
+ const v = arm === 0 ? a0(input, pos, ctx) : a1(input, pos, ctx)
2640
+ if (v !== FAIL) return v
2641
+ if (committed(ctx)) return FAIL
2642
+ const failed = ctx._fx
2643
+ if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return FAIL }
2644
+ }
2645
+ ctx._fe = pos; ctx._fx = choiceFx
2646
+ return FAIL
2647
+ }
2648
+ }
2649
+ if (n === 3) {
2650
+ const a0 = link(code[base]!), a1 = link(code[base + 1]!), a2 = link(code[base + 2]!)
2651
+ return (input, pos, ctx) => {
2652
+ const c = lead(input, pos)
2653
+ let arm = -1
2654
+ if (c >= 0 && c < 128) {
2655
+ const a = ascii[c]!
2656
+ if (a !== 0) arm = a - 1
2657
+ } else if (c >= 128) {
2658
+ for (let i = 0; i < hi.length; i += 3) {
2659
+ if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
2660
+ }
2661
+ }
2662
+ if (arm >= 0) {
2663
+ ctx._fc = false
2664
+ const v = arm === 0 ? a0(input, pos, ctx) : arm === 1 ? a1(input, pos, ctx) : a2(input, pos, ctx)
2665
+ if (v !== FAIL) return v
2666
+ if (committed(ctx)) return FAIL
2667
+ const failed = ctx._fx
2668
+ if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return FAIL }
2669
+ }
2670
+ ctx._fe = pos; ctx._fx = choiceFx
2671
+ return FAIL
2672
+ }
2673
+ }
2674
+ const bindExclusive = (start: number): ExclusiveChoiceBlock => {
2675
+ const at = (i: number): Piece => i < n ? link(code[base + i]!) : NEVER_PIECE
2676
+ const next = start + 4 < n ? bindExclusive(start + 4) : undefined
2677
+ return exclusiveChoiceBlock(
2678
+ start, at(start), at(start + 1), at(start + 2), at(start + 3), next,
2679
+ )
2680
+ }
2681
+ const selected = bindExclusive(0)
1837
2682
  // THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
1838
2683
  // ungated arm is one whose class is −1, and `resolveDispatch` clears
1839
2684
  // `exclusive` for exactly those arms (program.ts) — so `table.open` is
@@ -1852,15 +2697,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1852
2697
  }
1853
2698
  if (arm >= 0) {
1854
2699
  ctx._fc = false
1855
- const v = arms[arm]!(input, pos, ctx)
2700
+ const v = selected(arm, input, pos, ctx)
1856
2701
  if (v !== FAIL) return v
1857
2702
  // THE CUT — a committed failure fails the whole choice.
1858
2703
  if (committed(ctx)) return FAIL
1859
2704
  // THE DISPATCHED ARM'S OWN SET IS THE ANSWER, at the choice's own
1860
- // position — the rule both shipped engines apply (choice.ts:105,
1861
- // codegen.ts:1985). `_fx` already holds it; reporting the arm is
1862
- // declining to overwrite it. See `exec.ts` for why this is NOT
1863
- // furthest-failure merging and what the residual JSON gap is.
2705
+ // position. `_fx` already holds it; reporting the sole selected arm
2706
+ // is declining to overwrite it.
1864
2707
  const armFx = ctx._fx
1865
2708
  if (armFx !== undefined && armFx.length > 0) { ctx._fe = pos; return FAIL }
1866
2709
  }
@@ -1876,48 +2719,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1876
2719
  // excludes the char at `pos`. `null` means nullable or unmappable, and
1877
2720
  // those arms are always entered.
1878
2721
  const armCls = table.armCls
1879
- const gates: (ResolvedClass | null)[] = new Array<ResolvedClass | null>(n)
1880
- for (let i = 0; i < n; i++) gates[i] = armCls[i] ?? null
1881
-
1882
- /**
1883
- * THE GATE, PRECOMPUTED. One `Uint32Array` load replaces `n` class tests.
1884
- *
1885
- * `exec.ts` asked, per arm per execution, "does this arm's class hold the
1886
- * char at `pos`" — `n` calls into `classHas`, each a bounds check plus a
1887
- * `Uint8Array` load, on 136,760 choice executions per parse of
1888
- * `benchmark.less`. Every one of those answers is a function of the
1889
- * CHARACTER and the TABLE, both of which are known here, so the whole
1890
- * question is answered once at assembly: `cand[c]` is the bitmask of arms
1891
- * that could match `c`, and the loop visits only set bits, IN ORDER, so
1892
- * the source ordering the gate exists to preserve is preserved exactly.
1893
- *
1894
- * Slot 128 is EOF (`lead` returns −1): `classHas` rejects a negative code,
1895
- * so only the always-enter arms are candidates there. Code points ≥ 128
1896
- * keep the per-arm loop — the ranges make a table the wrong shape and
1897
- * astral input is not the hot path.
1898
- *
1899
- * Bounded at 32 arms because the mask is one word. Above that the general
1900
- * loop runs, which is the same code the ≥ 128 path takes, so there is no
1901
- * second implementation to drift.
1902
- */
1903
- const maskable = n <= 32
1904
- let cand: Uint32Array | undefined
1905
- if (maskable) {
1906
- const m = new Uint32Array(129)
1907
- for (let i = 0; i < n; i++) {
1908
- const cls = gates[i]!
1909
- const bit = 1 << i
2722
+ if (n === 2 || n === 3) {
2723
+ const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2724
+ const a2 = n === 3 ? link(code[base + 2]!) : undefined
2725
+ const e0 = fx[code[base + n]!] as string[]
2726
+ const e1 = fx[code[base + n + 1]!] as string[]
2727
+ const e2 = n === 3 ? fx[code[base + n + 2]!] as string[] : undefined
2728
+ const g0 = armCls[0] ?? null, g1 = armCls[1] ?? null
2729
+ const g2 = n === 3 ? armCls[2] ?? null : null
2730
+ const mask = new Uint32Array(129)
2731
+ const addGate = (cls: ResolvedClass | null, bit: number): void => {
1910
2732
  if (cls === null) {
1911
- for (let c = 0; c < 129; c++) m[c]! |= bit
1912
- continue
2733
+ for (let c = 0; c < 129; c++) mask[c]! |= bit
2734
+ return
1913
2735
  }
1914
- for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) m[c]! |= bit
2736
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) mask[c]! |= bit
1915
2737
  }
1916
- cand = m
1917
- }
2738
+ addGate(g0, 1); addGate(g1, 2)
2739
+ if (n === 3) addGate(g2, 4)
1918
2740
 
1919
- if (cand !== undefined) {
1920
- const mask = cand
1921
2741
  return (input, pos, ctx) => {
1922
2742
  const c = lead(input, pos)
1923
2743
  const need = markCst(ctx)
@@ -1929,47 +2749,158 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1929
2749
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1930
2750
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1931
2751
  let acc: string[] | undefined
2752
+ let best = pos
1932
2753
  if (c < 128) {
1933
- // `c` is −1 at EOF, which indexes slot 128 — the always-enter arms.
1934
- const bits0 = mask[c < 0 ? 128 : c]!
1935
- let bits = bits0
1936
- // Arms below `prev` have already contributed. Only the FAILURE path
1937
- // advances it, so a matching arm returns without paying for any of
1938
- // this — the skipped arms' sets are read on no other exit.
2754
+ const bits = mask[c < 0 ? 128 : c]!
1939
2755
  let prev = 0
1940
- while (bits !== 0) {
1941
- const i = 31 - Math.clz32(bits & -bits)
1942
- bits &= bits - 1
2756
+ if ((bits & 1) !== 0) {
2757
+ ctx._fc = false
2758
+ const v = a0(input, pos, ctx)
2759
+ if (v !== FAIL) return v
2760
+ prev = 1
2761
+ const at = ctx._fe ?? pos
2762
+ if (at > best) { best = at; acc = undefined }
2763
+ if (at === best) acc = accSet(ctx._fx, acc)
2764
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2765
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2766
+ }
2767
+ if ((bits & 2) !== 0) {
2768
+ ctx._fc = false
2769
+ const v = a1(input, pos, ctx)
2770
+ if (v !== FAIL) return v
2771
+ if (best === pos && prev < 1) acc = accSet(e0, acc)
2772
+ prev = 2
2773
+ const at = ctx._fe ?? pos
2774
+ if (at > best) { best = at; acc = undefined }
2775
+ if (at === best) acc = accSet(ctx._fx, acc)
2776
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2777
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2778
+ }
2779
+ if (n === 3 && (bits & 4) !== 0) {
1943
2780
  ctx._fc = false
1944
- const v = arms[i]!(input, pos, ctx)
2781
+ const v = a2!(input, pos, ctx)
1945
2782
  if (v !== FAIL) return v
1946
- for (let j = prev; j < i; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
1947
- prev = i + 1
1948
- acc = accSet(ctx._fx, acc)
1949
- // A committed arm cuts, and the interpreter reports the
1950
- // accumulation at the ARM's span (choice.ts:170), not at `pos`.
2783
+ if (best === pos && prev < 1) acc = accSet(e0, acc)
2784
+ if (best === pos && prev < 2) acc = accSet(e1, acc)
2785
+ prev = 3
2786
+ const at = ctx._fe ?? pos
2787
+ if (at > best) { best = at; acc = undefined }
2788
+ if (at === best) acc = accSet(ctx._fx, acc)
1951
2789
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1952
2790
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1953
2791
  }
1954
- for (let j = prev; j < n; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
2792
+ if (best === pos && prev < 1) acc = accSet(e0, acc)
2793
+ if (best === pos && prev < 2) acc = accSet(e1, acc)
2794
+ if (best === pos && n === 3 && prev < 3) acc = accSet(e2, acc)
1955
2795
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1956
2796
  return FAIL
1957
2797
  }
1958
- for (let i = 0; i < n; i++) {
1959
- const cls = gates[i]!
1960
- if (cls !== null && !classHas(cls, c)) { acc = accSet(armFx[i]!, acc); continue }
2798
+
2799
+ if (g0 === null || classHas(g0, c)) {
1961
2800
  ctx._fc = false
1962
- const v = arms[i]!(input, pos, ctx)
2801
+ const v = a0(input, pos, ctx)
1963
2802
  if (v !== FAIL) return v
1964
- acc = accSet(ctx._fx, acc)
2803
+ const at = ctx._fe ?? pos
2804
+ if (at > best) { best = at; acc = undefined }
2805
+ if (at === best) acc = accSet(ctx._fx, acc)
1965
2806
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1966
2807
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2808
+ } else if (best === pos) acc = accSet(e0, acc)
2809
+ if (g1 === null || classHas(g1, c)) {
2810
+ ctx._fc = false
2811
+ const v = a1(input, pos, ctx)
2812
+ if (v !== FAIL) return v
2813
+ const at = ctx._fe ?? pos
2814
+ if (at > best) { best = at; acc = undefined }
2815
+ if (at === best) acc = accSet(ctx._fx, acc)
2816
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2817
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2818
+ } else if (best === pos) acc = accSet(e1, acc)
2819
+ if (n === 3) {
2820
+ if (g2 === null || classHas(g2, c)) {
2821
+ ctx._fc = false
2822
+ const v = a2!(input, pos, ctx)
2823
+ if (v !== FAIL) return v
2824
+ const at = ctx._fe ?? pos
2825
+ if (at > best) { best = at; acc = undefined }
2826
+ if (at === best) acc = accSet(ctx._fx, acc)
2827
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2828
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2829
+ } else if (best === pos) acc = accSet(e2, acc)
1967
2830
  }
1968
2831
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1969
2832
  return FAIL
1970
2833
  }
1971
2834
  }
1972
2835
 
2836
+ const bindGeneral = (start: number, prior: ChoiceExpectedPrefix): GeneralChoiceBlock => {
2837
+ const arm = (i: number): Piece => i < n ? link(code[base + i]!) : NEVER_PIECE
2838
+ const expected = (i: number): readonly string[] => i < n
2839
+ ? fx[code[base + n + i]!] as string[]
2840
+ : EMPTY_FX
2841
+ const gate = (i: number): ResolvedClass | null => i < n ? armCls[i] ?? null : NEVER_CLASS
2842
+ const through = expectedBlock(
2843
+ start, expected(start), expected(start + 1), expected(start + 2), expected(start + 3), prior,
2844
+ )
2845
+ const next = start + 4 < n ? bindGeneral(start + 4, through) : undefined
2846
+ return generalChoiceBlock(
2847
+ start,
2848
+ arm(start), arm(start + 1), arm(start + 2), arm(start + 3),
2849
+ gate(start), gate(start + 1), gate(start + 2), gate(start + 3),
2850
+ through, next, n, choiceFx,
2851
+ )
2852
+ }
2853
+ const general = bindGeneral(0, EMPTY_EXPECTED_PREFIX)
2854
+
2855
+ if (n <= 32) {
2856
+ const mask = new Uint32Array(129)
2857
+ for (let i = 0; i < n; i++) {
2858
+ const cls = armCls[i] ?? null
2859
+ const bit = 1 << i
2860
+ if (cls === null) {
2861
+ for (let c = 0; c < 129; c++) mask[c]! |= bit
2862
+ continue
2863
+ }
2864
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) mask[c]! |= bit
2865
+ }
2866
+
2867
+ const bindMasked = (start: number, prior: ChoiceExpectedPrefix): MaskedChoiceBlock => {
2868
+ const arm = (i: number): Piece => i < n ? link(code[base + i]!) : NEVER_PIECE
2869
+ const expected = (i: number): readonly string[] => i < n
2870
+ ? fx[code[base + n + i]!] as string[]
2871
+ : EMPTY_FX
2872
+ const through = expectedBlock(
2873
+ start, expected(start), expected(start + 1), expected(start + 2), expected(start + 3), prior,
2874
+ )
2875
+ const next = start + 4 < n ? bindMasked(start + 4, through) : undefined
2876
+ return maskedChoiceBlock(
2877
+ start, arm(start), arm(start + 1), arm(start + 2), arm(start + 3),
2878
+ through, next, n, choiceFx,
2879
+ )
2880
+ }
2881
+ const masked = bindMasked(0, EMPTY_EXPECTED_PREFIX)
2882
+ return (input, pos, ctx) => {
2883
+ const c = lead(input, pos)
2884
+ const need = markCst(ctx)
2885
+ const mRaw = MRAW
2886
+ const mTl = MTL
2887
+ const mLv = MLV
2888
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
2889
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
2890
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2891
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2892
+ if (c < 128) {
2893
+ return masked(
2894
+ input, pos, ctx, mask[c < 0 ? 128 : c]!,
2895
+ need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
2896
+ )
2897
+ }
2898
+ return general(
2899
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
2900
+ )
2901
+ }
2902
+ }
2903
+
1973
2904
  return (input, pos, ctx) => {
1974
2905
  const c = lead(input, pos)
1975
2906
  const need = markCst(ctx)
@@ -1980,19 +2911,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1980
2911
  const mEr = need ? ctx._errors?.length ?? 0 : 0
1981
2912
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1982
2913
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1983
- let acc: string[] | undefined
1984
- for (let i = 0; i < n; i++) {
1985
- const cls = gates[i]!
1986
- if (cls !== null && !classHas(cls, c)) { acc = accSet(armFx[i]!, acc); continue }
1987
- ctx._fc = false
1988
- const v = arms[i]!(input, pos, ctx)
1989
- if (v !== FAIL) return v
1990
- acc = accSet(ctx._fx, acc)
1991
- if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1992
- if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1993
- }
1994
- ctx._fe = pos; ctx._fx = acc ?? choiceFx
1995
- return FAIL
2914
+ return general(
2915
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
2916
+ )
1996
2917
  }
1997
2918
  }
1998
2919
 
@@ -2106,6 +3027,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2106
3027
  const reportItem = (flags & 4) !== 0
2107
3028
  const itemFx = reportItem ? fx[code[ip + 6]!] as string[] : EMPTY_FX
2108
3029
  const collect = op === OP_REP
3030
+ // The item's finite, non-nullable first set is table data. A strict
3031
+ // optional iteration whose lead is outside it cannot run or commit, so
3032
+ // stop before entering the child graph. Tolerant assemblies keep the
3033
+ // ordinary failure path because it may recover the rejected input.
3034
+ // A separator-less repeat has no separator sentinel, so ip+7 carries
3035
+ // the optional item's class at no extra table width. Separated lists
3036
+ // keep that slot for their recovery sentinel and use the ordinary path.
3037
+ const itemClassIndex = sepIp < 0 ? code[ip + 7]! : -1
3038
+ const itemClass = !REC && itemClassIndex >= 0 ? cc[itemClassIndex]! : undefined
3039
+ const itemMayStart = itemClass === undefined
3040
+ ? undefined
3041
+ : (input: string, pos: number): boolean => classHas(itemClass, lead(input, pos))
2109
3042
  // `many()` — the min-0, separator-less repeat — is the only shape that runs
2110
3043
  // its FIRST item through `repItem` and so skips leading trivia. The shape
2111
3044
  // identifies itself, and it is table data, so it is decided here.
@@ -2236,6 +3169,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2236
3169
  const out: unknown[] | undefined = collect ? [] : undefined
2237
3170
  const hasTrivia = ctx.trivia !== undefined
2238
3171
  const needMark = rollbackNeeded(ctx)
3172
+ // A completions probe deliberately observes swallowed item failures.
3173
+ // Keep the ordinary child path while one is active so this structural
3174
+ // fast stop cannot erase an item completion at the cursor.
3175
+ const gateItems = itemMayStart !== undefined && ctx._probe === undefined
2239
3176
  let cur = pos
2240
3177
  let count = 0
2241
3178
  for (;;) {
@@ -2244,6 +3181,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2244
3181
  // the LOOP HEAD. Held to `count >= min` so an under-`min` list still
2245
3182
  // attempts the separator and reports its expected set.
2246
3183
  if (sep !== undefined && count > 0 && count >= min && cur >= input.length) break
3184
+ // With no trivia or separator to position, the item starts at `cur`.
3185
+ // This is the full 0.46 loop-head cut: no mark and no child setup.
3186
+ if (count >= min && sep === undefined && !hasTrivia
3187
+ && gateItems && !itemMayStart!(input, cur)) break
2247
3188
  // Per ITERATION, never hoisted: a preceding item's `node()` opened and
2248
3189
  // closed a capture buffer, so `ctx._cstBuf` is not the object it was
2249
3190
  // at the loop head. `needMark` is the pre-existing loop-invariant.
@@ -2280,6 +3221,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2280
3221
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
2281
3222
  break
2282
3223
  }
3224
+ if (count >= min && gateItems && !itemMayStart!(input, itemStart)) {
3225
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3226
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3227
+ break
3228
+ }
2283
3229
  ctx._fc = false
2284
3230
  const v = child(input, itemStart, ctx)
2285
3231
  if (v === FAIL) {
@@ -2317,75 +3263,69 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2317
3263
  const otherRouted = code[ip + 4]! === 1
2318
3264
  const n = code[ip + 5]!
2319
3265
  const armBase = ip + 6
2320
- const arms: Piece[] = new Array<Piece>(n)
2321
- for (let i = 0; i < n; i++) arms[i] = link(code[armBase + i]!)
2322
3266
  const byKey = spec.byKey
2323
3267
  const byFold = spec.byFold
2324
3268
  const hasFold = spec.byFold.size > 0
2325
3269
  const match = spec.match
2326
3270
  const matchN = match.length
2327
- const matchFn: Array<(key: string) => boolean> = new Array<(key: string) => boolean>(matchN)
2328
- const matchArm: number[] = new Array<number>(matchN)
2329
- for (let i = 0; i < matchN; i++) { matchFn[i] = linkMatcher(match[i]!); matchArm[i] = match[i]![3] }
2330
3271
  const routed = spec.routed
2331
3272
  const expected = spec.expected as string[]
2332
3273
 
3274
+ const validArm = (arm: number): boolean => Number.isInteger(arm) && arm >= 0 && arm < n
3275
+ if (routed.length !== n) throw new TypeError('table assembler: malformed dispatch routed arity')
3276
+ for (const arm of byKey.values()) if (!validArm(arm)) throw new TypeError('table assembler: malformed dispatch exact arm')
3277
+ for (const arm of byFold.values()) if (!validArm(arm)) throw new TypeError('table assembler: malformed dispatch folded arm')
3278
+ for (const m of match) if (!validArm(m[3])) throw new TypeError('table assembler: malformed dispatch matcher arm')
3279
+
3280
+ const neverMatch: DispatchMatcher = () => false
3281
+ const bindMatchers = (start: number): DispatchMatcherBlock => {
3282
+ const pred = (i: number): DispatchMatcher => i < matchN ? linkMatcher(match[i]!) : neverMatch
3283
+ const arm = (i: number): number => i < matchN ? match[i]![3] : -1
3284
+ const next = start + 4 < matchN ? bindMatchers(start + 4) : undefined
3285
+ return dispatchMatcherBlock(
3286
+ pred(start), arm(start), pred(start + 1), arm(start + 1),
3287
+ pred(start + 2), arm(start + 2), pred(start + 3), arm(start + 3), next,
3288
+ )
3289
+ }
3290
+ const matchBlock = matchN === 0 ? undefined : bindMatchers(0)
3291
+ const classify: (key: string) => number | undefined = hasFold
3292
+ ? matchBlock === undefined
3293
+ ? key => byKey.get(key) ?? byFold.get(asciiFoldKey(key))
3294
+ : key => byKey.get(key) ?? byFold.get(asciiFoldKey(key)) ?? matchBlock(key)
3295
+ : matchBlock === undefined
3296
+ ? key => byKey.get(key)
3297
+ : key => byKey.get(key) ?? matchBlock(key)
3298
+
3299
+ const neverBranch: DispatchBranch = () => FAIL
3300
+ const bindArms = (start: number): DispatchArmBlock => {
3301
+ const branch = (i: number): DispatchBranch => i < n
3302
+ ? dispatchBranch(link(code[armBase + i]!), routed[i] === 1)
3303
+ : neverBranch
3304
+ const next = start + 4 < n ? bindArms(start + 4) : undefined
3305
+ return dispatchArmBlock(
3306
+ start, branch(start), branch(start + 1), branch(start + 2), branch(start + 3), next,
3307
+ )
3308
+ }
3309
+ const runArm = n === 0 ? undefined : bindArms(0)
3310
+ const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted)
3311
+
2333
3312
  return (input, pos, ctx) => {
2334
3313
  const selectorMark = saveTriviaMark(ctx)
2335
3314
  const selVal = selector(input, pos, ctx)
2336
3315
  if (selVal === FAIL) return FAIL
2337
3316
  const selEnd = EC.e
2338
3317
  const key = selVal as string
2339
-
2340
- let arm = byKey.get(key)
2341
- if (arm === undefined && hasFold) arm = byFold.get(asciiFoldKey(key))
3318
+ const arm = classify(key)
2342
3319
  if (arm === undefined) {
2343
- for (let i = 0; i < matchN; i++) {
2344
- if (matchFn[i]!(key)) {
2345
- arm = matchArm[i]!
2346
- break
2347
- }
2348
- }
2349
- }
2350
-
2351
- let target: Piece
2352
- let usesRouted: boolean
2353
- if (arm === undefined) {
2354
- if (other === undefined) {
3320
+ if (runOther === undefined) {
2355
3321
  // No branch and no fallback: fail AT THE SELECTOR'S END.
2356
3322
  ctx._fe = selEnd
2357
3323
  ctx._fx = expected
2358
3324
  return FAIL
2359
3325
  }
2360
- target = other
2361
- usesRouted = otherRouted
2362
- } else {
2363
- target = arms[arm]!
2364
- usesRouted = routed[arm] === 1
2365
- }
2366
-
2367
- let mark = saveTriviaMark(ctx)
2368
- let v: unknown
2369
- if (usesRouted) {
2370
- const savedRouted = ctx._routed
2371
- rollbackTrivia(ctx, selectorMark)
2372
- mark = saveTriviaMark(ctx)
2373
- ctx._routed = { value: key, span: { start: pos, end: selEnd } }
2374
- try {
2375
- v = target(input, pos, ctx)
2376
- } finally {
2377
- ctx._routed = savedRouted
2378
- }
2379
- } else {
2380
- v = target(input, selEnd, ctx)
2381
- }
2382
- if (v === FAIL) {
2383
- rollbackTrivia(ctx, mark)
2384
- // A failed dispatch branch is COMMITTED: the selector already matched.
2385
- ctx._fc = true
2386
- return FAIL
3326
+ return runOther(input, pos, selEnd, key, selectorMark, ctx)
2387
3327
  }
2388
- return [key, v]
3328
+ return runArm!(arm, input, pos, selEnd, key, selectorMark, ctx)
2389
3329
  }
2390
3330
  }
2391
3331
 
@@ -2394,6 +3334,41 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2394
3334
  case OP_NODE:
2395
3335
  case OP_NODE_TRACK: {
2396
3336
  const flags = code[ip + 3]!
3337
+ const scalarChild = !hostCst && !cfg.tolerant && !cfg.probe && !cfg.coverage && !cfg.trackLines
3338
+ ? scalarTerminalNodeChild(code, ip)
3339
+ : -1
3340
+ if (scalarChild >= 0) {
3341
+ const recognize = scalarFor(scalarChild)!
3342
+ const spec = k[code[scalarChild + 1]!]
3343
+ const regexValue = code[scalarChild] === OP_RX
3344
+ const xf = fx[code[scalarChild + 2]!] as string[]
3345
+ const build = fns[code[ip + 1]!] as (
3346
+ children: readonly unknown[], fields: undefined, span: { start: number; end: number },
3347
+ rawChildren: readonly unknown[], triviaLog: readonly number[], state: undefined,
3348
+ ) => unknown
3349
+ return (input, pos, ctx) => {
3350
+ const end = recognize(input, pos)
3351
+ if (end < 0) {
3352
+ ctx._fe = pos; ctx._fx = xf
3353
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
3354
+ return FAIL
3355
+ }
3356
+ const value = regexValue ? input.slice(pos, end) : spec as string
3357
+ const leaf = { _tag: 'leaf', value, span: { start: pos, end } }
3358
+ const kids = [leaf]
3359
+ const rawKids = [leaf]
3360
+ const span = { start: pos, end }
3361
+ EC.e = end
3362
+ const nd = build(kids, undefined, span, rawKids, EMPTY_TL, undefined)
3363
+ if (ctx._cstBuf !== undefined || ctx._cstChildren !== undefined) {
3364
+ pushCstChild(ctx, nd, ctx._cstRawChildren !== undefined || ctx._cstBuf !== undefined
3365
+ ? rawEntry(nd, input, pos, end)
3366
+ : undefined)
3367
+ }
3368
+ EC.e = end
3369
+ return nd
3370
+ }
3371
+ }
2397
3372
  const child = link(code[ip + 2]!)
2398
3373
  const proj = code[ip + 4]!
2399
3374
  const buildIdx = code[ip + 1]!
@@ -2412,12 +3387,34 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2412
3387
  // 145,512 executions per parse of `benchmark.less`. `cstOutputHost(ctx.build)`
2413
3388
  // is fixed by `run()` before the entry is called, so it selects the piece.
2414
3389
  const build = buildIdx >= 0
2415
- ? fns[buildIdx] as (
2416
- children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
2417
- rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
2418
- ) => unknown
3390
+ ? fns[buildIdx] as NodeBuilder
2419
3391
  : undefined
2420
3392
 
3393
+ // The common AST-only bodies are separate FunctionLiterals, not a mode
3394
+ // branch in the generic node body. Their eligibility is entirely table
3395
+ // data plus this assembly's fixed option set; direct/custom contexts and
3396
+ // every richer node shape retain the generic implementation below.
3397
+ if (!hostCst && !tracked) {
3398
+ if (build !== undefined && proj < 0 && flags === 0) {
3399
+ return plainBuildNode(child, build)
3400
+ }
3401
+ if (build !== undefined && proj < 0 && flags === 2) {
3402
+ return childrenOnlyBuildNode(child, build)
3403
+ }
3404
+ if (build !== undefined && proj < 0 && flags === 18) {
3405
+ return childrenOnlyFieldsBuildNode(child, build)
3406
+ }
3407
+ if (build !== undefined && proj < 0 && flags === 32) {
3408
+ return collapseBuildNode(child, build)
3409
+ }
3410
+ if (build !== undefined && proj < 0 && flags === 34) {
3411
+ return childrenOnlyCollapseBuildNode(child, build)
3412
+ }
3413
+ if (build === undefined && proj >= 0 && flags === 0) {
3414
+ return plainProjectNode(child, proj, type)
3415
+ }
3416
+ }
3417
+
2421
3418
  // A STRUCTURAL node — no builder, no projection — is the only shape that
2422
3419
  // takes a per-node-type trivia-kind mask off the host (`node.ts:260`).
2423
3420
  // Assembly-time, so a node with a reducer pays nothing for the question.
@@ -2584,10 +3581,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2584
3581
  * Emitted source is attempted FIRST, and the closure walk above runs only for
2585
3582
  * what it refuses.
2586
3583
  *
2587
- * The reason it has to be source at all is in `emit-assembly.ts`'s header:
2588
- * V8's inline-cache feedback is per FUNCTION LITERAL, so every piece minted
2589
- * from one literal here shares one feedback vector and every call site inside
2590
- * it is megamorphic. That is not a property this file can rearrange away.
3584
+ * Source emission names each fixed grammar edge directly and can be carried
3585
+ * as a build-time factory for CSP. The closure path may bind the same fixed
3586
+ * topology through scalar captures; this ordering selects the generated form
3587
+ * where it exists without making shared-literal IC behavior an architecture
3588
+ * premise.
2591
3589
  *
2592
3590
  * The refusal is RECORDED, not swallowed. A grammar that silently drops to
2593
3591
  * the closure path is a permanently slow path nobody would ever find, which
@@ -2609,10 +3607,23 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2609
3607
  if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
2610
3608
  }
2611
3609
  for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
2612
- const validationRoots = [...Object.values(prog.rules), ...extraIps]
2613
- for (const ip of reachableSites(code, validationRoots)) {
2614
- if (code[ip] !== OP_DISPATCH) continue
2615
- validateDispatchSpec(dsp[code[ip + 2]!], code[ip + 5]!, code[ip + 4]!)
3610
+ const roots = [...Object.values(prog.rules), ...extraIps]
3611
+ for (const ip of reachableSites(code, roots)) {
3612
+ if (code[ip] === OP_DISPATCH) {
3613
+ const spec = dsp[code[ip + 2]!]
3614
+ validateDispatchSpec(spec, code[ip + 5]!, code[ip + 4]!)
3615
+ }
3616
+ if (!hostCst && !cfg.tolerant && !cfg.probe && !cfg.coverage && !cfg.trackLines) {
3617
+ if (code[ip] === OP_NODE) {
3618
+ const child = scalarTerminalNodeChild(code, ip)
3619
+ if (child >= 0) {
3620
+ rawScalarSpecs.add(code[child + 1]!)
3621
+ scalarFor(child)
3622
+ }
3623
+ }
3624
+ }
3625
+ const notChild = scalarTerminalNotChild(code, ip)
3626
+ if (notChild >= 0) scalarFor(notChild)
2616
3627
  }
2617
3628
  /**
2618
3629
  * THE BUILD'S OWN ANSWER, TRIED BEFORE THE CONSTRUCTOR.
@@ -2637,7 +3648,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2637
3648
  ? prog.asm?.find(a => a.key === cfgKey(cfg))
2638
3649
  : undefined
2639
3650
  if (pre !== undefined) {
2640
- const pools = rebuildPools(t.cc, t.fx, pre.plan)
3651
+ const pools = rebuildPools(t.cc, t.fx, t.disp, pre.plan)
2641
3652
  emitted = pre.factory(
2642
3653
  EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
2643
3654
  trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
@@ -2649,6 +3660,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2649
3660
  asciiFoldKey, ROUTED_FX,
2650
3661
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2651
3662
  matchesAt, recoverScan, orSentinel, captureError,
3663
+ scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
2652
3664
  )
2653
3665
  emitReached = new Set(pre.reached)
2654
3666
  }
@@ -2729,6 +3741,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2729
3741
  // object rather than to two separately constructed ones.
2730
3742
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2731
3743
  matchesAt, recoverScan, orSentinel, captureError,
3744
+ scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
2732
3745
  )
2733
3746
  emitReached = em.reached
2734
3747
  } catch (e) {