parseman 0.56.0 → 0.57.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 (75) hide show
  1. package/dist/analysis/diagnostics.js +379 -72
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +379 -72
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/node.d.ts +1 -1
  6. package/dist/compiler/build-arity.d.ts +1 -1
  7. package/dist/compiler/build-arity.d.ts.map +1 -1
  8. package/dist/cst/trivia-kinds.d.ts +9 -1
  9. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  10. package/dist/index.js +446 -75
  11. package/dist/index.js.map +3 -3
  12. package/dist/language-service/index.js +1 -0
  13. package/dist/language-service/index.js.map +1 -1
  14. package/dist/parse-context.d.ts.map +1 -1
  15. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  16. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  17. package/dist/plugin/index.d.ts +27 -1
  18. package/dist/plugin/index.d.ts.map +1 -1
  19. package/dist/plugin/index.js +930 -157
  20. package/dist/plugin/index.js.map +3 -3
  21. package/dist/plugin/ir-eval.d.ts +2 -0
  22. package/dist/plugin/ir-eval.d.ts.map +1 -1
  23. package/dist/regex/classes.d.ts.map +1 -1
  24. package/dist/run/index.js +1 -0
  25. package/dist/run/index.js.map +1 -1
  26. package/dist/table/assemble.d.ts +9 -0
  27. package/dist/table/assemble.d.ts.map +1 -1
  28. package/dist/table/child-slots.d.ts +4 -6
  29. package/dist/table/child-slots.d.ts.map +1 -1
  30. package/dist/table/compile-rule-map.d.ts +14 -2
  31. package/dist/table/compile-rule-map.d.ts.map +1 -1
  32. package/dist/table/emit-assembly.d.ts +17 -2
  33. package/dist/table/emit-assembly.d.ts.map +1 -1
  34. package/dist/table/emit.d.ts +5 -1
  35. package/dist/table/emit.d.ts.map +1 -1
  36. package/dist/table/encode.d.ts.map +1 -1
  37. package/dist/table/exec-baseline.d.ts.map +1 -1
  38. package/dist/table/exec.d.ts.map +1 -1
  39. package/dist/table/index.js +442 -70
  40. package/dist/table/index.js.map +3 -3
  41. package/dist/table/ops.d.ts +9 -6
  42. package/dist/table/ops.d.ts.map +1 -1
  43. package/dist/table/program.d.ts +13 -0
  44. package/dist/table/program.d.ts.map +1 -1
  45. package/dist/table/select-hot.d.ts +6 -0
  46. package/dist/table/select-hot.d.ts.map +1 -0
  47. package/dist/table/trivia-gap.d.ts +31 -0
  48. package/dist/table/trivia-gap.d.ts.map +1 -0
  49. package/dist/types.d.ts +17 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/version.d.ts +1 -1
  52. package/package.json +4 -1
  53. package/src/combinators/node.ts +6 -13
  54. package/src/combinators/trivia-skip.ts +44 -1
  55. package/src/compiler/build-arity.ts +3 -3
  56. package/src/cst/trivia-kinds.ts +12 -1
  57. package/src/parse-context.ts +1 -0
  58. package/src/plugin/entry-supercompiler.ts +8 -9
  59. package/src/plugin/index.ts +138 -13
  60. package/src/plugin/ir-eval.ts +7 -1
  61. package/src/regex/classes.ts +3 -0
  62. package/src/table/assemble.ts +122 -24
  63. package/src/table/child-slots.ts +4 -6
  64. package/src/table/compile-rule-map.ts +55 -15
  65. package/src/table/emit-assembly.ts +139 -26
  66. package/src/table/emit.ts +8 -7
  67. package/src/table/encode.ts +69 -21
  68. package/src/table/exec-baseline.ts +1 -3
  69. package/src/table/exec.ts +7 -6
  70. package/src/table/ops.ts +9 -6
  71. package/src/table/program.ts +24 -0
  72. package/src/table/select-hot.ts +70 -0
  73. package/src/table/trivia-gap.ts +109 -0
  74. package/src/types.ts +18 -0
  75. package/src/version.ts +1 -1
@@ -4,20 +4,17 @@ import type { HostMode } from '../cst/host-mode.ts'
4
4
  import { collectGrammarReflection, GRAMMAR_REFLECTION, type GrammarReflection } from '../cst/reflection.ts'
5
5
  import { encodeTableProgram, type TableSettings } from './encode.ts'
6
6
  import { defaultAssemblyCfgs, emitTableExpression } from './emit.ts'
7
+ import { emitAssemblySource, Unemittable } from './emit-assembly.ts'
8
+ import { selectHotSites } from './select-hot.ts'
9
+
10
+ /** The most the entry supercompiler may grow an assembly (`entry-supercompiler.ts`, its 5% source cap). */
11
+ const SUPERCOMPILE_GROWTH = 1.05
7
12
  import { tableRules } from './assemble.ts'
8
- import { closureArtifact } from './program.ts'
13
+ import { assemblyExtraIps, closureArtifact, resolveTable } from './program.ts'
9
14
  import type { TableProgram, TableRule } from './program.ts'
10
15
  import { buildGrammarPlan, type GrammarCoverageDefinition } from '../compiler/grammar-coverage-ids.ts'
11
16
  import { runDuplicationDiagnosticRules, type DuplicationOption } from './duplication-hook.ts'
12
17
 
13
- /**
14
- * Static assembly has a fixed source cost that tiny grammars cannot amortize.
15
- * The canonical size probe's composeLeaf is 62 words and grows by ~12 kB with
16
- * a factory; the shipping Jess leaves are 6,365/10,892 words. Keep the policy
17
- * between those measured populations so small artifacts retain table density.
18
- */
19
- const DEFAULT_PRECOMPILE_MIN_WORDS = 1_024
20
-
21
18
  /**
22
19
  * `compileRuleMap()` FOR THE TABLE LOWERING — the counterpart that did not exist.
23
20
  *
@@ -116,8 +113,16 @@ export type CompiledRuleMapTable = {
116
113
  /** Re-emit the same table call with construction-time artifact metadata. */
117
114
  replacementWithMetadata(
118
115
  metadataSource: string,
119
- options?: { readonly precompileDefault?: boolean },
116
+ options?: { readonly select?: ReadonlySet<number> },
120
117
  ): string
118
+ /**
119
+ * The sites a SELECTIVE default assembly emits within `budget` bytes — its
120
+ * fixed prelude plus the site bodies `select-hot.ts` ranks highest — and the
121
+ * bytes that spends. Empty for a CST or line-tracking table (only the default
122
+ * AST/no-lines option set is pre-compiled), for a budget smaller than the
123
+ * prelude, and for a table the emitter refuses, which names the construct.
124
+ */
125
+ selectAssembly(budget: number, profile?: ArrayLike<number>): { readonly sites: ReadonlySet<number>; readonly bytes: number; readonly refusal?: string }
121
126
  hostMode: HostMode
122
127
  hostBranchElided: boolean
123
128
  reflection: GrammarReflection
@@ -234,7 +239,7 @@ function withAmbient(
234
239
  export function compileRuleMapRunnable(
235
240
  ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
236
241
  opts: TableRuleMapOptions = {},
237
- ): Omit<CompiledRuleMapTable, 'replacement' | 'replacementWithMetadata'> | null {
242
+ ): Omit<CompiledRuleMapTable, 'replacement' | 'replacementWithMetadata' | 'selectAssembly'> | null {
238
243
  const encoded = encodeForRun(ruleMap, opts)
239
244
  if (encoded === null) return null
240
245
  const { prog, hostMode, plan } = encoded
@@ -331,23 +336,58 @@ export function compileRuleMap(
331
336
 
332
337
  const replacementWithMetadata = (
333
338
  metadataSource?: string,
334
- options: { readonly precompileDefault?: boolean } = {},
339
+ options: { readonly select?: ReadonlySet<number> } = {},
335
340
  ): string => emitTableExpression(artifact, {
336
341
  entry: null,
337
342
  runtimeRef: opts.runtimeRef ?? 'tableRules',
338
343
  fnSources: sources as string[],
339
- ...(options.precompileDefault === true && hostMode === 'ast' && prog.lines !== 1
340
- && prog.code.length >= DEFAULT_PRECOMPILE_MIN_WORDS
341
- ? { assemblies: defaultAssemblyCfgs(artifact).slice(0, 1) }
344
+ ...(options.select !== undefined && options.select.size > 0
345
+ ? { assemblies: [{ ...defaultAssemblyCfgs(artifact)[0]!, select: options.select }] }
342
346
  : {}),
343
347
  ...(metadataSource === undefined ? {} : { metadataSource }),
344
348
  })
345
349
  const replacement = replacementWithMetadata()
350
+ const selectAssembly = (budget: number, profile?: ArrayLike<number>): { sites: ReadonlySet<number>; bytes: number; refusal?: string } => {
351
+ const none = { sites: new Set<number>(), bytes: 0 }
352
+ if (hostMode !== 'ast' || prog.lines === 1) return none
353
+ const table = resolveTable(artifact)
354
+ const cfg = defaultAssemblyCfgs(artifact)[0]!
355
+ const roots = assemblyExtraIps(artifact)
356
+ let siteBytes, fixed
357
+ try {
358
+ siteBytes = emitAssemblySource(table, artifact, cfg, roots, true).siteBytes
359
+ // What any selective assembly costs before its first site body: the shared
360
+ // prelude and the factory's tail. A budget that cannot cover it buys nothing.
361
+ fixed = emitAssemblySource(table, artifact, cfg, roots, true, new Set()).source.length
362
+ } catch (e) {
363
+ if (e instanceof Unemittable) return { ...none, refusal: e.construct }
364
+ throw e
365
+ }
366
+ // The estimate (prelude plus site bodies) leaves out what selected sites add
367
+ // around their bodies — shared helpers and the `_px` wrappers of the closure
368
+ // sites they call — and the supercompiler may grow the source by up to 5%. So
369
+ // the selection is judged by the source it actually emits, scaled for that 5%,
370
+ // and narrowed until that fits.
371
+ const ceiling = budget / SUPERCOMPILE_GROWTH
372
+ let target = ceiling
373
+ for (let attempt = 0; attempt < 16; attempt++) {
374
+ const sites = selectHotSites(artifact.code, Object.values(artifact.rules), siteBytes, target - fixed, profile)
375
+ if (sites.size === 0) return none
376
+ const bytes = emitAssemblySource(table, artifact, cfg, roots, true, sites).source.length
377
+ if (bytes <= ceiling) return { sites, bytes: Math.ceil(bytes * SUPERCOMPILE_GROWTH) }
378
+ // 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.
381
+ target *= Math.min(ceiling / bytes, 0.99)
382
+ }
383
+ return none
384
+ }
346
385
 
347
386
  return {
348
387
  keys: ruleMap.map(([key]) => key),
349
388
  replacement,
350
389
  replacementWithMetadata,
390
+ selectAssembly,
351
391
  hostMode,
352
392
  // WHAT THE FLAG MEANS is "a DIRECT BUILDER's positioned-CST branch was
353
393
  // dropped", which is what the driver's `'ast' artifact + CST host` check keys
@@ -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_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 {
@@ -79,6 +79,14 @@ export type EmittedAssembly = {
79
79
  readonly end: () => number
80
80
  readonly begin: (ctx: ParseContext) => void
81
81
  readonly finish: () => void
82
+ /**
83
+ * Present only on a SELECTIVE assembly: the unselected sites its bodies call,
84
+ * the hook that binds them to closure pieces (with a setter for the closure
85
+ * engine's installed trivia scanner), and that same setter for this scope's.
86
+ */
87
+ readonly ext?: readonly number[]
88
+ readonly bind?: (setClosureScan: (s: unknown) => unknown, pieces: readonly EmittedPiece[]) => void
89
+ readonly scan?: (s: unknown) => unknown
82
90
  }
83
91
  /** The `new Function` result. Its parameters are `EMITTED_PARAMS`, in order. */
84
92
  export type EmittedFactory = (...args: readonly unknown[]) => EmittedAssembly
@@ -127,6 +135,9 @@ export const EMITTED_PARAMS = [
127
135
  // Appended for trivia-attributing fields (`field(…, { leadingTrivia })`);
128
136
  // old factories ignore them.
129
137
  'LEAD', 'triviaFieldCapture',
138
+ // Appended for the deferred-commit trivia skip (`trivia-gap.ts`); old
139
+ // factories ignore it.
140
+ 'skipDeferredTrivia',
130
141
  ] as const
131
142
 
132
143
  /**
@@ -162,7 +173,7 @@ function _skipTrivia(input,cur,ctx){
162
173
  const s=_pfScan
163
174
  if(s!==null&&ctx._triviaLog===undefined&&!(ctx.captureTrivia===true&&(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)))return s(input,cur)
164
175
  if(s!==null)return skipTriviaScanned(s,input,cur,ctx)
165
- if(needsDeferredTriviaCommit(ctx))return commitTriviaScan(scanTriviaCompact(input,cur,ctx))
176
+ if(needsDeferredTriviaCommit(ctx))return skipDeferredTrivia(input,cur,ctx)
166
177
  return advanceTrivia(input,cur,ctx)
167
178
  }
168
179
  function _pushLeaf(ctx,value,s,e){pushCstLeaf(ctx,{_tag:'leaf',value,span:{start:s,end:e}})}
@@ -479,6 +490,8 @@ export type EmitResult = {
479
490
  readonly armExpected: readonly (readonly string[])[][]
480
491
  /** The same three pools as indices, for a build-time emitter. */
481
492
  readonly plan: PoolPlan
493
+ /** Source length of each emitted site body, by site — what selection prices. */
494
+ readonly siteBytes: ReadonlyMap<number, number>
482
495
  }
483
496
 
484
497
  /** The candidate mask for one `CLS` row — the ONE definition, shared with the emitter. */
@@ -526,6 +539,11 @@ export function rebuildPools(
526
539
  * ordinary factory literal in a macro artifact. It permits macro-only code
527
540
  * shaping without perturbing the runtime `compile()` emitter, whose generated
528
541
  * parser must remain byte-identical when the optimization cannot affect it.
542
+ *
543
+ * `select`, when given, makes this a SELECTIVE (hybrid) assembly: only sites
544
+ * whose alias-resolved target is in the set get a body. Every other child a
545
+ * body calls is an EXTERNAL — `_px<ip>`, a wrapper bound after construction to
546
+ * the closure engine's piece for that site (`assemble.ts`).
529
547
  */
530
548
  export function emitAssemblySource(
531
549
  t: ResolvedTable,
@@ -541,6 +559,7 @@ export function emitAssemblySource(
541
559
  },
542
560
  extraIps: readonly number[] = [],
543
561
  staticBuild = false,
562
+ select?: ReadonlySet<number>,
544
563
  ): EmitResult {
545
564
  const { code, k, fx, disp, dsp, triviaLabelled } = t
546
565
  const swapLegal = !cfg.trackLines
@@ -716,6 +735,9 @@ export function emitAssemblySource(
716
735
 
717
736
  const bodies: string[] = []
718
737
  const byIp = new Map<number, string>()
738
+ /** Unselected sites a selective assembly calls into — see `select`. */
739
+ const externals: number[] = []
740
+ const siteBytes = new Map<number, number>()
719
741
  const reached = new Set<number>()
720
742
  const prelude: string[] = []
721
743
  const skipDefs: string[] = []
@@ -900,6 +922,28 @@ return 1
900
922
  * site property — nothing in `src` writes it except a caller-supplied
901
923
  * `ParseContext` (`types.ts:524`) — so it stays the one runtime read.
902
924
  */
925
+ /** The test an `OP_GATE` row at `gip` fails on, with its class hoisted. */
926
+ function gateMissTest(gip: number): string {
927
+ const resolved = t.cc[code[gip + 1]!]!
928
+ const ci = classes.push([resolved]) - 1
929
+ classPlan.push([code[gip + 1]!])
930
+ const cls = hoist('cl', `CLS[${ci}][0]`)
931
+ return isAsciiOnlyClass(resolved)
932
+ ? `${cls}.ascii[input.charCodeAt(pos)]!==1`
933
+ : `!classHas(${cls},lead(input,pos))`
934
+ }
935
+
936
+ /**
937
+ * An `optional`/`not`/`peek` child's first-character gate (`encode.ts`
938
+ * `lookaheadGate`), tested BEFORE the wrapper's mark: a miss is the wrapper's
939
+ * decided outcome, `miss`. Nothing where the child has no gate, or where a
940
+ * probe or tolerant parse forwards through it (`aliasOf`).
941
+ */
942
+ function lookaheadMiss(child: number, miss: string): string {
943
+ if (code[child] !== OP_GATE || cfg.tolerant || cfg.probe) return ''
944
+ return `if(${gateMissTest(child)}){${miss}}\n`
945
+ }
946
+
903
947
  function skipFor(l: SiteLabel): string {
904
948
  if (l.tri < 0) return '_skipTrivia'
905
949
  const ki = l.tri
@@ -919,9 +963,9 @@ return 1
919
963
  // through `scanTrivia`. `needsDeferredTriviaCommit` is implied by an open
920
964
  // buffer, so an in-node site skips the call that asks.
921
965
  skipDefs.push(l.buf
922
- ? `function ${nm}(input,cur,ctx){return commitTriviaScan(scanTriviaCompact(input,cur,ctx))}`
966
+ ? `function ${nm}(input,cur,ctx){return skipDeferredTrivia(input,cur,ctx)}`
923
967
  : `function ${nm}(input,cur,ctx){
924
- if(needsDeferredTriviaCommit(ctx))return commitTriviaScan(scanTriviaCompact(input,cur,ctx))
968
+ if(needsDeferredTriviaCommit(ctx))return skipDeferredTrivia(input,cur,ctx)
925
969
  return advanceTrivia(input,cur,ctx)}`)
926
970
  return nm
927
971
  }
@@ -1002,12 +1046,20 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1002
1046
  const target = resolveAlias(ip)
1003
1047
  const done = byIp.get(target)
1004
1048
  if (done !== undefined) return done
1049
+ if (select !== undefined && !select.has(target)) {
1050
+ const ext = `_px${target}`
1051
+ byIp.set(target, ext)
1052
+ externals.push(target)
1053
+ return ext
1054
+ }
1005
1055
  const fname = `_pf${target}`
1006
1056
  // Reserved BEFORE lowering, so a back-edge into a site still in flight binds
1007
1057
  // to the hoisted declaration rather than to a forwarding stub. This is the
1008
1058
  // whole of `assemble.ts`'s `inFlight` map and its one shared closure.
1009
1059
  byIp.set(target, fname)
1010
- bodies.push(lower(target, fname))
1060
+ const body = lower(target, fname)
1061
+ siteBytes.set(target, body.length)
1062
+ bodies.push(body)
1011
1063
  return fname
1012
1064
  }
1013
1065
 
@@ -1266,19 +1318,40 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1266
1318
 
1267
1319
  case OP_GATE: {
1268
1320
  const child = link(code[ip + 2]!)
1269
- const resolved = t.cc[code[ip + 1]!]!
1270
- const ci = classes.push([resolved]) - 1
1271
- classPlan.push([code[ip + 1]!])
1272
- const cls = hoist('cl', `CLS[${ci}][0]`)
1273
1321
  const xf = fxRef(code[ip + 3]!)
1274
1322
  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}
1323
+ if(${gateMissTest(ip)}){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1324
+ return ${child}(input,pos,ctx)
1325
+ }`
1326
+ }
1327
+
1328
+ case OP_ARMGATE: {
1329
+ // `choice({ gate, combinator })`, as `assemble.ts` lowers it. A blocked
1330
+ // arm is SKIPPED, not failed (choice.ts `continue`): it clears the cut,
1331
+ // sets no failure position for the ordered path to rank, and leaves the
1332
+ // arm's own expected set for the dispatch path.
1333
+ const pred = fnRef(code[ip + 1]!)
1334
+ const child = link(code[ip + 2]!)
1335
+ const xf = fxRef(code[ip + 3]!)
1336
+ return `${head}
1337
+ if(!${pred}(ctx.state)){ctx._fc=false;ctx._fe=-1;ctx._fx=${xf};return FAIL}
1278
1338
  return ${child}(input,pos,ctx)
1279
1339
  }`
1280
1340
  }
1281
1341
 
1342
+ case OP_WITHCTX: {
1343
+ // `withCtx(extra | derive, p)`, as `assemble.ts` lowers it: a negative
1344
+ // operand is a derive callback over the outer state, else a constant.
1345
+ // `finally` restores the outer state on every exit, a throw included.
1346
+ const ki = code[ip + 1]!
1347
+ const child = link(code[ip + 2]!)
1348
+ return `${head}
1349
+ const saved=ctx.state
1350
+ ctx.state=${ki < 0 ? `${fnRef(~ki)}(saved)` : kRef(ki)}
1351
+ try{return ${child}(input,pos,ctx)}finally{ctx.state=saved}
1352
+ }`
1353
+ }
1354
+
1282
1355
  case OP_XFORM: {
1283
1356
  const fn = fnRef(code[ip + 1]!)
1284
1357
  const child = link(code[ip + 2]!)
@@ -1589,7 +1662,7 @@ return FAIL
1589
1662
  const xf = fxRef(code[ip + 2]!)
1590
1663
  const p = tmp()
1591
1664
  return `${head}
1592
- ${emitMark(p, L.buf, L.raw, sinks)}
1665
+ ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${emitMark(p, L.buf, L.raw, sinks)}
1593
1666
  const v=${child}(input,pos,ctx)
1594
1667
  ${emitRollback(p, L.buf, L.raw, sinks)}
1595
1668
  if(v===FAIL){EC.e=pos;return null}
@@ -1604,7 +1677,7 @@ return FAIL
1604
1677
  const xf = fxRef(code[ip + 2]!)
1605
1678
  const p = tmp()
1606
1679
  return `${head}
1607
- ${emitMark(p, L.buf, L.raw, sinks)}
1680
+ ${lookaheadMiss(code[ip + 1]!, `ctx._fe=pos;ctx._fx=${xf};return FAIL`)}${emitMark(p, L.buf, L.raw, sinks)}
1608
1681
  const v=${child}(input,pos,ctx)
1609
1682
  ${emitRollback(p, L.buf, L.raw, sinks)}
1610
1683
  if(v===FAIL){ctx._fe=pos;ctx._fx=${xf};return FAIL}
@@ -1618,7 +1691,7 @@ return null
1618
1691
  const clean = !REC && failureRollbackClean(prog, ip)
1619
1692
  const p = tmp()
1620
1693
  return `${head}
1621
- ${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1694
+ ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1622
1695
  ctx._fc=false
1623
1696
  const v=${child}(input,pos,ctx)
1624
1697
  if(v===FAIL){
@@ -1735,7 +1808,7 @@ return v
1735
1808
  // array index feeding ONE call site, which is the megamorphic site
1736
1809
  // this unit exists to remove.
1737
1810
  const armSwitch = arms.map((a, i) => `case ${i}:v=${a}(input,pos,ctx);break`).join('\n')
1738
- const directAscii = staticBuild && table.hi.length === 0
1811
+ const directAscii = table.hi.length === 0
1739
1812
  return `${head}
1740
1813
  const c=${directAscii ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
1741
1814
  let arm=-1
@@ -1766,7 +1839,7 @@ return FAIL
1766
1839
  const expected = Array.from({ length: n }, (_, i) => fxRef(code[base + n + i]!))
1767
1840
  const gates = Array.from({ length: n }, (_, i) => table.armCls[i] ?? null)
1768
1841
  const maskable = n <= 32
1769
- const directAsciiMask = staticBuild && maskable && gates.every(isAsciiOnlyClass)
1842
+ const directAsciiMask = maskable && gates.every(isAsciiOnlyClass)
1770
1843
  const gateRefs = directAsciiMask
1771
1844
  ? []
1772
1845
  : Array.from({ length: n }, (_, i) => hoist('g', `DISP[${di}].armCls[${i}]`))
@@ -2001,6 +2074,9 @@ ${rollbackFor(i)}
2001
2074
  // A zero compatible-arm mask is stronger than a speculative miss: no
2002
2075
  // arm can run, so the choice's encoded flat expected set is already
2003
2076
  // the exact result. Do not walk the arm ladder merely to rebuild it.
2077
+ // The same holds when the mask admitted arms but none was entered
2078
+ // (`prev` still 0, so `acc` is unset): the catch-up would rebuild
2079
+ // `choiceFx`, the arms' flatMap, set by set.
2004
2080
  return `${head}
2005
2081
  const c=${directAsciiMask ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
2006
2082
  ${hasRollback ? emitMark(p, L.buf, L.raw, sinks) : ''}
@@ -2011,7 +2087,7 @@ const bits=${maskName}[${directAsciiMask ? 'c' : 'c<0?128:c'}]
2011
2087
  if(bits===0){ctx._fe=pos;ctx._fx=${choiceFx};return FAIL}
2012
2088
  ${startFailureExact ? '' : 'let prev=0'}
2013
2089
  ${maskArms}
2014
- ${startFailureExact ? '' : `if(best===pos)acc=${catchName}(${n},prev,acc)`}
2090
+ ${startFailureExact ? '' : `if(best===pos&&prev!==0)acc=${catchName}(${n},prev,acc)`}
2015
2091
  ctx._fe=pos;ctx._fx=${startFailureExact ? `best===pos?${choiceFx}:acc??${choiceFx}` : `acc??${choiceFx}`}
2016
2092
  return FAIL
2017
2093
  }
@@ -2123,7 +2199,7 @@ return [key,v]
2123
2199
  const ci = classes.push([resolved]) - 1
2124
2200
  classPlan.push([itemClassIndex])
2125
2201
  itemCls = hoist('ri', `CLS[${ci}][0]`)
2126
- itemAscii = staticBuild && isAsciiOnlyClass(resolved)
2202
+ itemAscii = isAsciiOnlyClass(resolved)
2127
2203
  }
2128
2204
  }
2129
2205
  const itemMiss = (at: string): string => itemAscii
@@ -2437,7 +2513,7 @@ return nd
2437
2513
  const keepChildren = !structural || cfg.hostReadsChildren !== false || collapse || unwrap
2438
2514
  const omitsRaw = !hostCst && build !== undefined && proj < 0 && (flags & 2) !== 0
2439
2515
  const ty = q(type)
2440
- const stArg = readsState ? 'st' : '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)'
2516
+ const stArg = 'ctx.state'
2441
2517
  const hostCall = `_pfHost(${ty},hostKids,fieldMap,span,rawKids,tlog,${stArg},${tags})`
2442
2518
  let value: string
2443
2519
  if (proj >= 0) {
@@ -2512,7 +2588,7 @@ ctx.captureTrivia=sCap
2512
2588
  if(v===FAIL)return FAIL
2513
2589
  const end=EC.e
2514
2590
  const span=${tracked ? 'spanLines(ctx,pos,end)' : '{start:pos,end}'}
2515
- const st=${readsState ? '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)' : 'undefined'}
2591
+ const st=${readsState ? 'ctx.state' : 'undefined'}
2516
2592
  let nd
2517
2593
  ${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
2594
  else ` : ''}{${value}}
@@ -2535,8 +2611,12 @@ return nd
2535
2611
  }
2536
2612
  }
2537
2613
 
2614
+ // A selective assembly serves only its selected sites; the closure engine
2615
+ // serves every other rule entry and scan root (`assemble.ts`).
2616
+ const served = (ip: number): boolean => select === undefined || select.has(resolveAlias(ip))
2538
2617
  const ruleEntries: string[] = []
2539
2618
  for (const [rname, entryIp] of Object.entries(prog.rules)) {
2619
+ if (!served(entryIp)) continue
2540
2620
  // `_r_<Name>` — the composition surface, in `codegen.ts`'s own spelling and
2541
2621
  // deliberately NOT namespaced, so a sibling calls it by name.
2542
2622
  const target = link(entryIp)
@@ -2547,7 +2627,40 @@ return nd
2547
2627
  // Sites the SCAN pool and the scan-skip sets reference. They are linked
2548
2628
  // through `subtreeComb` outside the emitted scope, so they need names.
2549
2629
  const extra: string[] = []
2550
- for (const ip of extraIps) extra.push(`${ip}:${link(ip)}`)
2630
+ const exported = new Set<number>()
2631
+ for (const ip of extraIps) {
2632
+ if (!served(ip)) continue
2633
+ exported.add(ip)
2634
+ extra.push(`${ip}:${link(ip)}`)
2635
+ }
2636
+ // Selected sites a CLOSURE piece may call: every selected child of an
2637
+ // unselected reachable site. Linking one may lower it for the first time,
2638
+ // when only the closure side reaches it.
2639
+ if (select !== undefined) {
2640
+ const kids: number[] = []
2641
+ for (const ip of reachable) {
2642
+ if (select.has(resolveAlias(ip))) continue
2643
+ kids.length = 0
2644
+ childSlots(code, ip, kids)
2645
+ for (const kid of kids) {
2646
+ const target = resolveAlias(kid)
2647
+ if (!select.has(target) || exported.has(target)) continue
2648
+ exported.add(target)
2649
+ extra.push(`${target}:${link(target)}`)
2650
+ }
2651
+ }
2652
+ }
2653
+ const hybrid = select === undefined ? '' : `,
2654
+ ext:[${externals.join(',')}],
2655
+ bind:function(xs,ps){_xScan=xs
2656
+ ${externals.map((ip, i) => `_xp${ip}=ps[${i}]`).join('\n')}},
2657
+ scan:function(s){const o=_pfScan;_pfScan=s;return o}`
2658
+ // 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.
2660
+ const externalDefs = select === undefined ? '' : `let _xScan
2661
+ ${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')}
2663
+ `
2551
2664
 
2552
2665
  // The per-label trivia scans sit AFTER the hoisted pool they close over: they
2553
2666
  // are `function` declarations, so a body may call one that is textually below
@@ -2558,7 +2671,7 @@ ${prelude.join('\n')}
2558
2671
  ${skipDefs.join('\n')}
2559
2672
  ${choiceDefs.join('\n')}
2560
2673
  ${bodies.join('\n')}
2561
- function _begin(ctx){
2674
+ ${externalDefs}function _begin(ctx){
2562
2675
  const host=ctx.build
2563
2676
  if(_pfDepth>0)_pfFrames.push([_pfScan,_pfHost,EC.e])
2564
2677
  _pfDepth++
@@ -2585,11 +2698,11 @@ pieces:{${ruleEntries.join(',')}},
2585
2698
  byIp:{${extra.join(',')}},
2586
2699
  end:function(){return EC.e},
2587
2700
  begin:_begin,
2588
- finish:_finish
2701
+ finish:_finish${hybrid}
2589
2702
  }`
2590
2703
 
2591
2704
  return {
2592
- source, reached, masks, classes, armExpected,
2705
+ source, reached, masks, classes, armExpected, siteBytes,
2593
2706
  plan: { classes: classPlan, armExpected: armExpectedPlan, masks: maskPlan },
2594
2707
  }
2595
2708
  }
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'
@@ -170,7 +170,7 @@ export function defaultAssemblyCfgs(prog: TableProgram): RunCfg[] {
170
170
  * not lower; that option set simply gets no entry, and `assemble.ts` runs the
171
171
  * closure engine for it and RECORDS why on `Assembly.emitRefusal`.
172
172
  */
173
- function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
173
+ function emitAssemblies(prog: TableProgram, cfgs: readonly AssemblyCfg[]): string[] {
174
174
  /**
175
175
  * `a:[]` IS NOT `a` ABSENT, and the difference is the whole property.
176
176
  *
@@ -201,9 +201,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
201
201
  const t = resolveTable(prog)
202
202
  // The scan pool and the scan-skip sets are linked from subtrees, so their
203
203
  // 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])
204
+ const extraIps = assemblyExtraIps(prog)
207
205
 
208
206
  const out: string[] = []
209
207
  const seen = new Set<number>()
@@ -213,7 +211,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
213
211
  seen.add(key)
214
212
  let em
215
213
  try {
216
- em = emitAssemblySource(t, prog, cfg, extraIps, true)
214
+ em = emitAssemblySource(t, prog, cfg, extraIps, true, cfg.select)
217
215
  } catch (e) {
218
216
  if (e instanceof Unemittable) continue
219
217
  throw e
@@ -233,6 +231,9 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
233
231
  return [`a:[${out.join(',')}],`]
234
232
  }
235
233
 
234
+ /** One pre-compiled option set; `select` makes it SELECTIVE (see `emitAssemblySource`). */
235
+ export type AssemblyCfg = RunCfg & { readonly select?: ReadonlySet<number> }
236
+
236
237
  export type EmitOptions = {
237
238
  /** Name of the exported binding. */
238
239
  readonly name?: string
@@ -244,7 +245,7 @@ export type EmitOptions = {
244
245
  * Supplying factories here is a low-level serialization experiment, not a
245
246
  * second normal compilation path; it is not used by the macro plugin.
246
247
  */
247
- readonly assemblies?: readonly RunCfg[]
248
+ readonly assemblies?: readonly AssemblyCfg[]
248
249
  /**
249
250
  * Sources for the author callbacks, in `prog.fns` order. A build has these
250
251
  * from the module it is lowering; pass `undefined` to emit a placeholder and