parseman 0.56.0 → 0.58.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 (106) hide show
  1. package/dist/analysis/diagnostics.js +2232 -1088
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +2247 -1103
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/combinators/node.d.ts +1 -1
  7. package/dist/compiler/build-arity.d.ts +1 -1
  8. package/dist/compiler/build-arity.d.ts.map +1 -1
  9. package/dist/compiler/token-alphabet.d.ts +11 -1
  10. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  11. package/dist/compiler/token-capability.js +69 -16
  12. package/dist/compiler/token-capability.js.map +2 -2
  13. package/dist/cst/capture-buffer.d.ts +6 -0
  14. package/dist/cst/capture-buffer.d.ts.map +1 -1
  15. package/dist/cst/trivia-kinds.d.ts +9 -1
  16. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  17. package/dist/index.js +1446 -236
  18. package/dist/index.js.map +3 -3
  19. package/dist/language-service/index.js +1 -0
  20. package/dist/language-service/index.js.map +1 -1
  21. package/dist/parse-context.d.ts.map +1 -1
  22. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  23. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  24. package/dist/plugin/index.d.ts +51 -1
  25. package/dist/plugin/index.d.ts.map +1 -1
  26. package/dist/plugin/index.js +2126 -380
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/plugin/ir-eval.d.ts +2 -0
  29. package/dist/plugin/ir-eval.d.ts.map +1 -1
  30. package/dist/regex/classes.d.ts.map +1 -1
  31. package/dist/run/index.js +1 -0
  32. package/dist/run/index.js.map +1 -1
  33. package/dist/table/assemble.d.ts +9 -0
  34. package/dist/table/assemble.d.ts.map +1 -1
  35. package/dist/table/child-slots.d.ts +4 -6
  36. package/dist/table/child-slots.d.ts.map +1 -1
  37. package/dist/table/compile-rule-map.d.ts +14 -2
  38. package/dist/table/compile-rule-map.d.ts.map +1 -1
  39. package/dist/table/emit-assembly.d.ts +17 -2
  40. package/dist/table/emit-assembly.d.ts.map +1 -1
  41. package/dist/table/emit.d.ts +5 -1
  42. package/dist/table/emit.d.ts.map +1 -1
  43. package/dist/table/encode.d.ts.map +1 -1
  44. package/dist/table/exec-baseline.d.ts.map +1 -1
  45. package/dist/table/exec.d.ts.map +1 -1
  46. package/dist/table/factor-choice.d.ts +42 -0
  47. package/dist/table/factor-choice.d.ts.map +1 -0
  48. package/dist/table/fail-clean.d.ts +3 -0
  49. package/dist/table/fail-clean.d.ts.map +1 -0
  50. package/dist/table/hold-gap.d.ts +8 -0
  51. package/dist/table/hold-gap.d.ts.map +1 -0
  52. package/dist/table/index.js +1798 -316
  53. package/dist/table/index.js.map +3 -3
  54. package/dist/table/lex-program.d.ts +7 -3
  55. package/dist/table/lex-program.d.ts.map +1 -1
  56. package/dist/table/ops.d.ts +17 -6
  57. package/dist/table/ops.d.ts.map +1 -1
  58. package/dist/table/program.d.ts +13 -0
  59. package/dist/table/program.d.ts.map +1 -1
  60. package/dist/table/run-support.d.ts +19 -0
  61. package/dist/table/run-support.d.ts.map +1 -1
  62. package/dist/table/scalar-terminal.d.ts +2 -0
  63. package/dist/table/scalar-terminal.d.ts.map +1 -1
  64. package/dist/table/select-hot.d.ts +6 -0
  65. package/dist/table/select-hot.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts +66 -0
  67. package/dist/table/site-labels.d.ts.map +1 -1
  68. package/dist/table/trivia-gap.d.ts +31 -0
  69. package/dist/table/trivia-gap.d.ts.map +1 -0
  70. package/dist/types.d.ts +17 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/version.d.ts +1 -1
  73. package/package.json +4 -1
  74. package/src/combinators/dispatch.ts +12 -3
  75. package/src/combinators/node.ts +6 -13
  76. package/src/combinators/trivia-skip.ts +44 -1
  77. package/src/compiler/build-arity.ts +3 -3
  78. package/src/compiler/token-alphabet.ts +57 -17
  79. package/src/cst/capture-buffer.ts +25 -0
  80. package/src/cst/trivia-kinds.ts +12 -1
  81. package/src/parse-context.ts +1 -0
  82. package/src/plugin/entry-supercompiler.ts +8 -9
  83. package/src/plugin/index.ts +362 -42
  84. package/src/plugin/ir-eval.ts +7 -1
  85. package/src/regex/classes.ts +3 -0
  86. package/src/table/assemble.ts +371 -83
  87. package/src/table/child-slots.ts +6 -8
  88. package/src/table/compile-rule-map.ts +64 -15
  89. package/src/table/emit-assembly.ts +244 -42
  90. package/src/table/emit.ts +11 -8
  91. package/src/table/encode.ts +149 -35
  92. package/src/table/exec-baseline.ts +1 -3
  93. package/src/table/exec.ts +34 -14
  94. package/src/table/factor-choice.ts +173 -0
  95. package/src/table/fail-clean.ts +130 -0
  96. package/src/table/hold-gap.ts +68 -0
  97. package/src/table/lex-program.ts +197 -43
  98. package/src/table/ops.ts +18 -7
  99. package/src/table/program.ts +24 -0
  100. package/src/table/run-support.ts +35 -0
  101. package/src/table/scalar-terminal.ts +8 -0
  102. package/src/table/select-hot.ts +70 -0
  103. package/src/table/site-labels.ts +135 -4
  104. package/src/table/trivia-gap.ts +109 -0
  105. package/src/types.ts +18 -0
  106. package/src/version.ts +1 -1
@@ -64,18 +64,19 @@
64
64
  * the three-way identity sweep gates this against it.
65
65
  */
66
66
  import { rebuildLineScan } from './line-scan.ts'
67
- import { scanRefs } from './program.ts'
67
+ import { assemblyExtraIps } from './program.ts'
68
68
  import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
69
69
  import { balanced, scanTo } from '../combinators/scanTo.ts'
70
70
  import { buildFieldMap } from '../compiler/fields.ts'
71
71
  import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
72
+ import { skipDeferredTrivia } from './trivia-gap.ts'
72
73
  import { asciiFoldKey } from '../combinators/dispatch.ts'
73
74
  import { projectChild, unwrapChild } from '../combinators/node.ts'
74
75
  import { cstOutputHost } from '../compiler/build-arity.ts'
75
76
  import { consumeTrivia } from '../combinators/trivia-skip.ts'
76
77
  import {
77
- advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTrivia, rollbackTriviaAt,
78
- saveTriviaMark, scanTrivia, scanTriviaCompact, skipTriviaScanned, type FastTriviaScanner,
78
+ advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTriviaAt,
79
+ scanTrivia, scanTriviaCompact, skipTriviaScanned, type FastTriviaScanner,
79
80
  } from '../combinators/trivia-skip.ts'
80
81
  import {
81
82
  cstCaptureActive, cstLeavesLen, cstTlLen,
@@ -89,7 +90,7 @@ import {
89
90
  OP_LIVE,
90
91
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
91
92
  OP_COV,
92
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
93
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
93
94
  OP_LEX_BODY, OP_LEX_PROGRAM,
94
95
  } from './ops.ts'
95
96
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
@@ -106,7 +107,9 @@ import {
106
107
  EMITTED_PARAMS, Unemittable, emitAssemblySource, rebuildPools,
107
108
  type EmittedAssembly, type EmittedFactory,
108
109
  } from './emit-assembly.ts'
109
- import { lead, rawEntry, spanLines } from './run-support.ts'
110
+ import { collapseEmpty, lead, rawEntry, spanLines } from './run-support.ts'
111
+ import { holdsGap, rowNullable } from './hold-gap.ts'
112
+ import { failCleanAnalysis } from './fail-clean.ts'
110
113
  /**
111
114
  * THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
112
115
  *
@@ -122,18 +125,18 @@ import { lead, rawEntry, spanLines } from './run-support.ts'
122
125
  * property read per terminal miss — the same price codegen pays.
123
126
  */
124
127
  import {
125
- classHas, decodeClassSpec, expandCompact, resolveTable,
128
+ classHas, decodeClassSpec, encodeClassSpec, expandCompact, ownTableProgram, resolveClass, resolveTable,
126
129
  validateDispatchSpec,
127
130
  type CompactProgram, type ResolvedClass, type ResolvedTable,
128
131
  type SubtreeRef, type TableProgram, type TableRule,
129
132
  } from './program.ts'
130
133
  import { stampRuleMap } from './stamp.ts'
131
- import { computeSiteLabels, reachableSites } from './site-labels.ts'
134
+ import { computeSiteLabels, gatedPassCollapse, passCollapse, reachableSites, scanHandoffNeeded, scopeIsIdentity, triviaLeadRanges } from './site-labels.ts'
132
135
  import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
133
136
  import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
134
137
  import {
135
138
  largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal, makeScalarRecognizer, scalarTerminalNodeChild,
136
- scalarTerminalNotChild, type ScalarRecognizer,
139
+ scalarTerminalNotChild, scalarTerminalPeekChild, type ScalarRecognizer,
137
140
  } from './scalar-terminal.ts'
138
141
 
139
142
  /**
@@ -240,13 +243,14 @@ type GeneralChoiceBlock = (
240
243
  type ExclusiveChoiceBlock = (arm: number, input: string, pos: number, ctx: ParseContext) => unknown
241
244
  type DispatchMatcher = (key: string) => boolean
242
245
  type DispatchMatcherBlock = (key: string) => number | undefined
246
+ /** The selector's rollback mark rides as the eight scalars a choice block takes. */
243
247
  type DispatchBranch = (
244
- input: string, pos: number, selEnd: number, key: string,
245
- selectorMark: ReturnType<typeof saveTriviaMark>, ctx: ParseContext,
248
+ input: string, pos: number, selEnd: number, key: string, ctx: ParseContext,
249
+ need: boolean, mRaw: number, mTl: number, mLv: number, mFl: number, mEr: number, mLog: number, mRoot: number,
246
250
  ) => unknown
247
251
  type DispatchArmBlock = (
248
- arm: number, input: string, pos: number, selEnd: number, key: string,
249
- selectorMark: ReturnType<typeof saveTriviaMark>, ctx: ParseContext,
252
+ arm: number, input: string, pos: number, selEnd: number, key: string, ctx: ParseContext,
253
+ need: boolean, mRaw: number, mTl: number, mLv: number, mFl: number, mEr: number, mLog: number, mRoot: number,
250
254
  ) => unknown
251
255
 
252
256
  /**
@@ -440,6 +444,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
440
444
  * reference.
441
445
  */
442
446
  const REC = prog.rec === 1 && cfg.tolerant
447
+ /** A completions-probe assembly: lookaheads restore `_probe.best` around their child. */
448
+ const PROBE = cfg.probe
443
449
  /**
444
450
  * The sync sentinel for a char-class index, built ONCE at assembly.
445
451
  *
@@ -477,6 +483,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
477
483
  // Value/CST/failure/cursor effects stay in those consumers.
478
484
  const scalarRecognizers: Array<ScalarRecognizer | undefined> = []
479
485
  const rawScalarSpecs = new Set<number>()
486
+ /**
487
+ * The class of an `optional`/`not`/`peek` child's first-character gate
488
+ * (`encode.ts` `lookaheadGate`), tested before the wrapper takes its mark.
489
+ * `null` where there is none, or where a probe or tolerant parse forwards
490
+ * through the gate (`OP_GATE` below).
491
+ */
492
+ function lookaheadClass(child: number): ResolvedClass | null {
493
+ return code[child] === OP_GATE && !cfg.tolerant && !cfg.probe ? cc[code[child + 1]!]! : null
494
+ }
480
495
  function scalarFor(child: number): ScalarRecognizer | undefined {
481
496
  const spec = code[child + 1]!
482
497
  let recognize = scalarRecognizers[spec]
@@ -487,9 +502,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
487
502
  return recognize
488
503
  }
489
504
 
490
- const labelExtraIps: number[] = []
491
- for (const s of prog.scans ?? []) for (const r of scanRefs(s)) labelExtraIps.push(r[0])
492
- for (const set of prog.scanSkip ?? []) for (const r of set) labelExtraIps.push(r[0])
505
+ const labelExtraIps = assemblyExtraIps(prog)
493
506
  const closureLabels = computeSiteLabels(
494
507
  code, [...Object.values(prog.rules), ...labelExtraIps], hostCst,
495
508
  )
@@ -504,8 +517,29 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
504
517
  */
505
518
  let SCAN: FastTriviaScanner | null = null
506
519
 
520
+ /**
521
+ * A SELECTIVE build-time assembly: emitted bodies for its selected sites, this
522
+ * engine for the rest. `link` hands out its exported sites, wrapped so the
523
+ * callee sees this engine's installed scanner — the one slot both engines keep
524
+ * separately (the end cell `EC` is already shared).
525
+ */
526
+ let HYBRID: EmittedAssembly | undefined
527
+ function hybridPiece(piece: Piece): Piece {
528
+ const setScan = HYBRID!.scan!
529
+ return (input, pos, ctx) => {
530
+ const saved = setScan(SCAN)
531
+ const value = piece(input, pos, ctx)
532
+ setScan(saved)
533
+ return value
534
+ }
535
+ }
536
+
507
537
  const memo = new Map<number, Piece>()
508
538
  const reached = new Set<number>()
539
+ /** Row nullability for `holdsGap`, memoised for this assembly. */
540
+ const NULLABLE = new Map<number, boolean>()
541
+ /** A child whose failure writes nothing needs no rollback mark (`fail-clean.ts`). */
542
+ const CLEAN = REC ? (): boolean => false : failCleanAnalysis(code)
509
543
 
510
544
  function pushLeaf(ctx: ParseContext, value: string, s: number, e: number): void {
511
545
  const lf: Leaf = { _tag: 'leaf', value, span: { start: s, end: e } }
@@ -739,6 +773,36 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
739
773
  }
740
774
  }
741
775
 
776
+ /**
777
+ * A collapse over one node is that node — `run-support.ts` `collapseEmpty` has
778
+ * the contract, and the one case the node's value cannot stand for itself.
779
+ */
780
+ function passCollapseNode(child: Piece, build: NodeBuilder, readsRaw: boolean): Piece {
781
+ return (input, pos, ctx) => {
782
+ const v = child(input, pos, ctx)
783
+ if (v !== undefined) return v
784
+ return collapseEmpty(ctx, build, input, pos, EC.e, readsRaw)
785
+ }
786
+ }
787
+
788
+ /**
789
+ * A collapse whose body is `optional`/`not` terms and then one node: where the
790
+ * character at `pos` opens none of those terms and no trivia, the body is that
791
+ * node (`site-labels.ts` `gatedPassCollapse`), so it runs as a pass-through
792
+ * collapse; anywhere else the node keeps its frame.
793
+ */
794
+ function gatedPassCollapseNode(
795
+ blockers: readonly ResolvedClass[], last: Piece, classic: Piece, build: NodeBuilder, readsRaw: boolean,
796
+ ): Piece {
797
+ return (input, pos, ctx) => {
798
+ const c = lead(input, pos)
799
+ for (let i = 0; i < blockers.length; i++) if (classHas(blockers[i]!, c)) return classic(input, pos, ctx)
800
+ const v = last(input, pos, ctx)
801
+ if (v !== undefined) return v
802
+ return collapseEmpty(ctx, build, input, pos, EC.e, readsRaw)
803
+ }
804
+ }
805
+
742
806
  function plainProjectNode(child: Piece, projection: number, type: string): Piece {
743
807
  return (input, pos, ctx) => {
744
808
  const sCh = ctx._cstChildren
@@ -906,9 +970,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
906
970
  // Appended rather than folded into the test above so the non-capturing path
907
971
  // keeps the exact branch it had.
908
972
  if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
909
- if (needsDeferredTriviaCommit(ctx)) {
910
- return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
911
- }
973
+ if (needsDeferredTriviaCommit(ctx)) return skipDeferredTrivia(input, cur, ctx)
912
974
  return advanceTrivia(input, cur, ctx)
913
975
  }
914
976
 
@@ -1159,7 +1221,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1159
1221
  if (next !== undefined) {
1160
1222
  return next(input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
1161
1223
  }
1162
- if (best === pos) acc = through(total, prev, acc)
1224
+ // No arm entered (`prev` 0, `acc` unset): the catch-up would rebuild
1225
+ // `choiceFx`, the arms' flatMap, set by set. It IS the answer.
1226
+ if (best === pos && prev !== 0) acc = through(total, prev, acc)
1163
1227
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1164
1228
  return FAIL
1165
1229
  }
@@ -1228,7 +1292,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1228
1292
  if (next !== undefined) {
1229
1293
  return next(input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
1230
1294
  }
1231
- if (best === pos) acc = through(total, prev, acc)
1295
+ // No arm entered (`prev` 0, `acc` unset): the catch-up would rebuild
1296
+ // `choiceFx`, the arms' flatMap, set by set. It IS the answer.
1297
+ if (best === pos && prev !== 0) acc = through(total, prev, acc)
1232
1298
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1233
1299
  return FAIL
1234
1300
  }
@@ -1264,21 +1330,30 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1264
1330
 
1265
1331
  function dispatchBranch(child: Piece, usesRouted: boolean): DispatchBranch {
1266
1332
  if (!usesRouted) {
1267
- return (input, _pos, selEnd, key, _selectorMark, ctx) => {
1268
- const mark = saveTriviaMark(ctx)
1333
+ // The arm runs after the selector's match, so it marks where it starts.
1334
+ return (input, _pos, selEnd, key, ctx) => {
1335
+ const need = markCst(ctx)
1336
+ const mRaw = MRAW
1337
+ const mTl = MTL
1338
+ const mLv = MLV
1339
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1340
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1341
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1342
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1269
1343
  const v = child(input, selEnd, ctx)
1270
1344
  if (v === FAIL) {
1271
- rollbackTrivia(ctx, mark)
1345
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1272
1346
  ctx._fc = true
1273
1347
  return FAIL
1274
1348
  }
1275
1349
  return [key, v]
1276
1350
  }
1277
1351
  }
1278
- return (input, pos, selEnd, key, selectorMark, ctx) => {
1352
+ // A routed arm re-reads the selector's match from `pos`: undo the selector,
1353
+ // which leaves every sink at the selector's mark, so that mark is the arm's too.
1354
+ return (input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot) => {
1279
1355
  const savedRouted = ctx._routed
1280
- rollbackTrivia(ctx, selectorMark)
1281
- const mark = saveTriviaMark(ctx)
1356
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1282
1357
  ctx._routed = { value: key, span: { start: pos, end: selEnd } }
1283
1358
  let v: unknown
1284
1359
  try {
@@ -1287,7 +1362,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1287
1362
  ctx._routed = savedRouted
1288
1363
  }
1289
1364
  if (v === FAIL) {
1290
- rollbackTrivia(ctx, mark)
1365
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1291
1366
  ctx._fc = true
1292
1367
  return FAIL
1293
1368
  }
@@ -1300,12 +1375,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1300
1375
  a0: DispatchBranch, a1: DispatchBranch, a2: DispatchBranch, a3: DispatchBranch,
1301
1376
  next: DispatchArmBlock | undefined,
1302
1377
  ): DispatchArmBlock {
1303
- return (arm, input, pos, selEnd, key, selectorMark, ctx) => {
1304
- if (arm === start) return a0(input, pos, selEnd, key, selectorMark, ctx)
1305
- if (arm === start + 1) return a1(input, pos, selEnd, key, selectorMark, ctx)
1306
- if (arm === start + 2) return a2(input, pos, selEnd, key, selectorMark, ctx)
1307
- if (arm === start + 3) return a3(input, pos, selEnd, key, selectorMark, ctx)
1308
- return next === undefined ? FAIL : next(arm, input, pos, selEnd, key, selectorMark, ctx)
1378
+ return (arm, input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot) => {
1379
+ if (arm === start) return a0(input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1380
+ if (arm === start + 1) return a1(input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1381
+ if (arm === start + 2) return a2(input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1382
+ if (arm === start + 3) return a3(input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1383
+ return next === undefined
1384
+ ? FAIL
1385
+ : next(arm, input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1309
1386
  }
1310
1387
  }
1311
1388
 
@@ -1339,22 +1416,29 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1339
1416
  if (done !== undefined) return done
1340
1417
  const flight = inFlight.get(ip)
1341
1418
  if (flight !== undefined) return flight.fwd
1419
+ const selected = HYBRID?.byIp[ip]
1420
+ if (selected !== undefined) {
1421
+ // The scanner handoff only where the emitted body reads the slot
1422
+ // (`scanHandoffNeeded`); elsewhere the body runs on its labels' scanners.
1423
+ const piece = scanHandoffNeeded(code, ip, closureLabels) ? hybridPiece(selected as Piece) : selected as Piece
1424
+ memo.set(ip, piece)
1425
+ reached.add(ip)
1426
+ return piece
1427
+ }
1342
1428
 
1343
1429
  let target: Piece | undefined
1344
1430
  const fwd: Piece = (input, pos, ctx) => target!(input, pos, ctx)
1345
1431
  const holder = { fwd, set: (p: Piece) => { target = p } }
1346
1432
  inFlight.set(ip, holder)
1347
1433
 
1348
- const op = code[ip]
1349
- // Match the emitted assembly's label-proven scope alias: rule entries in a
1350
- // single ambient trivia scope otherwise reinstall and restore the exact
1351
- // same trivia, labels and scanner on every call. Install the forwarding
1434
+ // Match the emitted assembly's label-proven scope alias (`scopeIsIdentity`):
1435
+ // rule entries in a single ambient trivia scope, and no-trivia scopes inside
1436
+ // a token, otherwise reinstall and restore the exact same trivia, labels and
1437
+ // scanner on every call. Install the forwarding
1352
1438
  // holder first so a recursive child can still link back through this site.
1353
- const piece = (op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[ip + 3]! === 0))
1354
- && code[ip + 1]! >= 0
1355
- && closureLabels.at(ip).tri === code[ip + 1]!
1439
+ const piece = scopeIsIdentity(code, ip, closureLabels.at(ip))
1356
1440
  ? link(code[ip + 2]!)
1357
- : lower(ip)
1441
+ : countedPiece(lookaheadLedNode(ip) ?? lower(ip), ip)
1358
1442
 
1359
1443
  inFlight.delete(ip)
1360
1444
  holder.set(piece)
@@ -1363,6 +1447,48 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1363
1447
  return piece
1364
1448
  }
1365
1449
 
1450
+ /**
1451
+ * A NODE WHOSE BODY OPENS WITH A LOOKAHEAD opens its frame after it. The body
1452
+ * is a sequence whose first term is `not`/`peek` of a direct terminal: it is
1453
+ * decided at `pos` without writing anything, and a node that fails there would
1454
+ * only open its frame to drop it. So the recognizer runs first, and the node
1455
+ * runs on success with that term already decided (`FIRST_DECIDED`). The
1456
+ * recognizer runs once on either path; the failure is the term's own.
1457
+ */
1458
+ let FIRST_DECIDED: Piece | undefined
1459
+ let NODE_BODY: Piece | undefined
1460
+ const DECIDED: Piece = (_input, pos) => { EC.e = pos; return null }
1461
+ function lookaheadLedNode(ip: number): Piece | undefined {
1462
+ const op = code[ip]
1463
+ if ((op !== OP_NODE && op !== OP_NODE_TRACK) || hostCst || REC || cfg.probe || cfg.coverage || cfg.trackLines) return undefined
1464
+ if (passCollapse(code, ip, hostCst) || gatedPassCollapse(code, ip, hostCst) !== undefined) return undefined
1465
+ const body = code[ip + 2]!
1466
+ const bop = code[body]
1467
+ if (bop !== OP_SEQ && bop !== OP_SEQV && bop !== OP_SEQX) return undefined
1468
+ const base = bop === OP_SEQX ? body + 3 : body + 2
1469
+ if (code[bop === OP_SEQX ? body + 2 : body + 1]! < 2) return undefined
1470
+ const first = code[base]!
1471
+ const negative = code[first] === OP_NOT
1472
+ const terminal = negative ? scalarTerminalNotChild(code, first)
1473
+ : code[first] === OP_PEEK ? scalarTerminalPeekChild(code, first) : -1
1474
+ if (terminal < 0) return undefined
1475
+ const recognize = scalarFor(terminal)!
1476
+ const xf = fx[code[first + 2]!] as string[]
1477
+ FIRST_DECIDED = DECIDED
1478
+ NODE_BODY = lower(body)
1479
+ const node = lower(ip)
1480
+ NODE_BODY = undefined
1481
+ return negative
1482
+ ? (input, pos, ctx) => {
1483
+ if (recognize(input, pos) >= 0) { ctx._fe = pos; ctx._fx = xf; EC.e = pos; return FAIL }
1484
+ return node(input, pos, ctx)
1485
+ }
1486
+ : (input, pos, ctx) => {
1487
+ if (recognize(input, pos) < 0) { ctx._fe = pos; ctx._fx = xf; return FAIL }
1488
+ return node(input, pos, ctx)
1489
+ }
1490
+ }
1491
+
1366
1492
  function lower(ip: number): Piece {
1367
1493
  const op = code[ip]
1368
1494
  switch (op) {
@@ -1996,7 +2122,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1996
2122
  }
1997
2123
  const child = link(code[ip + 1]!)
1998
2124
  const xf = fx[code[ip + 2]!] as string[]
2125
+ const gate = lookaheadClass(code[ip + 1]!)
1999
2126
  return (input, pos, ctx) => {
2127
+ if (gate !== null && !classHas(gate, lead(input, pos))) { EC.e = pos; return null }
2000
2128
  const need = markCst(ctx)
2001
2129
  const mRaw = MRAW
2002
2130
  const mTl = MTL
@@ -2005,8 +2133,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2005
2133
  const mEr = need ? ctx._errors?.length ?? 0 : 0
2006
2134
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2007
2135
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2136
+ // A completions probe sees nothing raised inside a lookahead
2137
+ // (`not.ts`, `rollbackLookahead`): those tokens are not reachable here.
2138
+ const probeBest = PROBE ? ctx._probe?.best : undefined
2008
2139
  const v = child(input, pos, ctx)
2009
2140
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2141
+ if (PROBE && ctx._probe !== undefined) ctx._probe.best = probeBest ?? null
2010
2142
  if (v === FAIL) { EC.e = pos; return null }
2011
2143
  // `not.ts:50` — the ASSERTION's own set, at the assertion's position.
2012
2144
  ctx._fe = pos
@@ -2016,9 +2148,27 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2016
2148
  }
2017
2149
 
2018
2150
  case OP_PEEK: {
2151
+ // A direct terminal decides the lookahead by its match alone: no mark,
2152
+ // no leaf to capture and roll back (`scalarTerminalPeekChild`).
2153
+ const scalarChild = scalarTerminalPeekChild(code, ip)
2154
+ if (scalarChild >= 0) {
2155
+ const recognize = scalarFor(scalarChild)!
2156
+ const xf = fx[code[ip + 2]!] as string[]
2157
+ return (input, pos, ctx) => {
2158
+ if (recognize(input, pos) < 0) {
2159
+ ctx._fe = pos
2160
+ ctx._fx = xf
2161
+ return FAIL
2162
+ }
2163
+ EC.e = pos
2164
+ return null
2165
+ }
2166
+ }
2019
2167
  const child = link(code[ip + 1]!)
2020
2168
  const xf = fx[code[ip + 2]!] as string[]
2169
+ const gate = lookaheadClass(code[ip + 1]!)
2021
2170
  return (input, pos, ctx) => {
2171
+ if (gate !== null && !classHas(gate, lead(input, pos))) { ctx._fe = pos; ctx._fx = xf; return FAIL }
2022
2172
  const need = markCst(ctx)
2023
2173
  const mRaw = MRAW
2024
2174
  const mTl = MTL
@@ -2027,8 +2177,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2027
2177
  const mEr = need ? ctx._errors?.length ?? 0 : 0
2028
2178
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2029
2179
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2180
+ const probeBest = PROBE ? ctx._probe?.best : undefined
2030
2181
  const v = child(input, pos, ctx)
2031
2182
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2183
+ if (PROBE && ctx._probe !== undefined) ctx._probe.best = probeBest ?? null
2032
2184
  // `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
2033
2185
  if (v === FAIL) { ctx._fe = pos; ctx._fx = xf; return FAIL }
2034
2186
  EC.e = pos
@@ -2038,8 +2190,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2038
2190
 
2039
2191
  case OP_OPT: {
2040
2192
  const child = link(code[ip + 1]!)
2193
+ const gate = lookaheadClass(code[ip + 1]!)
2194
+ const childClean = CLEAN(code[ip + 1]!)
2041
2195
  return (input, pos, ctx) => {
2042
- const need = markCst(ctx)
2196
+ if (gate !== null && !classHas(gate, lead(input, pos))) { EC.e = pos; return null }
2197
+ const need = !childClean && markCst(ctx)
2043
2198
  const mRaw = MRAW
2044
2199
  const mTl = MTL
2045
2200
  const mLv = MLV
@@ -2250,6 +2405,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2250
2405
  ? fns[reducer] as (value: unknown, span: { start: number; end: number }) => unknown
2251
2406
  : undefined
2252
2407
  const step = closureLabels.at(ip).buf ? nextTermBuffered : nextTerm
2408
+ const decided = FIRST_DECIDED
2409
+ FIRST_DECIDED = undefined
2410
+ const linkFirst = (): Piece => decided ?? link(code[base]!)
2253
2411
 
2254
2412
  const runnerBlock = (
2255
2413
  count: number,
@@ -2348,7 +2506,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2348
2506
  * sentinel published is the kind of state a later reader will trust.
2349
2507
  */
2350
2508
  if (REC) {
2351
- const first = link(code[base]!)
2509
+ const first = linkFirst()
2352
2510
  const firstSync = sentinelFor(code[base + n]!)
2353
2511
  const runnerAt = (i: number): TermRunner => {
2354
2512
  const sync = sentinelFor(code[base + n + i]!)
@@ -2430,15 +2588,32 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2430
2588
 
2431
2589
  // A `field(…, { leadingTrivia: true })` term is a boundary fact too: the
2432
2590
  // runner hands it the cursor from BEFORE the scan (`LEAD`, cell.ts).
2591
+ // So is a lookahead that holds its gap for the next term (`hold-gap.ts`).
2433
2592
  let hasAdj = false
2434
2593
  for (let i = 1; i < n; i++) {
2435
2594
  const kidIp = code[base + i]!
2436
- if (code[kidIp] === OP_ADJ || leadingTriviaFieldAt(code, k, kidIp)) { hasAdj = true; break }
2595
+ if (code[kidIp] === OP_ADJ || leadingTriviaFieldAt(code, k, kidIp) || holdsGap(code, k, base, n, i, NULLABLE)) { hasAdj = true; break }
2437
2596
  }
2438
2597
  if (hasAdj) {
2439
- const first = link(code[base]!)
2598
+ const first = linkFirst()
2440
2599
  const runnerAt = (i: number): TermRunner => {
2441
2600
  const kidIp = code[base + i]!
2601
+ if (holdsGap(code, k, base, n, i, NULLABLE)) {
2602
+ const kid = link(kidIp)
2603
+ return (input, cur, ctx) => {
2604
+ if (ctx.trivia === undefined) {
2605
+ const v = kid(input, cur, ctx)
2606
+ if (v === FAIL) return -1
2607
+ TERMV = v
2608
+ return EC.e
2609
+ }
2610
+ const scanEnd = skipTrivia(input, cur, ctx)
2611
+ const v = kid(input, scanEnd, ctx)
2612
+ if (v === FAIL) return -1
2613
+ TERMV = v
2614
+ return scanEnd
2615
+ }
2616
+ }
2442
2617
  if (code[kidIp] !== OP_ADJ) {
2443
2618
  const kid = link(kidIp)
2444
2619
  if (leadingTriviaFieldAt(code, k, kidIp)) {
@@ -2524,7 +2699,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2524
2699
  * runs, so there is no arity this fails to lower.
2525
2700
  */
2526
2701
  if (n === 1) {
2527
- const k0 = link(code[base]!)
2702
+ const k0 = linkFirst()
2528
2703
  if (fused) {
2529
2704
  return (input, pos, ctx) => {
2530
2705
  const v = k0(input, pos, ctx)
@@ -2547,7 +2722,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2547
2722
  }
2548
2723
 
2549
2724
  if (n === 2) {
2550
- const k0 = link(code[base]!), k1 = link(code[base + 1]!)
2725
+ const k0 = linkFirst(), k1 = link(code[base + 1]!)
2551
2726
  if (fused) {
2552
2727
  return (input, pos, ctx) => {
2553
2728
  const v0 = k0(input, pos, ctx)
@@ -2581,7 +2756,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2581
2756
  }
2582
2757
 
2583
2758
  if (n === 3) {
2584
- const k0 = link(code[base]!), k1 = link(code[base + 1]!), k2 = link(code[base + 2]!)
2759
+ const k0 = linkFirst(), k1 = link(code[base + 1]!), k2 = link(code[base + 2]!)
2585
2760
  if (fused) {
2586
2761
  return (input, pos, ctx) => {
2587
2762
  const v0 = k0(input, pos, ctx)
@@ -2683,7 +2858,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2683
2858
  start + count < n ? bind(start + count) : undefined,
2684
2859
  )
2685
2860
  }
2686
- const first = link(code[base]!)
2861
+ const first = linkFirst()
2687
2862
  const tail = bind(1)
2688
2863
  const runDirectTerms = (
2689
2864
  input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined,
@@ -2727,6 +2902,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2727
2902
  const n = code[ip + 2]!
2728
2903
  const choiceFx = fx[code[ip + 3]!] as string[]
2729
2904
  const base = ip + 4
2905
+ // The arms first, so the answer reads theirs rather than walking them again.
2906
+ for (let i = 0; i < n; i++) link(code[base + i]!)
2907
+ const armsClean = CLEAN(ip)
2730
2908
 
2731
2909
  // THE TWO CHOICE SHAPES ARE TWO PIECES. `exec.ts` tested `table.exclusive`
2732
2910
  // on every choice execution; it is table data, so it selects here.
@@ -2863,7 +3041,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2863
3041
 
2864
3042
  return (input, pos, ctx) => {
2865
3043
  const c = lead(input, pos)
2866
- const need = markCst(ctx)
3044
+ const need = !armsClean && markCst(ctx)
2867
3045
  const mRaw = MRAW
2868
3046
  const mTl = MTL
2869
3047
  const mLv = MLV
@@ -2915,6 +3093,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2915
3093
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2916
3094
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2917
3095
  }
3096
+ if (prev === 0) { ctx._fe = pos; ctx._fx = choiceFx; return FAIL }
2918
3097
  if (best === pos && prev < 1) acc = accSet(e0, acc)
2919
3098
  if (best === pos && prev < 2) acc = accSet(e1, acc)
2920
3099
  if (best === pos && n === 3 && prev < 3) acc = accSet(e2, acc)
@@ -3027,7 +3206,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3027
3206
  if (pn !== 0) {
3028
3207
  return (input, pos, ctx) => {
3029
3208
  const c = lead(input, pos)
3030
- const need = markCst(ctx)
3209
+ const need = !armsClean && markCst(ctx)
3031
3210
  const mRaw = MRAW
3032
3211
  const mTl = MTL
3033
3212
  const mLv = MLV
@@ -3057,7 +3236,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3057
3236
  }
3058
3237
  return (input, pos, ctx) => {
3059
3238
  const c = lead(input, pos)
3060
- const need = markCst(ctx)
3239
+ const need = !armsClean && markCst(ctx)
3061
3240
  const mRaw = MRAW
3062
3241
  const mTl = MTL
3063
3242
  const mLv = MLV
@@ -3079,7 +3258,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3079
3258
 
3080
3259
  return (input, pos, ctx) => {
3081
3260
  const c = lead(input, pos)
3082
- const need = markCst(ctx)
3261
+ const need = !armsClean && markCst(ctx)
3083
3262
  const mRaw = MRAW
3084
3263
  const mTl = MTL
3085
3264
  const mLv = MLV
@@ -3125,6 +3304,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3125
3304
  }
3126
3305
  }
3127
3306
 
3307
+ case OP_RGATE: {
3308
+ const cls = cc[code[ip + 1]!]!
3309
+ const xf = fx[code[ip + 3]!] as string[]
3310
+ const child = link(code[ip + 2]!)
3311
+ // Forwards under a probe or tolerant recovery, as OP_GATE does.
3312
+ if (cfg.tolerant || cfg.probe) return child
3313
+ return (input, pos, ctx) => {
3314
+ const item = ctx._routed
3315
+ if (item !== undefined && item.span.start === pos && !classHas(cls, lead(input, item.span.end))) {
3316
+ ctx._fe = item.span.end; ctx._fx = xf
3317
+ return FAIL
3318
+ }
3319
+ return child(input, pos, ctx)
3320
+ }
3321
+ }
3322
+
3128
3323
  case OP_ARMGATE: {
3129
3324
  // Predicate, arm and expected set all bound HERE — the body reads no
3130
3325
  // opcode, decodes no operand and tests no setting.
@@ -3134,9 +3329,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3134
3329
  return (input, pos, ctx) => {
3135
3330
  if (!pred(ctx.state)) {
3136
3331
  // A `continue` in the interpreter (choice.ts:150), not a failure —
3137
- // so no cut survives to the enclosing choice.
3332
+ // so no cut survives to the enclosing choice, and no failure position
3333
+ // ranks it on the ordered path. A dispatched choice reads `_fx` alone.
3138
3334
  ctx._fc = false
3139
- ctx._fe = pos
3335
+ ctx._fe = -1
3140
3336
  ctx._fx = xf
3141
3337
  return FAIL
3142
3338
  }
@@ -3221,6 +3417,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3221
3417
  // its FIRST item through `repItem` and so skips leading trivia. The shape
3222
3418
  // identifies itself, and it is table data, so it is decided here.
3223
3419
  const skipBeforeFirst = sepIp < 0 && min === 0
3420
+ const itemClean = sepIp < 0 && CLEAN(code[ip + 1]!) && !rowNullable(code, k, code[ip + 1]!, NULLABLE)
3224
3421
 
3225
3422
  /**
3226
3423
  * TOLERANT RECOVERY — the third implementation of `recoverScan`'s
@@ -3348,6 +3545,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3348
3545
  const out: unknown[] | undefined = collect ? [] : undefined
3349
3546
  const hasTrivia = ctx.trivia !== undefined
3350
3547
  const needMark = rollbackNeeded(ctx)
3548
+ // A clean item with no trivia scanned in front of it has nothing to undo.
3549
+ const markLater = needMark && (!itemClean || hasTrivia)
3550
+ const markFirst = needMark && (!itemClean || (hasTrivia && skipBeforeFirst))
3351
3551
  // A completions probe deliberately observes swallowed item failures.
3352
3552
  // Keep the ordinary child path while one is active so this structural
3353
3553
  // fast stop cannot erase an item completion at the cursor.
@@ -3367,14 +3567,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3367
3567
  // Per ITERATION, never hoisted: a preceding item's `node()` opened and
3368
3568
  // closed a capture buffer, so `ctx._cstBuf` is not the object it was
3369
3569
  // at the loop head. `needMark` is the pre-existing loop-invariant.
3370
- if (needMark) markCst(ctx)
3371
- const mRaw = needMark ? MRAW : 0
3372
- const mTl = needMark ? MTL : 0
3373
- const mLv = needMark ? MLV : 0
3374
- const mFl = needMark ? ctx._fields?.length ?? 0 : 0
3375
- const mEr = needMark ? ctx._errors?.length ?? 0 : 0
3376
- const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
3377
- const mRoot = needMark ? ctx._rootTriviaLog?.length ?? 0 : 0
3570
+ const markIt = count > 0 ? markLater : markFirst
3571
+ if (markIt) markCst(ctx)
3572
+ const mRaw = markIt ? MRAW : 0
3573
+ const mTl = markIt ? MTL : 0
3574
+ const mLv = markIt ? MLV : 0
3575
+ const mFl = markIt ? ctx._fields?.length ?? 0 : 0
3576
+ const mEr = markIt ? ctx._errors?.length ?? 0 : 0
3577
+ const mLog = markIt ? ctx._triviaLog?.length ?? 0 : 0
3578
+ const mRoot = markIt ? ctx._rootTriviaLog?.length ?? 0 : 0
3378
3579
  let itemStart = cur
3379
3580
  let sepEnd = -1
3380
3581
  const viaRepItem = sep === undefined && count >= min && (count > 0 || skipBeforeFirst)
@@ -3386,7 +3587,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3386
3587
  if (sepLeads) LEAD.at = cur
3387
3588
  const sv = sep(input, sp, ctx)
3388
3589
  if (sv === FAIL) {
3389
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3590
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3390
3591
  if (committed(ctx)) return FAIL
3391
3592
  break
3392
3593
  }
@@ -3397,19 +3598,19 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3397
3598
  itemStart = skipTrivia(input, itemStart, ctx)
3398
3599
  }
3399
3600
  if (itemStart >= input.length && viaRepItem) {
3400
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3601
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3401
3602
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3402
3603
  break
3403
3604
  }
3404
3605
  if (count >= min && gateItems && !itemMayStart!(input, itemStart)) {
3405
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3606
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3406
3607
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3407
3608
  break
3408
3609
  }
3409
3610
  ctx._fc = false
3410
3611
  const v = child(input, itemStart, ctx)
3411
3612
  if (v === FAIL) {
3412
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3613
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3413
3614
  if (committed(ctx)) return FAIL
3414
3615
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3415
3616
  break
@@ -3417,7 +3618,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3417
3618
  if (EC.e === itemStart && viaRepItem) {
3418
3619
  // Zero-width item: `repItem`'s TERMINATION device, not a semantic
3419
3620
  // filter — and it applies only where `repItem` runs.
3420
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3621
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3421
3622
  break
3422
3623
  }
3423
3624
  if (out !== undefined) out.push(v)
@@ -3490,7 +3691,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3490
3691
  const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted)
3491
3692
 
3492
3693
  return (input, pos, ctx) => {
3493
- const selectorMark = saveTriviaMark(ctx)
3694
+ const need = markCst(ctx)
3695
+ const mRaw = MRAW
3696
+ const mTl = MTL
3697
+ const mLv = MLV
3698
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
3699
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
3700
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
3701
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
3494
3702
  const selVal = selector(input, pos, ctx)
3495
3703
  if (selVal === FAIL) return FAIL
3496
3704
  const selEnd = EC.e
@@ -3503,9 +3711,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3503
3711
  ctx._fx = expected
3504
3712
  return FAIL
3505
3713
  }
3506
- return runOther(input, pos, selEnd, key, selectorMark, ctx)
3714
+ return runOther(input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3507
3715
  }
3508
- return runArm!(arm, input, pos, selEnd, key, selectorMark, ctx)
3716
+ return runArm!(arm, input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3509
3717
  }
3510
3718
  }
3511
3719
 
@@ -3549,7 +3757,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3549
3757
  return nd
3550
3758
  }
3551
3759
  }
3552
- const child = link(code[ip + 2]!)
3760
+ const child = NODE_BODY ?? link(code[ip + 2]!)
3761
+ NODE_BODY = undefined
3553
3762
  const proj = code[ip + 4]!
3554
3763
  const buildIdx = code[ip + 1]!
3555
3764
  const type = k[code[ip + 5]!] as string
@@ -3586,6 +3795,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3586
3795
  if (build !== undefined && proj < 0 && flags === 18) {
3587
3796
  return childrenOnlyFieldsBuildNode(child, build)
3588
3797
  }
3798
+ if (build !== undefined && passCollapse(code, ip, hostCst)) {
3799
+ return passCollapseNode(child, build, flags === 32)
3800
+ }
3801
+ // Not under recovery or a probe: there a gated-out term still runs.
3802
+ const gated = build !== undefined && !cfg.tolerant && !cfg.probe ? gatedPassCollapse(code, ip, hostCst) : undefined
3803
+ const triviaLead = gated === undefined ? undefined : triviaLeadRanges(closureLabels.at(code[ip + 2]!).tri, trivia)
3804
+ if (build !== undefined && gated !== undefined && triviaLead !== undefined) {
3805
+ const classic = flags === 32 ? collapseBuildNode(child, build) : childrenOnlyCollapseBuildNode(child, build)
3806
+ return gatedPassCollapseNode(
3807
+ [
3808
+ ...gated.leads.map(g => cc[code[g + 1]!]!),
3809
+ resolveClass(encodeClassSpec([
3810
+ ...triviaLead,
3811
+ ...gated.lits.map(i => { const c = (k[i] as string).codePointAt(0)!; return { lo: c, hi: c } }),
3812
+ ])),
3813
+ ],
3814
+ link(gated.last), classic, build, flags === 32,
3815
+ )
3816
+ }
3589
3817
  if (build !== undefined && proj < 0 && flags === 32) {
3590
3818
  return collapseBuildNode(child, build)
3591
3819
  }
@@ -3663,9 +3891,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3663
3891
  if (v === FAIL) return FAIL
3664
3892
  const end = EC.e
3665
3893
  const span = tracked ? spanLines(ctx, pos, end) : { start: pos, end }
3666
- const st = readsState && ctx.state !== undefined
3667
- ? Object.assign({}, ctx.state as Record<string, unknown>)
3668
- : undefined
3894
+ const st = readsState ? ctx.state : undefined
3669
3895
 
3670
3896
  let nd: unknown
3671
3897
  if (unwrap && kids.length === 1) {
@@ -3685,12 +3911,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3685
3911
  nd = kids[0]
3686
3912
  } else if (proj >= 0) {
3687
3913
  nd = hostCst && host !== undefined
3688
- ? host(type, kids, fieldMap, span, rawKids, tlog, readsState ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
3914
+ ? host(type, kids, fieldMap, span, rawKids, tlog, ctx.state, tags)
3689
3915
  : projectChild(kids, proj, type)
3690
3916
  } else if (build !== undefined) {
3691
3917
  if (hostCst && host !== undefined) {
3692
3918
  // A direct builder is bypassed under a CST host.
3693
- nd = host(type, kids, fieldMap, span, rawKids, tlog, readsState ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
3919
+ nd = host(type, kids, fieldMap, span, rawKids, tlog, ctx.state, tags)
3694
3920
  } else {
3695
3921
  nd = build(kids, fieldMap, span, rawKids, captureWide ? tlog : EMPTY_TL, st)
3696
3922
  }
@@ -3777,9 +4003,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3777
4003
  // The scan pool and the scan-skip sets are built FROM subtrees, so their
3778
4004
  // sites need emitted names too or `subtreeComb` would have to fall back to
3779
4005
  // a closure for them and the two engines would both be live in one parse.
3780
- const extraIps: number[] = []
3781
- for (const s of prog.scans ?? []) for (const r of scanRefs(s)) extraIps.push(r[0])
3782
- for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
4006
+ const extraIps = assemblyExtraIps(prog)
3783
4007
  const roots = [...Object.values(prog.rules), ...extraIps]
3784
4008
  for (const ip of reachableSites(code, roots)) {
3785
4009
  if (code[ip] === OP_DISPATCH) {
@@ -3815,6 +4039,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3815
4039
  }
3816
4040
  const notChild = scalarTerminalNotChild(code, ip)
3817
4041
  if (notChild >= 0) scalarFor(notChild)
4042
+ const peekChild = scalarTerminalPeekChild(code, ip)
4043
+ if (peekChild >= 0) scalarFor(peekChild)
3818
4044
  }
3819
4045
  /**
3820
4046
  * THE BUILD'S OWN ANSWER, TRIED BEFORE THE CONSTRUCTOR.
@@ -3852,9 +4078,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3852
4078
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
3853
4079
  matchesAt, recoverScan, orSentinel, captureError,
3854
4080
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3855
- LEAD, triviaFieldCapture,
4081
+ LEAD, triviaFieldCapture, skipDeferredTrivia,
4082
+ collapseEmpty,
3856
4083
  )
3857
4084
  emitReached = new Set(pre.reached)
4085
+ if (emitted.ext !== undefined) HYBRID = emitted
3858
4086
  }
3859
4087
  /**
3860
4088
  * ONCE A PROGRAM CARRIES ASSEMBLIES, THE CONSTRUCTOR IS OFF FOR IT — for
@@ -3934,7 +4162,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3934
4162
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
3935
4163
  matchesAt, recoverScan, orSentinel, captureError,
3936
4164
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3937
- LEAD, triviaFieldCapture,
4165
+ LEAD, triviaFieldCapture, skipDeferredTrivia,
4166
+ collapseEmpty,
3938
4167
  )
3939
4168
  emitReached = em.reached
3940
4169
  } catch (e) {
@@ -3947,7 +4176,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3947
4176
  /** One piece for a site, from whichever engine this assembly is running. */
3948
4177
  function pieceAt(ip: number): Piece {
3949
4178
  const em = emitted
3950
- if (em !== undefined) {
4179
+ if (em !== undefined && HYBRID === undefined) {
3951
4180
  const p = em.byIp[ip]
3952
4181
  if (p === undefined) throw new Error(`table emitter: no emitted body for site ${ip}`)
3953
4182
  return p as Piece
@@ -3982,6 +4211,28 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3982
4211
 
3983
4212
  /* ── link the rules ──────────────────────────────────────────────────────── */
3984
4213
 
4214
+ if (HYBRID !== undefined) {
4215
+ const em = HYBRID
4216
+ em.bind!(
4217
+ s => { const saved = SCAN; SCAN = s as FastTriviaScanner | null; return saved },
4218
+ em.ext!.map(ip => link(ip)),
4219
+ )
4220
+ const pieces: Record<string, Piece> = {}
4221
+ for (const [name, entryIp] of Object.entries(prog.rules)) {
4222
+ pieces[name] = (em.pieces[name] as Piece | undefined) ?? link(entryIp)
4223
+ }
4224
+ for (const ip of emitReached!) reached.add(ip)
4225
+ return {
4226
+ pieces,
4227
+ end: () => EC.e,
4228
+ begin: ctx => { begin(ctx); em.begin(ctx) },
4229
+ finish: () => { em.finish(); finish() },
4230
+ scanSkip,
4231
+ reached,
4232
+ emitRefusal: undefined,
4233
+ }
4234
+ }
4235
+
3985
4236
  if (emitted !== undefined) {
3986
4237
  const em = emitted
3987
4238
  return {
@@ -4034,6 +4285,43 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4034
4285
  return { pieces, end: () => EC.e, begin, finish, scanSkip, reached, emitRefusal }
4035
4286
  }
4036
4287
 
4288
+ /**
4289
+ * BUILD-TIME SITE PROFILING (`countSiteVisits`). Set only while that call runs;
4290
+ * `link` reads it when it LOWERS a site, never per parse, so an ordinary assembly
4291
+ * carries no counter and no branch.
4292
+ */
4293
+ let SITE_COUNTS: Int32Array | undefined
4294
+
4295
+ function countedPiece(piece: Piece, ip: number): Piece {
4296
+ const counts = SITE_COUNTS
4297
+ if (counts === undefined) return piece
4298
+ return (input, pos, ctx) => {
4299
+ counts[ip]!++
4300
+ return piece(input, pos, ctx)
4301
+ }
4302
+ }
4303
+
4304
+ /**
4305
+ * How many times each table site runs while `parse` drives the program — the
4306
+ * profile an optional corpus gives selective assembly (`select-hot.ts`). Runs the
4307
+ * CLOSURE engine, so it constructs no code; `counts[ip]` is the number of calls
4308
+ * of the piece lowered for site `ip` (an alias site counts at its target).
4309
+ */
4310
+ export function countSiteVisits(
4311
+ prog: TableProgram,
4312
+ parse: (rules: Record<string, TableRule>) => void,
4313
+ /** A replacement callback pool (same indices), e.g. stand-ins for inert builders. */
4314
+ fns: readonly unknown[] = prog.fns,
4315
+ ): Int32Array {
4316
+ const counts = new Int32Array(prog.code.length)
4317
+ // A fresh program, not a spread: a spread keeps the source's cached resolution,
4318
+ // which holds the ORIGINAL callback pool.
4319
+ const profiled = ownTableProgram({ ...prog, fns, asm: [] })
4320
+ SITE_COUNTS = counts
4321
+ try { parse(tableRules(profiled)) } finally { SITE_COUNTS = undefined }
4322
+ return counts
4323
+ }
4324
+
4037
4325
  /**
4038
4326
  * Assemblies for one resolved table, one per option set, built on demand.
4039
4327
  *