parseman 0.47.1 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +27 -21
  2. package/dist/analysis/diagnostics.js +2863 -353
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2854 -344
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/expect.d.ts +1 -0
  7. package/dist/combinators/expect.d.ts.map +1 -1
  8. package/dist/combinators/expected.d.ts +8 -0
  9. package/dist/combinators/expected.d.ts.map +1 -0
  10. package/dist/combinators/first-set.d.ts +8 -0
  11. package/dist/combinators/first-set.d.ts.map +1 -1
  12. package/dist/combinators/grammar.d.ts.map +1 -1
  13. package/dist/combinators/keywords.d.ts +4 -0
  14. package/dist/combinators/keywords.d.ts.map +1 -1
  15. package/dist/combinators/literal.d.ts.map +1 -1
  16. package/dist/combinators/not.d.ts.map +1 -1
  17. package/dist/combinators/peek.d.ts.map +1 -1
  18. package/dist/combinators/regex.d.ts.map +1 -1
  19. package/dist/combinators/repeat.d.ts.map +1 -1
  20. package/dist/combinators/sequence.d.ts.map +1 -1
  21. package/dist/combinators/trivia-skip.d.ts +16 -3
  22. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  23. package/dist/compiler/direct-projection.d.ts +12 -0
  24. package/dist/compiler/direct-projection.d.ts.map +1 -0
  25. package/dist/compiler/token-alphabet.d.ts +845 -5
  26. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  27. package/dist/compiler/token-capability.d.ts +3 -0
  28. package/dist/compiler/token-capability.d.ts.map +1 -0
  29. package/dist/compiler/token-capability.js +3192 -0
  30. package/dist/compiler/token-capability.js.map +6 -0
  31. package/dist/cst/trivia-charscan.d.ts +3 -0
  32. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  33. package/dist/cst/trivia-css-scanner.d.ts +5 -0
  34. package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
  35. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  36. package/dist/index.js +2862 -339
  37. package/dist/index.js.map +3 -3
  38. package/dist/language-service/index.js +5 -1
  39. package/dist/language-service/index.js.map +1 -1
  40. package/dist/plugin/index.js +2870 -347
  41. package/dist/plugin/index.js.map +3 -3
  42. package/dist/spec/index.js +315 -46
  43. package/dist/spec/index.js.map +2 -2
  44. package/dist/table/assemble.d.ts.map +1 -1
  45. package/dist/table/child-slots.d.ts.map +1 -1
  46. package/dist/table/compile-rule-map.d.ts +3 -1
  47. package/dist/table/compile-rule-map.d.ts.map +1 -1
  48. package/dist/table/emit-assembly.d.ts +12 -13
  49. package/dist/table/emit-assembly.d.ts.map +1 -1
  50. package/dist/table/emit.d.ts.map +1 -1
  51. package/dist/table/encode.d.ts.map +1 -1
  52. package/dist/table/exec-baseline.d.ts.map +1 -1
  53. package/dist/table/exec.d.ts.map +1 -1
  54. package/dist/table/index.js +2819 -300
  55. package/dist/table/index.js.map +3 -3
  56. package/dist/table/lex-body.d.ts +9 -0
  57. package/dist/table/lex-body.d.ts.map +1 -0
  58. package/dist/table/lex-program.d.ts +25 -0
  59. package/dist/table/lex-program.d.ts.map +1 -0
  60. package/dist/table/ops.d.ts +43 -7
  61. package/dist/table/ops.d.ts.map +1 -1
  62. package/dist/table/program.d.ts +50 -2
  63. package/dist/table/program.d.ts.map +1 -1
  64. package/dist/table/scalar-terminal.d.ts +13 -0
  65. package/dist/table/scalar-terminal.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts.map +1 -1
  67. package/dist/types.d.ts +2 -0
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/version.d.ts +1 -1
  70. package/package.json +11 -11
  71. package/src/combinators/expect.ts +1 -0
  72. package/src/combinators/expected.ts +17 -0
  73. package/src/combinators/first-set.ts +58 -5
  74. package/src/combinators/grammar.ts +8 -3
  75. package/src/combinators/keywords.ts +22 -6
  76. package/src/combinators/literal.ts +2 -1
  77. package/src/combinators/not.ts +5 -1
  78. package/src/combinators/peek.ts +5 -1
  79. package/src/combinators/regex.ts +5 -3
  80. package/src/combinators/repeat.ts +4 -10
  81. package/src/combinators/sequence.ts +17 -14
  82. package/src/combinators/trivia-skip.ts +46 -13
  83. package/src/compiler/direct-projection.ts +43 -0
  84. package/src/compiler/token-alphabet.ts +4316 -12
  85. package/src/compiler/token-capability.ts +6 -0
  86. package/src/cst/trivia-charscan.ts +34 -0
  87. package/src/cst/trivia-css-scanner.ts +142 -0
  88. package/src/cst/trivia-kinds.ts +3 -1
  89. package/src/plugin/index.ts +4 -4
  90. package/src/table/assemble.ts +1211 -221
  91. package/src/table/child-slots.ts +2 -1
  92. package/src/table/compile-rule-map.ts +21 -3
  93. package/src/table/emit-assembly.ts +328 -85
  94. package/src/table/emit.ts +16 -2
  95. package/src/table/encode.ts +242 -50
  96. package/src/table/exec-baseline.ts +3 -7
  97. package/src/table/exec.ts +121 -7
  98. package/src/table/lex-body.ts +30 -0
  99. package/src/table/lex-program.ts +422 -0
  100. package/src/table/ops.ts +45 -7
  101. package/src/table/program.ts +46 -2
  102. package/src/table/scalar-terminal.ts +51 -0
  103. package/src/table/site-labels.ts +11 -1
  104. package/src/table/stamp.ts +1 -1
  105. package/src/types.ts +1 -1
  106. package/src/version.ts +1 -1
  107. package/dist/analysis/diagnostics.cjs +0 -13824
  108. package/dist/analysis/diagnostics.cjs.map +0 -6
  109. package/dist/cli/index.cjs +0 -14925
  110. package/dist/cli/index.cjs.map +0 -6
  111. package/dist/index.cjs +0 -16391
  112. package/dist/index.cjs.map +0 -6
  113. package/dist/language-service/index.cjs +0 -1304
  114. package/dist/language-service/index.cjs.map +0 -6
  115. package/dist/oracle/index.cjs +0 -254
  116. package/dist/oracle/index.cjs.map +0 -6
  117. package/dist/plugin/index.cjs +0 -16097
  118. package/dist/plugin/index.cjs.map +0 -6
  119. package/dist/run/index.cjs +0 -577
  120. package/dist/run/index.cjs.map +0 -6
  121. package/dist/spec/index.cjs +0 -6236
  122. package/dist/spec/index.cjs.map +0 -6
  123. package/dist/table/index.cjs +0 -11463
  124. 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,
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,
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
@@ -705,6 +1028,218 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
705
1028
  return acc
706
1029
  }
707
1030
 
1031
+ /** No fixed child array is permitted to survive into a choice piece. */
1032
+ const NEVER_PIECE: Piece = () => FAIL
1033
+ const NEVER_CLASS: ResolvedClass = { ascii: new Uint8Array(128), hi: [] }
1034
+ const EMPTY_EXPECTED_PREFIX: ChoiceExpectedPrefix = (_target, _prev, acc) => acc
1035
+
1036
+ /**
1037
+ * Four directly captured static expected sets. The chain keeps skipped-arm
1038
+ * diagnostics lazy across block boundaries: a later successful arm never
1039
+ * materialises expectations for earlier char-excluded arms.
1040
+ */
1041
+ function expectedBlock(
1042
+ start: number,
1043
+ e0: readonly string[], e1: readonly string[], e2: readonly string[], e3: readonly string[],
1044
+ prior: ChoiceExpectedPrefix,
1045
+ ): ChoiceExpectedPrefix {
1046
+ return (target, prev, acc) => {
1047
+ if (prev < start) { acc = prior(start, prev, acc); prev = start }
1048
+ if (target > start && prev < start + 1) acc = accSet(e0, acc)
1049
+ if (target > start + 1 && prev < start + 2) acc = accSet(e1, acc)
1050
+ if (target > start + 2 && prev < start + 3) acc = accSet(e2, acc)
1051
+ if (target > start + 3 && prev < start + 4) acc = accSet(e3, acc)
1052
+ return acc
1053
+ }
1054
+ }
1055
+
1056
+ /** One four-arm ASCII-mask block, selected once when the arity is maskable. */
1057
+ function maskedChoiceBlock(
1058
+ start: number,
1059
+ a0: Piece, a1: Piece, a2: Piece, a3: Piece,
1060
+ through: ChoiceExpectedPrefix,
1061
+ next: MaskedChoiceBlock | undefined,
1062
+ total: number,
1063
+ choiceFx: string[],
1064
+ ): MaskedChoiceBlock {
1065
+ return (input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev) => {
1066
+ if ((bits & (1 << start)) !== 0) {
1067
+ ctx._fc = false
1068
+ const v = a0(input, pos, ctx)
1069
+ if (v !== FAIL) return v
1070
+ acc = through(start, prev, acc); prev = start + 1
1071
+ acc = accSet(ctx._fx, acc)
1072
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1073
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1074
+ }
1075
+ if ((bits & (1 << (start + 1))) !== 0) {
1076
+ ctx._fc = false
1077
+ const v = a1(input, pos, ctx)
1078
+ if (v !== FAIL) return v
1079
+ acc = through(start + 1, prev, acc); prev = start + 2
1080
+ acc = accSet(ctx._fx, acc)
1081
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1082
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1083
+ }
1084
+ if ((bits & (1 << (start + 2))) !== 0) {
1085
+ ctx._fc = false
1086
+ const v = a2(input, pos, ctx)
1087
+ if (v !== FAIL) return v
1088
+ acc = through(start + 2, prev, acc); prev = start + 3
1089
+ acc = accSet(ctx._fx, acc)
1090
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1091
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1092
+ }
1093
+ if ((bits & (1 << (start + 3))) !== 0) {
1094
+ ctx._fc = false
1095
+ const v = a3(input, pos, ctx)
1096
+ if (v !== FAIL) return v
1097
+ acc = through(start + 3, prev, acc); prev = start + 4
1098
+ 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)
1104
+ }
1105
+ 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) => {
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
+ acc = through(start, prev, acc); prev = start + 1
1128
+ acc = accSet(ctx._fx, acc)
1129
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1130
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1131
+ }
1132
+ if (g1 === null || classHas(g1, c)) {
1133
+ ctx._fc = false
1134
+ const v = a1(input, pos, ctx)
1135
+ if (v !== FAIL) return v
1136
+ acc = through(start + 1, prev, acc); prev = start + 2
1137
+ acc = accSet(ctx._fx, acc)
1138
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1139
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1140
+ }
1141
+ if (g2 === null || classHas(g2, c)) {
1142
+ ctx._fc = false
1143
+ const v = a2(input, pos, ctx)
1144
+ if (v !== FAIL) return v
1145
+ acc = through(start + 2, prev, acc); prev = start + 3
1146
+ acc = accSet(ctx._fx, acc)
1147
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1148
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1149
+ }
1150
+ if (g3 === null || classHas(g3, c)) {
1151
+ ctx._fc = false
1152
+ const v = a3(input, pos, ctx)
1153
+ if (v !== FAIL) return v
1154
+ acc = through(start + 3, prev, acc); prev = start + 4
1155
+ 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 (next !== undefined) {
1160
+ return next(input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev)
1161
+ }
1162
+ acc = through(total, prev, acc)
1163
+ ctx._fe = pos; ctx._fx = acc ?? choiceFx
1164
+ return FAIL
1165
+ }
1166
+ }
1167
+
1168
+ /** Four direct exclusive arms, chained for every arity above three. */
1169
+ function exclusiveChoiceBlock(
1170
+ start: number, a0: Piece, a1: Piece, a2: Piece, a3: Piece,
1171
+ next: ExclusiveChoiceBlock | undefined,
1172
+ ): ExclusiveChoiceBlock {
1173
+ return (arm, input, pos, ctx) => {
1174
+ if (arm === start) return a0(input, pos, ctx)
1175
+ if (arm === start + 1) return a1(input, pos, ctx)
1176
+ if (arm === start + 2) return a2(input, pos, ctx)
1177
+ if (arm === start + 3) return a3(input, pos, ctx)
1178
+ return next === undefined ? FAIL : next(arm, input, pos, ctx)
1179
+ }
1180
+ }
1181
+
1182
+ function dispatchMatcherBlock(
1183
+ m0: DispatchMatcher, a0: number, m1: DispatchMatcher, a1: number,
1184
+ m2: DispatchMatcher, a2: number, m3: DispatchMatcher, a3: number,
1185
+ next: DispatchMatcherBlock | undefined,
1186
+ ): DispatchMatcherBlock {
1187
+ return key => {
1188
+ if (m0(key)) return a0
1189
+ if (m1(key)) return a1
1190
+ if (m2(key)) return a2
1191
+ if (m3(key)) return a3
1192
+ return next?.(key)
1193
+ }
1194
+ }
1195
+
1196
+ function dispatchBranch(child: Piece, usesRouted: boolean): DispatchBranch {
1197
+ if (!usesRouted) {
1198
+ return (input, _pos, selEnd, key, _selectorMark, ctx) => {
1199
+ const mark = saveTriviaMark(ctx)
1200
+ const v = child(input, selEnd, ctx)
1201
+ if (v === FAIL) {
1202
+ rollbackTrivia(ctx, mark)
1203
+ ctx._fc = true
1204
+ return FAIL
1205
+ }
1206
+ return [key, v]
1207
+ }
1208
+ }
1209
+ return (input, pos, selEnd, key, selectorMark, ctx) => {
1210
+ const savedRouted = ctx._routed
1211
+ rollbackTrivia(ctx, selectorMark)
1212
+ const mark = saveTriviaMark(ctx)
1213
+ ctx._routed = { value: key, span: { start: pos, end: selEnd } }
1214
+ let v: unknown
1215
+ try {
1216
+ v = child(input, pos, ctx)
1217
+ } finally {
1218
+ ctx._routed = savedRouted
1219
+ }
1220
+ if (v === FAIL) {
1221
+ rollbackTrivia(ctx, mark)
1222
+ ctx._fc = true
1223
+ return FAIL
1224
+ }
1225
+ return [key, v]
1226
+ }
1227
+ }
1228
+
1229
+ function dispatchArmBlock(
1230
+ start: number,
1231
+ a0: DispatchBranch, a1: DispatchBranch, a2: DispatchBranch, a3: DispatchBranch,
1232
+ next: DispatchArmBlock | undefined,
1233
+ ): DispatchArmBlock {
1234
+ return (arm, input, pos, selEnd, key, selectorMark, ctx) => {
1235
+ if (arm === start) return a0(input, pos, selEnd, key, selectorMark, ctx)
1236
+ if (arm === start + 1) return a1(input, pos, selEnd, key, selectorMark, ctx)
1237
+ if (arm === start + 2) return a2(input, pos, selEnd, key, selectorMark, ctx)
1238
+ if (arm === start + 3) return a3(input, pos, selEnd, key, selectorMark, ctx)
1239
+ return next === undefined ? FAIL : next(arm, input, pos, selEnd, key, selectorMark, ctx)
1240
+ }
1241
+ }
1242
+
708
1243
 
709
1244
 
710
1245
  function trackLinesInto(ctx: ParseContext, input: string, end: number): void {
@@ -759,6 +1294,20 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
759
1294
  const s = k[code[ip + 1]!] as string
760
1295
  const len = s.length
761
1296
  const xf = fx[code[ip + 2]!] as string[]
1297
+ const shared = rawScalarSpecs.has(code[ip + 1]!) ? scalarFor(ip) : undefined
1298
+ if (shared !== undefined) {
1299
+ return (input, pos, ctx) => {
1300
+ const e = shared(input, pos)
1301
+ if (e >= 0) {
1302
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
1303
+ EC.e = e
1304
+ return s
1305
+ }
1306
+ ctx._fe = pos; ctx._fx = xf
1307
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
1308
+ return FAIL
1309
+ }
1310
+ }
762
1311
  // LENGTH IS TABLE DATA, so the COMPARE is chosen here rather than
763
1312
  // delegated to a builtin that has to rediscover it. See `litBodyNote`.
764
1313
  if (len === 1) {
@@ -884,6 +1433,21 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
884
1433
  case OP_RX: {
885
1434
  const re = k[code[ip + 1]!] as RegExp
886
1435
  const xf = fx[code[ip + 2]!] as string[]
1436
+ const shared = rawScalarSpecs.has(code[ip + 1]!) ? scalarFor(ip) : undefined
1437
+ if (shared !== undefined) {
1438
+ return (input, pos, ctx) => {
1439
+ const e = shared(input, pos)
1440
+ if (e >= 0) {
1441
+ const v = input.slice(pos, e)
1442
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
1443
+ EC.e = e
1444
+ return v
1445
+ }
1446
+ ctx._fe = pos; ctx._fx = xf
1447
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
1448
+ return FAIL
1449
+ }
1450
+ }
887
1451
  return (input, pos, ctx) => {
888
1452
  re.lastIndex = pos
889
1453
  // `regex()` rows are sticky and expose only their whole match. `exec()`
@@ -1307,6 +1871,21 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1307
1871
  /* ── zero-width ──────────────────────────────────────────────────────── */
1308
1872
 
1309
1873
  case OP_NOT: {
1874
+ const scalarChild = scalarTerminalNotChild(code, ip)
1875
+ if (scalarChild >= 0) {
1876
+ const recognize = scalarFor(scalarChild)!
1877
+ const xf = fx[code[ip + 2]!] as string[]
1878
+ return (input, pos, ctx) => {
1879
+ if (recognize(input, pos) < 0) {
1880
+ EC.e = pos
1881
+ return null
1882
+ }
1883
+ ctx._fe = pos
1884
+ ctx._fx = xf
1885
+ EC.e = pos
1886
+ return FAIL
1887
+ }
1888
+ }
1310
1889
  const child = link(code[ip + 1]!)
1311
1890
  const xf = fx[code[ip + 2]!] as string[]
1312
1891
  return (input, pos, ctx) => {
@@ -1420,6 +1999,93 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1420
1999
  }
1421
2000
  }
1422
2001
 
2002
+ case OP_LEX_BODY: {
2003
+ const recognize = t.lex[code[ip + 1]!]!
2004
+ const expected = fx[code[ip + 2]!] as string[]
2005
+ return (input, pos, ctx) => {
2006
+ const recognized = recognize(input, pos)
2007
+ if (recognized < 0) {
2008
+ ctx._fe = pos
2009
+ ctx._fx = expected as string[]
2010
+ if (ctx._probe !== undefined) failAt(ctx, expected, pos)
2011
+ return FAIL
2012
+ }
2013
+ const lineFlags = code[ip + 4]!
2014
+ const hasSuffix = (lineFlags & 4) !== 0
2015
+ const suffixMatched = hasSuffix && recognized % 2 === 1
2016
+ const end = (recognized - (suffixMatched ? 1 : 0)) / 2
2017
+ if ((lineFlags & 1) !== 0) {
2018
+ trackLinesInto(ctx, input, suffixMatched ? end - 1 : end)
2019
+ }
2020
+ if (hasSuffix) ctx._fc = false
2021
+ if (hasSuffix && suffixMatched && (lineFlags & 2) !== 0) {
2022
+ trackLinesInto(ctx, input, end)
2023
+ }
2024
+ if (hasSuffix && !suffixMatched) {
2025
+ const suffixExpected = fx[code[ip + 3]!] as string[]
2026
+ ctx._fe = end
2027
+ ctx._fx = suffixExpected
2028
+ if (ctx._probe !== undefined) failAt(ctx, suffixExpected, end)
2029
+ }
2030
+ const value = input.slice(pos, end)
2031
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
2032
+ EC.e = end
2033
+ return value
2034
+ }
2035
+ }
2036
+
2037
+ case OP_LEX_PROGRAM: {
2038
+ const run = t.lexPrograms[code[ip + 1]!]!
2039
+ const scanId = run.scan
2040
+ if (scanId !== undefined) {
2041
+ return (input, pos, ctx) => {
2042
+ const sTrivia = ctx.trivia, sKinds = ctx.triviaKindLabels
2043
+ const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
2044
+ const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
2045
+ const sOuterTl = ctx._triviaLog, sRootTl = ctx._rootTriviaLog
2046
+ const wasCapturing = cstCaptureActive(ctx)
2047
+ const sScan = SCAN
2048
+ SCAN = null
2049
+ ctx.trivia = undefined
2050
+ ctx.triviaKindLabels = undefined
2051
+ ctx._cstBuf = undefined
2052
+ ctx._cstChildren = undefined
2053
+ ctx._cstLeaves = undefined
2054
+ ctx._cstRawChildren = undefined
2055
+ ctx._cstTriviaLog = undefined
2056
+ ctx._triviaLog = undefined
2057
+ ctx._rootTriviaLog = undefined
2058
+ let end: number
2059
+ try { end = run(input, pos, ctx, scans[scanId]) }
2060
+ finally {
2061
+ SCAN = sScan
2062
+ ctx.trivia = sTrivia
2063
+ ctx.triviaKindLabels = sKinds
2064
+ ctx._cstBuf = sBuf
2065
+ ctx._cstChildren = sChildren
2066
+ ctx._cstLeaves = sLeaves
2067
+ ctx._cstRawChildren = sRaw
2068
+ ctx._cstTriviaLog = sTl
2069
+ ctx._triviaLog = sOuterTl
2070
+ ctx._rootTriviaLog = sRootTl
2071
+ }
2072
+ if (end < 0) return FAIL
2073
+ const value = input.slice(pos, end)
2074
+ if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
2075
+ EC.e = end
2076
+ return value
2077
+ }
2078
+ }
2079
+ return (input, pos, ctx) => {
2080
+ const end = run(input, pos, ctx)
2081
+ if (end < 0) return FAIL
2082
+ const value = input.slice(pos, end)
2083
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
2084
+ EC.e = end
2085
+ return value
2086
+ }
2087
+ }
2088
+
1423
2089
  case OP_LEAF: {
1424
2090
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1425
2091
  const child = link(code[ip + 2]!)
@@ -1464,36 +2130,60 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1464
2130
  const fused = op === OP_SEQX
1465
2131
  const base = fused ? ip + 3 : ip + 2
1466
2132
  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
2133
  const wantValues = op !== OP_SEQV
1471
- const fn = fused
1472
- ? fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
2134
+ const reducer = fused ? code[ip + 1]! : -1
2135
+ const projection = reducer < 0 ? ~reducer : -1
2136
+ const fn = fused && projection < 0
2137
+ ? fns[reducer] as (value: unknown, span: { start: number; end: number }) => unknown
1473
2138
  : undefined
1474
2139
 
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)
2140
+ const runnerBlock = (
2141
+ count: number,
2142
+ r0: TermRunner, r1: TermRunner, r2: TermRunner, r3: TermRunner,
2143
+ next: SequenceTermBlock | undefined,
2144
+ ): SequenceTermBlock => {
2145
+ if (count === 1) return (input, cur, ctx, values, inherited) => {
2146
+ cur = r0(input, cur, ctx, inherited)
1493
2147
  if (cur < 0) return -1
1494
2148
  if (values !== undefined) values.push(TERMV)
2149
+ return next === undefined ? cur : next(input, cur, ctx, values, inherited)
2150
+ }
2151
+ if (count === 2) return (input, cur, ctx, values, inherited) => {
2152
+ cur = r0(input, cur, ctx, inherited)
2153
+ if (cur < 0) return -1
2154
+ if (values !== undefined) values.push(TERMV)
2155
+ cur = r1(input, cur, ctx, inherited)
2156
+ if (cur < 0) return -1
2157
+ if (values !== undefined) values.push(TERMV)
2158
+ return next === undefined ? cur : next(input, cur, ctx, values, inherited)
2159
+ }
2160
+ if (count === 3) return (input, cur, ctx, values, inherited) => {
2161
+ cur = r0(input, cur, ctx, inherited)
2162
+ if (cur < 0) return -1
2163
+ if (values !== undefined) values.push(TERMV)
2164
+ cur = r1(input, cur, ctx, inherited)
2165
+ if (cur < 0) return -1
2166
+ if (values !== undefined) values.push(TERMV)
2167
+ cur = r2(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
+ 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
+ cur = r3(input, cur, ctx, inherited)
2183
+ if (cur < 0) return -1
2184
+ if (values !== undefined) values.push(TERMV)
2185
+ return next === undefined ? cur : next(input, cur, ctx, values, inherited)
1495
2186
  }
1496
- return cur
1497
2187
  }
1498
2188
 
1499
2189
  /**
@@ -1544,21 +2234,24 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1544
2234
  * sentinel published is the kind of state a later reader will trust.
1545
2235
  */
1546
2236
  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++) {
2237
+ const first = link(code[base]!)
2238
+ const firstSync = sentinelFor(code[base + n]!)
2239
+ const runnerAt = (i: number): TermRunner => {
2240
+ const sync = sentinelFor(code[base + n + i]!)
1551
2241
  const kidIp = code[base + i]!
1552
2242
  if (code[kidIp] !== OP_ADJ) {
1553
- const kid = kids[i]!
1554
- runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1555
- continue
2243
+ const kid = link(kidIp)
2244
+ return (input, cur, ctx, inherited) => {
2245
+ ctx._sync = sync ?? inherited
2246
+ return nextTerm(kid, input, cur, ctx)
2247
+ }
1556
2248
  }
1557
2249
  const negated = code[kidIp + 1] === 1
1558
2250
  const ki = code[kidIp + 2]!
1559
2251
  const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1560
2252
  const xf = fx[code[kidIp + 3]!] as string[]
1561
- runners[i] = (input, cur, ctx) => {
2253
+ return (input, cur, ctx, inherited) => {
2254
+ ctx._sync = sync ?? inherited
1562
2255
  if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1563
2256
  ctx._fe = cur; ctx._fx = xf
1564
2257
  return -1
@@ -1567,19 +2260,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1567
2260
  return cur
1568
2261
  }
1569
2262
  }
2263
+ const bindRunners = (start: number): SequenceTermBlock => {
2264
+ const count = Math.min(4, n - start)
2265
+ const at = (offset: number): TermRunner => runnerAt(start + offset)
2266
+ const never: TermRunner = () => -1
2267
+ return runnerBlock(
2268
+ count, at(0), count > 1 ? at(1) : never,
2269
+ count > 2 ? at(2) : never, count > 3 ? at(3) : never,
2270
+ start + count < n ? bindRunners(start + count) : undefined,
2271
+ )
2272
+ }
2273
+ const tail = n > 1 ? bindRunners(1) : undefined
1570
2274
  const runSyncTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1571
2275
  const inherited = ctx._sync
1572
- ctx._sync = syncs[0] ?? inherited
1573
- const v0 = kids[0]!(input, pos, ctx)
2276
+ ctx._sync = firstSync ?? inherited
2277
+ const v0 = first(input, pos, ctx)
1574
2278
  if (v0 === FAIL) { ctx._sync = inherited; return -1 }
1575
2279
  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
- }
2280
+ const cur = tail === undefined ? EC.e : tail(input, EC.e, ctx, values, inherited)
2281
+ if (cur < 0) { ctx._sync = inherited; return -1 }
1583
2282
  ctx._sync = inherited
1584
2283
  return cur
1585
2284
  }
@@ -1589,7 +2288,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1589
2288
  const cur = runSyncTerms(input, pos, ctx, values)
1590
2289
  if (cur < 0) return FAIL
1591
2290
  EC.e = cur
1592
- return fn!(values, { start: pos, end: cur })
2291
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1593
2292
  }
1594
2293
  }
1595
2294
  if (wantValues) {
@@ -1612,13 +2311,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1612
2311
  let hasAdj = false
1613
2312
  for (let i = 1; i < n; i++) if (code[code[base + i]!] === OP_ADJ) { hasAdj = true; break }
1614
2313
  if (hasAdj) {
1615
- const runners: TermRunner[] = new Array<TermRunner>(n)
1616
- for (let i = 1; i < n; i++) {
2314
+ const first = link(code[base]!)
2315
+ const runnerAt = (i: number): TermRunner => {
1617
2316
  const kidIp = code[base + i]!
1618
2317
  if (code[kidIp] !== OP_ADJ) {
1619
- const kid = kids[i]!
1620
- runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1621
- continue
2318
+ const kid = link(kidIp)
2319
+ return (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1622
2320
  }
1623
2321
  const negated = code[kidIp + 1] === 1
1624
2322
  const ki = code[kidIp + 2]!
@@ -1628,7 +2326,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1628
2326
  // the interpreter does.
1629
2327
  const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1630
2328
  const xf = fx[code[kidIp + 3]!] as string[]
1631
- runners[i] = (input, cur, ctx) => {
2329
+ return (input, cur, ctx) => {
1632
2330
  if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1633
2331
  ctx._fe = cur; ctx._fx = xf
1634
2332
  return -1
@@ -1637,17 +2335,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1637
2335
  return cur
1638
2336
  }
1639
2337
  }
2338
+ const bindRunners = (start: number): SequenceTermBlock => {
2339
+ const count = Math.min(4, n - start)
2340
+ const at = (offset: number): TermRunner => runnerAt(start + offset)
2341
+ const never: TermRunner = () => -1
2342
+ return runnerBlock(
2343
+ count, at(0), count > 1 ? at(1) : never,
2344
+ count > 2 ? at(2) : never, count > 3 ? at(3) : never,
2345
+ start + count < n ? bindRunners(start + count) : undefined,
2346
+ )
2347
+ }
2348
+ const tail = bindRunners(1)
1640
2349
  const runAdjTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1641
- const v0 = kids[0]!(input, pos, ctx)
2350
+ const v0 = first(input, pos, ctx)
1642
2351
  if (v0 === FAIL) return -1
1643
2352
  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
2353
+ return tail(input, EC.e, ctx, values)
1651
2354
  }
1652
2355
  if (fused) {
1653
2356
  return (input, pos, ctx) => {
@@ -1655,7 +2358,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1655
2358
  const cur = runAdjTerms(input, pos, ctx, values)
1656
2359
  if (cur < 0) return FAIL
1657
2360
  EC.e = cur
1658
- return fn!(values, { start: pos, end: cur })
2361
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1659
2362
  }
1660
2363
  }
1661
2364
  if (wantValues) {
@@ -1693,12 +2396,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1693
2396
  * runs, so there is no arity this fails to lower.
1694
2397
  */
1695
2398
  if (n === 1) {
1696
- const k0 = kids[0]!
2399
+ const k0 = link(code[base]!)
1697
2400
  if (fused) {
1698
2401
  return (input, pos, ctx) => {
1699
2402
  const v = k0(input, pos, ctx)
1700
2403
  if (v === FAIL) return FAIL
1701
- return fn!([v], { start: pos, end: EC.e })
2404
+ return projection === 0 ? v : fn!([v], { start: pos, end: EC.e })
1702
2405
  }
1703
2406
  }
1704
2407
  if (wantValues) {
@@ -1716,7 +2419,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1716
2419
  }
1717
2420
 
1718
2421
  if (n === 2) {
1719
- const k0 = kids[0]!, k1 = kids[1]!
2422
+ const k0 = link(code[base]!), k1 = link(code[base + 1]!)
1720
2423
  if (fused) {
1721
2424
  return (input, pos, ctx) => {
1722
2425
  const v0 = k0(input, pos, ctx)
@@ -1724,7 +2427,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1724
2427
  const cur = nextTerm(k1, input, EC.e, ctx)
1725
2428
  if (cur < 0) return FAIL
1726
2429
  EC.e = cur
1727
- return fn!([v0, TERMV], { start: pos, end: cur })
2430
+ return projection === 0 ? v0
2431
+ : projection === 1 ? TERMV
2432
+ : fn!([v0, TERMV], { start: pos, end: cur })
1728
2433
  }
1729
2434
  }
1730
2435
  if (wantValues) {
@@ -1748,7 +2453,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1748
2453
  }
1749
2454
 
1750
2455
  if (n === 3) {
1751
- const k0 = kids[0]!, k1 = kids[1]!, k2 = kids[2]!
2456
+ const k0 = link(code[base]!), k1 = link(code[base + 1]!), k2 = link(code[base + 2]!)
1752
2457
  if (fused) {
1753
2458
  return (input, pos, ctx) => {
1754
2459
  const v0 = k0(input, pos, ctx)
@@ -1759,7 +2464,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1759
2464
  cur = nextTerm(k2, input, cur, ctx)
1760
2465
  if (cur < 0) return FAIL
1761
2466
  EC.e = cur
1762
- return fn!([v0, v1, TERMV], { start: pos, end: cur })
2467
+ return projection === 0 ? v0
2468
+ : projection === 1 ? v1
2469
+ : projection === 2 ? TERMV
2470
+ : fn!([v0, v1, TERMV], { start: pos, end: cur })
1763
2471
  }
1764
2472
  }
1765
2473
  if (wantValues) {
@@ -1787,26 +2495,97 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1787
2495
  }
1788
2496
  }
1789
2497
 
2498
+ // Arbitrary strict arities use a bounded scalar block chain. Fixed
2499
+ // children are captured into prewritten four-term blocks at assembly;
2500
+ // the parse path has no `kids[i]`, fixed-child loop, or opcode lookup.
2501
+ const block = (
2502
+ count: number,
2503
+ k0: Piece, k1: Piece, k2: Piece, k3: Piece,
2504
+ next: SequenceTermBlock | undefined,
2505
+ ): SequenceTermBlock => {
2506
+ if (count === 1) return (input, cur, ctx, values) => {
2507
+ cur = nextTerm(k0, input, cur, ctx)
2508
+ if (cur < 0) return -1
2509
+ if (values !== undefined) values.push(TERMV)
2510
+ return next === undefined ? cur : next(input, cur, ctx, values)
2511
+ }
2512
+ if (count === 2) return (input, cur, ctx, values) => {
2513
+ cur = nextTerm(k0, input, cur, ctx)
2514
+ if (cur < 0) return -1
2515
+ if (values !== undefined) values.push(TERMV)
2516
+ cur = nextTerm(k1, input, cur, ctx)
2517
+ if (cur < 0) return -1
2518
+ if (values !== undefined) values.push(TERMV)
2519
+ return next === undefined ? cur : next(input, cur, ctx, values)
2520
+ }
2521
+ if (count === 3) return (input, cur, ctx, values) => {
2522
+ cur = nextTerm(k0, input, cur, ctx)
2523
+ if (cur < 0) return -1
2524
+ if (values !== undefined) values.push(TERMV)
2525
+ cur = nextTerm(k1, input, cur, ctx)
2526
+ if (cur < 0) return -1
2527
+ if (values !== undefined) values.push(TERMV)
2528
+ cur = nextTerm(k2, 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
+ 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
+ cur = nextTerm(k3, input, cur, ctx)
2544
+ if (cur < 0) return -1
2545
+ if (values !== undefined) values.push(TERMV)
2546
+ return next === undefined ? cur : next(input, cur, ctx, values)
2547
+ }
2548
+ }
2549
+ const bind = (start: number): SequenceTermBlock => {
2550
+ const count = Math.min(4, n - start)
2551
+ const child = (offset: number): Piece => link(code[base + start + offset]!)
2552
+ return block(
2553
+ count, child(0), count > 1 ? child(1) : NEVER_PIECE,
2554
+ count > 2 ? child(2) : NEVER_PIECE, count > 3 ? child(3) : NEVER_PIECE,
2555
+ start + count < n ? bind(start + count) : undefined,
2556
+ )
2557
+ }
2558
+ const first = link(code[base]!)
2559
+ const tail = bind(1)
2560
+ const runDirectTerms = (
2561
+ input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined,
2562
+ ): number => {
2563
+ const v0 = first(input, pos, ctx)
2564
+ if (v0 === FAIL) return -1
2565
+ if (values !== undefined) values.push(v0)
2566
+ return tail(input, EC.e, ctx, values)
2567
+ }
2568
+
1790
2569
  if (fused) {
1791
2570
  return (input, pos, ctx) => {
1792
2571
  const values: unknown[] = []
1793
- const cur = runTerms(input, pos, ctx, values)
2572
+ const cur = runDirectTerms(input, pos, ctx, values)
1794
2573
  if (cur < 0) return FAIL
1795
2574
  EC.e = cur
1796
- return fn!(values, { start: pos, end: cur })
2575
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1797
2576
  }
1798
2577
  }
1799
2578
  if (wantValues) {
1800
2579
  return (input, pos, ctx) => {
1801
2580
  const values: unknown[] = []
1802
- const cur = runTerms(input, pos, ctx, values)
2581
+ const cur = runDirectTerms(input, pos, ctx, values)
1803
2582
  if (cur < 0) return FAIL
1804
2583
  EC.e = cur
1805
2584
  return values
1806
2585
  }
1807
2586
  }
1808
2587
  return (input, pos, ctx) => {
1809
- const cur = runTerms(input, pos, ctx, undefined)
2588
+ const cur = runDirectTerms(input, pos, ctx, undefined)
1810
2589
  if (cur < 0) return FAIL
1811
2590
  EC.e = cur
1812
2591
  return undefined
@@ -1820,20 +2599,74 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1820
2599
  const n = code[ip + 2]!
1821
2600
  const choiceFx = fx[code[ip + 3]!] as string[]
1822
2601
  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
2602
 
1832
2603
  // THE TWO CHOICE SHAPES ARE TWO PIECES. `exec.ts` tested `table.exclusive`
1833
2604
  // on every choice execution; it is table data, so it selects here.
1834
2605
  if (table.exclusive) {
1835
2606
  const ascii = table.ascii
1836
2607
  const hi = table.hi
2608
+ // THE COMMON ARITIES OWN THEIR TOPOLOGY AS SCALARS. Do not build an
2609
+ // `arms[]` merely to rediscover the same two or three children at
2610
+ // every dispatch. The classifier remains table data; only its result
2611
+ // is turned into a direct captured call.
2612
+ if (n === 2) {
2613
+ const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2614
+ return (input, pos, ctx) => {
2615
+ const c = lead(input, pos)
2616
+ let arm = -1
2617
+ if (c >= 0 && c < 128) {
2618
+ const a = ascii[c]!
2619
+ if (a !== 0) arm = a - 1
2620
+ } else if (c >= 128) {
2621
+ for (let i = 0; i < hi.length; i += 3) {
2622
+ if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
2623
+ }
2624
+ }
2625
+ if (arm >= 0) {
2626
+ ctx._fc = false
2627
+ const v = arm === 0 ? a0(input, pos, ctx) : a1(input, pos, ctx)
2628
+ if (v !== FAIL) return v
2629
+ if (committed(ctx)) return FAIL
2630
+ const failed = ctx._fx
2631
+ if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return FAIL }
2632
+ }
2633
+ ctx._fe = pos; ctx._fx = choiceFx
2634
+ return FAIL
2635
+ }
2636
+ }
2637
+ if (n === 3) {
2638
+ const a0 = link(code[base]!), a1 = link(code[base + 1]!), a2 = link(code[base + 2]!)
2639
+ return (input, pos, ctx) => {
2640
+ const c = lead(input, pos)
2641
+ let arm = -1
2642
+ if (c >= 0 && c < 128) {
2643
+ const a = ascii[c]!
2644
+ if (a !== 0) arm = a - 1
2645
+ } else if (c >= 128) {
2646
+ for (let i = 0; i < hi.length; i += 3) {
2647
+ if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
2648
+ }
2649
+ }
2650
+ if (arm >= 0) {
2651
+ ctx._fc = false
2652
+ const v = arm === 0 ? a0(input, pos, ctx) : arm === 1 ? a1(input, pos, ctx) : a2(input, pos, ctx)
2653
+ if (v !== FAIL) return v
2654
+ if (committed(ctx)) return FAIL
2655
+ const failed = ctx._fx
2656
+ if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return FAIL }
2657
+ }
2658
+ ctx._fe = pos; ctx._fx = choiceFx
2659
+ return FAIL
2660
+ }
2661
+ }
2662
+ const bindExclusive = (start: number): ExclusiveChoiceBlock => {
2663
+ const at = (i: number): Piece => i < n ? link(code[base + i]!) : NEVER_PIECE
2664
+ const next = start + 4 < n ? bindExclusive(start + 4) : undefined
2665
+ return exclusiveChoiceBlock(
2666
+ start, at(start), at(start + 1), at(start + 2), at(start + 3), next,
2667
+ )
2668
+ }
2669
+ const selected = bindExclusive(0)
1837
2670
  // THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
1838
2671
  // ungated arm is one whose class is −1, and `resolveDispatch` clears
1839
2672
  // `exclusive` for exactly those arms (program.ts) — so `table.open` is
@@ -1852,7 +2685,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1852
2685
  }
1853
2686
  if (arm >= 0) {
1854
2687
  ctx._fc = false
1855
- const v = arms[arm]!(input, pos, ctx)
2688
+ const v = selected(arm, input, pos, ctx)
1856
2689
  if (v !== FAIL) return v
1857
2690
  // THE CUT — a committed failure fails the whole choice.
1858
2691
  if (committed(ctx)) return FAIL
@@ -1876,48 +2709,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1876
2709
  // excludes the char at `pos`. `null` means nullable or unmappable, and
1877
2710
  // those arms are always entered.
1878
2711
  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
2712
+ if (n === 2 || n === 3) {
2713
+ const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2714
+ const a2 = n === 3 ? link(code[base + 2]!) : undefined
2715
+ const e0 = fx[code[base + n]!] as string[]
2716
+ const e1 = fx[code[base + n + 1]!] as string[]
2717
+ const e2 = n === 3 ? fx[code[base + n + 2]!] as string[] : undefined
2718
+ const g0 = armCls[0] ?? null, g1 = armCls[1] ?? null
2719
+ const g2 = n === 3 ? armCls[2] ?? null : null
2720
+ const mask = new Uint32Array(129)
2721
+ const addGate = (cls: ResolvedClass | null, bit: number): void => {
1910
2722
  if (cls === null) {
1911
- for (let c = 0; c < 129; c++) m[c]! |= bit
1912
- continue
2723
+ for (let c = 0; c < 129; c++) mask[c]! |= bit
2724
+ return
1913
2725
  }
1914
- for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) m[c]! |= bit
2726
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) mask[c]! |= bit
1915
2727
  }
1916
- cand = m
1917
- }
2728
+ addGate(g0, 1); addGate(g1, 2)
2729
+ if (n === 3) addGate(g2, 4)
1918
2730
 
1919
- if (cand !== undefined) {
1920
- const mask = cand
1921
2731
  return (input, pos, ctx) => {
1922
2732
  const c = lead(input, pos)
1923
2733
  const need = markCst(ctx)
@@ -1930,46 +2740,144 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1930
2740
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1931
2741
  let acc: string[] | undefined
1932
2742
  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.
2743
+ const bits = mask[c < 0 ? 128 : c]!
1939
2744
  let prev = 0
1940
- while (bits !== 0) {
1941
- const i = 31 - Math.clz32(bits & -bits)
1942
- bits &= bits - 1
2745
+ if ((bits & 1) !== 0) {
1943
2746
  ctx._fc = false
1944
- const v = arms[i]!(input, pos, ctx)
2747
+ const v = a0(input, pos, ctx)
1945
2748
  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
2749
+ prev = 1
1948
2750
  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`.
1951
2751
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1952
2752
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1953
2753
  }
1954
- for (let j = prev; j < n; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
2754
+ if ((bits & 2) !== 0) {
2755
+ ctx._fc = false
2756
+ const v = a1(input, pos, ctx)
2757
+ if (v !== FAIL) return v
2758
+ if (prev < 1) acc = accSet(e0, acc)
2759
+ prev = 2
2760
+ acc = accSet(ctx._fx, acc)
2761
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2762
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2763
+ }
2764
+ if (n === 3 && (bits & 4) !== 0) {
2765
+ ctx._fc = false
2766
+ const v = a2!(input, pos, ctx)
2767
+ if (v !== FAIL) return v
2768
+ if (prev < 1) acc = accSet(e0, acc)
2769
+ if (prev < 2) acc = accSet(e1, acc)
2770
+ prev = 3
2771
+ acc = accSet(ctx._fx, acc)
2772
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2773
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2774
+ }
2775
+ if (prev < 1) acc = accSet(e0, acc)
2776
+ if (prev < 2) acc = accSet(e1, acc)
2777
+ if (n === 3 && prev < 3) acc = accSet(e2, acc)
1955
2778
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1956
2779
  return FAIL
1957
2780
  }
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 }
2781
+
2782
+ if (g0 === null || classHas(g0, c)) {
2783
+ ctx._fc = false
2784
+ const v = a0(input, pos, ctx)
2785
+ if (v !== FAIL) return v
2786
+ acc = accSet(ctx._fx, acc)
2787
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2788
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2789
+ } else acc = accSet(e0, acc)
2790
+ if (g1 === null || classHas(g1, c)) {
1961
2791
  ctx._fc = false
1962
- const v = arms[i]!(input, pos, ctx)
2792
+ const v = a1(input, pos, ctx)
1963
2793
  if (v !== FAIL) return v
1964
2794
  acc = accSet(ctx._fx, acc)
1965
2795
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1966
2796
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2797
+ } else acc = accSet(e1, acc)
2798
+ if (n === 3) {
2799
+ if (g2 === null || classHas(g2, c)) {
2800
+ ctx._fc = false
2801
+ const v = a2!(input, pos, ctx)
2802
+ if (v !== FAIL) return v
2803
+ acc = accSet(ctx._fx, acc)
2804
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2805
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2806
+ } else acc = accSet(e2, acc)
1967
2807
  }
1968
2808
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1969
2809
  return FAIL
1970
2810
  }
1971
2811
  }
1972
2812
 
2813
+ const bindGeneral = (start: number, prior: ChoiceExpectedPrefix): GeneralChoiceBlock => {
2814
+ const arm = (i: number): Piece => i < n ? link(code[base + i]!) : NEVER_PIECE
2815
+ const expected = (i: number): readonly string[] => i < n
2816
+ ? fx[code[base + n + i]!] as string[]
2817
+ : EMPTY_FX
2818
+ const gate = (i: number): ResolvedClass | null => i < n ? armCls[i] ?? null : NEVER_CLASS
2819
+ const through = expectedBlock(
2820
+ start, expected(start), expected(start + 1), expected(start + 2), expected(start + 3), prior,
2821
+ )
2822
+ const next = start + 4 < n ? bindGeneral(start + 4, through) : undefined
2823
+ return generalChoiceBlock(
2824
+ start,
2825
+ arm(start), arm(start + 1), arm(start + 2), arm(start + 3),
2826
+ gate(start), gate(start + 1), gate(start + 2), gate(start + 3),
2827
+ through, next, n, choiceFx,
2828
+ )
2829
+ }
2830
+ const general = bindGeneral(0, EMPTY_EXPECTED_PREFIX)
2831
+
2832
+ if (n <= 32) {
2833
+ const mask = new Uint32Array(129)
2834
+ for (let i = 0; i < n; i++) {
2835
+ const cls = armCls[i] ?? null
2836
+ const bit = 1 << i
2837
+ if (cls === null) {
2838
+ for (let c = 0; c < 129; c++) mask[c]! |= bit
2839
+ continue
2840
+ }
2841
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) mask[c]! |= bit
2842
+ }
2843
+
2844
+ const bindMasked = (start: number, prior: ChoiceExpectedPrefix): MaskedChoiceBlock => {
2845
+ const arm = (i: number): Piece => i < n ? link(code[base + i]!) : NEVER_PIECE
2846
+ const expected = (i: number): readonly string[] => i < n
2847
+ ? fx[code[base + n + i]!] as string[]
2848
+ : EMPTY_FX
2849
+ const through = expectedBlock(
2850
+ start, expected(start), expected(start + 1), expected(start + 2), expected(start + 3), prior,
2851
+ )
2852
+ const next = start + 4 < n ? bindMasked(start + 4, through) : undefined
2853
+ return maskedChoiceBlock(
2854
+ start, arm(start), arm(start + 1), arm(start + 2), arm(start + 3),
2855
+ through, next, n, choiceFx,
2856
+ )
2857
+ }
2858
+ const masked = bindMasked(0, EMPTY_EXPECTED_PREFIX)
2859
+ return (input, pos, ctx) => {
2860
+ const c = lead(input, pos)
2861
+ const need = markCst(ctx)
2862
+ const mRaw = MRAW
2863
+ const mTl = MTL
2864
+ const mLv = MLV
2865
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
2866
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
2867
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2868
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2869
+ if (c < 128) {
2870
+ return masked(
2871
+ input, pos, ctx, mask[c < 0 ? 128 : c]!,
2872
+ need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0,
2873
+ )
2874
+ }
2875
+ return general(
2876
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0,
2877
+ )
2878
+ }
2879
+ }
2880
+
1973
2881
  return (input, pos, ctx) => {
1974
2882
  const c = lead(input, pos)
1975
2883
  const need = markCst(ctx)
@@ -1980,19 +2888,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1980
2888
  const mEr = need ? ctx._errors?.length ?? 0 : 0
1981
2889
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1982
2890
  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
2891
+ return general(
2892
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0,
2893
+ )
1996
2894
  }
1997
2895
  }
1998
2896
 
@@ -2106,6 +3004,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2106
3004
  const reportItem = (flags & 4) !== 0
2107
3005
  const itemFx = reportItem ? fx[code[ip + 6]!] as string[] : EMPTY_FX
2108
3006
  const collect = op === OP_REP
3007
+ // The item's finite, non-nullable first set is table data. A strict
3008
+ // optional iteration whose lead is outside it cannot run or commit, so
3009
+ // stop before entering the child graph. Tolerant assemblies keep the
3010
+ // ordinary failure path because it may recover the rejected input.
3011
+ // A separator-less repeat has no separator sentinel, so ip+7 carries
3012
+ // the optional item's class at no extra table width. Separated lists
3013
+ // keep that slot for their recovery sentinel and use the ordinary path.
3014
+ const itemClassIndex = sepIp < 0 ? code[ip + 7]! : -1
3015
+ const itemClass = !REC && itemClassIndex >= 0 ? cc[itemClassIndex]! : undefined
3016
+ const itemMayStart = itemClass === undefined
3017
+ ? undefined
3018
+ : (input: string, pos: number): boolean => classHas(itemClass, lead(input, pos))
2109
3019
  // `many()` — the min-0, separator-less repeat — is the only shape that runs
2110
3020
  // its FIRST item through `repItem` and so skips leading trivia. The shape
2111
3021
  // identifies itself, and it is table data, so it is decided here.
@@ -2236,6 +3146,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2236
3146
  const out: unknown[] | undefined = collect ? [] : undefined
2237
3147
  const hasTrivia = ctx.trivia !== undefined
2238
3148
  const needMark = rollbackNeeded(ctx)
3149
+ // A completions probe deliberately observes swallowed item failures.
3150
+ // Keep the ordinary child path while one is active so this structural
3151
+ // fast stop cannot erase an item completion at the cursor.
3152
+ const gateItems = itemMayStart !== undefined && ctx._probe === undefined
2239
3153
  let cur = pos
2240
3154
  let count = 0
2241
3155
  for (;;) {
@@ -2244,6 +3158,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2244
3158
  // the LOOP HEAD. Held to `count >= min` so an under-`min` list still
2245
3159
  // attempts the separator and reports its expected set.
2246
3160
  if (sep !== undefined && count > 0 && count >= min && cur >= input.length) break
3161
+ // With no trivia or separator to position, the item starts at `cur`.
3162
+ // This is the full 0.46 loop-head cut: no mark and no child setup.
3163
+ if (count >= min && sep === undefined && !hasTrivia
3164
+ && gateItems && !itemMayStart!(input, cur)) break
2247
3165
  // Per ITERATION, never hoisted: a preceding item's `node()` opened and
2248
3166
  // closed a capture buffer, so `ctx._cstBuf` is not the object it was
2249
3167
  // at the loop head. `needMark` is the pre-existing loop-invariant.
@@ -2280,6 +3198,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2280
3198
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
2281
3199
  break
2282
3200
  }
3201
+ if (count >= min && gateItems && !itemMayStart!(input, itemStart)) {
3202
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3203
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3204
+ break
3205
+ }
2283
3206
  ctx._fc = false
2284
3207
  const v = child(input, itemStart, ctx)
2285
3208
  if (v === FAIL) {
@@ -2317,75 +3240,69 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2317
3240
  const otherRouted = code[ip + 4]! === 1
2318
3241
  const n = code[ip + 5]!
2319
3242
  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
3243
  const byKey = spec.byKey
2323
3244
  const byFold = spec.byFold
2324
3245
  const hasFold = spec.byFold.size > 0
2325
3246
  const match = spec.match
2326
3247
  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
3248
  const routed = spec.routed
2331
3249
  const expected = spec.expected as string[]
2332
3250
 
3251
+ const validArm = (arm: number): boolean => Number.isInteger(arm) && arm >= 0 && arm < n
3252
+ if (routed.length !== n) throw new TypeError('table assembler: malformed dispatch routed arity')
3253
+ for (const arm of byKey.values()) if (!validArm(arm)) throw new TypeError('table assembler: malformed dispatch exact arm')
3254
+ for (const arm of byFold.values()) if (!validArm(arm)) throw new TypeError('table assembler: malformed dispatch folded arm')
3255
+ for (const m of match) if (!validArm(m[3])) throw new TypeError('table assembler: malformed dispatch matcher arm')
3256
+
3257
+ const neverMatch: DispatchMatcher = () => false
3258
+ const bindMatchers = (start: number): DispatchMatcherBlock => {
3259
+ const pred = (i: number): DispatchMatcher => i < matchN ? linkMatcher(match[i]!) : neverMatch
3260
+ const arm = (i: number): number => i < matchN ? match[i]![3] : -1
3261
+ const next = start + 4 < matchN ? bindMatchers(start + 4) : undefined
3262
+ return dispatchMatcherBlock(
3263
+ pred(start), arm(start), pred(start + 1), arm(start + 1),
3264
+ pred(start + 2), arm(start + 2), pred(start + 3), arm(start + 3), next,
3265
+ )
3266
+ }
3267
+ const matchBlock = matchN === 0 ? undefined : bindMatchers(0)
3268
+ const classify: (key: string) => number | undefined = hasFold
3269
+ ? matchBlock === undefined
3270
+ ? key => byKey.get(key) ?? byFold.get(asciiFoldKey(key))
3271
+ : key => byKey.get(key) ?? byFold.get(asciiFoldKey(key)) ?? matchBlock(key)
3272
+ : matchBlock === undefined
3273
+ ? key => byKey.get(key)
3274
+ : key => byKey.get(key) ?? matchBlock(key)
3275
+
3276
+ const neverBranch: DispatchBranch = () => FAIL
3277
+ const bindArms = (start: number): DispatchArmBlock => {
3278
+ const branch = (i: number): DispatchBranch => i < n
3279
+ ? dispatchBranch(link(code[armBase + i]!), routed[i] === 1)
3280
+ : neverBranch
3281
+ const next = start + 4 < n ? bindArms(start + 4) : undefined
3282
+ return dispatchArmBlock(
3283
+ start, branch(start), branch(start + 1), branch(start + 2), branch(start + 3), next,
3284
+ )
3285
+ }
3286
+ const runArm = n === 0 ? undefined : bindArms(0)
3287
+ const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted)
3288
+
2333
3289
  return (input, pos, ctx) => {
2334
3290
  const selectorMark = saveTriviaMark(ctx)
2335
3291
  const selVal = selector(input, pos, ctx)
2336
3292
  if (selVal === FAIL) return FAIL
2337
3293
  const selEnd = EC.e
2338
3294
  const key = selVal as string
2339
-
2340
- let arm = byKey.get(key)
2341
- if (arm === undefined && hasFold) arm = byFold.get(asciiFoldKey(key))
3295
+ const arm = classify(key)
2342
3296
  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) {
3297
+ if (runOther === undefined) {
2355
3298
  // No branch and no fallback: fail AT THE SELECTOR'S END.
2356
3299
  ctx._fe = selEnd
2357
3300
  ctx._fx = expected
2358
3301
  return FAIL
2359
3302
  }
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
3303
+ return runOther(input, pos, selEnd, key, selectorMark, ctx)
2387
3304
  }
2388
- return [key, v]
3305
+ return runArm!(arm, input, pos, selEnd, key, selectorMark, ctx)
2389
3306
  }
2390
3307
  }
2391
3308
 
@@ -2394,6 +3311,41 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2394
3311
  case OP_NODE:
2395
3312
  case OP_NODE_TRACK: {
2396
3313
  const flags = code[ip + 3]!
3314
+ const scalarChild = !hostCst && !cfg.tolerant && !cfg.probe && !cfg.coverage && !cfg.trackLines
3315
+ ? scalarTerminalNodeChild(code, ip)
3316
+ : -1
3317
+ if (scalarChild >= 0) {
3318
+ const recognize = scalarFor(scalarChild)!
3319
+ const spec = k[code[scalarChild + 1]!]
3320
+ const regexValue = code[scalarChild] === OP_RX
3321
+ const xf = fx[code[scalarChild + 2]!] as string[]
3322
+ const build = fns[code[ip + 1]!] as (
3323
+ children: readonly unknown[], fields: undefined, span: { start: number; end: number },
3324
+ rawChildren: readonly unknown[], triviaLog: readonly number[], state: undefined,
3325
+ ) => unknown
3326
+ return (input, pos, ctx) => {
3327
+ const end = recognize(input, pos)
3328
+ if (end < 0) {
3329
+ ctx._fe = pos; ctx._fx = xf
3330
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
3331
+ return FAIL
3332
+ }
3333
+ const value = regexValue ? input.slice(pos, end) : spec as string
3334
+ const leaf = { _tag: 'leaf', value, span: { start: pos, end } }
3335
+ const kids = [leaf]
3336
+ const rawKids = [leaf]
3337
+ const span = { start: pos, end }
3338
+ EC.e = end
3339
+ const nd = build(kids, undefined, span, rawKids, EMPTY_TL, undefined)
3340
+ if (ctx._cstBuf !== undefined || ctx._cstChildren !== undefined) {
3341
+ pushCstChild(ctx, nd, ctx._cstRawChildren !== undefined || ctx._cstBuf !== undefined
3342
+ ? rawEntry(nd, input, pos, end)
3343
+ : undefined)
3344
+ }
3345
+ EC.e = end
3346
+ return nd
3347
+ }
3348
+ }
2397
3349
  const child = link(code[ip + 2]!)
2398
3350
  const proj = code[ip + 4]!
2399
3351
  const buildIdx = code[ip + 1]!
@@ -2412,12 +3364,34 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2412
3364
  // 145,512 executions per parse of `benchmark.less`. `cstOutputHost(ctx.build)`
2413
3365
  // is fixed by `run()` before the entry is called, so it selects the piece.
2414
3366
  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
3367
+ ? fns[buildIdx] as NodeBuilder
2419
3368
  : undefined
2420
3369
 
3370
+ // The common AST-only bodies are separate FunctionLiterals, not a mode
3371
+ // branch in the generic node body. Their eligibility is entirely table
3372
+ // data plus this assembly's fixed option set; direct/custom contexts and
3373
+ // every richer node shape retain the generic implementation below.
3374
+ if (!hostCst && !tracked) {
3375
+ if (build !== undefined && proj < 0 && flags === 0) {
3376
+ return plainBuildNode(child, build)
3377
+ }
3378
+ if (build !== undefined && proj < 0 && flags === 2) {
3379
+ return childrenOnlyBuildNode(child, build)
3380
+ }
3381
+ if (build !== undefined && proj < 0 && flags === 18) {
3382
+ return childrenOnlyFieldsBuildNode(child, build)
3383
+ }
3384
+ if (build !== undefined && proj < 0 && flags === 32) {
3385
+ return collapseBuildNode(child, build)
3386
+ }
3387
+ if (build !== undefined && proj < 0 && flags === 34) {
3388
+ return childrenOnlyCollapseBuildNode(child, build)
3389
+ }
3390
+ if (build === undefined && proj >= 0 && flags === 0) {
3391
+ return plainProjectNode(child, proj, type)
3392
+ }
3393
+ }
3394
+
2421
3395
  // A STRUCTURAL node — no builder, no projection — is the only shape that
2422
3396
  // takes a per-node-type trivia-kind mask off the host (`node.ts:260`).
2423
3397
  // Assembly-time, so a node with a reducer pays nothing for the question.
@@ -2584,10 +3558,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2584
3558
  * Emitted source is attempted FIRST, and the closure walk above runs only for
2585
3559
  * what it refuses.
2586
3560
  *
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.
3561
+ * Source emission names each fixed grammar edge directly and can be carried
3562
+ * as a build-time factory for CSP. The closure path may bind the same fixed
3563
+ * topology through scalar captures; this ordering selects the generated form
3564
+ * where it exists without making shared-literal IC behavior an architecture
3565
+ * premise.
2591
3566
  *
2592
3567
  * The refusal is RECORDED, not swallowed. A grammar that silently drops to
2593
3568
  * the closure path is a permanently slow path nobody would ever find, which
@@ -2609,10 +3584,23 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2609
3584
  if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
2610
3585
  }
2611
3586
  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]!)
3587
+ const roots = [...Object.values(prog.rules), ...extraIps]
3588
+ for (const ip of reachableSites(code, roots)) {
3589
+ if (code[ip] === OP_DISPATCH) {
3590
+ const spec = dsp[code[ip + 2]!]
3591
+ validateDispatchSpec(spec, code[ip + 5]!, code[ip + 4]!)
3592
+ }
3593
+ if (!hostCst && !cfg.tolerant && !cfg.probe && !cfg.coverage && !cfg.trackLines) {
3594
+ if (code[ip] === OP_NODE) {
3595
+ const child = scalarTerminalNodeChild(code, ip)
3596
+ if (child >= 0) {
3597
+ rawScalarSpecs.add(code[child + 1]!)
3598
+ scalarFor(child)
3599
+ }
3600
+ }
3601
+ }
3602
+ const notChild = scalarTerminalNotChild(code, ip)
3603
+ if (notChild >= 0) scalarFor(notChild)
2616
3604
  }
2617
3605
  /**
2618
3606
  * THE BUILD'S OWN ANSWER, TRIED BEFORE THE CONSTRUCTOR.
@@ -2637,7 +3625,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2637
3625
  ? prog.asm?.find(a => a.key === cfgKey(cfg))
2638
3626
  : undefined
2639
3627
  if (pre !== undefined) {
2640
- const pools = rebuildPools(t.cc, t.fx, pre.plan)
3628
+ const pools = rebuildPools(t.cc, t.fx, t.disp, pre.plan)
2641
3629
  emitted = pre.factory(
2642
3630
  EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
2643
3631
  trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
@@ -2649,6 +3637,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2649
3637
  asciiFoldKey, ROUTED_FX,
2650
3638
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2651
3639
  matchesAt, recoverScan, orSentinel, captureError,
3640
+ scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
2652
3641
  )
2653
3642
  emitReached = new Set(pre.reached)
2654
3643
  }
@@ -2729,6 +3718,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2729
3718
  // object rather than to two separately constructed ones.
2730
3719
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2731
3720
  matchesAt, recoverScan, orSentinel, captureError,
3721
+ scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
2732
3722
  )
2733
3723
  emitReached = em.reached
2734
3724
  } catch (e) {