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
@@ -1,5 +1,5 @@
1
1
  import {
2
- OP_ADJ, OP_ARMGATE, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT,
2
+ OP_ADJ, OP_ARMGATE, OP_RGATE, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT,
3
3
  OP_FIELD, OP_GATE, OP_GREEDY, OP_GUARD, OP_LABEL, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_LIT_CI,
4
4
  OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_LIVE, OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT,
5
5
  OP_PEEK, OP_REJECT, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX, OP_RX_TRACK, OP_SCAN,
@@ -53,7 +53,7 @@ export function childSlots(code: ArrayLike<number>, ip: number, out: number[]):
53
53
 
54
54
  // Single child at `ip + 2`.
55
55
  case OP_GATE: case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM:
56
- case OP_NODE: case OP_NODE_TRACK: case OP_FIELD: case OP_LEAF: case OP_SOURCE_LEAF: case OP_ARMGATE:
56
+ case OP_NODE: case OP_NODE_TRACK: case OP_FIELD: case OP_LEAF: case OP_SOURCE_LEAF: case OP_ARMGATE: case OP_RGATE:
57
57
  out.push(code[ip + 2]!)
58
58
  return true
59
59
 
@@ -369,18 +369,27 @@ export function compileRuleMap(
369
369
  // the selection is judged by the source it actually emits, scaled for that 5%,
370
370
  // and narrowed until that fits.
371
371
  const ceiling = budget / SUPERCOMPILE_GROWTH
372
+ // A budget that cannot cover the prelude buys nothing, by design: no refusal.
373
+ if (ceiling <= fixed) return none
374
+ // Narrowed until it fits, with no attempt limit: the target shrinks by at
375
+ // least 1% a round, so the selection either fits or empties. An empty one
376
+ // says why — a build that asked for an assembly and silently shipped none is
377
+ // a regression no test sees (the artifact still parses, on the closures).
372
378
  let target = ceiling
373
- for (let attempt = 0; attempt < 16; attempt++) {
379
+ for (;;) {
374
380
  const sites = selectHotSites(artifact.code, Object.values(artifact.rules), siteBytes, target - fixed, profile)
375
- if (sites.size === 0) return none
381
+ if (sites.size === 0) {
382
+ return {
383
+ ...none,
384
+ refusal: `a site within its ${Math.floor(ceiling)} B selection ceiling (the prelude alone is ${fixed} B)`,
385
+ }
386
+ }
376
387
  const bytes = emitAssemblySource(table, artifact, cfg, roots, true, sites).source.length
377
388
  if (bytes <= ceiling) return { sites, bytes: Math.ceil(bytes * SUPERCOMPILE_GROWTH) }
378
389
  // Scale by the overshoot, and by at least 1%: near the ceiling a site is
379
- // the unit, and the ratio alone can stall a few bytes over it until the
380
- // attempts run out and nothing ships.
390
+ // the unit, and the ratio alone can stall a few bytes over it.
381
391
  target *= Math.min(ceiling / bytes, 0.99)
382
392
  }
383
- return none
384
393
  }
385
394
 
386
395
  return {
@@ -47,7 +47,7 @@
47
47
  import type { ParseContext } from '../types.ts'
48
48
  import { regexCanMatchEmpty } from '../regex/first-set.ts'
49
49
  import {
50
- OP_ADJ, OP_ARMGATE, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
50
+ OP_ADJ, OP_ARMGATE, OP_RGATE, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
51
51
  OP_LABEL, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
52
52
  OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX,
53
53
  OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_WITHCTX, OP_XFORM,
@@ -60,13 +60,14 @@ import {
60
60
  import { emitShapeMatch, scanShapeFromRegex } from './scan-shapes.ts'
61
61
  import {
62
62
  CAP_OFF, CAP_ON, RAW_CAPTURE, RAW_OMIT, TRI_NONE, TRI_UNKNOWN,
63
- computeSiteLabels, reachableSites, type SiteLabel,
63
+ computeSiteLabels, gatedPassCollapse, passCollapse, reachableSites, scopeIsIdentity, triviaLeadRanges, type SiteLabel,
64
64
  } from './site-labels.ts'
65
65
  import {
66
66
  largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal,
67
- scalarTerminalNodeChild, scalarTerminalNotChild, type LeadingScalarSequence,
67
+ scalarTerminalNodeChild, scalarTerminalNotChild, scalarTerminalPeekChild, type LeadingScalarSequence,
68
68
  } from './scalar-terminal.ts'
69
69
  import { childSlots } from './child-slots.ts'
70
+ import { holdsGap } from './hold-gap.ts'
70
71
  import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
71
72
  import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA } from '../combinators/field-trivia.ts'
72
73
 
@@ -138,6 +139,9 @@ export const EMITTED_PARAMS = [
138
139
  // Appended for the deferred-commit trivia skip (`trivia-gap.ts`); old
139
140
  // factories ignore it.
140
141
  'skipDeferredTrivia',
142
+ // Appended for the pass-through collapse (`run-support.ts`); old factories
143
+ // ignore it.
144
+ 'collapseEmpty',
141
145
  ] as const
142
146
 
143
147
  /**
@@ -436,6 +440,30 @@ ${scanned}
436
440
  }`
437
441
  }
438
442
 
443
+ /**
444
+ * A lookahead term that HOLDS its gap (`hold-gap.ts`): the gap is scanned and
445
+ * kept, the lookahead runs at its end, and the cursor stays there for the next
446
+ * term, which cannot match empty.
447
+ */
448
+ function emitHoldTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string): string {
449
+ const fast = `const ${t}v=${callee}(input,cur,ctx)
450
+ if(${t}v===FAIL)return FAIL
451
+ ${dst}=${t}v
452
+ cur=EC.e`
453
+ if (l.tri === TRI_NONE) return fast
454
+ const held = `const ${t}s=${skip}(input,cur,ctx)
455
+ const ${t}v=${callee}(input,${t}s,ctx)
456
+ if(${t}v===FAIL)return FAIL
457
+ ${dst}=${t}v
458
+ cur=${t}s`
459
+ if (l.tri !== TRI_UNKNOWN) return held
460
+ return `if(ctx.trivia===undefined){
461
+ ${fast}
462
+ }else{
463
+ ${held}
464
+ }`
465
+ }
466
+
439
467
  /**
440
468
  * The repetition's LEAD-TRIVIA skip, as a BLOCK.
441
469
  *
@@ -603,6 +631,8 @@ export function emitAssemblySource(
603
631
  // the closure recognizer pool: distinct terminal rows sharing one spec share
604
632
  // both the recognizer and the ordinary-terminal lowering.
605
633
  const scalarSpecs = new Set<number>()
634
+ // Row nullability for `holdsGap`, memoised for this emit.
635
+ const nullable = new Map<number, boolean>()
606
636
  // Large choices are different: admitting every wrapped scalar would add a
607
637
  // recognizer call to many arms whose one-character gate already did all the
608
638
  // useful work. A multi-character literal can still refine an overlapping
@@ -1115,17 +1145,9 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1115
1145
  // The label is what makes this decidable: `tri >= 0` can ONLY have come from
1116
1146
  // an enclosing scope carrying that slot, and that scope set
1117
1147
  // `ctx.triviaKindLabels` and `_pfScan` from the same slot, so all three are
1118
- // already the values this row would write. `OP_SCOPE_PLAIN` has no root
1119
- // policy by construction; a policy-bearing `OP_SCOPE` aliases only when its
1120
- // literal policy is zero. `OP_SCOPE_CAP` also raises `captureTrivia`.
1121
- //
1122
- // `ki >= 0` is required rather than implied: `TRI_NONE` and `TRI_UNKNOWN` are
1123
- // themselves negative, so comparing a negative operand against a lattice
1124
- // element would read "unknown" as a match.
1125
- if ((op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[ip + 3]! === 0)) && code[ip + 1]! >= 0
1126
- && labels.at(ip).tri === code[ip + 1]!) {
1127
- return code[ip + 2]!
1128
- }
1148
+ // already the values this row would write. A scope with no trivia under
1149
+ // `TRI_NONE` is the same case (`scopeIsIdentity`).
1150
+ if (scopeIsIdentity(code, ip, labels.at(ip))) return code[ip + 2]!
1129
1151
  return undefined
1130
1152
  }
1131
1153
 
@@ -1325,6 +1347,23 @@ return ${child}(input,pos,ctx)
1325
1347
  }`
1326
1348
  }
1327
1349
 
1350
+ case OP_RGATE: {
1351
+ const child = link(code[ip + 2]!)
1352
+ if (cfg.tolerant || cfg.probe) return `${head}
1353
+ return ${child}(input,pos,ctx)
1354
+ }`
1355
+ const resolved = t.cc[code[ip + 1]!]!
1356
+ const ci = classes.push([resolved]) - 1
1357
+ classPlan.push([code[ip + 1]!])
1358
+ const cls = hoist('cl', `CLS[${ci}][0]`)
1359
+ const xf = fxRef(code[ip + 3]!)
1360
+ return `${head}
1361
+ const it=ctx._routed
1362
+ if(it!==undefined&&it.span.start===pos&&!classHas(${cls},lead(input,it.span.end))){ctx._fe=it.span.end;ctx._fx=${xf};return FAIL}
1363
+ return ${child}(input,pos,ctx)
1364
+ }`
1365
+ }
1366
+
1328
1367
  case OP_ARMGATE: {
1329
1368
  // `choice({ gate, combinator })`, as `assemble.ts` lowers it. A blocked
1330
1369
  // arm is SKIPPED, not failed (choice.ts `continue`): it clears the cut,
@@ -1663,9 +1702,9 @@ return FAIL
1663
1702
  const p = tmp()
1664
1703
  return `${head}
1665
1704
  ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${emitMark(p, L.buf, L.raw, sinks)}
1666
- const v=${child}(input,pos,ctx)
1705
+ ${cfg.probe ? 'const pb=ctx._probe?.best\n' : ''}const v=${child}(input,pos,ctx)
1667
1706
  ${emitRollback(p, L.buf, L.raw, sinks)}
1668
- if(v===FAIL){EC.e=pos;return null}
1707
+ ${cfg.probe ? 'if(ctx._probe!==undefined)ctx._probe.best=pb??null\n' : ''}if(v===FAIL){EC.e=pos;return null}
1669
1708
  ctx._fe=pos
1670
1709
  ctx._fx=${xf}
1671
1710
  return FAIL
@@ -1673,14 +1712,24 @@ return FAIL
1673
1712
  }
1674
1713
 
1675
1714
  case OP_PEEK: {
1715
+ const scalarChild = scalarTerminalPeekChild(code, ip)
1716
+ if (scalarChild >= 0) {
1717
+ const recognize = recognizerRef(code[scalarChild + 1]!)
1718
+ const xf = fxRef(code[ip + 2]!)
1719
+ return `${head}
1720
+ if(${recognize}(input,pos)<0){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1721
+ EC.e=pos
1722
+ return null
1723
+ }`
1724
+ }
1676
1725
  const child = link(code[ip + 1]!)
1677
1726
  const xf = fxRef(code[ip + 2]!)
1678
1727
  const p = tmp()
1679
1728
  return `${head}
1680
1729
  ${lookaheadMiss(code[ip + 1]!, `ctx._fe=pos;ctx._fx=${xf};return FAIL`)}${emitMark(p, L.buf, L.raw, sinks)}
1681
- const v=${child}(input,pos,ctx)
1730
+ ${cfg.probe ? 'const pb=ctx._probe?.best\n' : ''}const v=${child}(input,pos,ctx)
1682
1731
  ${emitRollback(p, L.buf, L.raw, sinks)}
1683
- if(v===FAIL){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1732
+ ${cfg.probe ? 'if(ctx._probe!==undefined)ctx._probe.best=pb??null\n' : ''}if(v===FAIL){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1684
1733
  EC.e=pos
1685
1734
  return null
1686
1735
  }`
@@ -1779,7 +1828,9 @@ return v
1779
1828
  // The callee expression hands it over AFTER the scan statement, right
1780
1829
  // before the call, so a field inside the trivia grammar cannot take it.
1781
1830
  const callee = leadingTriviaFieldAt(code, k, childIp) ? `(LEAD.at=cur,${kids[i]!})` : kids[i]!
1782
- parts.push(emitTerm(callee, vn, tmp(), L, skipFor(L)))
1831
+ parts.push(holdsGap(code, k, base, n, i, nullable)
1832
+ ? emitHoldTerm(callee, vn, tmp(), L, skipFor(L))
1833
+ : emitTerm(callee, vn, tmp(), L, skipFor(L)))
1783
1834
  }
1784
1835
  names.push(vn)
1785
1836
  }
@@ -2433,6 +2484,35 @@ return nd
2433
2484
  // The trailing-trivia bit (128) rides on the children-only shape as it
2434
2485
  // does in the closure assembler: a consume after the body, no capture.
2435
2486
  const leanFlags = flags & ~128
2487
+ // THE PASS-THROUGH COLLAPSE (`run-support.ts` `collapseEmpty`): a
2488
+ // collapse over one node is that node. `passCollapse` is the closure
2489
+ // engine's test too.
2490
+ if (build !== undefined && passCollapse(code, ip, hostCst)) {
2491
+ return `${head}
2492
+ const v=${child}(input,pos,ctx)
2493
+ if(v!==undefined)return v
2494
+ return collapseEmpty(ctx,${build},input,pos,EC.e,${flags === 32})
2495
+ }`
2496
+ }
2497
+ // A GATED PASS COLLAPSE (`site-labels.ts` `gatedPassCollapse`): where the
2498
+ // character at `pos` opens none of the body's optional terms and no
2499
+ // trivia, the body is its node term, run as a pass-through collapse.
2500
+ // Not under recovery or a probe: there a gated-out term still runs.
2501
+ const gated = build !== undefined && !cfg.tolerant && !cfg.probe ? gatedPassCollapse(code, ip, hostCst) : undefined
2502
+ const triviaLead = gated === undefined ? undefined : triviaLeadRanges(labels.at(code[ip + 2]!).tri, t.trivia)
2503
+ const gp = gated !== undefined && triviaLead !== undefined && triviaLead.length <= 16
2504
+ ? `const gc=lead(input,pos)
2505
+ if(!(${[
2506
+ ...gated.leads.map(g => `!(${gateMissTest(g)})`),
2507
+ ...gated.lits.map(i => `gc===${(k[i] as string).codePointAt(0)!}`),
2508
+ ...triviaLead.map(r => r.lo === r.hi ? `gc===${r.lo}` : `(gc>=${r.lo}&&gc<=${r.hi})`),
2509
+ ].join('||') || 'false'})){
2510
+ const gv=${link(gated.last)}(input,pos,ctx)
2511
+ if(gv!==undefined)return gv
2512
+ return collapseEmpty(ctx,${build},input,pos,EC.e,${flags === 32})
2513
+ }
2514
+ `
2515
+ : ''
2436
2516
  if (!hostCst && !tracked && build !== undefined && proj < 0
2437
2517
  && (leanFlags === 2 || (flags === 18 || flags === 34))) {
2438
2518
  const fields = flags === 18
@@ -2470,7 +2550,7 @@ sCh.push(nd)
2470
2550
  if(sRaw!==undefined)sRaw.push(rawEntry(nd,input,pos,end))
2471
2551
  }`
2472
2552
  return `${head}
2473
- const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
2553
+ ${gp}const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
2474
2554
  const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf,sFields=ctx._fields
2475
2555
  const flat=[]
2476
2556
  ctx._cstBuf=undefined
@@ -2565,7 +2645,7 @@ const hostKids=kids
2565
2645
  const rawKids=buf.raw??(buf.rawSingle!==undefined?[buf.rawSingle]:EMPTY_CH)
2566
2646
  const tlog=buf.tl??EMPTY_TLOG`
2567
2647
  return `${head}
2568
- const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
2648
+ ${!hostCst && !tracked ? gp : ''}const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
2569
2649
  const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf
2570
2650
  ${openCapture}
2571
2651
  ctx.captureTrivia=${captureWide}
@@ -2656,10 +2736,19 @@ bind:function(xs,ps){_xScan=xs
2656
2736
  ${externals.map((ip, i) => `_xp${ip}=ps[${i}]`).join('\n')}},
2657
2737
  scan:function(s){const o=_pfScan;_pfScan=s;return o}`
2658
2738
  // One wrapper per external: the closure engine reads its OWN installed trivia
2659
- // scanner, so the emitted one is handed across for the call and restored.
2739
+ // scanner, so the emitted one is handed across for the call and restored. A
2740
+ // site whose label names its trivia hands that trivia's own scanner, which is
2741
+ // what any scope installing it would have installed: the emitted slot is read
2742
+ // only where the labels cannot say (`site-labels.ts` `scanHandoffNeeded`).
2743
+ const scannerFor = (ip: number): string => {
2744
+ const ki = labels.at(ip).tri
2745
+ if (ki === TRI_UNKNOWN) return '_pfScan'
2746
+ if (ki === TRI_NONE) return 'null'
2747
+ return swapLegal && !triviaLabelled[ki]! && t.triviaScan[ki] != null ? hoist('ts', `TRIVIASCAN[${ki}]`) : 'null'
2748
+ }
2660
2749
  const externalDefs = select === undefined ? '' : `let _xScan
2661
2750
  ${externals.map(ip => `let _xp${ip}
2662
- function _px${ip}(input,pos,ctx){const o=_xScan(_pfScan);const v=_xp${ip}(input,pos,ctx);_xScan(o);return v}`).join('\n')}
2751
+ function _px${ip}(input,pos,ctx){const o=_xScan(${scannerFor(ip)});const v=_xp${ip}(input,pos,ctx);_xScan(o);return v}`).join('\n')}
2663
2752
  `
2664
2753
 
2665
2754
  // The per-label trivia scans sit AFTER the hoisted pool they close over: they
package/src/table/emit.ts CHANGED
@@ -35,7 +35,9 @@ function emitClassPool(classes: readonly string[]): string {
35
35
  if (classes.length < 2) return array
36
36
  const delimiter = '|/~?^`'.split('').find(ch => classes.every(spec => !spec.includes(ch)))
37
37
  if (delimiter === undefined) return array
38
- const split = `${jsString(classes.join(delimiter))}.split(${jsString(delimiter)})`
38
+ // A call a bundler cannot prove pure is kept even when nothing reads its result,
39
+ // so a sibling variant shaken out of a bundle left its whole pool behind.
40
+ const split = `/* @__PURE__ */ ${jsString(classes.join(delimiter))}.split(${jsString(delimiter)})`
39
41
  return split.length < array.length ? split : array
40
42
  }
41
43
 
@@ -1,4 +1,5 @@
1
1
  import { scanRefs } from './program.ts'
2
+ import { factorSharedEnds } from './factor-choice.ts'
2
3
  import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
3
4
  import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
4
5
  import { childrenOf } from '../analysis/gating.ts'
@@ -16,7 +17,7 @@ import {
16
17
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
17
18
  OP_LIVE, OP_ATTEMPT, OP_LABEL,
18
19
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
19
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_COV,
20
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_COV,
20
21
  OP_LEX_BODY, OP_LEX_PROGRAM,
21
22
  } from './ops.ts'
22
23
  import { adjacencyExpected } from '../combinators/adjacency.ts'
@@ -27,8 +28,8 @@ import type { BalancedSpec } from '../combinators/scanTo.ts'
27
28
  import type { DispatchSpec, LexBodySpec, LexProgramSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
28
29
  import {
29
30
  lexProgramDigest,
30
- LEX_NODE_CHOICE2, LEX_NODE_CHOICE4, LEX_NODE_CHOICE8,
31
- LEX_NODE_NOT, LEX_NODE_OPTIONAL,
31
+ LEX_NODE_CHOICE,
32
+ LEX_NODE_NOT, LEX_NODE_OPTIONAL, LEX_NODE_REPEAT,
32
33
  LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
33
34
  LEX_NODE_TERMINAL,
34
35
  } from './lex-program.ts'
@@ -298,15 +299,18 @@ class Encoder {
298
299
  }
299
300
  case 'choice': {
300
301
  for (const arm of node.arms) if (!emitNode(arm)) return false
301
- const op = node.arms.length === 2 ? LEX_NODE_CHOICE2
302
- : node.arms.length === 4 ? LEX_NODE_CHOICE4
303
- : node.arms.length === 8 ? LEX_NODE_CHOICE8 : -1
304
- if (op < 0) return false
305
- words.push(op, this.expected(node.expected))
302
+ // The arm classes and the exclusive (one-arm) decision are the
303
+ // character choice's own (`case 'choice'`), under the trivia the
304
+ // encoder would hold at that row, so both lowerings rank the same
305
+ // failures.
306
+ const final = classifyFinalChoice(
307
+ node.parsers, this.refResolver(), node.cleared ? undefined : this.activeTrivia,
308
+ )
309
+ const classes = final.firstSets.map((first, i) => final.nullable[i] ? -1 : this.charClass(first))
310
+ const exclusive = final.exclusive && classes.every(cls => cls >= 0)
311
+ words.push(LEX_NODE_CHOICE, node.arms.length, exclusive ? 1 : 0, this.expected(node.expected))
306
312
  for (let i = 0; i < node.arms.length; i++) {
307
- const first = node.firstSets[i]!
308
- words.push(first.kind === 'ranges' ? this.charClass(first) : -1)
309
- words.push(this.expected(node.armExpected[i]!))
313
+ words.push(classes[i]!, this.expected(node.armExpected[i]!))
310
314
  }
311
315
  return true
312
316
  }
@@ -318,6 +322,10 @@ class Encoder {
318
322
  if (!emitNode(node.body)) return false
319
323
  words.push(LEX_NODE_OPTIONAL)
320
324
  return true
325
+ case 'repeat':
326
+ if (!emitNode(node.body)) return false
327
+ words.push(LEX_NODE_REPEAT, node.min, node.max)
328
+ return true
321
329
  }
322
330
  }
323
331
  if (!emitNode(body.root)) return undefined
@@ -940,6 +948,54 @@ class Encoder {
940
948
  return this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), ip)
941
949
  }
942
950
 
951
+ /**
952
+ * The `OP_RGATE` operands of a choice arm that opens with `routed()`, or
953
+ * `undefined`. Through `node()`/scope wrappers and first sequence terms, the
954
+ * arm's next term after the routed token is found; the gate is that term's
955
+ * first characters, with the trivia in force at the boundary before it, so
956
+ * the arm is entered only where it can get past the token. The term must be
957
+ * unable to match empty or to commit, so a miss is exactly that term failing
958
+ * at the token's end with its own set.
959
+ */
960
+ private routedArmGate(arm: Combinator<unknown>): { cls: number; fx: number } | undefined {
961
+ type Follow = 'end' | { next: Combinator<unknown>; boundary: Combinator<unknown> | undefined } | undefined
962
+ const walk = (p: Combinator<unknown>, trivia: Combinator<unknown> | undefined, depth: number): Follow => {
963
+ if (depth > 16) return undefined
964
+ const d = p._def as ParserDef
965
+ switch (d.tag) {
966
+ case 'routed': return 'end'
967
+ case 'node': return walk(d.parser, trivia, depth + 1)
968
+ case 'grammar':
969
+ return walk(d.parser, d.clearTrivia === true ? undefined : d.triviaParser ?? trivia, depth + 1)
970
+ case 'sequence': {
971
+ const head = d.parsers[0]
972
+ if (head === undefined) return undefined
973
+ const r = walk(head, trivia, depth + 1)
974
+ if (r !== 'end') return r
975
+ const next = d.parsers[1]
976
+ return next === undefined ? 'end' : { next, boundary: trivia }
977
+ }
978
+ default: return undefined
979
+ }
980
+ }
981
+ const follow = walk(arm, this.activeTrivia, 0)
982
+ if (follow === undefined || follow === 'end') return undefined
983
+ const rr = this.refResolver()
984
+ const { next, boundary } = follow
985
+ if (matchesEmpty(next, new Set(), rr) || mayCommitFailure(next, rr, this.commitFailureMemo)) return undefined
986
+ // A miss reports `deriveExpected(next)`, a static set. Only a terminal or a
987
+ // label fails with exactly that set: a sequence over an `optional` prefix
988
+ // reports the prefix's labels statically but not when it runs, and a choice
989
+ // whose arms share an opener reports that opener once statically, once per
990
+ // arm when it runs.
991
+ const nextTag = (next._def as ParserDef).tag
992
+ if (nextTag !== 'literal' && nextTag !== 'regex' && nextTag !== 'keywords' && nextTag !== 'label') return undefined
993
+ const first = firstSetOf(next, new Set(), rr, boundary)
994
+ const cls = this.charClass(boundary === undefined ? first : union(first, firstSetOf(boundary, new Set(), rr)))
995
+ if (cls < 0) return undefined
996
+ return { cls, fx: this.expected(deriveExpected(next)) }
997
+ }
998
+
943
999
  node(p: Combinator<unknown>): Emitted {
944
1000
  // See `refMemo`: a reference is memoised per REFERENCING trivia scope, every
945
1001
  // other construct once for the program.
@@ -1101,6 +1157,12 @@ class Encoder {
1101
1157
  return head
1102
1158
  }
1103
1159
  case 'choice': {
1160
+ // Arms that differ in one term are that term's choice (`factor-choice.ts`).
1161
+ // Not under a coverage plan, which credits the arms as written.
1162
+ if (this.plan === undefined) {
1163
+ const factored = factorSharedEnds(d)
1164
+ if (factored !== undefined) return this.node(factored).ip
1165
+ }
1104
1166
  // `greedyClassify` is NOT a choice at all — one arm runs and another arm
1105
1167
  // is credited — so it gets its own row rather than an arm ordering.
1106
1168
  if (d.strategy.tag === 'greedyClassify') {
@@ -1169,11 +1231,15 @@ class Encoder {
1169
1231
  const kids = arms.map((c, k) => {
1170
1232
  const src = order[k]!
1171
1233
  const checks = d.autoNot[src]
1172
- const inner = this.node(c).ip
1234
+ const gate = d.gates[src]
1235
+ const plain = (checks === null || checks === undefined || checks.length === 0)
1236
+ && (gate === null || gate === undefined)
1237
+ const routedGate = plain ? this.routedArmGate(c) : undefined
1238
+ const body = this.node(c).ip
1239
+ const inner = routedGate === undefined ? body : this.emit(OP_RGATE, routedGate.cls, body, routedGate.fx)
1173
1240
  const ip = checks === null || checks === undefined || checks.length === 0
1174
1241
  ? inner
1175
1242
  : this.reject(inner, checks)
1176
- const gate = d.gates[src]
1177
1243
  const gated = gate === null || gate === undefined
1178
1244
  ? ip
1179
1245
  : this.emit(OP_ARMGATE, this.fn(gate, d.gateSrcs?.[src] ?? null), ip, this.expected(deriveExpected(c)))
@@ -1794,7 +1860,7 @@ class Encoder {
1794
1860
  }
1795
1861
  const slots = (ip: number): number[] => {
1796
1862
  switch (this.code[ip]) {
1797
- case OP_GATE: return [ip + 2]
1863
+ case OP_GATE: case OP_RGATE: return [ip + 2]
1798
1864
  case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: return [ip + 1]
1799
1865
  case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_SOURCE_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
1800
1866
  case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
package/src/table/exec.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  OP_LIVE,
23
23
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
24
24
  OP_COV,
25
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
25
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
26
26
  OP_LEX_BODY, OP_LEX_PROGRAM,
27
27
  } from './ops.ts'
28
28
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
@@ -49,7 +49,7 @@ import {
49
49
  type CompactProgram, type ResolvedClass, type ResolvedDispatch, type ResolvedDispatchSpec, type ResolvedTable,
50
50
  type SubtreeRef, type TableProgram, type TableRule,
51
51
  } from './program.ts'
52
- import { scalarTerminalNotChild } from './scalar-terminal.ts'
52
+ import { scalarTerminalNotChild, scalarTerminalPeekChild } from './scalar-terminal.ts'
53
53
 
54
54
  /**
55
55
  * THE SHARED DRIVER.
@@ -923,6 +923,7 @@ function makeDriver(
923
923
  const lit = k[code[child + 1]!] as string
924
924
  if (!input.startsWith(lit, cur)) {
925
925
  ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
926
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, cur)
926
927
  { if (REC) ctx._sync = inheritedSync; return FAIL }
927
928
  }
928
929
  const e = cur + lit.length
@@ -937,6 +938,7 @@ function makeDriver(
937
938
  const m = re.exec(input)
938
939
  if (m === null) {
939
940
  ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
941
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, cur)
940
942
  { if (REC) ctx._sync = inheritedSync; return FAIL }
941
943
  }
942
944
  const mv = m[0]
@@ -1118,6 +1120,16 @@ function makeDriver(
1118
1120
  return sup
1119
1121
  }
1120
1122
 
1123
+ case OP_RGATE: {
1124
+ const item = ctx._routed
1125
+ if (item !== undefined && item.span.start === pos && ctx._probe === undefined && !ctx._tolerant
1126
+ && !classHas(cc[code[ip + 1]!]!, lead(input, item.span.end))) {
1127
+ ctx._fe = item.span.end; ctx._fx = fx[code[ip + 3]!] as string[]
1128
+ return FAIL
1129
+ }
1130
+ return exec(code[ip + 2]!, input, pos, ctx)
1131
+ }
1132
+
1121
1133
  case OP_ARMGATE: {
1122
1134
  if (!(fns[code[ip + 1]!] as (s: unknown) => boolean)(ctx.state)) {
1123
1135
  // choice.ts:150 is a `continue`, not a failure — the arm is treated as
@@ -1574,15 +1586,14 @@ function makeDriver(
1574
1586
  }
1575
1587
 
1576
1588
  case OP_NOT: {
1577
- // The shipping generic closure/emitted NOT bodies predate pure-global
1578
- // lookahead rollback. Keep the reference driver aligned with them for
1579
- // every generic child; only the direct-terminal shape this lane replaces
1580
- // needs the reference oracle to hide the terminal's diagnostic writes.
1589
+ // The generic closure/emitted NOT bodies leave the child's failure
1590
+ // registers; only the direct-terminal shape hides them. Every NOT hides
1591
+ // the child's completions from the probe, as `not.ts` does.
1581
1592
  const scalarChild = scalarTerminalNotChild(code, ip) >= 0
1582
1593
  const savedFc = scalarChild ? ctx._fc : false
1583
1594
  const savedFe = scalarChild ? ctx._fe : -1
1584
1595
  const savedFx = scalarChild ? ctx._fx : undefined
1585
- const savedProbe = scalarChild ? ctx._probe?.best : undefined
1596
+ const savedProbe = ctx._probe?.best
1586
1597
  const need = rollbackNeeded(ctx)
1587
1598
  const mRaw = need ? cstRawLen(ctx) : 0
1588
1599
  const mTl = need ? cstTlLen(ctx) : 0
@@ -1597,8 +1608,8 @@ function makeDriver(
1597
1608
  ctx._fc = savedFc
1598
1609
  ctx._fe = savedFe
1599
1610
  ctx._fx = savedFx
1600
- if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
1601
1611
  }
1612
+ if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
1602
1613
  if (v === FAIL) { EC.e = pos; return null }
1603
1614
  // `not.ts:50` — the ASSERTION's own set, at the assertion's position.
1604
1615
  ctx._fe = pos
@@ -1607,6 +1618,12 @@ function makeDriver(
1607
1618
  }
1608
1619
 
1609
1620
  case OP_PEEK: {
1621
+ // A direct terminal is reduced to its recognizer in both assemblies
1622
+ // (`scalarTerminalPeekChild`), as under `not()`: hide its own diagnostic
1623
+ // writes here too.
1624
+ const scalarChild = scalarTerminalPeekChild(code, ip) >= 0
1625
+ const savedFc = scalarChild ? ctx._fc : false
1626
+ const savedProbe = ctx._probe?.best
1610
1627
  const need = rollbackNeeded(ctx)
1611
1628
  const mRaw = need ? cstRawLen(ctx) : 0
1612
1629
  const mTl = need ? cstTlLen(ctx) : 0
@@ -1617,6 +1634,8 @@ function makeDriver(
1617
1634
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1618
1635
  const v = exec(code[ip + 1]!, input, pos, ctx)
1619
1636
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1637
+ if (scalarChild) ctx._fc = savedFc
1638
+ if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
1620
1639
  // `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
1621
1640
  if (v === FAIL) { ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]; return FAIL }
1622
1641
  EC.e = pos