parseman 0.57.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 (64) hide show
  1. package/dist/analysis/diagnostics.js +1799 -962
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +1928 -1091
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/compiler/token-alphabet.d.ts +11 -1
  7. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  8. package/dist/compiler/token-capability.js +69 -16
  9. package/dist/compiler/token-capability.js.map +2 -2
  10. package/dist/cst/capture-buffer.d.ts +6 -0
  11. package/dist/cst/capture-buffer.d.ts.map +1 -1
  12. package/dist/index.js +1016 -177
  13. package/dist/index.js.map +3 -3
  14. package/dist/plugin/index.d.ts +25 -1
  15. package/dist/plugin/index.d.ts.map +1 -1
  16. package/dist/plugin/index.js +1209 -236
  17. package/dist/plugin/index.js.map +3 -3
  18. package/dist/table/assemble.d.ts.map +1 -1
  19. package/dist/table/compile-rule-map.d.ts.map +1 -1
  20. package/dist/table/emit-assembly.d.ts +1 -1
  21. package/dist/table/emit-assembly.d.ts.map +1 -1
  22. package/dist/table/emit.d.ts.map +1 -1
  23. package/dist/table/encode.d.ts.map +1 -1
  24. package/dist/table/exec.d.ts.map +1 -1
  25. package/dist/table/factor-choice.d.ts +42 -0
  26. package/dist/table/factor-choice.d.ts.map +1 -0
  27. package/dist/table/fail-clean.d.ts +3 -0
  28. package/dist/table/fail-clean.d.ts.map +1 -0
  29. package/dist/table/hold-gap.d.ts +8 -0
  30. package/dist/table/hold-gap.d.ts.map +1 -0
  31. package/dist/table/index.js +1369 -259
  32. package/dist/table/index.js.map +3 -3
  33. package/dist/table/lex-program.d.ts +7 -3
  34. package/dist/table/lex-program.d.ts.map +1 -1
  35. package/dist/table/ops.d.ts +8 -0
  36. package/dist/table/ops.d.ts.map +1 -1
  37. package/dist/table/run-support.d.ts +19 -0
  38. package/dist/table/run-support.d.ts.map +1 -1
  39. package/dist/table/scalar-terminal.d.ts +2 -0
  40. package/dist/table/scalar-terminal.d.ts.map +1 -1
  41. package/dist/table/site-labels.d.ts +66 -0
  42. package/dist/table/site-labels.d.ts.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/combinators/dispatch.ts +12 -3
  46. package/src/compiler/token-alphabet.ts +57 -17
  47. package/src/cst/capture-buffer.ts +25 -0
  48. package/src/plugin/index.ts +230 -35
  49. package/src/table/assemble.ts +252 -62
  50. package/src/table/child-slots.ts +2 -2
  51. package/src/table/compile-rule-map.ts +14 -5
  52. package/src/table/emit-assembly.ts +112 -23
  53. package/src/table/emit.ts +3 -1
  54. package/src/table/encode.ts +80 -14
  55. package/src/table/exec.ts +27 -8
  56. package/src/table/factor-choice.ts +173 -0
  57. package/src/table/fail-clean.ts +130 -0
  58. package/src/table/hold-gap.ts +68 -0
  59. package/src/table/lex-program.ts +197 -43
  60. package/src/table/ops.ts +9 -1
  61. package/src/table/run-support.ts +35 -0
  62. package/src/table/scalar-terminal.ts +8 -0
  63. package/src/table/site-labels.ts +135 -4
  64. package/src/version.ts +1 -1
@@ -75,8 +75,8 @@ import { projectChild, unwrapChild } from '../combinators/node.ts'
75
75
  import { cstOutputHost } from '../compiler/build-arity.ts'
76
76
  import { consumeTrivia } from '../combinators/trivia-skip.ts'
77
77
  import {
78
- advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTrivia, rollbackTriviaAt,
79
- saveTriviaMark, scanTrivia, scanTriviaCompact, skipTriviaScanned, type FastTriviaScanner,
78
+ advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTriviaAt,
79
+ scanTrivia, scanTriviaCompact, skipTriviaScanned, type FastTriviaScanner,
80
80
  } from '../combinators/trivia-skip.ts'
81
81
  import {
82
82
  cstCaptureActive, cstLeavesLen, cstTlLen,
@@ -90,7 +90,7 @@ import {
90
90
  OP_LIVE,
91
91
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
92
92
  OP_COV,
93
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
93
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
94
94
  OP_LEX_BODY, OP_LEX_PROGRAM,
95
95
  } from './ops.ts'
96
96
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
@@ -107,7 +107,9 @@ import {
107
107
  EMITTED_PARAMS, Unemittable, emitAssemblySource, rebuildPools,
108
108
  type EmittedAssembly, type EmittedFactory,
109
109
  } from './emit-assembly.ts'
110
- 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'
111
113
  /**
112
114
  * THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
113
115
  *
@@ -123,18 +125,18 @@ import { lead, rawEntry, spanLines } from './run-support.ts'
123
125
  * property read per terminal miss — the same price codegen pays.
124
126
  */
125
127
  import {
126
- classHas, decodeClassSpec, expandCompact, ownTableProgram, resolveTable,
128
+ classHas, decodeClassSpec, encodeClassSpec, expandCompact, ownTableProgram, resolveClass, resolveTable,
127
129
  validateDispatchSpec,
128
130
  type CompactProgram, type ResolvedClass, type ResolvedTable,
129
131
  type SubtreeRef, type TableProgram, type TableRule,
130
132
  } from './program.ts'
131
133
  import { stampRuleMap } from './stamp.ts'
132
- import { computeSiteLabels, reachableSites } from './site-labels.ts'
134
+ import { computeSiteLabels, gatedPassCollapse, passCollapse, reachableSites, scanHandoffNeeded, scopeIsIdentity, triviaLeadRanges } from './site-labels.ts'
133
135
  import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
134
136
  import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
135
137
  import {
136
138
  largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal, makeScalarRecognizer, scalarTerminalNodeChild,
137
- scalarTerminalNotChild, type ScalarRecognizer,
139
+ scalarTerminalNotChild, scalarTerminalPeekChild, type ScalarRecognizer,
138
140
  } from './scalar-terminal.ts'
139
141
 
140
142
  /**
@@ -241,13 +243,14 @@ type GeneralChoiceBlock = (
241
243
  type ExclusiveChoiceBlock = (arm: number, input: string, pos: number, ctx: ParseContext) => unknown
242
244
  type DispatchMatcher = (key: string) => boolean
243
245
  type DispatchMatcherBlock = (key: string) => number | undefined
246
+ /** The selector's rollback mark rides as the eight scalars a choice block takes. */
244
247
  type DispatchBranch = (
245
- input: string, pos: number, selEnd: number, key: string,
246
- 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,
247
250
  ) => unknown
248
251
  type DispatchArmBlock = (
249
- arm: number, input: string, pos: number, selEnd: number, key: string,
250
- 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,
251
254
  ) => unknown
252
255
 
253
256
  /**
@@ -441,6 +444,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
441
444
  * reference.
442
445
  */
443
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
444
449
  /**
445
450
  * The sync sentinel for a char-class index, built ONCE at assembly.
446
451
  *
@@ -531,6 +536,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
531
536
 
532
537
  const memo = new Map<number, Piece>()
533
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)
534
543
 
535
544
  function pushLeaf(ctx: ParseContext, value: string, s: number, e: number): void {
536
545
  const lf: Leaf = { _tag: 'leaf', value, span: { start: s, end: e } }
@@ -764,6 +773,36 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
764
773
  }
765
774
  }
766
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
+
767
806
  function plainProjectNode(child: Piece, projection: number, type: string): Piece {
768
807
  return (input, pos, ctx) => {
769
808
  const sCh = ctx._cstChildren
@@ -1291,21 +1330,30 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1291
1330
 
1292
1331
  function dispatchBranch(child: Piece, usesRouted: boolean): DispatchBranch {
1293
1332
  if (!usesRouted) {
1294
- return (input, _pos, selEnd, key, _selectorMark, ctx) => {
1295
- 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
1296
1343
  const v = child(input, selEnd, ctx)
1297
1344
  if (v === FAIL) {
1298
- rollbackTrivia(ctx, mark)
1345
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1299
1346
  ctx._fc = true
1300
1347
  return FAIL
1301
1348
  }
1302
1349
  return [key, v]
1303
1350
  }
1304
1351
  }
1305
- 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) => {
1306
1355
  const savedRouted = ctx._routed
1307
- rollbackTrivia(ctx, selectorMark)
1308
- const mark = saveTriviaMark(ctx)
1356
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1309
1357
  ctx._routed = { value: key, span: { start: pos, end: selEnd } }
1310
1358
  let v: unknown
1311
1359
  try {
@@ -1314,7 +1362,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1314
1362
  ctx._routed = savedRouted
1315
1363
  }
1316
1364
  if (v === FAIL) {
1317
- rollbackTrivia(ctx, mark)
1365
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1318
1366
  ctx._fc = true
1319
1367
  return FAIL
1320
1368
  }
@@ -1327,12 +1375,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1327
1375
  a0: DispatchBranch, a1: DispatchBranch, a2: DispatchBranch, a3: DispatchBranch,
1328
1376
  next: DispatchArmBlock | undefined,
1329
1377
  ): DispatchArmBlock {
1330
- return (arm, input, pos, selEnd, key, selectorMark, ctx) => {
1331
- if (arm === start) return a0(input, pos, selEnd, key, selectorMark, ctx)
1332
- if (arm === start + 1) return a1(input, pos, selEnd, key, selectorMark, ctx)
1333
- if (arm === start + 2) return a2(input, pos, selEnd, key, selectorMark, ctx)
1334
- if (arm === start + 3) return a3(input, pos, selEnd, key, selectorMark, ctx)
1335
- 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)
1336
1386
  }
1337
1387
  }
1338
1388
 
@@ -1368,7 +1418,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1368
1418
  if (flight !== undefined) return flight.fwd
1369
1419
  const selected = HYBRID?.byIp[ip]
1370
1420
  if (selected !== undefined) {
1371
- const piece = hybridPiece(selected as Piece)
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
1372
1424
  memo.set(ip, piece)
1373
1425
  reached.add(ip)
1374
1426
  return piece
@@ -1379,16 +1431,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1379
1431
  const holder = { fwd, set: (p: Piece) => { target = p } }
1380
1432
  inFlight.set(ip, holder)
1381
1433
 
1382
- const op = code[ip]
1383
- // Match the emitted assembly's label-proven scope alias: rule entries in a
1384
- // single ambient trivia scope otherwise reinstall and restore the exact
1385
- // 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
1386
1438
  // holder first so a recursive child can still link back through this site.
1387
- const piece = (op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[ip + 3]! === 0))
1388
- && code[ip + 1]! >= 0
1389
- && closureLabels.at(ip).tri === code[ip + 1]!
1439
+ const piece = scopeIsIdentity(code, ip, closureLabels.at(ip))
1390
1440
  ? link(code[ip + 2]!)
1391
- : countedPiece(lower(ip), ip)
1441
+ : countedPiece(lookaheadLedNode(ip) ?? lower(ip), ip)
1392
1442
 
1393
1443
  inFlight.delete(ip)
1394
1444
  holder.set(piece)
@@ -1397,6 +1447,48 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1397
1447
  return piece
1398
1448
  }
1399
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
+
1400
1492
  function lower(ip: number): Piece {
1401
1493
  const op = code[ip]
1402
1494
  switch (op) {
@@ -2041,8 +2133,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2041
2133
  const mEr = need ? ctx._errors?.length ?? 0 : 0
2042
2134
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2043
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
2044
2139
  const v = child(input, pos, ctx)
2045
2140
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2141
+ if (PROBE && ctx._probe !== undefined) ctx._probe.best = probeBest ?? null
2046
2142
  if (v === FAIL) { EC.e = pos; return null }
2047
2143
  // `not.ts:50` — the ASSERTION's own set, at the assertion's position.
2048
2144
  ctx._fe = pos
@@ -2052,6 +2148,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2052
2148
  }
2053
2149
 
2054
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
+ }
2055
2167
  const child = link(code[ip + 1]!)
2056
2168
  const xf = fx[code[ip + 2]!] as string[]
2057
2169
  const gate = lookaheadClass(code[ip + 1]!)
@@ -2065,8 +2177,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2065
2177
  const mEr = need ? ctx._errors?.length ?? 0 : 0
2066
2178
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2067
2179
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2180
+ const probeBest = PROBE ? ctx._probe?.best : undefined
2068
2181
  const v = child(input, pos, ctx)
2069
2182
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2183
+ if (PROBE && ctx._probe !== undefined) ctx._probe.best = probeBest ?? null
2070
2184
  // `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
2071
2185
  if (v === FAIL) { ctx._fe = pos; ctx._fx = xf; return FAIL }
2072
2186
  EC.e = pos
@@ -2077,9 +2191,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2077
2191
  case OP_OPT: {
2078
2192
  const child = link(code[ip + 1]!)
2079
2193
  const gate = lookaheadClass(code[ip + 1]!)
2194
+ const childClean = CLEAN(code[ip + 1]!)
2080
2195
  return (input, pos, ctx) => {
2081
2196
  if (gate !== null && !classHas(gate, lead(input, pos))) { EC.e = pos; return null }
2082
- const need = markCst(ctx)
2197
+ const need = !childClean && markCst(ctx)
2083
2198
  const mRaw = MRAW
2084
2199
  const mTl = MTL
2085
2200
  const mLv = MLV
@@ -2290,6 +2405,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2290
2405
  ? fns[reducer] as (value: unknown, span: { start: number; end: number }) => unknown
2291
2406
  : undefined
2292
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]!)
2293
2411
 
2294
2412
  const runnerBlock = (
2295
2413
  count: number,
@@ -2388,7 +2506,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2388
2506
  * sentinel published is the kind of state a later reader will trust.
2389
2507
  */
2390
2508
  if (REC) {
2391
- const first = link(code[base]!)
2509
+ const first = linkFirst()
2392
2510
  const firstSync = sentinelFor(code[base + n]!)
2393
2511
  const runnerAt = (i: number): TermRunner => {
2394
2512
  const sync = sentinelFor(code[base + n + i]!)
@@ -2470,15 +2588,32 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2470
2588
 
2471
2589
  // A `field(…, { leadingTrivia: true })` term is a boundary fact too: the
2472
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`).
2473
2592
  let hasAdj = false
2474
2593
  for (let i = 1; i < n; i++) {
2475
2594
  const kidIp = code[base + i]!
2476
- 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 }
2477
2596
  }
2478
2597
  if (hasAdj) {
2479
- const first = link(code[base]!)
2598
+ const first = linkFirst()
2480
2599
  const runnerAt = (i: number): TermRunner => {
2481
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
+ }
2482
2617
  if (code[kidIp] !== OP_ADJ) {
2483
2618
  const kid = link(kidIp)
2484
2619
  if (leadingTriviaFieldAt(code, k, kidIp)) {
@@ -2564,7 +2699,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2564
2699
  * runs, so there is no arity this fails to lower.
2565
2700
  */
2566
2701
  if (n === 1) {
2567
- const k0 = link(code[base]!)
2702
+ const k0 = linkFirst()
2568
2703
  if (fused) {
2569
2704
  return (input, pos, ctx) => {
2570
2705
  const v = k0(input, pos, ctx)
@@ -2587,7 +2722,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2587
2722
  }
2588
2723
 
2589
2724
  if (n === 2) {
2590
- const k0 = link(code[base]!), k1 = link(code[base + 1]!)
2725
+ const k0 = linkFirst(), k1 = link(code[base + 1]!)
2591
2726
  if (fused) {
2592
2727
  return (input, pos, ctx) => {
2593
2728
  const v0 = k0(input, pos, ctx)
@@ -2621,7 +2756,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2621
2756
  }
2622
2757
 
2623
2758
  if (n === 3) {
2624
- 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]!)
2625
2760
  if (fused) {
2626
2761
  return (input, pos, ctx) => {
2627
2762
  const v0 = k0(input, pos, ctx)
@@ -2723,7 +2858,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2723
2858
  start + count < n ? bind(start + count) : undefined,
2724
2859
  )
2725
2860
  }
2726
- const first = link(code[base]!)
2861
+ const first = linkFirst()
2727
2862
  const tail = bind(1)
2728
2863
  const runDirectTerms = (
2729
2864
  input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined,
@@ -2767,6 +2902,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2767
2902
  const n = code[ip + 2]!
2768
2903
  const choiceFx = fx[code[ip + 3]!] as string[]
2769
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)
2770
2908
 
2771
2909
  // THE TWO CHOICE SHAPES ARE TWO PIECES. `exec.ts` tested `table.exclusive`
2772
2910
  // on every choice execution; it is table data, so it selects here.
@@ -2903,7 +3041,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2903
3041
 
2904
3042
  return (input, pos, ctx) => {
2905
3043
  const c = lead(input, pos)
2906
- const need = markCst(ctx)
3044
+ const need = !armsClean && markCst(ctx)
2907
3045
  const mRaw = MRAW
2908
3046
  const mTl = MTL
2909
3047
  const mLv = MLV
@@ -3068,7 +3206,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3068
3206
  if (pn !== 0) {
3069
3207
  return (input, pos, ctx) => {
3070
3208
  const c = lead(input, pos)
3071
- const need = markCst(ctx)
3209
+ const need = !armsClean && markCst(ctx)
3072
3210
  const mRaw = MRAW
3073
3211
  const mTl = MTL
3074
3212
  const mLv = MLV
@@ -3098,7 +3236,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3098
3236
  }
3099
3237
  return (input, pos, ctx) => {
3100
3238
  const c = lead(input, pos)
3101
- const need = markCst(ctx)
3239
+ const need = !armsClean && markCst(ctx)
3102
3240
  const mRaw = MRAW
3103
3241
  const mTl = MTL
3104
3242
  const mLv = MLV
@@ -3120,7 +3258,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3120
3258
 
3121
3259
  return (input, pos, ctx) => {
3122
3260
  const c = lead(input, pos)
3123
- const need = markCst(ctx)
3261
+ const need = !armsClean && markCst(ctx)
3124
3262
  const mRaw = MRAW
3125
3263
  const mTl = MTL
3126
3264
  const mLv = MLV
@@ -3166,6 +3304,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3166
3304
  }
3167
3305
  }
3168
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
+
3169
3323
  case OP_ARMGATE: {
3170
3324
  // Predicate, arm and expected set all bound HERE — the body reads no
3171
3325
  // opcode, decodes no operand and tests no setting.
@@ -3263,6 +3417,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3263
3417
  // its FIRST item through `repItem` and so skips leading trivia. The shape
3264
3418
  // identifies itself, and it is table data, so it is decided here.
3265
3419
  const skipBeforeFirst = sepIp < 0 && min === 0
3420
+ const itemClean = sepIp < 0 && CLEAN(code[ip + 1]!) && !rowNullable(code, k, code[ip + 1]!, NULLABLE)
3266
3421
 
3267
3422
  /**
3268
3423
  * TOLERANT RECOVERY — the third implementation of `recoverScan`'s
@@ -3390,6 +3545,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3390
3545
  const out: unknown[] | undefined = collect ? [] : undefined
3391
3546
  const hasTrivia = ctx.trivia !== undefined
3392
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))
3393
3551
  // A completions probe deliberately observes swallowed item failures.
3394
3552
  // Keep the ordinary child path while one is active so this structural
3395
3553
  // fast stop cannot erase an item completion at the cursor.
@@ -3409,14 +3567,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3409
3567
  // Per ITERATION, never hoisted: a preceding item's `node()` opened and
3410
3568
  // closed a capture buffer, so `ctx._cstBuf` is not the object it was
3411
3569
  // at the loop head. `needMark` is the pre-existing loop-invariant.
3412
- if (needMark) markCst(ctx)
3413
- const mRaw = needMark ? MRAW : 0
3414
- const mTl = needMark ? MTL : 0
3415
- const mLv = needMark ? MLV : 0
3416
- const mFl = needMark ? ctx._fields?.length ?? 0 : 0
3417
- const mEr = needMark ? ctx._errors?.length ?? 0 : 0
3418
- const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
3419
- 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
3420
3579
  let itemStart = cur
3421
3580
  let sepEnd = -1
3422
3581
  const viaRepItem = sep === undefined && count >= min && (count > 0 || skipBeforeFirst)
@@ -3428,7 +3587,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3428
3587
  if (sepLeads) LEAD.at = cur
3429
3588
  const sv = sep(input, sp, ctx)
3430
3589
  if (sv === FAIL) {
3431
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3590
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3432
3591
  if (committed(ctx)) return FAIL
3433
3592
  break
3434
3593
  }
@@ -3439,19 +3598,19 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3439
3598
  itemStart = skipTrivia(input, itemStart, ctx)
3440
3599
  }
3441
3600
  if (itemStart >= input.length && viaRepItem) {
3442
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3601
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3443
3602
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3444
3603
  break
3445
3604
  }
3446
3605
  if (count >= min && gateItems && !itemMayStart!(input, itemStart)) {
3447
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3606
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3448
3607
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3449
3608
  break
3450
3609
  }
3451
3610
  ctx._fc = false
3452
3611
  const v = child(input, itemStart, ctx)
3453
3612
  if (v === FAIL) {
3454
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3613
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3455
3614
  if (committed(ctx)) return FAIL
3456
3615
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3457
3616
  break
@@ -3459,7 +3618,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3459
3618
  if (EC.e === itemStart && viaRepItem) {
3460
3619
  // Zero-width item: `repItem`'s TERMINATION device, not a semantic
3461
3620
  // filter — and it applies only where `repItem` runs.
3462
- if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3621
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3463
3622
  break
3464
3623
  }
3465
3624
  if (out !== undefined) out.push(v)
@@ -3532,7 +3691,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3532
3691
  const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted)
3533
3692
 
3534
3693
  return (input, pos, ctx) => {
3535
- 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
3536
3702
  const selVal = selector(input, pos, ctx)
3537
3703
  if (selVal === FAIL) return FAIL
3538
3704
  const selEnd = EC.e
@@ -3545,9 +3711,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3545
3711
  ctx._fx = expected
3546
3712
  return FAIL
3547
3713
  }
3548
- return runOther(input, pos, selEnd, key, selectorMark, ctx)
3714
+ return runOther(input, pos, selEnd, key, ctx, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3549
3715
  }
3550
- 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)
3551
3717
  }
3552
3718
  }
3553
3719
 
@@ -3591,7 +3757,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3591
3757
  return nd
3592
3758
  }
3593
3759
  }
3594
- const child = link(code[ip + 2]!)
3760
+ const child = NODE_BODY ?? link(code[ip + 2]!)
3761
+ NODE_BODY = undefined
3595
3762
  const proj = code[ip + 4]!
3596
3763
  const buildIdx = code[ip + 1]!
3597
3764
  const type = k[code[ip + 5]!] as string
@@ -3628,6 +3795,25 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3628
3795
  if (build !== undefined && proj < 0 && flags === 18) {
3629
3796
  return childrenOnlyFieldsBuildNode(child, build)
3630
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
+ }
3631
3817
  if (build !== undefined && proj < 0 && flags === 32) {
3632
3818
  return collapseBuildNode(child, build)
3633
3819
  }
@@ -3853,6 +4039,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3853
4039
  }
3854
4040
  const notChild = scalarTerminalNotChild(code, ip)
3855
4041
  if (notChild >= 0) scalarFor(notChild)
4042
+ const peekChild = scalarTerminalPeekChild(code, ip)
4043
+ if (peekChild >= 0) scalarFor(peekChild)
3856
4044
  }
3857
4045
  /**
3858
4046
  * THE BUILD'S OWN ANSWER, TRIED BEFORE THE CONSTRUCTOR.
@@ -3891,6 +4079,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3891
4079
  matchesAt, recoverScan, orSentinel, captureError,
3892
4080
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3893
4081
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4082
+ collapseEmpty,
3894
4083
  )
3895
4084
  emitReached = new Set(pre.reached)
3896
4085
  if (emitted.ext !== undefined) HYBRID = emitted
@@ -3974,6 +4163,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3974
4163
  matchesAt, recoverScan, orSentinel, captureError,
3975
4164
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3976
4165
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4166
+ collapseEmpty,
3977
4167
  )
3978
4168
  emitReached = em.reached
3979
4169
  } catch (e) {