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
@@ -47,10 +47,10 @@
47
47
  import type { ParseContext } from '../types.ts'
48
48
  import { regexCanMatchEmpty } from '../regex/first-set.ts'
49
49
  import {
50
- OP_ADJ, 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
- OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
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,
54
54
  OP_LEX_BODY, OP_LEX_PROGRAM,
55
55
  } from './ops.ts'
56
56
  import {
@@ -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
 
@@ -79,6 +80,14 @@ export type EmittedAssembly = {
79
80
  readonly end: () => number
80
81
  readonly begin: (ctx: ParseContext) => void
81
82
  readonly finish: () => void
83
+ /**
84
+ * Present only on a SELECTIVE assembly: the unselected sites its bodies call,
85
+ * the hook that binds them to closure pieces (with a setter for the closure
86
+ * engine's installed trivia scanner), and that same setter for this scope's.
87
+ */
88
+ readonly ext?: readonly number[]
89
+ readonly bind?: (setClosureScan: (s: unknown) => unknown, pieces: readonly EmittedPiece[]) => void
90
+ readonly scan?: (s: unknown) => unknown
82
91
  }
83
92
  /** The `new Function` result. Its parameters are `EMITTED_PARAMS`, in order. */
84
93
  export type EmittedFactory = (...args: readonly unknown[]) => EmittedAssembly
@@ -127,6 +136,12 @@ export const EMITTED_PARAMS = [
127
136
  // Appended for trivia-attributing fields (`field(…, { leadingTrivia })`);
128
137
  // old factories ignore them.
129
138
  'LEAD', 'triviaFieldCapture',
139
+ // Appended for the deferred-commit trivia skip (`trivia-gap.ts`); old
140
+ // factories ignore it.
141
+ 'skipDeferredTrivia',
142
+ // Appended for the pass-through collapse (`run-support.ts`); old factories
143
+ // ignore it.
144
+ 'collapseEmpty',
130
145
  ] as const
131
146
 
132
147
  /**
@@ -162,7 +177,7 @@ function _skipTrivia(input,cur,ctx){
162
177
  const s=_pfScan
163
178
  if(s!==null&&ctx._triviaLog===undefined&&!(ctx.captureTrivia===true&&(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)))return s(input,cur)
164
179
  if(s!==null)return skipTriviaScanned(s,input,cur,ctx)
165
- if(needsDeferredTriviaCommit(ctx))return commitTriviaScan(scanTriviaCompact(input,cur,ctx))
180
+ if(needsDeferredTriviaCommit(ctx))return skipDeferredTrivia(input,cur,ctx)
166
181
  return advanceTrivia(input,cur,ctx)
167
182
  }
168
183
  function _pushLeaf(ctx,value,s,e){pushCstLeaf(ctx,{_tag:'leaf',value,span:{start:s,end:e}})}
@@ -425,6 +440,30 @@ ${scanned}
425
440
  }`
426
441
  }
427
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
+
428
467
  /**
429
468
  * The repetition's LEAD-TRIVIA skip, as a BLOCK.
430
469
  *
@@ -479,6 +518,8 @@ export type EmitResult = {
479
518
  readonly armExpected: readonly (readonly string[])[][]
480
519
  /** The same three pools as indices, for a build-time emitter. */
481
520
  readonly plan: PoolPlan
521
+ /** Source length of each emitted site body, by site — what selection prices. */
522
+ readonly siteBytes: ReadonlyMap<number, number>
482
523
  }
483
524
 
484
525
  /** The candidate mask for one `CLS` row — the ONE definition, shared with the emitter. */
@@ -526,6 +567,11 @@ export function rebuildPools(
526
567
  * ordinary factory literal in a macro artifact. It permits macro-only code
527
568
  * shaping without perturbing the runtime `compile()` emitter, whose generated
528
569
  * parser must remain byte-identical when the optimization cannot affect it.
570
+ *
571
+ * `select`, when given, makes this a SELECTIVE (hybrid) assembly: only sites
572
+ * whose alias-resolved target is in the set get a body. Every other child a
573
+ * body calls is an EXTERNAL — `_px<ip>`, a wrapper bound after construction to
574
+ * the closure engine's piece for that site (`assemble.ts`).
529
575
  */
530
576
  export function emitAssemblySource(
531
577
  t: ResolvedTable,
@@ -541,6 +587,7 @@ export function emitAssemblySource(
541
587
  },
542
588
  extraIps: readonly number[] = [],
543
589
  staticBuild = false,
590
+ select?: ReadonlySet<number>,
544
591
  ): EmitResult {
545
592
  const { code, k, fx, disp, dsp, triviaLabelled } = t
546
593
  const swapLegal = !cfg.trackLines
@@ -584,6 +631,8 @@ export function emitAssemblySource(
584
631
  // the closure recognizer pool: distinct terminal rows sharing one spec share
585
632
  // both the recognizer and the ordinary-terminal lowering.
586
633
  const scalarSpecs = new Set<number>()
634
+ // Row nullability for `holdsGap`, memoised for this emit.
635
+ const nullable = new Map<number, boolean>()
587
636
  // Large choices are different: admitting every wrapped scalar would add a
588
637
  // recognizer call to many arms whose one-character gate already did all the
589
638
  // useful work. A multi-character literal can still refine an overlapping
@@ -716,6 +765,9 @@ export function emitAssemblySource(
716
765
 
717
766
  const bodies: string[] = []
718
767
  const byIp = new Map<number, string>()
768
+ /** Unselected sites a selective assembly calls into — see `select`. */
769
+ const externals: number[] = []
770
+ const siteBytes = new Map<number, number>()
719
771
  const reached = new Set<number>()
720
772
  const prelude: string[] = []
721
773
  const skipDefs: string[] = []
@@ -900,6 +952,28 @@ return 1
900
952
  * site property — nothing in `src` writes it except a caller-supplied
901
953
  * `ParseContext` (`types.ts:524`) — so it stays the one runtime read.
902
954
  */
955
+ /** The test an `OP_GATE` row at `gip` fails on, with its class hoisted. */
956
+ function gateMissTest(gip: number): string {
957
+ const resolved = t.cc[code[gip + 1]!]!
958
+ const ci = classes.push([resolved]) - 1
959
+ classPlan.push([code[gip + 1]!])
960
+ const cls = hoist('cl', `CLS[${ci}][0]`)
961
+ return isAsciiOnlyClass(resolved)
962
+ ? `${cls}.ascii[input.charCodeAt(pos)]!==1`
963
+ : `!classHas(${cls},lead(input,pos))`
964
+ }
965
+
966
+ /**
967
+ * An `optional`/`not`/`peek` child's first-character gate (`encode.ts`
968
+ * `lookaheadGate`), tested BEFORE the wrapper's mark: a miss is the wrapper's
969
+ * decided outcome, `miss`. Nothing where the child has no gate, or where a
970
+ * probe or tolerant parse forwards through it (`aliasOf`).
971
+ */
972
+ function lookaheadMiss(child: number, miss: string): string {
973
+ if (code[child] !== OP_GATE || cfg.tolerant || cfg.probe) return ''
974
+ return `if(${gateMissTest(child)}){${miss}}\n`
975
+ }
976
+
903
977
  function skipFor(l: SiteLabel): string {
904
978
  if (l.tri < 0) return '_skipTrivia'
905
979
  const ki = l.tri
@@ -919,9 +993,9 @@ return 1
919
993
  // through `scanTrivia`. `needsDeferredTriviaCommit` is implied by an open
920
994
  // buffer, so an in-node site skips the call that asks.
921
995
  skipDefs.push(l.buf
922
- ? `function ${nm}(input,cur,ctx){return commitTriviaScan(scanTriviaCompact(input,cur,ctx))}`
996
+ ? `function ${nm}(input,cur,ctx){return skipDeferredTrivia(input,cur,ctx)}`
923
997
  : `function ${nm}(input,cur,ctx){
924
- if(needsDeferredTriviaCommit(ctx))return commitTriviaScan(scanTriviaCompact(input,cur,ctx))
998
+ if(needsDeferredTriviaCommit(ctx))return skipDeferredTrivia(input,cur,ctx)
925
999
  return advanceTrivia(input,cur,ctx)}`)
926
1000
  return nm
927
1001
  }
@@ -1002,12 +1076,20 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1002
1076
  const target = resolveAlias(ip)
1003
1077
  const done = byIp.get(target)
1004
1078
  if (done !== undefined) return done
1079
+ if (select !== undefined && !select.has(target)) {
1080
+ const ext = `_px${target}`
1081
+ byIp.set(target, ext)
1082
+ externals.push(target)
1083
+ return ext
1084
+ }
1005
1085
  const fname = `_pf${target}`
1006
1086
  // Reserved BEFORE lowering, so a back-edge into a site still in flight binds
1007
1087
  // to the hoisted declaration rather than to a forwarding stub. This is the
1008
1088
  // whole of `assemble.ts`'s `inFlight` map and its one shared closure.
1009
1089
  byIp.set(target, fname)
1010
- bodies.push(lower(target, fname))
1090
+ const body = lower(target, fname)
1091
+ siteBytes.set(target, body.length)
1092
+ bodies.push(body)
1011
1093
  return fname
1012
1094
  }
1013
1095
 
@@ -1063,17 +1145,9 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1063
1145
  // The label is what makes this decidable: `tri >= 0` can ONLY have come from
1064
1146
  // an enclosing scope carrying that slot, and that scope set
1065
1147
  // `ctx.triviaKindLabels` and `_pfScan` from the same slot, so all three are
1066
- // already the values this row would write. `OP_SCOPE_PLAIN` has no root
1067
- // policy by construction; a policy-bearing `OP_SCOPE` aliases only when its
1068
- // literal policy is zero. `OP_SCOPE_CAP` also raises `captureTrivia`.
1069
- //
1070
- // `ki >= 0` is required rather than implied: `TRI_NONE` and `TRI_UNKNOWN` are
1071
- // themselves negative, so comparing a negative operand against a lattice
1072
- // element would read "unknown" as a match.
1073
- if ((op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[ip + 3]! === 0)) && code[ip + 1]! >= 0
1074
- && labels.at(ip).tri === code[ip + 1]!) {
1075
- return code[ip + 2]!
1076
- }
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]!
1077
1151
  return undefined
1078
1152
  }
1079
1153
 
@@ -1266,19 +1340,57 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1266
1340
 
1267
1341
  case OP_GATE: {
1268
1342
  const child = link(code[ip + 2]!)
1343
+ const xf = fxRef(code[ip + 3]!)
1344
+ return `${head}
1345
+ if(${gateMissTest(ip)}){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1346
+ return ${child}(input,pos,ctx)
1347
+ }`
1348
+ }
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
+ }`
1269
1355
  const resolved = t.cc[code[ip + 1]!]!
1270
1356
  const ci = classes.push([resolved]) - 1
1271
1357
  classPlan.push([code[ip + 1]!])
1272
1358
  const cls = hoist('cl', `CLS[${ci}][0]`)
1273
1359
  const xf = fxRef(code[ip + 3]!)
1274
1360
  return `${head}
1275
- if(${staticBuild && isAsciiOnlyClass(resolved)
1276
- ? `${cls}.ascii[input.charCodeAt(pos)]!==1`
1277
- : `!classHas(${cls},lead(input,pos))`}){ctx._fe=pos;ctx._fx=${xf};return FAIL}
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
+
1367
+ case OP_ARMGATE: {
1368
+ // `choice({ gate, combinator })`, as `assemble.ts` lowers it. A blocked
1369
+ // arm is SKIPPED, not failed (choice.ts `continue`): it clears the cut,
1370
+ // sets no failure position for the ordered path to rank, and leaves the
1371
+ // arm's own expected set for the dispatch path.
1372
+ const pred = fnRef(code[ip + 1]!)
1373
+ const child = link(code[ip + 2]!)
1374
+ const xf = fxRef(code[ip + 3]!)
1375
+ return `${head}
1376
+ if(!${pred}(ctx.state)){ctx._fc=false;ctx._fe=-1;ctx._fx=${xf};return FAIL}
1278
1377
  return ${child}(input,pos,ctx)
1279
1378
  }`
1280
1379
  }
1281
1380
 
1381
+ case OP_WITHCTX: {
1382
+ // `withCtx(extra | derive, p)`, as `assemble.ts` lowers it: a negative
1383
+ // operand is a derive callback over the outer state, else a constant.
1384
+ // `finally` restores the outer state on every exit, a throw included.
1385
+ const ki = code[ip + 1]!
1386
+ const child = link(code[ip + 2]!)
1387
+ return `${head}
1388
+ const saved=ctx.state
1389
+ ctx.state=${ki < 0 ? `${fnRef(~ki)}(saved)` : kRef(ki)}
1390
+ try{return ${child}(input,pos,ctx)}finally{ctx.state=saved}
1391
+ }`
1392
+ }
1393
+
1282
1394
  case OP_XFORM: {
1283
1395
  const fn = fnRef(code[ip + 1]!)
1284
1396
  const child = link(code[ip + 2]!)
@@ -1589,10 +1701,10 @@ return FAIL
1589
1701
  const xf = fxRef(code[ip + 2]!)
1590
1702
  const p = tmp()
1591
1703
  return `${head}
1592
- ${emitMark(p, L.buf, L.raw, sinks)}
1593
- const v=${child}(input,pos,ctx)
1704
+ ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${emitMark(p, L.buf, L.raw, sinks)}
1705
+ ${cfg.probe ? 'const pb=ctx._probe?.best\n' : ''}const v=${child}(input,pos,ctx)
1594
1706
  ${emitRollback(p, L.buf, L.raw, sinks)}
1595
- 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}
1596
1708
  ctx._fe=pos
1597
1709
  ctx._fx=${xf}
1598
1710
  return FAIL
@@ -1600,14 +1712,24 @@ return FAIL
1600
1712
  }
1601
1713
 
1602
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
+ }
1603
1725
  const child = link(code[ip + 1]!)
1604
1726
  const xf = fxRef(code[ip + 2]!)
1605
1727
  const p = tmp()
1606
1728
  return `${head}
1607
- ${emitMark(p, L.buf, L.raw, sinks)}
1608
- const v=${child}(input,pos,ctx)
1729
+ ${lookaheadMiss(code[ip + 1]!, `ctx._fe=pos;ctx._fx=${xf};return FAIL`)}${emitMark(p, L.buf, L.raw, sinks)}
1730
+ ${cfg.probe ? 'const pb=ctx._probe?.best\n' : ''}const v=${child}(input,pos,ctx)
1609
1731
  ${emitRollback(p, L.buf, L.raw, sinks)}
1610
- 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}
1611
1733
  EC.e=pos
1612
1734
  return null
1613
1735
  }`
@@ -1618,7 +1740,7 @@ return null
1618
1740
  const clean = !REC && failureRollbackClean(prog, ip)
1619
1741
  const p = tmp()
1620
1742
  return `${head}
1621
- ${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1743
+ ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1622
1744
  ctx._fc=false
1623
1745
  const v=${child}(input,pos,ctx)
1624
1746
  if(v===FAIL){
@@ -1706,7 +1828,9 @@ return v
1706
1828
  // The callee expression hands it over AFTER the scan statement, right
1707
1829
  // before the call, so a field inside the trivia grammar cannot take it.
1708
1830
  const callee = leadingTriviaFieldAt(code, k, childIp) ? `(LEAD.at=cur,${kids[i]!})` : kids[i]!
1709
- 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)))
1710
1834
  }
1711
1835
  names.push(vn)
1712
1836
  }
@@ -1735,7 +1859,7 @@ return v
1735
1859
  // array index feeding ONE call site, which is the megamorphic site
1736
1860
  // this unit exists to remove.
1737
1861
  const armSwitch = arms.map((a, i) => `case ${i}:v=${a}(input,pos,ctx);break`).join('\n')
1738
- const directAscii = staticBuild && table.hi.length === 0
1862
+ const directAscii = table.hi.length === 0
1739
1863
  return `${head}
1740
1864
  const c=${directAscii ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
1741
1865
  let arm=-1
@@ -1766,7 +1890,7 @@ return FAIL
1766
1890
  const expected = Array.from({ length: n }, (_, i) => fxRef(code[base + n + i]!))
1767
1891
  const gates = Array.from({ length: n }, (_, i) => table.armCls[i] ?? null)
1768
1892
  const maskable = n <= 32
1769
- const directAsciiMask = staticBuild && maskable && gates.every(isAsciiOnlyClass)
1893
+ const directAsciiMask = maskable && gates.every(isAsciiOnlyClass)
1770
1894
  const gateRefs = directAsciiMask
1771
1895
  ? []
1772
1896
  : Array.from({ length: n }, (_, i) => hoist('g', `DISP[${di}].armCls[${i}]`))
@@ -2001,6 +2125,9 @@ ${rollbackFor(i)}
2001
2125
  // A zero compatible-arm mask is stronger than a speculative miss: no
2002
2126
  // arm can run, so the choice's encoded flat expected set is already
2003
2127
  // the exact result. Do not walk the arm ladder merely to rebuild it.
2128
+ // The same holds when the mask admitted arms but none was entered
2129
+ // (`prev` still 0, so `acc` is unset): the catch-up would rebuild
2130
+ // `choiceFx`, the arms' flatMap, set by set.
2004
2131
  return `${head}
2005
2132
  const c=${directAsciiMask ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
2006
2133
  ${hasRollback ? emitMark(p, L.buf, L.raw, sinks) : ''}
@@ -2011,7 +2138,7 @@ const bits=${maskName}[${directAsciiMask ? 'c' : 'c<0?128:c'}]
2011
2138
  if(bits===0){ctx._fe=pos;ctx._fx=${choiceFx};return FAIL}
2012
2139
  ${startFailureExact ? '' : 'let prev=0'}
2013
2140
  ${maskArms}
2014
- ${startFailureExact ? '' : `if(best===pos)acc=${catchName}(${n},prev,acc)`}
2141
+ ${startFailureExact ? '' : `if(best===pos&&prev!==0)acc=${catchName}(${n},prev,acc)`}
2015
2142
  ctx._fe=pos;ctx._fx=${startFailureExact ? `best===pos?${choiceFx}:acc??${choiceFx}` : `acc??${choiceFx}`}
2016
2143
  return FAIL
2017
2144
  }
@@ -2123,7 +2250,7 @@ return [key,v]
2123
2250
  const ci = classes.push([resolved]) - 1
2124
2251
  classPlan.push([itemClassIndex])
2125
2252
  itemCls = hoist('ri', `CLS[${ci}][0]`)
2126
- itemAscii = staticBuild && isAsciiOnlyClass(resolved)
2253
+ itemAscii = isAsciiOnlyClass(resolved)
2127
2254
  }
2128
2255
  }
2129
2256
  const itemMiss = (at: string): string => itemAscii
@@ -2357,6 +2484,35 @@ return nd
2357
2484
  // The trailing-trivia bit (128) rides on the children-only shape as it
2358
2485
  // does in the closure assembler: a consume after the body, no capture.
2359
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
+ : ''
2360
2516
  if (!hostCst && !tracked && build !== undefined && proj < 0
2361
2517
  && (leanFlags === 2 || (flags === 18 || flags === 34))) {
2362
2518
  const fields = flags === 18
@@ -2394,7 +2550,7 @@ sCh.push(nd)
2394
2550
  if(sRaw!==undefined)sRaw.push(rawEntry(nd,input,pos,end))
2395
2551
  }`
2396
2552
  return `${head}
2397
- 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
2398
2554
  const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf,sFields=ctx._fields
2399
2555
  const flat=[]
2400
2556
  ctx._cstBuf=undefined
@@ -2437,7 +2593,7 @@ return nd
2437
2593
  const keepChildren = !structural || cfg.hostReadsChildren !== false || collapse || unwrap
2438
2594
  const omitsRaw = !hostCst && build !== undefined && proj < 0 && (flags & 2) !== 0
2439
2595
  const ty = q(type)
2440
- const stArg = readsState ? 'st' : '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)'
2596
+ const stArg = 'ctx.state'
2441
2597
  const hostCall = `_pfHost(${ty},hostKids,fieldMap,span,rawKids,tlog,${stArg},${tags})`
2442
2598
  let value: string
2443
2599
  if (proj >= 0) {
@@ -2489,7 +2645,7 @@ const hostKids=kids
2489
2645
  const rawKids=buf.raw??(buf.rawSingle!==undefined?[buf.rawSingle]:EMPTY_CH)
2490
2646
  const tlog=buf.tl??EMPTY_TLOG`
2491
2647
  return `${head}
2492
- 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
2493
2649
  const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf
2494
2650
  ${openCapture}
2495
2651
  ctx.captureTrivia=${captureWide}
@@ -2512,7 +2668,7 @@ ctx.captureTrivia=sCap
2512
2668
  if(v===FAIL)return FAIL
2513
2669
  const end=EC.e
2514
2670
  const span=${tracked ? 'spanLines(ctx,pos,end)' : '{start:pos,end}'}
2515
- const st=${readsState ? '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)' : 'undefined'}
2671
+ const st=${readsState ? 'ctx.state' : 'undefined'}
2516
2672
  let nd
2517
2673
  ${unwrap ? 'if(kids.length===1)nd=unwrapChild(kids[0])\nelse ' : ''}${collapse ? 'if(kids.length===1)nd=kids[0]\nelse ' : ''}${collapsible ? `if(_pfHost!==undefined&&_pfHost._parsemanCstCollapse!==undefined&&kids.length===1&&rawKids.length===1&&_pfHost._parsemanCstCollapse(${ty},kids[0],kids,rawKids))nd=kids[0]
2518
2674
  else ` : ''}{${value}}
@@ -2535,8 +2691,12 @@ return nd
2535
2691
  }
2536
2692
  }
2537
2693
 
2694
+ // A selective assembly serves only its selected sites; the closure engine
2695
+ // serves every other rule entry and scan root (`assemble.ts`).
2696
+ const served = (ip: number): boolean => select === undefined || select.has(resolveAlias(ip))
2538
2697
  const ruleEntries: string[] = []
2539
2698
  for (const [rname, entryIp] of Object.entries(prog.rules)) {
2699
+ if (!served(entryIp)) continue
2540
2700
  // `_r_<Name>` — the composition surface, in `codegen.ts`'s own spelling and
2541
2701
  // deliberately NOT namespaced, so a sibling calls it by name.
2542
2702
  const target = link(entryIp)
@@ -2547,7 +2707,49 @@ return nd
2547
2707
  // Sites the SCAN pool and the scan-skip sets reference. They are linked
2548
2708
  // through `subtreeComb` outside the emitted scope, so they need names.
2549
2709
  const extra: string[] = []
2550
- for (const ip of extraIps) extra.push(`${ip}:${link(ip)}`)
2710
+ const exported = new Set<number>()
2711
+ for (const ip of extraIps) {
2712
+ if (!served(ip)) continue
2713
+ exported.add(ip)
2714
+ extra.push(`${ip}:${link(ip)}`)
2715
+ }
2716
+ // Selected sites a CLOSURE piece may call: every selected child of an
2717
+ // unselected reachable site. Linking one may lower it for the first time,
2718
+ // when only the closure side reaches it.
2719
+ if (select !== undefined) {
2720
+ const kids: number[] = []
2721
+ for (const ip of reachable) {
2722
+ if (select.has(resolveAlias(ip))) continue
2723
+ kids.length = 0
2724
+ childSlots(code, ip, kids)
2725
+ for (const kid of kids) {
2726
+ const target = resolveAlias(kid)
2727
+ if (!select.has(target) || exported.has(target)) continue
2728
+ exported.add(target)
2729
+ extra.push(`${target}:${link(target)}`)
2730
+ }
2731
+ }
2732
+ }
2733
+ const hybrid = select === undefined ? '' : `,
2734
+ ext:[${externals.join(',')}],
2735
+ bind:function(xs,ps){_xScan=xs
2736
+ ${externals.map((ip, i) => `_xp${ip}=ps[${i}]`).join('\n')}},
2737
+ scan:function(s){const o=_pfScan;_pfScan=s;return o}`
2738
+ // One wrapper per external: the closure engine reads its OWN installed trivia
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
+ }
2749
+ const externalDefs = select === undefined ? '' : `let _xScan
2750
+ ${externals.map(ip => `let _xp${ip}
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')}
2752
+ `
2551
2753
 
2552
2754
  // The per-label trivia scans sit AFTER the hoisted pool they close over: they
2553
2755
  // are `function` declarations, so a body may call one that is textually below
@@ -2558,7 +2760,7 @@ ${prelude.join('\n')}
2558
2760
  ${skipDefs.join('\n')}
2559
2761
  ${choiceDefs.join('\n')}
2560
2762
  ${bodies.join('\n')}
2561
- function _begin(ctx){
2763
+ ${externalDefs}function _begin(ctx){
2562
2764
  const host=ctx.build
2563
2765
  if(_pfDepth>0)_pfFrames.push([_pfScan,_pfHost,EC.e])
2564
2766
  _pfDepth++
@@ -2585,11 +2787,11 @@ pieces:{${ruleEntries.join(',')}},
2585
2787
  byIp:{${extra.join(',')}},
2586
2788
  end:function(){return EC.e},
2587
2789
  begin:_begin,
2588
- finish:_finish
2790
+ finish:_finish${hybrid}
2589
2791
  }`
2590
2792
 
2591
2793
  return {
2592
- source, reached, masks, classes, armExpected,
2794
+ source, reached, masks, classes, armExpected, siteBytes,
2593
2795
  plan: { classes: classPlan, armExpected: armExpectedPlan, masks: maskPlan },
2594
2796
  }
2595
2797
  }
package/src/table/emit.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { scanRefs } from './program.ts'
1
+ import { assemblyExtraIps } from './program.ts'
2
2
  import type { FoldedProgram, TableProgram } from './program.ts'
3
3
  import { resolveTable } from './program.ts'
4
4
  import { EMITTED_PARAMS, Unemittable, emitAssemblySource } from './emit-assembly.ts'
@@ -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
 
@@ -170,7 +172,7 @@ export function defaultAssemblyCfgs(prog: TableProgram): RunCfg[] {
170
172
  * not lower; that option set simply gets no entry, and `assemble.ts` runs the
171
173
  * closure engine for it and RECORDS why on `Assembly.emitRefusal`.
172
174
  */
173
- function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
175
+ function emitAssemblies(prog: TableProgram, cfgs: readonly AssemblyCfg[]): string[] {
174
176
  /**
175
177
  * `a:[]` IS NOT `a` ABSENT, and the difference is the whole property.
176
178
  *
@@ -201,9 +203,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
201
203
  const t = resolveTable(prog)
202
204
  // The scan pool and the scan-skip sets are linked from subtrees, so their
203
205
  // sites need emitted names too — same list `assemble.ts` builds.
204
- const extraIps: number[] = []
205
- for (const s of prog.scans ?? []) for (const r of scanRefs(s)) extraIps.push(r[0])
206
- for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
206
+ const extraIps = assemblyExtraIps(prog)
207
207
 
208
208
  const out: string[] = []
209
209
  const seen = new Set<number>()
@@ -213,7 +213,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
213
213
  seen.add(key)
214
214
  let em
215
215
  try {
216
- em = emitAssemblySource(t, prog, cfg, extraIps, true)
216
+ em = emitAssemblySource(t, prog, cfg, extraIps, true, cfg.select)
217
217
  } catch (e) {
218
218
  if (e instanceof Unemittable) continue
219
219
  throw e
@@ -233,6 +233,9 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
233
233
  return [`a:[${out.join(',')}],`]
234
234
  }
235
235
 
236
+ /** One pre-compiled option set; `select` makes it SELECTIVE (see `emitAssemblySource`). */
237
+ export type AssemblyCfg = RunCfg & { readonly select?: ReadonlySet<number> }
238
+
236
239
  export type EmitOptions = {
237
240
  /** Name of the exported binding. */
238
241
  readonly name?: string
@@ -244,7 +247,7 @@ export type EmitOptions = {
244
247
  * Supplying factories here is a low-level serialization experiment, not a
245
248
  * second normal compilation path; it is not used by the macro plugin.
246
249
  */
247
- readonly assemblies?: readonly RunCfg[]
250
+ readonly assemblies?: readonly AssemblyCfg[]
248
251
  /**
249
252
  * Sources for the author callbacks, in `prog.fns` order. A build has these
250
253
  * from the module it is lowering; pass `undefined` to emit a placeholder and