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
@@ -570,11 +570,54 @@ function regexTriviaScanner(parser: Combinator<unknown>): FastTriviaScanner | nu
570
570
  // and every trivia gap in every dialect went through the labelled char scanner.
571
571
  const d = parser._def.tag === 'label' ? parser._def.parser._def : parser._def
572
572
  if (d.tag !== 'regex' || d.flags) return null
573
- const source = d.source
573
+ return sourceScanner(d.source)
574
+ }
575
+
576
+ function sourceScanner(source: string): FastTriviaScanner | null {
574
577
  return classRunSource(source)
575
578
  ?? altStarSource(source)
576
579
  ?? prefixRunSource(source)
577
580
  ?? delimitedSource(source)
581
+ ?? sharedPrefixAltSource(source)
582
+ }
583
+
584
+ /**
585
+ * `<literal>(?:<alt>|<alt>…)`: a literal every alternative shares, as in jess's
586
+ * one comment arm `\/(?:\*(?:[^*]|\*(?!\/))*\*\/|\/[^\n\r]*)` for both `/*…*\/`
587
+ * and `//…`. Each `<literal><alt>` is lowered on its own, and the scanner takes
588
+ * the first that matches, as the regex does. Without it that arm had no
589
+ * scanner, so neither did the trivia it sits in.
590
+ */
591
+ function sharedPrefixAltSource(source: string): FastTriviaScanner | null {
592
+ const m = /^((?:\\.|[^\\[\]()*+?|^$.{}])+)\(\?:(.*)\)$/.exec(source)
593
+ if (!m) return null
594
+ const alts: string[] = []
595
+ const body = m[2]!
596
+ let depth = 0
597
+ let start = 0
598
+ for (let i = 0; i < body.length; i++) {
599
+ const c = body[i]
600
+ if (c === '\\') { i++; continue }
601
+ if (c === '[') { while (i < body.length && body[i] !== ']') i += body[i] === '\\' ? 2 : 1; continue }
602
+ if (c === '(') depth++
603
+ else if (c === ')' && --depth < 0) return null
604
+ else if (c === '|' && depth === 0) { alts.push(body.slice(start, i)); start = i + 1 }
605
+ }
606
+ if (depth !== 0 || alts.length === 0) return null
607
+ alts.push(body.slice(start))
608
+ const scanners: FastTriviaScanner[] = []
609
+ for (const alt of alts) {
610
+ const scanner = sourceScanner(m[1]! + alt)
611
+ if (scanner === null) return null
612
+ scanners.push(scanner)
613
+ }
614
+ return (input, cur) => {
615
+ for (const scanner of scanners) {
616
+ const end = scanner(input, cur)
617
+ if (end > cur) return end
618
+ }
619
+ return cur
620
+ }
578
621
  }
579
622
 
580
623
  /**
@@ -338,7 +338,7 @@ export function confirmedArityForDef(def: NodeDef): number | null {
338
338
  where: `node("${def.type ?? '<inferred>'}")`,
339
339
  subject,
340
340
  fellBackTo: `could not confirm its declared arity — ${unconfirmableReason(def, src)} — `
341
- + 'so this node captures children, fields and raw children, logs trivia, and clones `_ctx.state` on every match',
341
+ + 'so this node captures children, fields and raw children, logs trivia, and hands it `_ctx.state` on every match',
342
342
  otherwise: 'only the tiers the reducer actually declares would be captured '
343
343
  + '(arity >= 1 children, >= 2 fields, >= 4 raw, >= 5 trivia, >= 6 state). '
344
344
  + 'Declare it with `node(..., { buildArity: n })` if it cannot be derived',
@@ -371,9 +371,9 @@ export function buildReadsTrivia(def: NodeDef): boolean {
371
371
  return arity >= 5
372
372
  }
373
373
 
374
- /** Build reads the 6th (state) arg? Unknown/unparseable → true (keep state clone). */
374
+ /** Build reads the 6th (state) arg? Unknown/unparseable → true (hand it the state). */
375
375
  export function buildReadsState(def: NodeDef): boolean {
376
- if (!def.build) return true // structural node: host may read state — keep clone
376
+ if (!def.build) return true // structural node: host may read state
377
377
  const arity = confirmedArityForDef(def)
378
378
  if (arity === null) return true
379
379
  return arity >= 6
@@ -37,6 +37,7 @@ import type { BalancedSpec } from '../combinators/scanTo.ts'
37
37
  import { deriveExpected } from '../combinators/expect.ts'
38
38
  import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
39
39
  import { keywordsRegExp } from '../combinators/keywords.ts'
40
+ import { hasOwnTriviaBoundary } from '../combinators/trivia-boundary.ts'
40
41
 
41
42
  /** A terminal in the derived alphabet, with its globally assigned id. */
42
43
  export type TokenTerminal =
@@ -135,12 +136,17 @@ export type ExecutableLexNode =
135
136
  | {
136
137
  readonly kind: 'choice'
137
138
  readonly arms: readonly ExecutableLexNode[]
138
- readonly firstSets: readonly FirstSet[]
139
+ /** The authored arms: the encoder classifies them as the character choice would. */
140
+ readonly parsers: readonly Combinator<unknown>[]
141
+ /** Inside a no-trivia scope of the token, where the encoder's trivia is none. */
142
+ readonly cleared: boolean
139
143
  readonly armExpected: readonly (readonly string[])[]
140
144
  readonly expected: readonly string[]
141
145
  }
142
146
  | { readonly kind: 'not'; readonly body: ExecutableLexNode; readonly expected: readonly string[] }
143
147
  | { readonly kind: 'optional'; readonly body: ExecutableLexNode }
148
+ /** A separator-less `many`/`oneOrMore`; `max` −1 is unbounded. */
149
+ | { readonly kind: 'repeat'; readonly body: ExecutableLexNode; readonly min: number; readonly max: number }
144
150
 
145
151
  export type ExecutableFixedLexBody = {
146
152
  readonly kind: 'fixed-tree'
@@ -204,9 +210,22 @@ export function selectOptionalSuffixLexBody(ir: LexicalIr): LexBodyCandidate {
204
210
  /** Unwrap only scopes that token() has already installed identically. The
205
211
  * returned parser is still the authored terminal/sequence; no effectful wrapper
206
212
  * or final named winner is guessed from construction metadata. */
213
+ /**
214
+ * A rule reference made with no trivia in force reinstalls the rule's own
215
+ * ambient trivia when the rule has a position that reads it (`ref.ts`, the
216
+ * encoder's `scopedRef`), and inside a token none is in force. Such a rule's
217
+ * terms are then trivia-separated, which a lexical body does not model, so the
218
+ * reference is not lowered.
219
+ */
220
+ function reinstallsRuleTrivia(reference: Combinator<unknown>, target: Combinator<unknown>): boolean {
221
+ return (reference._meta.grammarTrivia ?? target._meta.grammarTrivia) !== undefined
222
+ && hasOwnTriviaBoundary(target)
223
+ }
224
+
207
225
  function directTokenBodyParser(
208
226
  parser: Combinator<unknown>,
209
227
  resolve?: (name: string) => Combinator<unknown> | undefined,
228
+ scope?: { cleared: boolean },
210
229
  ): Combinator<unknown> | undefined {
211
230
  const outer = parser._def
212
231
  if (outer.tag !== 'token') return undefined
@@ -220,12 +239,13 @@ function directTokenBodyParser(
220
239
  || wrapper.captureTrivia === true || wrapper.rootCapture === 'opaque'
221
240
  || wrapper.constructionTrackLines !== 'inherit'
222
241
  || wrapper.constructionCaptureTriviaKinds !== undefined) return undefined
242
+ if (scope !== undefined) scope.cleared = true
223
243
  body = wrapper.parser
224
244
  continue
225
245
  }
226
246
  if (wrapper.tag === 'lazy') {
227
247
  const target = resolvedLazyTarget(body, resolve)
228
- if (target === undefined) return undefined
248
+ if (target === undefined || reinstallsRuleTrivia(body, target)) return undefined
229
249
  body = target
230
250
  continue
231
251
  }
@@ -436,6 +456,7 @@ function directFixedLexNode(
436
456
  resolve: ((name: string) => Combinator<unknown> | undefined) | undefined,
437
457
  active: Set<Combinator<unknown>>,
438
458
  pureOnly = false,
459
+ cleared = false,
439
460
  ): ExecutableLexNode | undefined {
440
461
  let current = parser
441
462
  const wrappers = new Set<Combinator<unknown>>()
@@ -444,7 +465,7 @@ function directFixedLexNode(
444
465
  const def = current._def
445
466
  if (def.tag === 'lazy') {
446
467
  const target = resolvedLazyTarget(current, resolve)
447
- if (target === undefined) return undefined
468
+ if (target === undefined || reinstallsRuleTrivia(current, target)) return undefined
448
469
  current = target
449
470
  continue
450
471
  }
@@ -459,43 +480,57 @@ function directFixedLexNode(
459
480
  if (terminal !== undefined) return { kind: 'terminal', terminal }
460
481
  const def = current._def
461
482
  if (def.tag === 'grammar') {
483
+ // A scope with no trivia of its own writes nothing under a token that is
484
+ // not already in force: a cleared one runs as its child in the table
485
+ // (`scopeIsIdentity`) and an inheriting one encodes no row. Its line
486
+ // policy is the table's own (`hasScopedTrackLines`). So it is its child
487
+ // at any position.
488
+ if (def.triviaParser === undefined && def.captureTrivia !== true
489
+ && def.rootCapture !== 'opaque' && def.constructionCaptureTriviaKinds === undefined) {
490
+ return directFixedLexNode(def.parser, resolve, active, pureOnly, cleared || def.clearTrivia === true)
491
+ }
462
492
  if (!pureOnly || (def.parser._def.tag !== 'token'
463
493
  && (def.clearTrivia !== true || def.triviaParser !== undefined
464
494
  || def.captureTrivia === true || def.rootCapture === 'opaque'))) return undefined
465
- return directFixedLexNode(def.parser, resolve, active, true)
495
+ return directFixedLexNode(def.parser, resolve, active, true, cleared)
466
496
  }
467
497
  if (def.tag === 'sequence') {
468
498
  if (def.parsers.length < 2 || def.parsers.length > 5) return undefined
469
- const parts = def.parsers.map(part => directFixedLexNode(part, resolve, active, pureOnly))
499
+ const parts = def.parsers.map(part => directFixedLexNode(part, resolve, active, pureOnly, cleared))
470
500
  if (parts.some(part => part === undefined)) return undefined
471
501
  return { kind: 'sequence', parts: parts as ExecutableLexNode[] }
472
502
  }
473
503
  if (def.tag === 'choice') {
474
- if ((def.parsers.length !== 2 && def.parsers.length !== 4 && def.parsers.length !== 8)
504
+ if (def.parsers.length < 2
475
505
  || def.strategy.tag !== 'firstMatch'
476
506
  || def.gates.some(gate => gate !== null)
477
507
  || def.autoNot.some(check => check !== null)) return undefined
478
- const arms = def.parsers.map(arm => directFixedLexNode(arm, resolve, active, pureOnly))
508
+ const arms = def.parsers.map(arm => directFixedLexNode(arm, resolve, active, pureOnly, cleared))
479
509
  if (arms.some(arm => arm === undefined)) return undefined
480
- const final = classifyFinalChoice(def.parsers, resolve)
481
510
  return {
482
511
  kind: 'choice', arms: arms as ExecutableLexNode[],
483
- firstSets: final.firstSets,
512
+ parsers: def.parsers, cleared,
484
513
  armExpected: def.parsers.map(deriveExpected),
485
514
  expected: def.parsers.flatMap(deriveExpected),
486
515
  }
487
516
  }
488
517
  if (def.tag === 'not') {
489
- const body = directFixedLexNode(def.parser, resolve, active, true)
518
+ const body = directFixedLexNode(def.parser, resolve, active, true, cleared)
490
519
  return body === undefined ? undefined : {
491
520
  kind: 'not', body,
492
521
  expected: assertionFailureExpected(false, def.parser._tag),
493
522
  }
494
523
  }
495
524
  if (def.tag === 'optional') {
496
- const body = directFixedLexNode(def.parser, resolve, active, pureOnly)
525
+ const body = directFixedLexNode(def.parser, resolve, active, pureOnly, cleared)
497
526
  return body === undefined ? undefined : { kind: 'optional', body }
498
527
  }
528
+ if (def.tag === 'many' || def.tag === 'oneOrMore') {
529
+ const body = directFixedLexNode(def.parser, resolve, active, pureOnly, cleared)
530
+ return body === undefined ? undefined : {
531
+ kind: 'repeat', body, min: def.tag === 'many' ? 0 : def.min, max: def.max ?? -1,
532
+ }
533
+ }
499
534
  return undefined
500
535
  } finally {
501
536
  active.delete(current)
@@ -506,9 +541,10 @@ export function directFixedTokenBody(
506
541
  parser: Combinator<unknown>,
507
542
  resolve?: (name: string) => Combinator<unknown> | undefined,
508
543
  ): ExecutableFixedLexBody | undefined {
509
- const body = directTokenBodyParser(parser, resolve)
544
+ const scope = { cleared: false }
545
+ const body = directTokenBodyParser(parser, resolve, scope)
510
546
  if (body === undefined) return undefined
511
- const root = directFixedLexNode(body, resolve, new Set())
547
+ const root = directFixedLexNode(body, resolve, new Set(), false, scope.cleared)
512
548
  return root === undefined ? undefined : { kind: 'fixed-tree', root }
513
549
  }
514
550
 
@@ -1556,8 +1592,11 @@ function normalizeLexical(
1556
1592
  if (stack.has(parser)) return { refusal: 'recursive token body' }
1557
1593
  stack.add(parser)
1558
1594
  try {
1559
- return withLexicalControl(session, parser._def.tag, () =>
1560
- normalizeLexicalBody(parser, resolve, stack, session))
1595
+ // A nested `balanced()` is its scanner machine, not the transform that
1596
+ // builds its value — `sequence('@', balanced('{', '}'))` as a skipper.
1597
+ return normalizeBalancedLexical(parser, resolve, session)
1598
+ ?? withLexicalControl(session, parser._def.tag, () =>
1599
+ normalizeLexicalBody(parser, resolve, stack, session))
1561
1600
  } finally {
1562
1601
  stack.delete(parser)
1563
1602
  }
@@ -1841,8 +1880,9 @@ function fixedBindingProjection(
1841
1880
  // and the emitted adapter captures that site's named body after linking every
1842
1881
  // scan root. Only ambient trivia/scanSkip remain live runtime callbacks; they
1843
1882
  // are scanner POLICY, never binding authority for these authored own children.
1844
- // Recovery/withCtx still have no equivalent named template.
1845
- if (edge.parentTag === 'recover' || edge.parentTag === 'withCtx') readerMask = 0b011
1883
+ // Recovery still has no equivalent named template. `withCtx` has one: the
1884
+ // emitter's OP_WITHCTX body calls its child by the name `link()` minted.
1885
+ if (edge.parentTag === 'recover') readerMask = 0b011
1846
1886
  // Sequence strict, adjacency, and recovery bodies now all use scalar block
1847
1887
  // chains, so no supported variant is withheld here.
1848
1888
  const words = [
@@ -111,6 +111,31 @@ export function pushCstChild(ctx: ParseContext, built: unknown, rawEntry: unknow
111
111
  if (ctx._cstRawChildren) ctx._cstRawChildren.push(rawEntry)
112
112
  }
113
113
 
114
+ /**
115
+ * Undo the LAST `pushCstChild` into the active collector, whichever form it took:
116
+ * a child the lazy buffer's empty sentinel dropped (`undefined` first) leaves no
117
+ * semantic entry to remove, only its raw one.
118
+ */
119
+ export function popCstChild(ctx: ParseContext): void {
120
+ const b = ctx._cstBuf
121
+ if (b) {
122
+ const ch = b.ch
123
+ if (b.rawOnly !== true && ch) {
124
+ ch.pop()
125
+ if (ch.length === 1) { b.single = ch[0]; b.ch = undefined }
126
+ }
127
+ if (b.noRaw === true) b.rawLen = (b.rawLen ?? 1) - 1
128
+ else if (b.raw) {
129
+ b.raw.pop()
130
+ if (b.raw.length === 1) { b.rawSingle = b.raw[0]; b.raw = undefined }
131
+ } else b.rawSingle = undefined
132
+ return
133
+ }
134
+ if (ctx._cstChildren) ctx._cstChildren.pop()
135
+ else if (ctx._cstLeaves) ctx._cstLeaves.pop()
136
+ if (ctx._cstRawChildren) ctx._cstRawChildren.pop()
137
+ }
138
+
114
139
  export function cstRawLen(ctx: ParseContext): number {
115
140
  const b = ctx._cstBuf
116
141
  if (b) {
@@ -1,4 +1,4 @@
1
- import type { Combinator, ParseContext } from '../types.ts'
1
+ import type { CharRange, Combinator, ParseContext } from '../types.ts'
2
2
  import { pushCstTriviaEntry, pushTriviaLogEntry } from './capture-buffer.ts'
3
3
  import { startsFirstSet } from '../combinators/first-set.ts'
4
4
  import { createDetachedParseContext } from '../parse-context.ts'
@@ -20,6 +20,14 @@ export type LabeledTriviaSpec = {
20
20
  */
21
21
  readonly visitor: CssTriviaVisitor | null
22
22
  readonly charArms: readonly TriviaArmMatcher[] | null
23
+ /**
24
+ * `table/trivia-gap.ts`'s root-selection fast path, for the selection it was
25
+ * last primed with: the selected arms' first characters, and the unlabelled
26
+ * scanner that answers a run free of them (`null`: no fast path).
27
+ */
28
+ rootKinds: Readonly<Record<string, number>> | undefined
29
+ rootLeaders: readonly CharRange[]
30
+ rootScan: ((input: string, cur: number) => number) | null
23
31
  }
24
32
 
25
33
  /** See `trivia-skip.ts`: bounded scalar memoisation avoids a WeakMap in the
@@ -114,6 +122,9 @@ export function labeledTriviaSpec(arms: LabeledTriviaSpec['arms'], minRepeats: n
114
122
  minRepeats,
115
123
  visitor: commonLabeledTriviaVisitor({ arms, minRepeats }),
116
124
  charArms: charArmsFor({ arms, minRepeats }),
125
+ rootKinds: undefined,
126
+ rootLeaders: [],
127
+ rootScan: null,
117
128
  }
118
129
  }
119
130
 
@@ -68,6 +68,7 @@ export function createParseContext(): ParseContext {
68
68
  _rootTriviaKindIndex: undefined,
69
69
  _rootTriviaStrictScopes: undefined,
70
70
  _rootTriviaCapture: undefined,
71
+ _gap: undefined,
71
72
  _cstTriviaLog: undefined,
72
73
  _fields: undefined,
73
74
  _cstBuf: undefined,
@@ -11,7 +11,7 @@ import { parseSync } from 'oxc-parser'
11
11
  import { OP_ATTEMPT, OP_NODE, OP_NODE_TRACK, OP_REPV, OP_SCAN } from '../table/ops.ts'
12
12
  import { emitAssemblySource, type EmitResult } from '../table/emit-assembly.ts'
13
13
  import { defaultAssemblyCfgs } from '../table/emit.ts'
14
- import { resolveTable, type TableProgram } from '../table/program.ts'
14
+ import { assemblyExtraIps, resolveTable, type TableProgram } from '../table/program.ts'
15
15
 
16
16
  type Ast = { type: string; start?: number; end?: number; [key: string]: unknown }
17
17
 
@@ -229,20 +229,19 @@ export function supercompileEntryAssembly(
229
229
  * then finds the precompiled assembly through the table object's AST shape and
230
230
  * replaces only that factory body. Any mismatch returns the canonical source.
231
231
  */
232
- export function supercompileRuleMapReplacement(prog: TableProgram, replacement: string): string {
232
+ export function supercompileRuleMapReplacement(
233
+ prog: TableProgram,
234
+ replacement: string,
235
+ select?: ReadonlySet<number>,
236
+ ): string {
233
237
  const cfg = defaultAssemblyCfgs(prog)[0]
234
238
  if (cfg === undefined) return replacement
235
- const extraIps: number[] = []
236
- for (const scan of prog.scans ?? []) {
237
- for (const ref of scan.skip) extraIps.push(ref[0])
238
- if (scan.sentinel !== undefined) extraIps.push(scan.sentinel[0])
239
- }
240
- for (const set of prog.scanSkip ?? []) for (const ref of set) extraIps.push(ref[0])
239
+ const extraIps = assemblyExtraIps(prog)
241
240
 
242
241
  let canonical: EmitResult
243
242
  let candidate: EntrySupercompileResult
244
243
  try {
245
- canonical = emitAssemblySource(resolveTable(prog), prog, cfg, extraIps, true)
244
+ canonical = emitAssemblySource(resolveTable(prog), prog, cfg, extraIps, true, select)
246
245
  candidate = supercompileEmittedAssembly(prog, cfg, extraIps, canonical)
247
246
  } catch {
248
247
  return replacement