parseman 0.47.0 → 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 +2955 -357
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2946 -348
  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 +19 -1
  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 +2959 -348
  37. package/dist/index.js.map +3 -3
  38. package/dist/language-service/index.js +7 -1
  39. package/dist/language-service/index.js.map +1 -1
  40. package/dist/plugin/index.js +2969 -358
  41. package/dist/plugin/index.js.map +3 -3
  42. package/dist/spec/index.js +366 -49
  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 +13 -14
  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 +4839 -2232
  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 +47 -4
  61. package/dist/table/ops.d.ts.map +1 -1
  62. package/dist/table/program.d.ts +53 -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 +12 -11
  71. package/src/combinators/expect.ts +1 -0
  72. package/src/combinators/expected.ts +17 -0
  73. package/src/combinators/first-set.ts +82 -3
  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 +35 -16
  82. package/src/combinators/trivia-skip.ts +75 -11
  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 +1224 -222
  91. package/src/table/child-slots.ts +4 -3
  92. package/src/table/compile-rule-map.ts +21 -3
  93. package/src/table/emit-assembly.ts +363 -109
  94. package/src/table/emit.ts +26 -1
  95. package/src/table/encode.ts +246 -54
  96. package/src/table/exec-baseline.ts +3 -7
  97. package/src/table/exec.ts +140 -18
  98. package/src/table/lex-body.ts +30 -0
  99. package/src/table/lex-program.ts +422 -0
  100. package/src/table/ops.ts +50 -4
  101. package/src/table/program.ts +67 -2
  102. package/src/table/scalar-terminal.ts +51 -0
  103. package/src/table/site-labels.ts +13 -3
  104. package/src/table/stamp.ts +3 -3
  105. package/src/types.ts +1 -1
  106. package/src/version.ts +1 -1
  107. package/dist/analysis/diagnostics.cjs +0 -13736
  108. package/dist/analysis/diagnostics.cjs.map +0 -6
  109. package/dist/cli/index.cjs +0 -14837
  110. package/dist/cli/index.cjs.map +0 -6
  111. package/dist/index.cjs +0 -16303
  112. package/dist/index.cjs.map +0 -6
  113. package/dist/language-service/index.cjs +0 -1302
  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 -16009
  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 -6188
  122. package/dist/spec/index.cjs.map +0 -6
  123. package/dist/table/index.cjs +0 -11375
  124. package/dist/table/index.cjs.map +0 -6
@@ -71,22 +71,23 @@ 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, 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
- cstCaptureActive, cstLeavesLen,
78
+ cstCaptureActive, cstLeavesLen, cstTlLen,
79
79
  demoteCapturedToRaw, pushCstChild, pushCstLeaf,
80
80
  type CstCaptureBuf,
81
81
  } from '../cst/capture-buffer.ts'
82
82
  import {
83
83
  OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
84
84
  OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
85
- OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_EXPECT, OP_SEQX, OP_SCAN,
85
+ OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
86
86
  OP_LIVE,
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'
@@ -119,12 +120,17 @@ import { lead, rawEntry, spanLines } from './run-support.ts'
119
120
  */
120
121
  import {
121
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
127
  import { stampRuleMap } from './stamp.ts'
128
+ import { reachableSites } from './site-labels.ts'
126
129
  import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
127
130
  import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
131
+ import {
132
+ makeScalarRecognizer, scalarTerminalNodeChild, scalarTerminalNotChild, type ScalarRecognizer,
133
+ } from './scalar-terminal.ts'
128
134
 
129
135
  /**
130
136
  * Is the EMITTED engine (`emit-assembly.ts`) enabled for this process?
@@ -138,7 +144,7 @@ const EMIT_ENABLED = (globalThis as { process?: { env?: Record<string, string |
138
144
  .process?.env?.PM_TABLE_EMIT !== '0'
139
145
 
140
146
  /** Failure sentinel — SHARED with the other two engines, see `cell.ts`. */
141
- import { FAIL, newEndCell, type EndCell } from './cell.ts'
147
+ import { FAIL, newEndCell } from './cell.ts'
142
148
 
143
149
  const EMPTY_TL: readonly number[] = Object.freeze([])
144
150
  /** `finishCstBuf`'s two empty sentinels, held here so the node piece can inline it. */
@@ -166,10 +172,20 @@ function linkMatcher(m: readonly [number, string, string, number]): (key: string
166
172
  case 1: return key => key.endsWith(value)
167
173
  case 3: return key => asciiFoldKey(key).startsWith(value)
168
174
  case 4: return key => asciiFoldKey(key).endsWith(value)
169
- // A fresh RegExp per test, as `matchesDispatchMatcher` builds — a cached one
170
- // would carry `lastIndex` across parses whenever the author's pattern was
171
- // sticky or global.
172
- 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
+ }
173
189
  }
174
190
  }
175
191
 
@@ -197,6 +213,36 @@ const NO_COVERAGE = (_id: string): void => {}
197
213
  * `{ value, end }` pair would be an allocation per row.
198
214
  */
199
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
200
246
 
201
247
  /**
202
248
  * ONE NON-FIRST SEQUENCE TERM, in a sequence that carries an adjacency
@@ -204,7 +250,14 @@ type Piece = (input: string, pos: number, ctx: ParseContext) => unknown
204
250
  * `nextTerm`'s protocol, so a bound term and a bound assertion are
205
251
  * interchangeable in the loop and the loop tests neither.
206
252
  */
207
- 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
208
261
 
209
262
  /**
210
263
  * The option set an assembly is specialised for.
@@ -414,6 +467,21 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
414
467
  */
415
468
  const EC = newEndCell()
416
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
+
417
485
  /**
418
486
  * The INSTALLED trivia scanner for the scope currently running.
419
487
  *
@@ -432,6 +500,263 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
432
500
  pushCstLeaf(ctx, lf)
433
501
  }
434
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
+
435
760
  /** Mirrors `exec.ts`'s `rollbackNeeded` — a runtime question about live sinks. */
436
761
  function rollbackNeeded(ctx: ParseContext): boolean {
437
762
  return ctx._cstBuf !== undefined
@@ -561,9 +886,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
561
886
  // keeps the exact branch it had.
562
887
  if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
563
888
  if (needsDeferredTriviaCommit(ctx)) {
564
- const scan = scanTrivia(input, cur, ctx)
565
- scan.commit()
566
- return scan.end
889
+ return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
567
890
  }
568
891
  return advanceTrivia(input, cur, ctx)
569
892
  }
@@ -661,23 +984,23 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
661
984
  TERMV = v
662
985
  return EC.e > scanEnd ? EC.e : cur
663
986
  }
664
- // SCALAR MARKS — no per-term mark object, as `exec.ts` established.
665
- const need = markCst(ctx)
666
- const mRaw = MRAW
667
- const mTl = MTL
668
- const mLv = MLV
669
- const mFl = need ? ctx._fields?.length ?? 0 : 0
670
- const mEr = need ? ctx._errors?.length ?? 0 : 0
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.
989
+ const need = rollbackNeeded(ctx)
990
+ const mTl = need ? cstTlLen(ctx) : 0
671
991
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
672
992
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
673
993
  const scanEnd = skipTrivia(input, cur, ctx)
994
+ const scanTl = need ? cstTlLen(ctx) : 0
995
+ const scanLog = need ? ctx._triviaLog?.length ?? 0 : 0
996
+ const scanRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
674
997
  const v = child(input, scanEnd, ctx)
675
998
  if (v === FAIL) return -1
676
999
  TERMV = v
677
1000
  if (EC.e > scanEnd) return EC.e
678
1001
  // The term matched nothing, so the trivia in front of it was never consumed
679
1002
  // by anything — unrecord it and leave the cursor where it was.
680
- if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1003
+ if (need) rollbackScannedTriviaAt(ctx, mTl, scanTl, mLog, scanLog, mRoot, scanRoot)
681
1004
  return cur
682
1005
  }
683
1006
 
@@ -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()`
@@ -1212,7 +1776,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1212
1776
  }
1213
1777
 
1214
1778
  case OP_SCOPE:
1215
- case OP_SCOPE_CAP: {
1779
+ case OP_SCOPE_CAP:
1780
+ case OP_SCOPE_PLAIN: {
1216
1781
  const ki = code[ip + 1]!
1217
1782
  const scopeTrivia = ki < 0 ? undefined : (trivia[ki] as ParseContext['trivia'])
1218
1783
  const scopeLabels = scopeTrivia?._meta.triviaKindLabels
@@ -1261,7 +1826,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1261
1826
  SCAN = savedScan
1262
1827
  return v
1263
1828
  }
1264
- return scopeRootPolicy(scopePiece, code[ip + 3]!)
1829
+ return scopeRootPolicy(scopePiece, code[ip] === OP_SCOPE_PLAIN ? 0 : code[ip + 3]!)
1265
1830
  }
1266
1831
 
1267
1832
  case OP_ROUTED: {
@@ -1306,6 +1871,21 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1306
1871
  /* ── zero-width ──────────────────────────────────────────────────────── */
1307
1872
 
1308
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
+ }
1309
1889
  const child = link(code[ip + 1]!)
1310
1890
  const xf = fx[code[ip + 2]!] as string[]
1311
1891
  return (input, pos, ctx) => {
@@ -1419,6 +1999,93 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1419
1999
  }
1420
2000
  }
1421
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
+
1422
2089
  case OP_LEAF: {
1423
2090
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1424
2091
  const child = link(code[ip + 2]!)
@@ -1463,36 +2130,60 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1463
2130
  const fused = op === OP_SEQX
1464
2131
  const base = fused ? ip + 3 : ip + 2
1465
2132
  const n = code[fused ? ip + 2 : ip + 1]!
1466
- // Children bound as a plain array of DIRECT references, laid out once.
1467
- const kids: Piece[] = new Array<Piece>(n)
1468
- for (let i = 0; i < n; i++) kids[i] = link(code[base + i]!)
1469
2133
  const wantValues = op !== OP_SEQV
1470
- const fn = fused
1471
- ? 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
1472
2138
  : undefined
1473
2139
 
1474
- /**
1475
- * The generic term loop, for arities the unrolled pieces below do not
1476
- * cover.
1477
- *
1478
- * `exec.ts` open-coded a LIT/RX fast path here to dodge "a JS call frame
1479
- * plus a switch dispatch". Half of that cost is gone by construction — a
1480
- * piece has no switch — and the other half is what TurboFan removes by
1481
- * INLINING a small monomorphic child. Re-emitting the terminal bodies
1482
- * would instead grow this function past the inlining budget, which is the
1483
- * defect being fixed, so they are not re-emitted.
1484
- */
1485
- const runTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1486
- const v0 = kids[0]!(input, pos, ctx)
1487
- if (v0 === FAIL) return -1
1488
- if (values !== undefined) values.push(v0)
1489
- let cur = EC.e
1490
- for (let i = 1; i < n; i++) {
1491
- 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)
1492
2147
  if (cur < 0) return -1
1493
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)
1494
2186
  }
1495
- return cur
1496
2187
  }
1497
2188
 
1498
2189
  /**
@@ -1543,21 +2234,24 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1543
2234
  * sentinel published is the kind of state a later reader will trust.
1544
2235
  */
1545
2236
  if (REC) {
1546
- const syncs: (Combinator<unknown> | undefined)[] = new Array<Combinator<unknown> | undefined>(n)
1547
- for (let i = 0; i < n; i++) syncs[i] = sentinelFor(code[base + n + i]!)
1548
- const runners: TermRunner[] = new Array<TermRunner>(n)
1549
- 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]!)
1550
2241
  const kidIp = code[base + i]!
1551
2242
  if (code[kidIp] !== OP_ADJ) {
1552
- const kid = kids[i]!
1553
- runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1554
- 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
+ }
1555
2248
  }
1556
2249
  const negated = code[kidIp + 1] === 1
1557
2250
  const ki = code[kidIp + 2]!
1558
2251
  const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1559
2252
  const xf = fx[code[kidIp + 3]!] as string[]
1560
- runners[i] = (input, cur, ctx) => {
2253
+ return (input, cur, ctx, inherited) => {
2254
+ ctx._sync = sync ?? inherited
1561
2255
  if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1562
2256
  ctx._fe = cur; ctx._fx = xf
1563
2257
  return -1
@@ -1566,19 +2260,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1566
2260
  return cur
1567
2261
  }
1568
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
1569
2274
  const runSyncTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1570
2275
  const inherited = ctx._sync
1571
- ctx._sync = syncs[0] ?? inherited
1572
- const v0 = kids[0]!(input, pos, ctx)
2276
+ ctx._sync = firstSync ?? inherited
2277
+ const v0 = first(input, pos, ctx)
1573
2278
  if (v0 === FAIL) { ctx._sync = inherited; return -1 }
1574
2279
  if (values !== undefined) values.push(v0)
1575
- let cur = EC.e
1576
- for (let i = 1; i < n; i++) {
1577
- ctx._sync = syncs[i] ?? inherited
1578
- cur = runners[i]!(input, cur, ctx)
1579
- if (cur < 0) { ctx._sync = inherited; return -1 }
1580
- if (values !== undefined) values.push(TERMV)
1581
- }
2280
+ const cur = tail === undefined ? EC.e : tail(input, EC.e, ctx, values, inherited)
2281
+ if (cur < 0) { ctx._sync = inherited; return -1 }
1582
2282
  ctx._sync = inherited
1583
2283
  return cur
1584
2284
  }
@@ -1588,7 +2288,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1588
2288
  const cur = runSyncTerms(input, pos, ctx, values)
1589
2289
  if (cur < 0) return FAIL
1590
2290
  EC.e = cur
1591
- return fn!(values, { start: pos, end: cur })
2291
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1592
2292
  }
1593
2293
  }
1594
2294
  if (wantValues) {
@@ -1611,13 +2311,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1611
2311
  let hasAdj = false
1612
2312
  for (let i = 1; i < n; i++) if (code[code[base + i]!] === OP_ADJ) { hasAdj = true; break }
1613
2313
  if (hasAdj) {
1614
- const runners: TermRunner[] = new Array<TermRunner>(n)
1615
- for (let i = 1; i < n; i++) {
2314
+ const first = link(code[base]!)
2315
+ const runnerAt = (i: number): TermRunner => {
1616
2316
  const kidIp = code[base + i]!
1617
2317
  if (code[kidIp] !== OP_ADJ) {
1618
- const kid = kids[i]!
1619
- runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1620
- continue
2318
+ const kid = link(kidIp)
2319
+ return (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1621
2320
  }
1622
2321
  const negated = code[kidIp + 1] === 1
1623
2322
  const ki = code[kidIp + 2]!
@@ -1627,7 +2326,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1627
2326
  // the interpreter does.
1628
2327
  const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1629
2328
  const xf = fx[code[kidIp + 3]!] as string[]
1630
- runners[i] = (input, cur, ctx) => {
2329
+ return (input, cur, ctx) => {
1631
2330
  if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1632
2331
  ctx._fe = cur; ctx._fx = xf
1633
2332
  return -1
@@ -1636,17 +2335,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1636
2335
  return cur
1637
2336
  }
1638
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)
1639
2349
  const runAdjTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1640
- const v0 = kids[0]!(input, pos, ctx)
2350
+ const v0 = first(input, pos, ctx)
1641
2351
  if (v0 === FAIL) return -1
1642
2352
  if (values !== undefined) values.push(v0)
1643
- let cur = EC.e
1644
- for (let i = 1; i < n; i++) {
1645
- cur = runners[i]!(input, cur, ctx)
1646
- if (cur < 0) return -1
1647
- if (values !== undefined) values.push(TERMV)
1648
- }
1649
- return cur
2353
+ return tail(input, EC.e, ctx, values)
1650
2354
  }
1651
2355
  if (fused) {
1652
2356
  return (input, pos, ctx) => {
@@ -1654,7 +2358,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1654
2358
  const cur = runAdjTerms(input, pos, ctx, values)
1655
2359
  if (cur < 0) return FAIL
1656
2360
  EC.e = cur
1657
- return fn!(values, { start: pos, end: cur })
2361
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1658
2362
  }
1659
2363
  }
1660
2364
  if (wantValues) {
@@ -1692,12 +2396,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1692
2396
  * runs, so there is no arity this fails to lower.
1693
2397
  */
1694
2398
  if (n === 1) {
1695
- const k0 = kids[0]!
2399
+ const k0 = link(code[base]!)
1696
2400
  if (fused) {
1697
2401
  return (input, pos, ctx) => {
1698
2402
  const v = k0(input, pos, ctx)
1699
2403
  if (v === FAIL) return FAIL
1700
- return fn!([v], { start: pos, end: EC.e })
2404
+ return projection === 0 ? v : fn!([v], { start: pos, end: EC.e })
1701
2405
  }
1702
2406
  }
1703
2407
  if (wantValues) {
@@ -1715,7 +2419,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1715
2419
  }
1716
2420
 
1717
2421
  if (n === 2) {
1718
- const k0 = kids[0]!, k1 = kids[1]!
2422
+ const k0 = link(code[base]!), k1 = link(code[base + 1]!)
1719
2423
  if (fused) {
1720
2424
  return (input, pos, ctx) => {
1721
2425
  const v0 = k0(input, pos, ctx)
@@ -1723,7 +2427,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1723
2427
  const cur = nextTerm(k1, input, EC.e, ctx)
1724
2428
  if (cur < 0) return FAIL
1725
2429
  EC.e = cur
1726
- 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 })
1727
2433
  }
1728
2434
  }
1729
2435
  if (wantValues) {
@@ -1747,7 +2453,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1747
2453
  }
1748
2454
 
1749
2455
  if (n === 3) {
1750
- 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]!)
1751
2457
  if (fused) {
1752
2458
  return (input, pos, ctx) => {
1753
2459
  const v0 = k0(input, pos, ctx)
@@ -1758,7 +2464,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1758
2464
  cur = nextTerm(k2, input, cur, ctx)
1759
2465
  if (cur < 0) return FAIL
1760
2466
  EC.e = cur
1761
- 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 })
1762
2471
  }
1763
2472
  }
1764
2473
  if (wantValues) {
@@ -1786,26 +2495,97 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1786
2495
  }
1787
2496
  }
1788
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
+
1789
2569
  if (fused) {
1790
2570
  return (input, pos, ctx) => {
1791
2571
  const values: unknown[] = []
1792
- const cur = runTerms(input, pos, ctx, values)
2572
+ const cur = runDirectTerms(input, pos, ctx, values)
1793
2573
  if (cur < 0) return FAIL
1794
2574
  EC.e = cur
1795
- return fn!(values, { start: pos, end: cur })
2575
+ return projection >= 0 ? values[projection] : fn!(values, { start: pos, end: cur })
1796
2576
  }
1797
2577
  }
1798
2578
  if (wantValues) {
1799
2579
  return (input, pos, ctx) => {
1800
2580
  const values: unknown[] = []
1801
- const cur = runTerms(input, pos, ctx, values)
2581
+ const cur = runDirectTerms(input, pos, ctx, values)
1802
2582
  if (cur < 0) return FAIL
1803
2583
  EC.e = cur
1804
2584
  return values
1805
2585
  }
1806
2586
  }
1807
2587
  return (input, pos, ctx) => {
1808
- const cur = runTerms(input, pos, ctx, undefined)
2588
+ const cur = runDirectTerms(input, pos, ctx, undefined)
1809
2589
  if (cur < 0) return FAIL
1810
2590
  EC.e = cur
1811
2591
  return undefined
@@ -1819,20 +2599,74 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1819
2599
  const n = code[ip + 2]!
1820
2600
  const choiceFx = fx[code[ip + 3]!] as string[]
1821
2601
  const base = ip + 4
1822
- const arms: Piece[] = new Array<Piece>(n)
1823
- for (let i = 0; i < n; i++) arms[i] = link(code[base + i]!)
1824
- // PER-ARM STATIC SETS, resolved once at assembly. An arm the char gate
1825
- // declines to enter still owes its expectation: the interpreter has no
1826
- // such gate, enters the arm, and its opener's failure is what lands in
1827
- // the accumulation.
1828
- const armFx: (string[])[] = new Array<string[]>(n)
1829
- for (let i = 0; i < n; i++) armFx[i] = fx[code[base + n + i]!] as string[]
1830
2602
 
1831
2603
  // THE TWO CHOICE SHAPES ARE TWO PIECES. `exec.ts` tested `table.exclusive`
1832
2604
  // on every choice execution; it is table data, so it selects here.
1833
2605
  if (table.exclusive) {
1834
2606
  const ascii = table.ascii
1835
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)
1836
2670
  // THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
1837
2671
  // ungated arm is one whose class is −1, and `resolveDispatch` clears
1838
2672
  // `exclusive` for exactly those arms (program.ts) — so `table.open` is
@@ -1851,7 +2685,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1851
2685
  }
1852
2686
  if (arm >= 0) {
1853
2687
  ctx._fc = false
1854
- const v = arms[arm]!(input, pos, ctx)
2688
+ const v = selected(arm, input, pos, ctx)
1855
2689
  if (v !== FAIL) return v
1856
2690
  // THE CUT — a committed failure fails the whole choice.
1857
2691
  if (committed(ctx)) return FAIL
@@ -1875,48 +2709,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1875
2709
  // excludes the char at `pos`. `null` means nullable or unmappable, and
1876
2710
  // those arms are always entered.
1877
2711
  const armCls = table.armCls
1878
- const gates: (ResolvedClass | null)[] = new Array<ResolvedClass | null>(n)
1879
- for (let i = 0; i < n; i++) gates[i] = armCls[i] ?? null
1880
-
1881
- /**
1882
- * THE GATE, PRECOMPUTED. One `Uint32Array` load replaces `n` class tests.
1883
- *
1884
- * `exec.ts` asked, per arm per execution, "does this arm's class hold the
1885
- * char at `pos`" — `n` calls into `classHas`, each a bounds check plus a
1886
- * `Uint8Array` load, on 136,760 choice executions per parse of
1887
- * `benchmark.less`. Every one of those answers is a function of the
1888
- * CHARACTER and the TABLE, both of which are known here, so the whole
1889
- * question is answered once at assembly: `cand[c]` is the bitmask of arms
1890
- * that could match `c`, and the loop visits only set bits, IN ORDER, so
1891
- * the source ordering the gate exists to preserve is preserved exactly.
1892
- *
1893
- * Slot 128 is EOF (`lead` returns −1): `classHas` rejects a negative code,
1894
- * so only the always-enter arms are candidates there. Code points ≥ 128
1895
- * keep the per-arm loop — the ranges make a table the wrong shape and
1896
- * astral input is not the hot path.
1897
- *
1898
- * Bounded at 32 arms because the mask is one word. Above that the general
1899
- * loop runs, which is the same code the ≥ 128 path takes, so there is no
1900
- * second implementation to drift.
1901
- */
1902
- const maskable = n <= 32
1903
- let cand: Uint32Array | undefined
1904
- if (maskable) {
1905
- const m = new Uint32Array(129)
1906
- for (let i = 0; i < n; i++) {
1907
- const cls = gates[i]!
1908
- 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 => {
1909
2722
  if (cls === null) {
1910
- for (let c = 0; c < 129; c++) m[c]! |= bit
1911
- continue
2723
+ for (let c = 0; c < 129; c++) mask[c]! |= bit
2724
+ return
1912
2725
  }
1913
- 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
1914
2727
  }
1915
- cand = m
1916
- }
2728
+ addGate(g0, 1); addGate(g1, 2)
2729
+ if (n === 3) addGate(g2, 4)
1917
2730
 
1918
- if (cand !== undefined) {
1919
- const mask = cand
1920
2731
  return (input, pos, ctx) => {
1921
2732
  const c = lead(input, pos)
1922
2733
  const need = markCst(ctx)
@@ -1929,46 +2740,144 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1929
2740
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1930
2741
  let acc: string[] | undefined
1931
2742
  if (c < 128) {
1932
- // `c` is −1 at EOF, which indexes slot 128 — the always-enter arms.
1933
- const bits0 = mask[c < 0 ? 128 : c]!
1934
- let bits = bits0
1935
- // Arms below `prev` have already contributed. Only the FAILURE path
1936
- // advances it, so a matching arm returns without paying for any of
1937
- // this — the skipped arms' sets are read on no other exit.
2743
+ const bits = mask[c < 0 ? 128 : c]!
1938
2744
  let prev = 0
1939
- while (bits !== 0) {
1940
- const i = 31 - Math.clz32(bits & -bits)
1941
- bits &= bits - 1
2745
+ if ((bits & 1) !== 0) {
2746
+ ctx._fc = false
2747
+ const v = a0(input, pos, ctx)
2748
+ if (v !== FAIL) return v
2749
+ prev = 1
2750
+ acc = accSet(ctx._fx, acc)
2751
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2752
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2753
+ }
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) {
1942
2765
  ctx._fc = false
1943
- const v = arms[i]!(input, pos, ctx)
2766
+ const v = a2!(input, pos, ctx)
1944
2767
  if (v !== FAIL) return v
1945
- for (let j = prev; j < i; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
1946
- prev = i + 1
2768
+ if (prev < 1) acc = accSet(e0, acc)
2769
+ if (prev < 2) acc = accSet(e1, acc)
2770
+ prev = 3
1947
2771
  acc = accSet(ctx._fx, acc)
1948
- // A committed arm cuts, and the interpreter reports the
1949
- // accumulation at the ARM's span (choice.ts:170), not at `pos`.
1950
2772
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1951
2773
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1952
2774
  }
1953
- for (let j = prev; j < n; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
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)
1954
2778
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1955
2779
  return FAIL
1956
2780
  }
1957
- for (let i = 0; i < n; i++) {
1958
- const cls = gates[i]!
1959
- 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)) {
1960
2791
  ctx._fc = false
1961
- const v = arms[i]!(input, pos, ctx)
2792
+ const v = a1(input, pos, ctx)
1962
2793
  if (v !== FAIL) return v
1963
2794
  acc = accSet(ctx._fx, acc)
1964
2795
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1965
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)
1966
2807
  }
1967
2808
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1968
2809
  return FAIL
1969
2810
  }
1970
2811
  }
1971
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
+
1972
2881
  return (input, pos, ctx) => {
1973
2882
  const c = lead(input, pos)
1974
2883
  const need = markCst(ctx)
@@ -1979,19 +2888,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1979
2888
  const mEr = need ? ctx._errors?.length ?? 0 : 0
1980
2889
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1981
2890
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1982
- let acc: string[] | undefined
1983
- for (let i = 0; i < n; i++) {
1984
- const cls = gates[i]!
1985
- if (cls !== null && !classHas(cls, c)) { acc = accSet(armFx[i]!, acc); continue }
1986
- ctx._fc = false
1987
- const v = arms[i]!(input, pos, ctx)
1988
- if (v !== FAIL) return v
1989
- acc = accSet(ctx._fx, acc)
1990
- if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1991
- if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1992
- }
1993
- ctx._fe = pos; ctx._fx = acc ?? choiceFx
1994
- return FAIL
2891
+ return general(
2892
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0,
2893
+ )
1995
2894
  }
1996
2895
  }
1997
2896
 
@@ -2105,6 +3004,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2105
3004
  const reportItem = (flags & 4) !== 0
2106
3005
  const itemFx = reportItem ? fx[code[ip + 6]!] as string[] : EMPTY_FX
2107
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))
2108
3019
  // `many()` — the min-0, separator-less repeat — is the only shape that runs
2109
3020
  // its FIRST item through `repItem` and so skips leading trivia. The shape
2110
3021
  // identifies itself, and it is table data, so it is decided here.
@@ -2235,6 +3146,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2235
3146
  const out: unknown[] | undefined = collect ? [] : undefined
2236
3147
  const hasTrivia = ctx.trivia !== undefined
2237
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
2238
3153
  let cur = pos
2239
3154
  let count = 0
2240
3155
  for (;;) {
@@ -2243,6 +3158,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2243
3158
  // the LOOP HEAD. Held to `count >= min` so an under-`min` list still
2244
3159
  // attempts the separator and reports its expected set.
2245
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
2246
3165
  // Per ITERATION, never hoisted: a preceding item's `node()` opened and
2247
3166
  // closed a capture buffer, so `ctx._cstBuf` is not the object it was
2248
3167
  // at the loop head. `needMark` is the pre-existing loop-invariant.
@@ -2279,6 +3198,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2279
3198
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
2280
3199
  break
2281
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
+ }
2282
3206
  ctx._fc = false
2283
3207
  const v = child(input, itemStart, ctx)
2284
3208
  if (v === FAIL) {
@@ -2316,69 +3240,69 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2316
3240
  const otherRouted = code[ip + 4]! === 1
2317
3241
  const n = code[ip + 5]!
2318
3242
  const armBase = ip + 6
2319
- const arms: Piece[] = new Array<Piece>(n)
2320
- for (let i = 0; i < n; i++) arms[i] = link(code[armBase + i]!)
2321
3243
  const byKey = spec.byKey
2322
3244
  const byFold = spec.byFold
2323
3245
  const hasFold = spec.byFold.size > 0
2324
3246
  const match = spec.match
2325
3247
  const matchN = match.length
2326
- const matchFn: Array<(key: string) => boolean> = new Array<(key: string) => boolean>(matchN)
2327
- const matchArm: number[] = new Array<number>(matchN)
2328
- for (let i = 0; i < matchN; i++) { matchFn[i] = linkMatcher(match[i]!); matchArm[i] = match[i]![3] }
2329
3248
  const routed = spec.routed
2330
3249
  const expected = spec.expected as string[]
2331
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
+
2332
3289
  return (input, pos, ctx) => {
2333
3290
  const selectorMark = saveTriviaMark(ctx)
2334
3291
  const selVal = selector(input, pos, ctx)
2335
3292
  if (selVal === FAIL) return FAIL
2336
3293
  const selEnd = EC.e
2337
3294
  const key = selVal as string
2338
-
2339
- let arm = byKey.get(key)
2340
- if (arm === undefined && hasFold) arm = byFold.get(asciiFoldKey(key))
2341
- if (arm === undefined) {
2342
- for (let i = 0; i < matchN; i++) {
2343
- if (matchFn[i]!(key)) {
2344
- arm = matchArm[i]!
2345
- break
2346
- }
2347
- }
2348
- }
2349
-
2350
- let target: Piece
2351
- let usesRouted: boolean
3295
+ const arm = classify(key)
2352
3296
  if (arm === undefined) {
2353
- if (other === undefined) {
3297
+ if (runOther === undefined) {
2354
3298
  // No branch and no fallback: fail AT THE SELECTOR'S END.
2355
3299
  ctx._fe = selEnd
2356
3300
  ctx._fx = expected
2357
3301
  return FAIL
2358
3302
  }
2359
- target = other
2360
- usesRouted = otherRouted
2361
- } else {
2362
- target = arms[arm]!
2363
- usesRouted = routed[arm] === 1
2364
- }
2365
-
2366
- const savedRouted = ctx._routed
2367
- let mark = saveTriviaMark(ctx)
2368
- if (usesRouted) {
2369
- rollbackTrivia(ctx, selectorMark)
2370
- mark = saveTriviaMark(ctx)
2371
- ctx._routed = { value: key, span: { start: pos, end: selEnd } }
3303
+ return runOther(input, pos, selEnd, key, selectorMark, ctx)
2372
3304
  }
2373
- const v = target(input, usesRouted ? pos : selEnd, ctx)
2374
- if (usesRouted) ctx._routed = savedRouted
2375
- if (v === FAIL) {
2376
- rollbackTrivia(ctx, mark)
2377
- // A failed dispatch branch is COMMITTED: the selector already matched.
2378
- ctx._fc = true
2379
- return FAIL
2380
- }
2381
- return [key, v]
3305
+ return runArm!(arm, input, pos, selEnd, key, selectorMark, ctx)
2382
3306
  }
2383
3307
  }
2384
3308
 
@@ -2387,6 +3311,41 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2387
3311
  case OP_NODE:
2388
3312
  case OP_NODE_TRACK: {
2389
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
+ }
2390
3349
  const child = link(code[ip + 2]!)
2391
3350
  const proj = code[ip + 4]!
2392
3351
  const buildIdx = code[ip + 1]!
@@ -2405,12 +3364,34 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2405
3364
  // 145,512 executions per parse of `benchmark.less`. `cstOutputHost(ctx.build)`
2406
3365
  // is fixed by `run()` before the entry is called, so it selects the piece.
2407
3366
  const build = buildIdx >= 0
2408
- ? fns[buildIdx] as (
2409
- children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
2410
- rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
2411
- ) => unknown
3367
+ ? fns[buildIdx] as NodeBuilder
2412
3368
  : undefined
2413
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
+
2414
3395
  // A STRUCTURAL node — no builder, no projection — is the only shape that
2415
3396
  // takes a per-node-type trivia-kind mask off the host (`node.ts:260`).
2416
3397
  // Assembly-time, so a node with a reducer pays nothing for the question.
@@ -2577,10 +3558,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2577
3558
  * Emitted source is attempted FIRST, and the closure walk above runs only for
2578
3559
  * what it refuses.
2579
3560
  *
2580
- * The reason it has to be source at all is in `emit-assembly.ts`'s header:
2581
- * V8's inline-cache feedback is per FUNCTION LITERAL, so every piece minted
2582
- * from one literal here shares one feedback vector and every call site inside
2583
- * 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.
2584
3566
  *
2585
3567
  * The refusal is RECORDED, not swallowed. A grammar that silently drops to
2586
3568
  * the closure path is a permanently slow path nobody would ever find, which
@@ -2602,6 +3584,24 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2602
3584
  if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
2603
3585
  }
2604
3586
  for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
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)
3604
+ }
2605
3605
  /**
2606
3606
  * THE BUILD'S OWN ANSWER, TRIED BEFORE THE CONSTRUCTOR.
2607
3607
  *
@@ -2625,18 +3625,19 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2625
3625
  ? prog.asm?.find(a => a.key === cfgKey(cfg))
2626
3626
  : undefined
2627
3627
  if (pre !== undefined) {
2628
- const pools = rebuildPools(t.cc, t.fx, pre.plan)
3628
+ const pools = rebuildPools(t.cc, t.fx, t.disp, pre.plan)
2629
3629
  emitted = pre.factory(
2630
3630
  EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
2631
3631
  trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
2632
3632
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
2633
- cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, failAt,
3633
+ cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
2634
3634
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
2635
3635
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
2636
3636
  scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
2637
3637
  asciiFoldKey, ROUTED_FX,
2638
3638
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2639
3639
  matchesAt, recoverScan, orSentinel, captureError,
3640
+ scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
2640
3641
  )
2641
3642
  emitReached = new Set(pre.reached)
2642
3643
  }
@@ -2705,7 +3706,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2705
3706
  EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia,
2706
3707
  trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
2707
3708
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
2708
- cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, failAt,
3709
+ cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
2709
3710
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
2710
3711
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
2711
3712
  scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
@@ -2717,6 +3718,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2717
3718
  // object rather than to two separately constructed ones.
2718
3719
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2719
3720
  matchesAt, recoverScan, orSentinel, captureError,
3721
+ scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
2720
3722
  )
2721
3723
  emitReached = em.reached
2722
3724
  } catch (e) {