parseman 0.47.0 → 0.48.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 (124) hide show
  1. package/README.md +27 -21
  2. package/dist/analysis/diagnostics.js +2955 -357
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2946 -348
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/expect.d.ts +1 -0
  7. package/dist/combinators/expect.d.ts.map +1 -1
  8. package/dist/combinators/expected.d.ts +8 -0
  9. package/dist/combinators/expected.d.ts.map +1 -0
  10. package/dist/combinators/first-set.d.ts +8 -0
  11. package/dist/combinators/first-set.d.ts.map +1 -1
  12. package/dist/combinators/grammar.d.ts.map +1 -1
  13. package/dist/combinators/keywords.d.ts +4 -0
  14. package/dist/combinators/keywords.d.ts.map +1 -1
  15. package/dist/combinators/literal.d.ts.map +1 -1
  16. package/dist/combinators/not.d.ts.map +1 -1
  17. package/dist/combinators/peek.d.ts.map +1 -1
  18. package/dist/combinators/regex.d.ts.map +1 -1
  19. package/dist/combinators/repeat.d.ts.map +1 -1
  20. package/dist/combinators/sequence.d.ts.map +1 -1
  21. package/dist/combinators/trivia-skip.d.ts +19 -1
  22. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  23. package/dist/compiler/direct-projection.d.ts +12 -0
  24. package/dist/compiler/direct-projection.d.ts.map +1 -0
  25. package/dist/compiler/token-alphabet.d.ts +845 -5
  26. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  27. package/dist/compiler/token-capability.d.ts +3 -0
  28. package/dist/compiler/token-capability.d.ts.map +1 -0
  29. package/dist/compiler/token-capability.js +3192 -0
  30. package/dist/compiler/token-capability.js.map +6 -0
  31. package/dist/cst/trivia-charscan.d.ts +3 -0
  32. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  33. package/dist/cst/trivia-css-scanner.d.ts +5 -0
  34. package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
  35. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  36. package/dist/index.js +2959 -348
  37. package/dist/index.js.map +3 -3
  38. package/dist/language-service/index.js +7 -1
  39. package/dist/language-service/index.js.map +1 -1
  40. package/dist/plugin/index.js +2969 -358
  41. package/dist/plugin/index.js.map +3 -3
  42. package/dist/spec/index.js +366 -49
  43. package/dist/spec/index.js.map +2 -2
  44. package/dist/table/assemble.d.ts.map +1 -1
  45. package/dist/table/child-slots.d.ts.map +1 -1
  46. package/dist/table/compile-rule-map.d.ts +3 -1
  47. package/dist/table/compile-rule-map.d.ts.map +1 -1
  48. package/dist/table/emit-assembly.d.ts +13 -14
  49. package/dist/table/emit-assembly.d.ts.map +1 -1
  50. package/dist/table/emit.d.ts.map +1 -1
  51. package/dist/table/encode.d.ts.map +1 -1
  52. package/dist/table/exec-baseline.d.ts.map +1 -1
  53. package/dist/table/exec.d.ts.map +1 -1
  54. package/dist/table/index.js +4839 -2232
  55. package/dist/table/index.js.map +3 -3
  56. package/dist/table/lex-body.d.ts +9 -0
  57. package/dist/table/lex-body.d.ts.map +1 -0
  58. package/dist/table/lex-program.d.ts +25 -0
  59. package/dist/table/lex-program.d.ts.map +1 -0
  60. package/dist/table/ops.d.ts +47 -4
  61. package/dist/table/ops.d.ts.map +1 -1
  62. package/dist/table/program.d.ts +53 -2
  63. package/dist/table/program.d.ts.map +1 -1
  64. package/dist/table/scalar-terminal.d.ts +13 -0
  65. package/dist/table/scalar-terminal.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts.map +1 -1
  67. package/dist/types.d.ts +2 -0
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/version.d.ts +1 -1
  70. package/package.json +12 -11
  71. package/src/combinators/expect.ts +1 -0
  72. package/src/combinators/expected.ts +17 -0
  73. package/src/combinators/first-set.ts +82 -3
  74. package/src/combinators/grammar.ts +8 -3
  75. package/src/combinators/keywords.ts +22 -6
  76. package/src/combinators/literal.ts +2 -1
  77. package/src/combinators/not.ts +5 -1
  78. package/src/combinators/peek.ts +5 -1
  79. package/src/combinators/regex.ts +5 -3
  80. package/src/combinators/repeat.ts +4 -10
  81. package/src/combinators/sequence.ts +35 -16
  82. package/src/combinators/trivia-skip.ts +75 -11
  83. package/src/compiler/direct-projection.ts +43 -0
  84. package/src/compiler/token-alphabet.ts +4316 -12
  85. package/src/compiler/token-capability.ts +6 -0
  86. package/src/cst/trivia-charscan.ts +34 -0
  87. package/src/cst/trivia-css-scanner.ts +142 -0
  88. package/src/cst/trivia-kinds.ts +3 -1
  89. package/src/plugin/index.ts +4 -4
  90. package/src/table/assemble.ts +1224 -222
  91. package/src/table/child-slots.ts +4 -3
  92. package/src/table/compile-rule-map.ts +21 -3
  93. package/src/table/emit-assembly.ts +363 -109
  94. package/src/table/emit.ts +26 -1
  95. package/src/table/encode.ts +246 -54
  96. package/src/table/exec-baseline.ts +3 -7
  97. package/src/table/exec.ts +140 -18
  98. package/src/table/lex-body.ts +30 -0
  99. package/src/table/lex-program.ts +422 -0
  100. package/src/table/ops.ts +50 -4
  101. package/src/table/program.ts +67 -2
  102. package/src/table/scalar-terminal.ts +51 -0
  103. package/src/table/site-labels.ts +13 -3
  104. package/src/table/stamp.ts +3 -3
  105. package/src/types.ts +1 -1
  106. package/src/version.ts +1 -1
  107. package/dist/analysis/diagnostics.cjs +0 -13736
  108. package/dist/analysis/diagnostics.cjs.map +0 -6
  109. package/dist/cli/index.cjs +0 -14837
  110. package/dist/cli/index.cjs.map +0 -6
  111. package/dist/index.cjs +0 -16303
  112. package/dist/index.cjs.map +0 -6
  113. package/dist/language-service/index.cjs +0 -1302
  114. package/dist/language-service/index.cjs.map +0 -6
  115. package/dist/oracle/index.cjs +0 -254
  116. package/dist/oracle/index.cjs.map +0 -6
  117. package/dist/plugin/index.cjs +0 -16009
  118. package/dist/plugin/index.cjs.map +0 -6
  119. package/dist/run/index.cjs +0 -577
  120. package/dist/run/index.cjs.map +0 -6
  121. package/dist/spec/index.cjs +0 -6188
  122. package/dist/spec/index.cjs.map +0 -6
  123. package/dist/table/index.cjs +0 -11375
  124. package/dist/table/index.cjs.map +0 -6
@@ -9,16 +9,11 @@
9
9
  *
10
10
  * ## Why text, and not a rearrangement of `assemble.ts`
11
11
  *
12
- * V8 attaches inline-cache feedback per FUNCTION LITERAL, not per closure. The
13
- * second closure minted from a `CreateClosure` site moves its `FeedbackCell` to
14
- * `kManyClosures`, and the vector is shared from then on. `assemble.ts` mints 18
15
- * arity-2 `SEQV` closures for `less/stylesheet` from ONE literal, so the two
16
- * child call sites inside that literal see 18 unrelated callees and go
17
- * megamorphic — a hash lookup in the megamorphic stub cache, no inlining, no
18
- * type feedback for the callee body. No rearrangement of a file that MINTS
19
- * closures can produce a distinct literal per site. Emitting one
20
- * `function _pf<N>` per site can: inside `_pf12` the call `_pf7(…)` names ONE
21
- * binding and stays monomorphic.
12
+ * Emission makes each fixed grammar edge a direct generated identifier and is
13
+ * also the form a build can serialise as an ordinary function literal for CSP.
14
+ * The closure assembler can bind the same topology directly through scalar
15
+ * captures; emission's distinct job is to print that binding, not to assert a
16
+ * V8 property that the semantic design depends on.
22
17
  *
23
18
  * ## What may be shared, and what must be emitted
24
19
  *
@@ -54,13 +49,15 @@ import {
54
49
  OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
55
50
  OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
56
51
  OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX,
57
- OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
52
+ 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_LEX_BODY, OP_LEX_PROGRAM,
58
54
  } from './ops.ts'
59
- import type { ResolvedClass, ResolvedTable, TableProgram } from './program.ts'
55
+ import { validateDispatchSpec, type ResolvedClass, type ResolvedTable, type TableProgram } from './program.ts'
60
56
  import { emitShapeMatch, scanShapeFromRegex } from './scan-shapes.ts'
61
57
  import {
62
- CAP_OFF, CAP_ON, TRI_NONE, TRI_UNKNOWN, TOP, computeSiteLabels, reachableSites, type SiteLabel,
58
+ CAP_OFF, CAP_ON, TRI_NONE, TRI_UNKNOWN, computeSiteLabels, reachableSites, type SiteLabel,
63
59
  } from './site-labels.ts'
60
+ import { scalarTerminalNodeChild, scalarTerminalNotChild } from './scalar-terminal.ts'
64
61
 
65
62
  /** What the compiled factory hands back — the emitted twin of `Assembly`. */
66
63
  export type EmittedPiece = (input: string, pos: number, ctx: ParseContext) => unknown
@@ -94,7 +91,7 @@ export class Unemittable extends Error {
94
91
  export const EMITTED_PARAMS = [
95
92
  'EC', 'FAIL', 'K', 'FX', 'FNS', 'MASK', 'CLS', 'AFX', 'TRIVIA', 'TRIVIALABELS', 'TRIVIASCAN',
96
93
  'SCANS', 'DISP', 'DSP', 'EMPTY_FX', 'EMPTY_CH', 'EMPTY_TLOG', 'EMPTY_TL',
97
- 'cstCaptureActive', 'pushCstLeaf', 'pushCstChild', 'rollbackTriviaAt', 'failAt',
94
+ 'cstCaptureActive', 'pushCstLeaf', 'pushCstChild', 'rollbackTriviaAt', 'rollbackScannedTriviaAt', 'failAt',
98
95
  'classHas', 'consumeTrivia', 'buildFieldMap', 'projectChild', 'unwrapChild',
99
96
  'demoteCapturedToRaw', 'cstLeavesLen', 'skipTriviaScanned', 'needsDeferredTriviaCommit',
100
97
  'scanTrivia', 'advanceTrivia', 'refuseUnclassifiedRootScope', 'spanLines', 'rawEntry', 'lead',
@@ -106,6 +103,16 @@ export const EMITTED_PARAMS = [
106
103
  // span, expected set and CST embedding are produced by the SAME code in every
107
104
  // engine and cannot drift.
108
105
  'SENTS', 'matchesAt', 'recoverScan', 'orSentinel', 'captureError',
106
+ // Pure scalar terminal recognizers, indexed by the terminal's existing const
107
+ // operand. Appended for compatibility with older precompiled factories.
108
+ 'RECOG',
109
+ // Appended so precompiled factories produced by earlier runtimes keep every
110
+ // positional helper binding. Older factories ignore this trailing argument.
111
+ 'commitTriviaScan', 'scanTriviaCompact', 'LEX',
112
+ // Appended for old precompiled-factory ABI: old factories ignore it.
113
+ 'adjacencyHolds',
114
+ // Appended for selected composite lexical programs; old factories ignore it.
115
+ 'LEXPROG',
109
116
  ] as const
110
117
 
111
118
  /**
@@ -131,7 +138,7 @@ function _skipTrivia(input,cur,ctx){
131
138
  const s=_pfScan
132
139
  if(s!==null&&ctx._triviaLog===undefined&&!(ctx.captureTrivia===true&&(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)))return s(input,cur)
133
140
  if(s!==null)return skipTriviaScanned(s,input,cur,ctx)
134
- if(needsDeferredTriviaCommit(ctx)){const sc=scanTrivia(input,cur,ctx);sc.commit();return sc.end}
141
+ if(needsDeferredTriviaCommit(ctx))return commitTriviaScan(scanTriviaCompact(input,cur,ctx))
135
142
  return advanceTrivia(input,cur,ctx)
136
143
  }
137
144
  function _pushLeaf(ctx,value,s,e){pushCstLeaf(ctx,{_tag:'leaf',value,span:{start:s,end:e}})}
@@ -145,6 +152,13 @@ if(b.raw!==undefined)b.raw.push(l)
145
152
  else if(b.rawSingle!==undefined){b.raw=[b.rawSingle,l];b.rawSingle=undefined}
146
153
  else b.rawSingle=l
147
154
  }
155
+ function _capturedFlatChildren(children){
156
+ if(children.length===0)return EMPTY_CH
157
+ if(children[0]!==undefined)return children
158
+ let first=1
159
+ while(first<children.length&&children[first]===undefined)first++
160
+ return first===children.length?EMPTY_CH:children.slice(first)
161
+ }
148
162
  function _accSet(ax,acc){
149
163
  if(ax===undefined||ax.length===0)return acc
150
164
  if(acc===undefined)return ax.slice()
@@ -311,19 +325,24 @@ function emitRollback(t: string, buf: boolean, s: Sinks = NO_SINKS): string {
311
325
  * only the first branch, a known slot keeps only the second (with the scope's own
312
326
  * scanner already bound into `skip`), and `TRI_UNKNOWN` keeps both.
313
327
  */
314
- function emitTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string, s: Sinks): string {
328
+ function emitTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string): string {
315
329
  const fast = `const ${t}v=${callee}(input,cur,ctx)
316
330
  if(${t}v===FAIL)return FAIL
317
331
  ${dst}=${t}v
318
332
  cur=EC.e`
319
333
  if (l.tri === TRI_NONE) return fast
320
- const scanned = `${emitMark(t, l.buf, s)}
334
+ const scanned = `const ${t}tl=ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)
335
+ const ${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
336
+ const ${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
321
337
  const ${t}s=${skip}(input,cur,ctx)
338
+ const ${t}stl=ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)
339
+ const ${t}slg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
340
+ const ${t}srt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
322
341
  const ${t}v=${callee}(input,${t}s,ctx)
323
342
  if(${t}v===FAIL)return FAIL
324
343
  ${dst}=${t}v
325
344
  if(EC.e>${t}s)cur=EC.e
326
- else{${emitRollback(t, l.buf, s)}}`
345
+ else{rollbackScannedTriviaAt(ctx,${t}tl,${t}stl,${t}lg,${t}slg,${t}rt,${t}srt)}`
327
346
  if (l.tri !== TRI_UNKNOWN) return scanned
328
347
  return `if(ctx.trivia===undefined){
329
348
  ${fast}
@@ -365,7 +384,11 @@ export type PoolPlan = {
365
384
  readonly classes: readonly (readonly number[])[]
366
385
  /** Per `AFX` row: the `fx` index of each arm's expected set. */
367
386
  readonly armExpected: readonly (readonly number[])[]
368
- /** Per `MASK` row: the `CLS` index the mask is computed from. */
387
+ /**
388
+ * Per `MASK` row: a non-negative legacy `CLS` row index, or `~dispIndex` for
389
+ * a directly bound choice. The negative form names the EXISTING resolved
390
+ * dispatch row rather than serialising a second copy of its arm classes.
391
+ */
369
392
  readonly masks: readonly number[]
370
393
  }
371
394
 
@@ -403,13 +426,18 @@ function maskForClassRow(gates: readonly (ResolvedClass | null)[]): Uint32Array
403
426
  export function rebuildPools(
404
427
  cc: readonly ResolvedClass[],
405
428
  fx: readonly (readonly string[])[],
429
+ disp: ResolvedTable['disp'],
406
430
  plan: PoolPlan,
407
431
  ): { masks: Uint32Array[]; classes: (ResolvedClass | null)[][]; armExpected: (readonly string[])[][] } {
408
432
  const classes = plan.classes.map(row => row.map(i => (i < 0 ? null : cc[i] ?? null)))
409
433
  return {
410
434
  classes,
411
435
  armExpected: plan.armExpected.map(row => row.map(i => fx[i] ?? [])),
412
- masks: plan.masks.map(ci => maskForClassRow(classes[ci]!)),
436
+ masks: plan.masks.map(source => {
437
+ const row = source >= 0 ? classes[source] : disp[~source]?.armCls
438
+ if (row === undefined) throw new TypeError(`table emitter: invalid MASK plan source ${source}`)
439
+ return maskForClassRow(row)
440
+ }),
413
441
  }
414
442
  }
415
443
 
@@ -471,6 +499,17 @@ export function emitAssemblySource(
471
499
  // emitted scope supplies a context this pass cannot see.
472
500
  const roots = [...Object.values(prog.rules), ...extraIps]
473
501
  const labels = computeSiteLabels(code, roots, hostCst)
502
+ // Eligibility is pooled by the terminal's existing constant operand, matching
503
+ // the closure recognizer pool: distinct terminal rows sharing one spec share
504
+ // both the recognizer and the ordinary-terminal lowering.
505
+ const scalarSpecs = new Set<number>()
506
+ if (!hostCst && !cfg.tolerant && !cfg.probe && !cfg.coverage && !cfg.trackLines) {
507
+ for (const ip of reachableSites(code, roots)) {
508
+ if (code[ip] !== OP_NODE) continue
509
+ const child = scalarTerminalNodeChild(code, ip)
510
+ if (child >= 0) scalarSpecs.add(code[child + 1]!)
511
+ }
512
+ }
474
513
 
475
514
  // THE SIDE-SINK CENSUS, over the same reachable set the labels are computed
476
515
  // from. `OP_FIELD` is the only ROW that writes `_fields` and `OP_EXPECT` the
@@ -515,6 +554,7 @@ export function emitAssemblySource(
515
554
  const reached = new Set<number>()
516
555
  const prelude: string[] = []
517
556
  const skipDefs: string[] = []
557
+ const choiceDefs: string[] = []
518
558
  const skipPool = new Map<string, string>()
519
559
  const pool = new Map<string, string>()
520
560
  const masks: Uint32Array[] = []
@@ -543,6 +583,7 @@ export function emitAssemblySource(
543
583
  const kRef = (i: number): string => hoist('k', `K[${i}]`)
544
584
  const fxRef = (i: number): string => hoist('fx', `FX[${i}]`)
545
585
  const fnRef = (i: number): string => hoist('fn', `FNS[${i}]`)
586
+ const recognizerRef = (i: number): string => hoist('rec', `RECOG[${i}]`)
546
587
  /**
547
588
  * The sync sentinel for a char-class index, hoisted once per class.
548
589
  *
@@ -593,9 +634,9 @@ export function emitAssemblySource(
593
634
  // through `scanTrivia`. `needsDeferredTriviaCommit` is implied by an open
594
635
  // buffer, so an in-node site skips the call that asks.
595
636
  skipDefs.push(l.buf
596
- ? `function ${nm}(input,cur,ctx){const s=scanTrivia(input,cur,ctx);s.commit();return s.end}`
637
+ ? `function ${nm}(input,cur,ctx){return commitTriviaScan(scanTriviaCompact(input,cur,ctx))}`
597
638
  : `function ${nm}(input,cur,ctx){
598
- if(needsDeferredTriviaCommit(ctx)){const s=scanTrivia(input,cur,ctx);s.commit();return s.end}
639
+ if(needsDeferredTriviaCommit(ctx))return commitTriviaScan(scanTriviaCompact(input,cur,ctx))
599
640
  return advanceTrivia(input,cur,ctx)}`)
600
641
  return nm
601
642
  }
@@ -729,22 +770,22 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
729
770
  if (op === OP_GATE && (cfg.tolerant || cfg.probe)) return code[ip + 2]!
730
771
  if (op === OP_RULE) return code[ip + 1]!
731
772
  // A SCOPE THAT INSTALLS WHAT IS ALREADY INSTALLED. `encode.ts:520` wraps
732
- // EVERY rule of a `rules({ trivia }, …)` map in its own `OP_SCOPE`, so a
773
+ // EVERY rule of a `rules({ trivia }, …)` map in its own `OP_SCOPE_PLAIN`, so a
733
774
  // grammar with one ambient trivia re-installs the same slot at every rule
734
775
  // entry — six context stores, a scanner swap and their six restores, per
735
776
  // call, to arrive at the values already there.
736
777
  //
737
778
  // The label is what makes this decidable: `tri >= 0` can ONLY have come from
738
- // an enclosing `OP_SCOPE` carrying that slot, and that scope set
779
+ // an enclosing scope carrying that slot, and that scope set
739
780
  // `ctx.triviaKindLabels` and `_pfScan` from the same slot, so all three are
740
- // already the values this row would write. Restricted to plain `OP_SCOPE`
741
- // with no root-capture policy: `OP_SCOPE_CAP` also raises `captureTrivia`,
742
- // and the two flag bits are a real refusal and a real save/restore.
781
+ // already the values this row would write. `OP_SCOPE_PLAIN` has no root
782
+ // policy by construction; a policy-bearing `OP_SCOPE` aliases only when its
783
+ // literal policy is zero. `OP_SCOPE_CAP` also raises `captureTrivia`.
743
784
  //
744
785
  // `ki >= 0` is required rather than implied: `TRI_NONE` and `TRI_UNKNOWN` are
745
786
  // themselves negative, so comparing a negative operand against a lattice
746
787
  // element would read "unknown" as a match.
747
- if (op === OP_SCOPE && code[ip + 3]! === 0 && code[ip + 1]! >= 0
788
+ if ((op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[ip + 3]! === 0)) && code[ip + 1]! >= 0
748
789
  && labels.at(ip).tri === code[ip + 1]!) {
749
790
  return code[ip + 2]!
750
791
  }
@@ -783,6 +824,19 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
783
824
  const s = k[code[ip + 1]!] as string
784
825
  const xf = fxRef(code[ip + 2]!)
785
826
  const track = op === OP_LIT_TRACK
827
+ if (!track && scalarSpecs.has(code[ip + 1]!)) {
828
+ const recognize = recognizerRef(code[ip + 1]!)
829
+ return `${head}
830
+ const e=${recognize}(input,pos)
831
+ if(e>=0){
832
+ ${captureLeaf(q(s))}
833
+ EC.e=e
834
+ return ${q(s)}
835
+ }
836
+ ctx._fe=pos;ctx._fx=${xf}
837
+ return FAIL
838
+ }`
839
+ }
786
840
  // LENGTH IS TABLE DATA, so the compare is chosen here — the emitted twin
787
841
  // of the closure engine's length-keyed literal bodies.
788
842
  const test = s.length === 1
@@ -857,6 +911,20 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
857
911
  case OP_RX_TRACK: {
858
912
  const xf = fxRef(code[ip + 2]!)
859
913
  const track = op === OP_RX_TRACK
914
+ if (!track && scalarSpecs.has(code[ip + 1]!)) {
915
+ const recognize = recognizerRef(code[ip + 1]!)
916
+ return `${head}
917
+ const e=${recognize}(input,pos)
918
+ if(e>=0){
919
+ const v=input.slice(pos,e)
920
+ ${captureLeaf('v')}
921
+ EC.e=e
922
+ return v
923
+ }
924
+ ctx._fe=pos;ctx._fx=${xf}
925
+ return FAIL
926
+ }`
927
+ }
860
928
  // THE MATCH ARRAY IS THE COST, not the matching. `re.exec` allocates one
861
929
  // per row — 6,005 rows per `json/document` parse, 12.9% of everything
862
930
  // executed — and every field of it but `[0]` is discarded here. A shape
@@ -1016,10 +1084,11 @@ return r.value
1016
1084
  }
1017
1085
 
1018
1086
  case OP_SCOPE:
1019
- case OP_SCOPE_CAP: {
1087
+ case OP_SCOPE_CAP:
1088
+ case OP_SCOPE_PLAIN: {
1020
1089
  const ki = code[ip + 1]!
1021
1090
  const cap = op === OP_SCOPE_CAP
1022
- const flags = code[ip + 3]!
1091
+ const flags = op === OP_SCOPE_PLAIN ? 0 : code[ip + 3]!
1023
1092
  const child = link(code[ip + 2]!)
1024
1093
  // THE SWAP, RESOLVED AT EMIT. `swapLegal` is `!trackLines`, an option;
1025
1094
  // the other two are table data. All three are known here, so the body
@@ -1103,6 +1172,75 @@ return out
1103
1172
  }`
1104
1173
  }
1105
1174
 
1175
+ case OP_LEX_BODY: {
1176
+ const recognize = hoist('lex', `LEX[${code[ip + 1]!}]`)
1177
+ const expected = fxRef(code[ip + 2]!)
1178
+ const suffixExpected = fxRef(code[ip + 3]!)
1179
+ const lineFlags = code[ip + 4]!
1180
+ const hasSuffix = (lineFlags & 4) !== 0
1181
+ return `${head}
1182
+ const r=${recognize}(input,pos)
1183
+ if(r<0){ctx._fe=pos;ctx._fx=${expected};if(ctx._probe!==undefined)failAt(ctx,${expected},pos);return FAIL}
1184
+ const sm=${hasSuffix ? 'r%2===1' : 'false'},e=(r-(sm?1:0))/2
1185
+ ${(lineFlags & 1) !== 0 ? '_trackLines(ctx,input,sm?e-1:e)' : ''}
1186
+ ${hasSuffix ? 'ctx._fc=false' : ''}
1187
+ ${hasSuffix && (lineFlags & 2) !== 0 ? 'if(sm)_trackLines(ctx,input,e)' : ''}
1188
+ ${hasSuffix ? `if(!sm){ctx._fe=e;ctx._fx=${suffixExpected};if(ctx._probe!==undefined)failAt(ctx,${suffixExpected},e)}` : ''}
1189
+ const v=input.slice(pos,e)
1190
+ if(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)pushCstLeaf(ctx,{_tag:'leaf',value:v,span:{start:pos,end:e}})
1191
+ EC.e=e
1192
+ return v
1193
+ }`
1194
+ }
1195
+
1196
+ case OP_LEX_PROGRAM: {
1197
+ const programId = code[ip + 1]!
1198
+ const run = hoist('lexProgram', `LEXPROG[${programId}]`)
1199
+ const scanId = t.lexPrograms[programId]!.scan
1200
+ if (scanId !== undefined) return `${head}
1201
+ const sTri=ctx.trivia,sKinds=ctx.triviaKindLabels,sScan=_pfScan
1202
+ const sBuf=ctx._cstBuf,sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
1203
+ const sOtl=ctx._triviaLog,sRtl=ctx._rootTriviaLog
1204
+ const wasCap=ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined
1205
+ _pfScan=null
1206
+ ctx.trivia=undefined
1207
+ ctx.triviaKindLabels=undefined
1208
+ ctx._cstBuf=undefined
1209
+ ctx._cstChildren=undefined
1210
+ ctx._cstLeaves=undefined
1211
+ ctx._cstRawChildren=undefined
1212
+ ctx._cstTriviaLog=undefined
1213
+ ctx._triviaLog=undefined
1214
+ ctx._rootTriviaLog=undefined
1215
+ let e
1216
+ try{e=${run}(input,pos,ctx,SCANS[${scanId}])}finally{
1217
+ _pfScan=sScan
1218
+ ctx.trivia=sTri
1219
+ ctx.triviaKindLabels=sKinds
1220
+ ctx._cstBuf=sBuf
1221
+ ctx._cstChildren=sCh
1222
+ ctx._cstLeaves=sLv
1223
+ ctx._cstRawChildren=sRaw
1224
+ ctx._cstTriviaLog=sTl
1225
+ ctx._triviaLog=sOtl
1226
+ ctx._rootTriviaLog=sRtl
1227
+ }
1228
+ if(e<0)return FAIL
1229
+ const v=input.slice(pos,e)
1230
+ if(wasCap)pushCstLeaf(ctx,{_tag:'leaf',value:v,span:{start:pos,end:e}})
1231
+ EC.e=e
1232
+ return v
1233
+ }`
1234
+ return `${head}
1235
+ const e=${run}(input,pos,ctx)
1236
+ if(e<0)return FAIL
1237
+ const v=input.slice(pos,e)
1238
+ if(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)pushCstLeaf(ctx,{_tag:'leaf',value:v,span:{start:pos,end:e}})
1239
+ EC.e=e
1240
+ return v
1241
+ }`
1242
+ }
1243
+
1106
1244
  /* ── transaction ─────────────────────────────────────────────────────── */
1107
1245
 
1108
1246
  case OP_ATTEMPT: {
@@ -1120,6 +1258,18 @@ return FAIL
1120
1258
  }
1121
1259
 
1122
1260
  case OP_NOT: {
1261
+ const scalarChild = scalarTerminalNotChild(code, ip)
1262
+ if (scalarChild >= 0) {
1263
+ const recognize = recognizerRef(code[scalarChild + 1]!)
1264
+ const xf = fxRef(code[ip + 2]!)
1265
+ return `${head}
1266
+ if(${recognize}(input,pos)<0){EC.e=pos;return null}
1267
+ ctx._fe=pos
1268
+ ctx._fx=${xf}
1269
+ EC.e=pos
1270
+ return FAIL
1271
+ }`
1272
+ }
1123
1273
  const child = link(code[ip + 1]!)
1124
1274
  const xf = fxRef(code[ip + 2]!)
1125
1275
  const p = tmp()
@@ -1172,19 +1322,14 @@ return v
1172
1322
  const base = fused ? ip + 3 : ip + 2
1173
1323
  const n = code[fused ? ip + 2 : ip + 1]!
1174
1324
  const wantValues = op !== OP_SEQV
1175
- // ADJACENCY changes the PARENT's term-boundary emission: the assertion
1176
- // must be answered at the cursor, BEFORE the ambient trivia scan.
1177
- // Refused rather than lowered wrong — a piece handed the post-scan
1178
- // position finds the gap already consumed and answers "adjacent" every
1179
- // time, SILENTLY (`ops.ts:226-251`).
1180
- for (let i = 1; i < n; i++) {
1181
- if (code[code[base + i]!] === OP_ADJ) {
1182
- throw new Unemittable('a sequence carrying an adjacency assertion (OP_ADJ)')
1183
- }
1184
- }
1185
- const fn = fused ? fnRef(code[ip + 1]!) : undefined
1325
+ const reducer = fused ? code[ip + 1]! : -1
1326
+ const projection = reducer < 0 ? ~reducer : -1
1327
+ const fn = fused && projection < 0 ? fnRef(reducer) : undefined
1186
1328
  const kids: string[] = []
1187
- for (let i = 0; i < n; i++) kids.push(link(code[base + i]!))
1329
+ for (let i = 0; i < n; i++) {
1330
+ const childIp = code[base + i]!
1331
+ kids.push(code[childIp] === OP_ADJ ? '' : link(childIp))
1332
+ }
1188
1333
  /**
1189
1334
  * A RECOVERY SEQUENCE PUBLISHES `ctx._sync`, and that is the WHOLE of
1190
1335
  * "recovery config" — no grammar carries any (`assemble.ts:1446`).
@@ -1219,7 +1364,9 @@ return v
1219
1364
  parts.push(`const v0=${kids[0]}(input,pos,ctx)`, 'if(v0===FAIL)return FAIL')
1220
1365
  const close = (): string => REC ? `${parts.join('\n')}\n}finally{ctx._sync=${sy}}\n}` : `${parts.join('\n')}\n}`
1221
1366
  if (n === 1) {
1222
- parts.push(fused ? `return ${fn}([v0],{start:pos,end:EC.e})` : wantValues ? 'return [v0]' : 'return undefined')
1367
+ parts.push(fused
1368
+ ? projection === 0 ? 'return v0' : `return ${fn}([v0],{start:pos,end:EC.e})`
1369
+ : wantValues ? 'return [v0]' : 'return undefined')
1223
1370
  return close()
1224
1371
  }
1225
1372
  parts.push('let cur=EC.e')
@@ -1228,12 +1375,22 @@ return v
1228
1375
  const vn = `v${i}`
1229
1376
  parts.push(`let ${vn}`)
1230
1377
  if (REC) parts.push(pub(i))
1231
- parts.push(emitTerm(kids[i]!, vn, tmp(), L, skipFor(L), sinks))
1378
+ const childIp = code[base + i]!
1379
+ if (code[childIp] === OP_ADJ) {
1380
+ const negated = code[childIp + 1] === 1
1381
+ const ki = code[childIp + 2]!
1382
+ const kinds = ki < 0 ? 'undefined' : `K[${ki}]`
1383
+ const expected = fxRef(code[childIp + 3]!)
1384
+ parts.push(
1385
+ `if(!adjacencyHolds(input,cur,ctx,${negated ? 'true' : 'false'},${kinds})){ctx._fe=cur;ctx._fx=${expected};return FAIL}`,
1386
+ `${vn}=null`,
1387
+ )
1388
+ } else parts.push(emitTerm(kids[i]!, vn, tmp(), L, skipFor(L)))
1232
1389
  names.push(vn)
1233
1390
  }
1234
1391
  parts.push('EC.e=cur')
1235
1392
  parts.push(fused
1236
- ? `return ${fn}([${names.join(',')}],{start:pos,end:cur})`
1393
+ ? projection >= 0 ? `return ${names[projection]}` : `return ${fn}([${names.join(',')}],{start:pos,end:cur})`
1237
1394
  : wantValues ? `return [${names.join(',')}]` : 'return undefined')
1238
1395
  return close()
1239
1396
  }
@@ -1245,11 +1402,6 @@ return v
1245
1402
  const base = ip + 4
1246
1403
  const arms: string[] = []
1247
1404
  for (let i = 0; i < n; i++) arms.push(link(code[base + i]!))
1248
- const axi = armExpected.push(
1249
- Array.from({ length: n }, (_, i) => t.fx[code[base + n + i]!] as readonly string[]),
1250
- ) - 1
1251
- armExpectedPlan.push(Array.from({ length: n }, (_, i) => code[base + n + i]!))
1252
- const afx = hoist('afx', `AFX[${axi}]`)
1253
1405
 
1254
1406
  if (table.exclusive) {
1255
1407
  // NO OPEN ARMS EXIST under `exclusive` — `resolveDispatch` clears the
@@ -1283,52 +1435,51 @@ return FAIL
1283
1435
  }`
1284
1436
  }
1285
1437
 
1286
- // THE PER-ARM GATE. `assemble.ts` precomputes a candidate bitmask so one
1287
- // `Uint32Array` load replaces `n` class tests, then walks the set bits
1288
- // through `arms[i]`. The mask survives here; the indexed CALL does not.
1289
- const ci = classes.push(Array.from({ length: n }, (_, i) => table.armCls[i] ?? null)) - 1
1290
- // `resolveDispatch` builds `armCls[a]` as `cc[disp[di][a]]`, or null when
1291
- // that index is negative (program.ts:419-423), so the arm's CLASS INDEX
1292
- // is the dispatch row itself. Nothing is re-derived here.
1293
- classPlan.push(Array.from({ length: n }, (_, i) => prog.disp[code[ip + 1]!]![i] ?? -1))
1294
- const gates = classes[ci]!
1295
- const gRef = hoist('g', `CLS[${ci}]`)
1296
- const maskable = n <= 32
1297
- let maskName = ''
1298
- if (maskable) {
1299
- maskName = hoist('mk', `MASK[${masks.push(maskForClassRow(gates)) - 1}]`)
1300
- // The mask is a pure function of `gates`, i.e. of `CLS[ci]`, so the
1301
- // plan records the class-pool index rather than 129 words per site.
1302
- maskPlan.push(ci)
1303
- }
1304
- const p = tmp()
1305
- // THE SKIPPED ARMS' SETS STAY LAZY. `prev` and the catch-up loop are the
1306
- // closure engine's, kept verbatim: they run only on a FAILURE path, and
1307
- // an arm that MATCHES returns without paying for any of it. Unrolling
1308
- // them would put an `_accSet` — and its `slice()` — on the success path.
1309
- const maskArms = arms.map((a, i) => `
1310
- if((bits&${1 << i})!==0){
1438
+ {
1439
+ // Direct topology for every fixed arity. The emitted body names every
1440
+ // child, gate and expected set; no AFX/CLS row survives merely to feed
1441
+ // an indexed winner back into one call site.
1442
+ const di = code[ip + 1]!
1443
+ const expected = Array.from({ length: n }, (_, i) => fxRef(code[base + n + i]!))
1444
+ const gates = Array.from({ length: n }, (_, i) => table.armCls[i] ?? null)
1445
+ const gateRefs = Array.from({ length: n }, (_, i) => hoist('g', `DISP[${di}].armCls[${i}]`))
1446
+ const maskable = n <= 32
1447
+ const maskName = maskable
1448
+ ? hoist('mk', `MASK[${masks.push(maskForClassRow(gates)) - 1}]`)
1449
+ : ''
1450
+ // `~di` points back to the authoritative dispatch row. A precompiled
1451
+ // module reconstructs only the input-indexed mask from that row; it
1452
+ // cannot carry a second mutable answer for the arms' classes.
1453
+ if (maskable) maskPlan.push(~di)
1454
+ const p = tmp()
1455
+ const catchName = maskable ? `_cx${uid++}_` : ''
1456
+ if (maskable) {
1457
+ const catchCases = expected.map((e, i) =>
1458
+ `case ${i}:if(target<=${i})return acc;acc=_accSet(${e},acc)`).join('\n')
1459
+ choiceDefs.push(`function ${catchName}(target,prev,acc){switch(prev){\n${catchCases}\n}return acc}`)
1460
+ }
1461
+ const maskArms = maskable ? arms.map((arm, i) => {
1462
+ return `if((bits&${1 << i})!==0){
1311
1463
  ctx._fc=false
1312
- {const v=${a}(input,pos,ctx)
1464
+ {const v=${arm}(input,pos,ctx)
1313
1465
  if(v!==FAIL)return v}
1314
- for(let j=prev;j<${i};j++)if((bits&(1<<j))===0)acc=_accSet(${afx}[j],acc)
1466
+ acc=${catchName}(${i},prev,acc)
1315
1467
  prev=${i + 1}
1316
1468
  acc=_accSet(ctx._fx,acc)
1317
1469
  if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
1318
1470
  ${emitRollback(p, L.buf, sinks)}
1319
- }`).join('')
1320
-
1321
- const generalArms = arms.map((a, i) => `
1322
- if(${gRef}[${i}]===null||classHas(${gRef}[${i}],c)){
1471
+ }`
1472
+ }).join('\n') : ''
1473
+ const generalArms = arms.map((arm, i) => `if(${gateRefs[i]}===null||classHas(${gateRefs[i]},c)){
1323
1474
  ctx._fc=false
1324
- {const v=${a}(input,pos,ctx)
1475
+ {const v=${arm}(input,pos,ctx)
1325
1476
  if(v!==FAIL)return v}
1326
1477
  acc=_accSet(ctx._fx,acc)
1327
1478
  if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
1328
1479
  ${emitRollback(p, L.buf, sinks)}
1329
- }else acc=_accSet(${afx}[${i}],acc)`).join('')
1480
+ }else acc=_accSet(${expected[i]},acc)`).join('\n')
1330
1481
 
1331
- return `${head}
1482
+ return `${head}
1332
1483
  const c=lead(input,pos)
1333
1484
  ${emitMark(p, L.buf, sinks)}
1334
1485
  let acc
@@ -1336,45 +1487,58 @@ ${maskable ? `if(c<128){
1336
1487
  const bits=${maskName}[c<0?128:c]
1337
1488
  let prev=0
1338
1489
  ${maskArms}
1339
- for(let j=prev;j<${n};j++)if((bits&(1<<j))===0)acc=_accSet(${afx}[j],acc)
1490
+ acc=${catchName}(${n},prev,acc)
1340
1491
  ctx._fe=pos;ctx._fx=acc??${choiceFx}
1341
1492
  return FAIL
1342
1493
  }
1343
- ` : ''}${generalArms}
1494
+ ` : ''}
1495
+ ${generalArms}
1344
1496
  ctx._fe=pos;ctx._fx=acc??${choiceFx}
1345
1497
  return FAIL
1346
1498
  }`
1499
+ }
1500
+
1347
1501
  }
1348
1502
 
1349
1503
  case OP_DISPATCH: {
1350
1504
  const di = code[ip + 2]!
1351
- const spec = dsp[di]!
1505
+ const spec = dsp[di]
1352
1506
  const selector = link(code[ip + 1]!)
1353
1507
  const otherIp = code[ip + 3]!
1354
1508
  const other = otherIp >= 0 ? link(otherIp) : undefined
1355
1509
  const otherRouted = code[ip + 4]! === 1
1356
1510
  const n = code[ip + 5]!
1511
+ validateDispatchSpec(spec, n, code[ip + 4]!)
1357
1512
  const armBase = ip + 6
1358
1513
  const arms: string[] = []
1359
1514
  for (let i = 0; i < n; i++) arms.push(link(code[armBase + i]!))
1360
1515
  const bk = hoist('bk', `DSP[${di}].byKey`)
1361
- const rt = hoist('rt', `DSP[${di}].routed`)
1362
1516
  const dx = hoist('dx', `DSP[${di}].expected`)
1363
1517
  // THE MATCHER ARMS, AS SOURCE. `exec.ts`'s `linkMatcher` mints one
1364
1518
  // closure per arm from four literals it already has in hand; the four
1365
1519
  // are TABLE DATA, so here they are the test itself and no closure, no
1366
1520
  // pool and no indexed call exist. Kinds are `matcherClaims`'s exactly
1367
1521
  // — 0/1 raw prefix/suffix, 3/4 the pre-folded pair, anything else a
1368
- // regex. The regex is built PER TEST, deliberately: a hoisted one
1369
- // carries `lastIndex` across parses whenever the author's pattern is
1370
- // sticky or global (`exec.ts:140-144`).
1522
+ // regex. Public `matches()` refuses global/sticky patterns, so its
1523
+ // RegExp can be compiled once into the assembly prelude. Hand-built
1524
+ // low-level rows with g/y or invalid flags retain the old per-test
1525
+ // construction and therefore the old `lastIndex`/throw behavior.
1371
1526
  const claims = (m: readonly [number, string, string, number]): string => {
1372
1527
  switch (m[0]) {
1373
1528
  case 0: return `key.startsWith(${q(m[1])})`
1374
1529
  case 1: return `key.endsWith(${q(m[1])})`
1375
1530
  case 3: return `asciiFoldKey(key).startsWith(${q(m[1])})`
1376
1531
  case 4: return `asciiFoldKey(key).endsWith(${q(m[1])})`
1377
- default: return `new RegExp(${q(m[1])},${q(m[2])}).test(key)`
1532
+ default: {
1533
+ if (!m[2].includes('g') && !m[2].includes('y')) {
1534
+ try {
1535
+ // Validate without changing malformed-row error timing.
1536
+ new RegExp(m[1], m[2])
1537
+ return `${hoist('dm', `new RegExp(${q(m[1])},${q(m[2])})`)}.test(key)`
1538
+ } catch {}
1539
+ }
1540
+ return `new RegExp(${q(m[1])},${q(m[2])}).test(key)`
1541
+ }
1378
1542
  }
1379
1543
  }
1380
1544
  const chain = spec.match.length === 0
@@ -1385,6 +1549,19 @@ return FAIL
1385
1549
  : ''
1386
1550
  const m1 = tmp()
1387
1551
  const m2 = tmp()
1552
+ const routedCall = (target: string): string => `{const savedRouted=ctx._routed
1553
+ ${emitRollback(m1, L.buf, sinks)}
1554
+ ${emitMark(m2, L.buf, sinks, false)}
1555
+ ctx._routed={value:key,span:{start:pos,end:selEnd}}
1556
+ try{v=${target}(input,pos,ctx)}finally{ctx._routed=savedRouted}
1557
+ break}`
1558
+ const plainCall = (target: string): string => `v=${target}(input,selEnd,ctx);break`
1559
+ const armCases = arms.map((target, i) => `case ${i}:${spec.routed[i] === 1
1560
+ ? routedCall(target)
1561
+ : plainCall(target)}`).join('\n')
1562
+ const fallbackCase = other === undefined ? '' : `default:${otherRouted
1563
+ ? routedCall(other)
1564
+ : plainCall(other)}`
1388
1565
  // THE SELECTOR RUNS ONCE and the key it returns picks the arm — that is
1389
1566
  // what `dispatch()` buys over a choice of arms that each re-parse the
1390
1567
  // opener. A routed arm rewinds the selector's trivia capture and gets
@@ -1396,24 +1573,16 @@ if(sv===FAIL)return FAIL
1396
1573
  const selEnd=EC.e
1397
1574
  const key=sv
1398
1575
  let arm=${bk}.get(key)
1399
- ${fold}${chain}let ur
1576
+ ${fold}${chain}
1400
1577
  if(arm===undefined){
1401
- ${other === undefined ? `ctx._fe=selEnd;ctx._fx=${dx};return FAIL` : `ur=${String(otherRouted)}`}
1402
- }else ur=${rt}[arm]===1
1403
- const savedRouted=ctx._routed
1404
- ${emitMark(m2, L.buf, sinks)}
1405
- if(ur){
1406
- ${emitRollback(m1, L.buf, sinks)}
1407
- ${emitMark(m2, L.buf, sinks, false)}
1408
- ctx._routed={value:key,span:{start:pos,end:selEnd}}
1578
+ ${other === undefined ? `ctx._fe=selEnd;ctx._fx=${dx};return FAIL` : ''}
1409
1579
  }
1410
- const at=ur?pos:selEnd
1580
+ ${emitMark(m2, L.buf, sinks)}
1411
1581
  let v
1412
1582
  switch(arm){
1413
- ${arms.map((a, i) => `case ${i}:v=${a}(input,at,ctx);break`).join('\n')}
1414
- ${other === undefined ? '' : `default:v=${other}(input,at,ctx)`}
1583
+ ${armCases}
1584
+ ${fallbackCase}
1415
1585
  }
1416
- if(ur)ctx._routed=savedRouted
1417
1586
  if(v===FAIL){
1418
1587
  ${emitRollback(m2, L.buf, sinks)}
1419
1588
  ctx._fc=true
@@ -1436,6 +1605,15 @@ return [key,v]
1436
1605
  const reportItem = (flags & 4) !== 0
1437
1606
  const itemFx = reportItem ? fxRef(code[ip + 6]!) : 'EMPTY_FX'
1438
1607
  const collect = op === OP_REP
1608
+ let itemCls: string | undefined
1609
+ if (!REC) {
1610
+ const itemClassIndex = sepIp < 0 ? code[ip + 7]! : -1
1611
+ if (itemClassIndex >= 0) {
1612
+ const ci = classes.push([t.cc[itemClassIndex]!]) - 1
1613
+ classPlan.push([itemClassIndex])
1614
+ itemCls = hoist('ri', `CLS[${ci}][0]`)
1615
+ }
1616
+ }
1439
1617
  const skipBeforeFirst = sepIp < 0 && min === 0
1440
1618
  const p = tmp()
1441
1619
  // `viaRepItem` is `sep === undefined && count >= min && (count > 0 ||
@@ -1455,7 +1633,6 @@ return [key,v]
1455
1633
  const skip = skipFor(L)
1456
1634
  const knownTrivia = L.tri === TRI_NONE ? false : L.tri !== TRI_UNKNOWN ? true : undefined
1457
1635
  const hasTrivia = knownTrivia === undefined ? 'hasTrivia' : String(knownTrivia)
1458
- const needMark = L.buf ? 'true' : 'needMark'
1459
1636
  // THE `_fields` AND `_errors` MARKS ARE TAKEN ONLY WHERE THE TABLE HAS A
1460
1637
  // WRITER, which is the same emit-time census `emitMark` applies to the
1461
1638
  // non-loop marks. Before `OP_FIELD` and `OP_EXPECT` were emittable the
@@ -1541,10 +1718,10 @@ continue
1541
1718
  `
1542
1719
  return `${head}
1543
1720
  const out=${collect ? '[]' : 'undefined'}
1544
- ${knownTrivia === undefined ? 'const hasTrivia=ctx.trivia!==undefined\n' : ''}${L.buf ? '' : 'const needMark=_rollbackNeeded(ctx)\n'}${REC ? `const ${my}=ctx._sync\n` : ''}let cur=pos
1721
+ ${knownTrivia === undefined ? 'const hasTrivia=ctx.trivia!==undefined\n' : ''}${L.buf ? '' : 'const needMark=_rollbackNeeded(ctx)\n'}${REC ? `const ${my}=ctx._sync\n` : ''}${itemCls === undefined ? '' : `const ${p}gate=ctx._probe===undefined\n`}let cur=pos
1545
1722
  let count=0
1546
1723
  for(;;){
1547
- ${max >= 0 ? `if(count>=${max})break\n` : ''}${sep !== undefined ? `if(count>0&&count>=${min}&&cur>=input.length)break\n` : ''}let ${p}raw=0,${p}tl=0,${p}lv=0,${p}lg=0,${p}rt=0${sinks.fd ? `,${p}fd=0` : ''}${sinks.er ? `,${p}er=0` : ''}
1724
+ ${max >= 0 ? `if(count>=${max})break\n` : ''}${sep !== undefined ? `if(count>0&&count>=${min}&&cur>=input.length)break\n` : ''}${itemCls !== undefined && sep === undefined ? `if(count>=${min}&&${p}gate&&${hasTrivia === 'false' ? 'true' : hasTrivia === 'true' ? 'false' : '!hasTrivia'}&&!classHas(${itemCls},lead(input,cur)))break\n` : ''}let ${p}raw=0,${p}tl=0,${p}lv=0,${p}lg=0,${p}rt=0${sinks.fd ? `,${p}fd=0` : ''}${sinks.er ? `,${p}er=0` : ''}
1548
1725
  ${markBody}
1549
1726
  let itemStart=cur
1550
1727
  let sepEnd=-1
@@ -1566,7 +1743,11 @@ itemStart=${hasTrivia === 'false' ? 'EC.e' : hasTrivia === 'true' ? `${skip}(inp
1566
1743
  ${rb}
1567
1744
  ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
1568
1745
  }
1569
- ctx._fc=false
1746
+ ${itemCls === undefined ? '' : `if(count>=${min}&&${p}gate&&!classHas(${itemCls},lead(input,itemStart))){
1747
+ ${rb}
1748
+ ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
1749
+ }
1750
+ `}ctx._fc=false
1570
1751
  const v=${child}(input,itemStart,ctx)
1571
1752
  if(v===FAIL){
1572
1753
  ${REC
@@ -1603,6 +1784,26 @@ return out
1603
1784
  case OP_NODE:
1604
1785
  case OP_NODE_TRACK: {
1605
1786
  const flags = code[ip + 3]!
1787
+ const scalarChild = scalarTerminalNodeChild(code, ip)
1788
+ if (scalarChild >= 0 && scalarSpecs.has(code[scalarChild + 1]!)) {
1789
+ const recognize = recognizerRef(code[scalarChild + 1]!)
1790
+ const spec = code[scalarChild] === OP_RX ? null : k[code[scalarChild + 1]!] as string
1791
+ const value = spec === null ? 'input.slice(pos,end)' : q(spec)
1792
+ const xf = fxRef(code[scalarChild + 2]!)
1793
+ const build = fnRef(code[ip + 1]!)
1794
+ return `${head}
1795
+ const end=${recognize}(input,pos)
1796
+ if(end<0){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1797
+ const value=${value}
1798
+ const leaf={_tag:'leaf',value,span:{start:pos,end}}
1799
+ const kids=[leaf],rawKids=[leaf],span={start:pos,end}
1800
+ EC.e=end
1801
+ const nd=${build}(kids,undefined,span,rawKids,EMPTY_TL,undefined)
1802
+ ${L.buf ? 'pushCstChild(ctx,nd,rawEntry(nd,input,pos,end))' : 'if(ctx._cstBuf!==undefined||ctx._cstChildren!==undefined)pushCstChild(ctx,nd,rawEntry(nd,input,pos,end))'}
1803
+ EC.e=end
1804
+ return nd
1805
+ }`
1806
+ }
1606
1807
  const child = link(code[ip + 2]!)
1607
1808
  const proj = code[ip + 4]!
1608
1809
  const buildIdx = code[ip + 1]!
@@ -1620,6 +1821,58 @@ return out
1620
1821
  // interpreter's node case — the most-executed non-terminal in any of
1621
1822
  // these grammars. It selects the emitted shape instead.
1622
1823
  const build = buildIdx >= 0 ? fnRef(buildIdx) : undefined
1824
+ // The closure assembler's direct-builder capture tiers are table facts,
1825
+ // not closure-only semantics. Print the same body for a precompiled
1826
+ // assembly so a large composeLeaf artifact does not fall back to opening
1827
+ // the generic raw/trivia buffer that its reducer arity proved unread.
1828
+ if (!hostCst && !tracked && build !== undefined && proj < 0
1829
+ && (flags === 2 || flags === 18 || flags === 34)) {
1830
+ const fields = flags === 18
1831
+ const collapseChildren = flags === 34
1832
+ const publish = `if(sBuf!==undefined){
1833
+ if(sBuf.rawOnly!==true){
1834
+ if(sBuf.ch!==undefined)sBuf.ch.push(nd)
1835
+ else if(sBuf.single!==undefined){sBuf.ch=[sBuf.single,nd];sBuf.single=undefined}
1836
+ else sBuf.single=nd
1837
+ }
1838
+ const rawNd=rawEntry(nd,input,pos,end)
1839
+ if(sBuf.raw!==undefined)sBuf.raw.push(rawNd)
1840
+ else if(sBuf.rawSingle!==undefined){sBuf.raw=[sBuf.rawSingle,rawNd];sBuf.rawSingle=undefined}
1841
+ else sBuf.rawSingle=rawNd
1842
+ }else if(sCh!==undefined){
1843
+ sCh.push(nd)
1844
+ if(sRaw!==undefined)sRaw.push(rawEntry(nd,input,pos,end))
1845
+ }`
1846
+ return `${head}
1847
+ const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
1848
+ const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf,sFields=ctx._fields
1849
+ const flat=[]
1850
+ ctx._cstBuf=undefined
1851
+ ctx._cstChildren=flat
1852
+ ctx._cstLeaves=flat
1853
+ ctx._cstRawChildren=undefined
1854
+ ctx._cstTriviaLog=undefined
1855
+ ctx.captureTrivia=false
1856
+ ctx._fields=${fields ? '[]' : 'undefined'}
1857
+ const v=${child}(input,pos,ctx)
1858
+ const captured=_capturedFlatChildren(flat)
1859
+ ${fields ? 'const fieldMap=buildFieldMap(ctx._fields)\n' : ''}ctx._fields=sFields
1860
+ ctx._cstBuf=sBuf
1861
+ ctx._cstChildren=sCh
1862
+ ctx._cstLeaves=sLv
1863
+ ctx._cstRawChildren=sRaw
1864
+ ctx._cstTriviaLog=sTl
1865
+ ctx.captureTrivia=sCap
1866
+ if(v===FAIL)return FAIL
1867
+ const end=EC.e
1868
+ ${collapseChildren
1869
+ ? `const nd=captured.length===1?captured[0]:${build}(captured,undefined,{start:pos,end},EMPTY_CH,EMPTY_TL,undefined)`
1870
+ : `const nd=${build}(captured,${fields ? 'fieldMap' : 'undefined'},{start:pos,end},EMPTY_CH,EMPTY_TL,undefined)`}
1871
+ ${publish}
1872
+ EC.e=end
1873
+ return nd
1874
+ }`
1875
+ }
1623
1876
  const structural = build === undefined && proj < 0
1624
1877
  const grammarCapture = (flags & 1) !== 0 || trailingTrivia
1625
1878
  const hostCapturesThisType = structural && cfg.hostCaptureTrivia !== undefined
@@ -1734,6 +1987,7 @@ return nd
1734
1987
  const source = `${RUNTIME_PRELUDE}
1735
1988
  ${prelude.join('\n')}
1736
1989
  ${skipDefs.join('\n')}
1990
+ ${choiceDefs.join('\n')}
1737
1991
  ${bodies.join('\n')}
1738
1992
  function _begin(ctx){
1739
1993
  const host=ctx.build