parseman 0.48.1 → 0.50.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 (83) hide show
  1. package/dist/analysis/commitment.d.ts +2 -1
  2. package/dist/analysis/commitment.d.ts.map +1 -1
  3. package/dist/analysis/diagnostics.js +1931 -861
  4. package/dist/analysis/diagnostics.js.map +3 -3
  5. package/dist/analysis/rebuild.d.ts.map +1 -1
  6. package/dist/cli/index.js +1236 -164
  7. package/dist/cli/index.js.map +3 -3
  8. package/dist/combinators/trivia-skip.d.ts +1 -1
  9. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  10. package/dist/compiler/build-arity.d.ts +16 -0
  11. package/dist/compiler/build-arity.d.ts.map +1 -1
  12. package/dist/compiler/degradation.d.ts +0 -2
  13. package/dist/compiler/degradation.d.ts.map +1 -1
  14. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  15. package/dist/compiler/linker.d.ts.map +1 -1
  16. package/dist/compiler/token-alphabet.d.ts +11 -0
  17. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  18. package/dist/compiler/token-capability.js +185 -20
  19. package/dist/compiler/token-capability.js.map +1 -1
  20. package/dist/cst/capture-buffer.d.ts +5 -1
  21. package/dist/cst/capture-buffer.d.ts.map +1 -1
  22. package/dist/index.js +1354 -278
  23. package/dist/index.js.map +3 -3
  24. package/dist/language-service/index.js +2 -1
  25. package/dist/language-service/index.js.map +1 -1
  26. package/dist/plugin/direct-builder-static.d.ts +20 -0
  27. package/dist/plugin/direct-builder-static.d.ts.map +1 -1
  28. package/dist/plugin/entry-supercompiler.d.ts +48 -0
  29. package/dist/plugin/entry-supercompiler.d.ts.map +1 -0
  30. package/dist/plugin/evaluator.d.ts +13 -0
  31. package/dist/plugin/evaluator.d.ts.map +1 -1
  32. package/dist/plugin/index.d.ts.map +1 -1
  33. package/dist/plugin/index.js +2118 -376
  34. package/dist/plugin/index.js.map +3 -3
  35. package/dist/run/index.js +2 -1
  36. package/dist/run/index.js.map +1 -1
  37. package/dist/spec/index.js +29 -7
  38. package/dist/spec/index.js.map +1 -1
  39. package/dist/table/assemble.d.ts.map +1 -1
  40. package/dist/table/compile-rule-map.d.ts +6 -5
  41. package/dist/table/compile-rule-map.d.ts.map +1 -1
  42. package/dist/table/emit-assembly.d.ts +6 -1
  43. package/dist/table/emit-assembly.d.ts.map +1 -1
  44. package/dist/table/encode.d.ts.map +1 -1
  45. package/dist/table/index.js +1298 -231
  46. package/dist/table/index.js.map +3 -3
  47. package/dist/table/ops.d.ts +3 -2
  48. package/dist/table/ops.d.ts.map +1 -1
  49. package/dist/table/program.d.ts +15 -1
  50. package/dist/table/program.d.ts.map +1 -1
  51. package/dist/table/scalar-terminal.d.ts +30 -0
  52. package/dist/table/scalar-terminal.d.ts.map +1 -1
  53. package/dist/table/site-labels.d.ts +8 -0
  54. package/dist/table/site-labels.d.ts.map +1 -1
  55. package/dist/types.d.ts +8 -2
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/package.json +1 -1
  59. package/src/analysis/commitment.ts +19 -8
  60. package/src/analysis/rebuild.ts +1 -0
  61. package/src/combinators/trivia-skip.ts +2 -2
  62. package/src/compiler/build-arity.ts +50 -0
  63. package/src/compiler/degradation.ts +0 -6
  64. package/src/compiler/ir-serialize.ts +17 -3
  65. package/src/compiler/linker.ts +14 -1
  66. package/src/compiler/token-alphabet.ts +322 -28
  67. package/src/cst/capture-buffer.ts +34 -11
  68. package/src/plugin/direct-builder-static.ts +116 -12
  69. package/src/plugin/entry-supercompiler.ts +567 -0
  70. package/src/plugin/evaluator.ts +60 -2
  71. package/src/plugin/index.ts +147 -4
  72. package/src/table/assemble.ts +125 -42
  73. package/src/table/compile-rule-map.ts +8 -7
  74. package/src/table/compile.ts +7 -8
  75. package/src/table/emit-assembly.ts +646 -126
  76. package/src/table/emit.ts +1 -1
  77. package/src/table/encode.ts +132 -7
  78. package/src/table/ops.ts +3 -2
  79. package/src/table/program.ts +44 -3
  80. package/src/table/scalar-terminal.ts +213 -1
  81. package/src/table/site-labels.ts +22 -9
  82. package/src/types.ts +1 -1
  83. package/src/version.ts +1 -1
@@ -2313,6 +2313,7 @@ function familyHasContextFreeFiniteEnd(
2313
2313
  function compatibleLanguageViews(
2314
2314
  arm: LexicalIr,
2315
2315
  family: LexicalIr,
2316
+ coversContinuation: (family: LexicalIr, boundary: string) => boolean = boundaryCoversContinuation,
2316
2317
  ): ReadonlyArray<{ readonly ir: LexicalIr; readonly relation: 'equal' | 'prefix' }> | undefined {
2317
2318
  if (JSON.stringify(arm) === JSON.stringify(family)) return [{ ir: arm, relation: 'equal' }]
2318
2319
  const values = finiteLexicalValues(arm)
@@ -2332,7 +2333,7 @@ function compatibleLanguageViews(
2332
2333
  ? [value] : asciiCaseVariants(value, armCaseInsensitive)
2333
2334
  const endIsTotal = spellings !== undefined
2334
2335
  && (familyHasContextFreeFiniteEnd(family, spellings)
2335
- || armBoundary !== undefined && boundaryCoversContinuation(family, armBoundary))
2336
+ || armBoundary !== undefined && coversContinuation(family, armBoundary))
2336
2337
  if (endIsTotal && spellings !== undefined
2337
2338
  && spellings.every(spelling => lexicalIrAcceptsExact(family, spelling))) {
2338
2339
  relation = 'equal'
@@ -2463,18 +2464,45 @@ function ownedScanConsumers(
2463
2464
  return out
2464
2465
  }
2465
2466
 
2466
- function containsDynamicScanConsumer(
2467
- root: Combinator<unknown>,
2467
+ /**
2468
+ * Whether each parser can reach a dynamic scan consumer.
2469
+ *
2470
+ * This is reverse reachability rather than one DFS per decision/arm. Starting
2471
+ * from scan consumers and walking parents is cycle-safe by construction: every
2472
+ * node in an SCC that can reach a consumer is eventually marked, without the
2473
+ * false negative a recursive "currently visiting" memo creates.
2474
+ */
2475
+ function dynamicScanReachability(
2476
+ roots: ReadonlyArray<Combinator<unknown>>,
2468
2477
  resolve?: (name: string) => Combinator<unknown> | undefined,
2469
- seen = new Set<Combinator<unknown>>(),
2470
- ): boolean {
2471
- if (seen.has(root)) return false
2472
- if (balancedSpecOf(root) !== undefined || root._def.tag === 'scanTo') return true
2473
- seen.add(root)
2474
- for (const child of tokenChildren(root, resolve)) {
2475
- if (containsDynamicScanConsumer(child, resolve, seen)) return true
2478
+ ): ReadonlySet<Combinator<unknown>> {
2479
+ const parents = new Map<Combinator<unknown>, Set<Combinator<unknown>>>()
2480
+ const nodes: Combinator<unknown>[] = []
2481
+ const seen = new Set<Combinator<unknown>>()
2482
+ const walk = (parser: Combinator<unknown>): void => {
2483
+ if (seen.has(parser)) return
2484
+ seen.add(parser)
2485
+ nodes.push(parser)
2486
+ for (const child of tokenChildren(parser, resolve)) {
2487
+ let owners = parents.get(child)
2488
+ if (owners === undefined) parents.set(child, owners = new Set())
2489
+ owners.add(parser)
2490
+ walk(child)
2491
+ }
2476
2492
  }
2477
- return false
2493
+ for (const root of roots) walk(root)
2494
+
2495
+ const reachable = new Set(nodes.filter(parser =>
2496
+ parser._def.tag === 'scanTo' || balancedSpecOf(parser) !== undefined))
2497
+ const queue = [...reachable]
2498
+ for (let index = 0; index < queue.length; index++) {
2499
+ for (const parent of parents.get(queue[index]!) ?? []) {
2500
+ if (reachable.has(parent)) continue
2501
+ reachable.add(parent)
2502
+ queue.push(parent)
2503
+ }
2504
+ }
2505
+ return reachable
2478
2506
  }
2479
2507
 
2480
2508
  function predicateViews(def: Extract<ParserDef, { tag: 'dispatch' }>): Array<{
@@ -2545,6 +2573,248 @@ type BindingEdgeCandidate = Omit<
2545
2573
  readonly childOrdinal: number
2546
2574
  }
2547
2575
 
2576
+ type LexicalCapabilityRawReceipt = Readonly<{
2577
+ capabilities: readonly Readonly<Pick<CapabilityCandidate,
2578
+ 'bodyId' | 'parser' | 'atom' | 'path' | 'context' | 'contextKey' | 'recognitionContextKey'>>[]
2579
+ bindingEdges: readonly Readonly<BindingEdgeCandidate>[]
2580
+ parserAuthorities: readonly Combinator<unknown>[]
2581
+ }>
2582
+
2583
+ function capabilityOccurrenceReceipt(inventory: {
2584
+ candidates: readonly CapabilityCandidate[]
2585
+ bindingEdges: readonly BindingEdgeCandidate[]
2586
+ }): LexicalCapabilityRawReceipt {
2587
+ const parserAuthorities: Combinator<unknown>[] = []
2588
+ const parserAuthoritySet = new Set<Combinator<unknown>>()
2589
+ const addParserAuthority = (parser: Combinator<unknown> | undefined): void => {
2590
+ if (parser === undefined || parserAuthoritySet.has(parser)) return
2591
+ parserAuthoritySet.add(parser)
2592
+ parserAuthorities.push(parser)
2593
+ }
2594
+ for (const candidate of inventory.candidates) {
2595
+ addParserAuthority(candidate.parser)
2596
+ addParserAuthority(candidate.context.trivia)
2597
+ for (const parser of candidate.context.scanSkip) addParserAuthority(parser)
2598
+ }
2599
+ return Object.freeze({
2600
+ capabilities: Object.freeze(inventory.candidates.map(candidate => Object.freeze({
2601
+ bodyId: candidate.bodyId,
2602
+ parser: candidate.parser,
2603
+ atom: candidate.atom,
2604
+ path: candidate.path,
2605
+ context: candidate.context,
2606
+ contextKey: candidate.contextKey,
2607
+ recognitionContextKey: candidate.recognitionContextKey,
2608
+ }))),
2609
+ bindingEdges: Object.freeze(inventory.bindingEdges.map(edge => Object.freeze({ ...edge }))),
2610
+ parserAuthorities: Object.freeze(parserAuthorities),
2611
+ })
2612
+ }
2613
+
2614
+ type LexicalCapabilityClosureTables = Readonly<{
2615
+ capabilities: readonly LexicalCapabilitySite[]
2616
+ languages: readonly LexicalCapabilityLanguage[]
2617
+ bindingEdges: readonly LexicalBindingEdge[]
2618
+ bindingProjections: readonly LexicalBindingProjection[]
2619
+ terminalProjections: readonly LexicalTerminalProjection[]
2620
+ decisionFamilies: readonly LexicalDecisionFamily[]
2621
+ decisionOutcomes: readonly LexicalDecisionOutcome[]
2622
+ decisions: readonly LexicalDecisionSite[]
2623
+ decisionEffectPlan: LexicalDecisionEffectInventory
2624
+ transitionDiagnostics: readonly LexicalTransitionDiagnosticPlan[]
2625
+ controlPlans: readonly LexicalControlPlan[]
2626
+ boundaryPlans: readonly LexicalBoundaryPlan[]
2627
+ materializationPlans: readonly LexicalMaterializationPlan[]
2628
+ grammarWrapperSpecs: readonly LexicalGrammarWrapperSpec[]
2629
+ grammarCaptureTriviaKinds: readonly (readonly string[])[]
2630
+ boundaryTopologies: readonly LexicalBoundaryTopology[]
2631
+ contextSnapshots: readonly LexicalContextSnapshot[]
2632
+ ownSkipPlans: readonly LexicalOwnSkipPlan[]
2633
+ scanConsumerPolicies: readonly LexicalScanConsumerPolicy[]
2634
+ }>
2635
+
2636
+ type LexicalCapabilityClosureReceipt = Readonly<{
2637
+ raw: LexicalCapabilityRawReceipt
2638
+ callbacks: readonly LexicalDecisionCallbackAuthority['callback'][]
2639
+ snapshot: LexicalCapabilityClosureTables
2640
+ }>
2641
+
2642
+ function lexicalClosureSnapshotValue(
2643
+ value: unknown,
2644
+ parserAuthorities: ReadonlySet<Combinator<unknown>>,
2645
+ seen: Set<object> = new Set(),
2646
+ ): unknown {
2647
+ if (value === null || typeof value !== 'object') return value
2648
+ if (parserAuthorities.has(value as Combinator<unknown>)) return value
2649
+ if (seen.has(value)) throw new Error('parseman: cyclic lexical closure receipt value')
2650
+ seen.add(value)
2651
+ let result: unknown
2652
+ if (Array.isArray(value)) {
2653
+ result = Object.freeze(value.map(entry => lexicalClosureSnapshotValue(
2654
+ entry, parserAuthorities, seen,
2655
+ )))
2656
+ } else {
2657
+ result = Object.freeze(Object.fromEntries(Object.keys(value).map(key => [
2658
+ key, lexicalClosureSnapshotValue(
2659
+ (value as Record<string, unknown>)[key], parserAuthorities, seen,
2660
+ ),
2661
+ ])))
2662
+ }
2663
+ seen.delete(value)
2664
+ return result
2665
+ }
2666
+
2667
+ function lexicalClosureSnapshot(
2668
+ tables: LexicalCapabilityClosureTables,
2669
+ raw: LexicalCapabilityRawReceipt,
2670
+ ): LexicalCapabilityClosureTables {
2671
+ return lexicalClosureSnapshotValue(
2672
+ tables, new Set(raw.parserAuthorities),
2673
+ ) as LexicalCapabilityClosureTables
2674
+ }
2675
+
2676
+ function lexicalClosureValuesEqual(
2677
+ expected: unknown,
2678
+ actual: unknown,
2679
+ parserAuthorities: ReadonlySet<Combinator<unknown>>,
2680
+ ): boolean {
2681
+ if (Object.is(expected, actual)) return true
2682
+ if (expected === null || actual === null || typeof expected !== typeof actual) return false
2683
+ if (typeof expected === 'function') return false
2684
+ if (typeof expected !== 'object' || typeof actual !== 'object') return false
2685
+ if (parserAuthorities.has(expected as Combinator<unknown>)) return false
2686
+ if (Array.isArray(expected) || Array.isArray(actual)) {
2687
+ return Array.isArray(expected) && Array.isArray(actual)
2688
+ && expected.length === actual.length
2689
+ && expected.every((entry, index) => lexicalClosureValuesEqual(
2690
+ entry, actual[index], parserAuthorities,
2691
+ ))
2692
+ }
2693
+ const expectedKeys = Object.keys(expected)
2694
+ const actualKeys = Object.keys(actual)
2695
+ return expectedKeys.length === actualKeys.length
2696
+ && expectedKeys.every(key => Object.hasOwn(actual, key)
2697
+ && lexicalClosureValuesEqual(
2698
+ (expected as Record<string, unknown>)[key],
2699
+ (actual as Record<string, unknown>)[key],
2700
+ parserAuthorities,
2701
+ ))
2702
+ }
2703
+
2704
+ function lexicalCapabilityClosureReceipt(
2705
+ raw: LexicalCapabilityRawReceipt,
2706
+ tables: LexicalCapabilityClosureTables,
2707
+ ): LexicalCapabilityClosureReceipt {
2708
+ const callbacks = Object.freeze(tables.decisionEffectPlan.decisionCallbackAuthorities
2709
+ .map(authority => authority.callback))
2710
+ return Object.freeze({
2711
+ raw,
2712
+ callbacks,
2713
+ snapshot: lexicalClosureSnapshot(tables, raw),
2714
+ })
2715
+ }
2716
+
2717
+ function closureTablesFromInventory(
2718
+ alphabet: Omit<LexicalCapabilityInventory, 'capabilityComplete'>,
2719
+ ): LexicalCapabilityClosureTables {
2720
+ const { capabilityLanguages: languages, ...tables } = alphabet
2721
+ return { ...tables, languages }
2722
+ }
2723
+
2724
+ function rawLexicalClosureMatches(
2725
+ raw: LexicalCapabilityRawReceipt,
2726
+ alphabet: Omit<LexicalCapabilityInventory, 'capabilityComplete'>,
2727
+ ): boolean {
2728
+ if (raw.capabilities.length !== alphabet.capabilities.length
2729
+ || raw.bindingEdges.length !== alphabet.bindingEdges.length
2730
+ || raw.bindingEdges.length !== alphabet.bindingProjections.length) return false
2731
+ const snapshotIdByKey = new Map(alphabet.contextSnapshots.map(snapshot => [snapshot.key, snapshot.id]))
2732
+ const parserAuthorities = new Set(raw.parserAuthorities)
2733
+ for (let id = 0; id < raw.capabilities.length; id++) {
2734
+ const expected = raw.capabilities[id]!
2735
+ const actual = alphabet.capabilities[id]
2736
+ if (actual === undefined
2737
+ || actual.id !== id
2738
+ || actual.bodyId !== expected.bodyId
2739
+ || actual.parser !== expected.parser
2740
+ || actual.atom !== expected.atom
2741
+ || actual.path !== expected.path
2742
+ || actual.contextKey !== expected.contextKey
2743
+ || actual.contextSnapshotId !== snapshotIdByKey.get(expected.contextKey)
2744
+ || actual.recognitionContextKey !== expected.recognitionContextKey
2745
+ || !lexicalClosureValuesEqual(expected.context, actual.context, parserAuthorities)) return false
2746
+ }
2747
+ for (let id = 0; id < raw.bindingEdges.length; id++) {
2748
+ const expected = raw.bindingEdges[id]!
2749
+ const actual = alphabet.bindingEdges[id]
2750
+ const expectedProjection = fixedBindingProjection(expected, id, id)
2751
+ const actualProjection = alphabet.bindingProjections[id]
2752
+ const complete = expectedProjection.readerMask === ALL_BINDING_READERS
2753
+ && expectedProjection.variantMask === ALL_BINDING_VARIANTS
2754
+ const expectedStatus = complete ? COMPLETE_CAPABILITY : gap(FIXED_TUPLE_BINDING_GAP)
2755
+ if (actual === undefined || actualProjection === undefined
2756
+ || actual.id !== id
2757
+ || actual.path !== expected.path
2758
+ || actual.contextKey !== expected.contextKey
2759
+ || actual.childContextKey !== expected.childContextKey
2760
+ || actual.contextSnapshotId !== snapshotIdByKey.get(expected.contextKey)
2761
+ || actual.parentTag !== expected.parentTag
2762
+ || actual.childTag !== expected.childTag
2763
+ || actual.projectionId !== id
2764
+ || !lexicalClosureValuesEqual(expectedStatus, actual.status, parserAuthorities)
2765
+ || !lexicalClosureValuesEqual(expectedProjection, actualProjection, parserAuthorities)) return false
2766
+ }
2767
+ return true
2768
+ }
2769
+
2770
+ function lexicalClosureFailure(detail: string): never {
2771
+ throw new Error(
2772
+ `parseman: lexical capability census is incomplete: occurrence/derived bijection is incomplete (${detail})`,
2773
+ )
2774
+ }
2775
+
2776
+ function assertLexicalCapabilityBijection(
2777
+ receipt: LexicalCapabilityClosureReceipt,
2778
+ alphabet: Omit<LexicalCapabilityInventory, 'capabilityComplete'>,
2779
+ ): void {
2780
+ if (!rawLexicalClosureMatches(receipt.raw, alphabet)) lexicalClosureFailure('raw')
2781
+ const actual = closureTablesFromInventory(alphabet)
2782
+ const parserAuthorities = new Set(receipt.raw.parserAuthorities)
2783
+ const failedTable = (Object.keys(receipt.snapshot) as (keyof LexicalCapabilityClosureTables)[])
2784
+ .find(key => !lexicalClosureValuesEqual(
2785
+ receipt.snapshot[key], actual[key], parserAuthorities,
2786
+ ))
2787
+ if (failedTable !== undefined) lexicalClosureFailure(failedTable)
2788
+ }
2789
+
2790
+ /** Test-only production-seam model: snapshot the supplied derived tables while
2791
+ * retaining a raw graph receipt captured before derivation. A dropped raw
2792
+ * occurrence must fail even though the post-derivation snapshot agrees. */
2793
+ export function assertLexicalCapabilityProductionBijection(
2794
+ roots: ReadonlyArray<Combinator<unknown>>,
2795
+ alphabet: Omit<LexicalCapabilityInventory, 'capabilityComplete'>,
2796
+ resolve?: (name: string) => Combinator<unknown> | undefined,
2797
+ ): void {
2798
+ const raw = capabilityOccurrenceReceipt(lexicalCapabilityCandidates(roots, resolve))
2799
+ const receipt = lexicalCapabilityClosureReceipt(raw, closureTablesFromInventory(alphabet))
2800
+ assertLexicalCapabilityBijection(receipt, alphabet)
2801
+ }
2802
+
2803
+ /**
2804
+ * Test/diagnostic entry for the one-pass production closure guard. This helper
2805
+ * deliberately rebuilds the expected inventory for a supplied test artifact;
2806
+ * production consumes the session-local raw receipt and never walks the graph
2807
+ * or recomputes lexical analyses a second time.
2808
+ */
2809
+ export function assertLexicalCapabilityOccurrenceBijection(
2810
+ roots: ReadonlyArray<Combinator<unknown>>,
2811
+ alphabet: Omit<LexicalCapabilityInventory, 'capabilityComplete'>,
2812
+ resolve?: (name: string) => Combinator<unknown> | undefined,
2813
+ ): void {
2814
+ const expected = lexicalCapabilityInventory(roots, resolve)
2815
+ assertLexicalCapabilityBijection(expected.closureReceipt, alphabet)
2816
+ }
2817
+
2548
2818
  type CapabilityContext = LexicalCapabilityContext
2549
2819
 
2550
2820
  /**
@@ -2687,6 +2957,7 @@ function decisionViewKey(view: LexicalDecisionOutcomeView): string {
2687
2957
  function lexicalDecisionInventory(
2688
2958
  candidates: readonly CapabilityCandidate[],
2689
2959
  contextSnapshotIdByKey: ReadonlyMap<string, number>,
2960
+ dynamicScanConsumers: ReadonlySet<Combinator<unknown>>,
2690
2961
  resolve?: (name: string) => Combinator<unknown> | undefined,
2691
2962
  ): {
2692
2963
  families: LexicalDecisionFamily[]
@@ -2697,6 +2968,15 @@ function lexicalDecisionInventory(
2697
2968
  const familyIrByKey = new Map<string, LexicalIr>()
2698
2969
  const viewsByKey = new Map<string, PendingDecisionView>()
2699
2970
  const pending: PendingDecisionSite[] = []
2971
+ const boundaryCoverage = new Map<string, boolean>()
2972
+ const coversContinuation = (family: LexicalIr, boundary: string): boolean => {
2973
+ const key = `${JSON.stringify(family)}\u0000${boundary}`
2974
+ const prior = boundaryCoverage.get(key)
2975
+ if (prior !== undefined) return prior
2976
+ const covered = boundaryCoversContinuation(family, boundary)
2977
+ boundaryCoverage.set(key, covered)
2978
+ return covered
2979
+ }
2700
2980
  const addFamily = (lead: DecisionLead): string => {
2701
2981
  if (!familyIrByKey.has(lead.key)) familyIrByKey.set(lead.key, lead.ir)
2702
2982
  return lead.key
@@ -2717,7 +2997,7 @@ function lexicalDecisionInventory(
2717
2997
  pure = []
2718
2998
  }
2719
2999
  for (let armId = 0; armId < arms.length; armId++) {
2720
- if (!containsDynamicScanConsumer(arms[armId]!, resolve)) {
3000
+ if (!dynamicScanConsumers.has(arms[armId]!)) {
2721
3001
  pure.push(armId)
2722
3002
  continue
2723
3003
  }
@@ -2747,8 +3027,8 @@ function lexicalDecisionInventory(
2747
3027
  const contextSnapshotId = contextSnapshotIdByKey.get(candidate.contextKey)
2748
3028
  if (contextSnapshotId === undefined) throw new Error('parseman: decision occurrence lost its context snapshot')
2749
3029
  const dynamicScan = candidate.atom === 'dispatch' && def.tag === 'dispatch'
2750
- ? containsDynamicScanConsumer(def.selector, resolve)
2751
- : containsDynamicScanConsumer(candidate.parser, resolve)
3030
+ ? dynamicScanConsumers.has(def.selector)
3031
+ : dynamicScanConsumers.has(candidate.parser)
2752
3032
  if (dynamicScan) {
2753
3033
  precisionNotes.push('dynamic scan consumer forbids pending range reuse')
2754
3034
  }
@@ -2904,7 +3184,7 @@ function lexicalDecisionInventory(
2904
3184
  && !intersects(armFirst, familyFirst)) {
2905
3185
  return { armId, acceptance: { kind: 'impossible' }, usesRouted: false, dynamicGate }
2906
3186
  }
2907
- const compatible = compatibleLanguageViews(lead.ir, familyIr)
3187
+ const compatible = compatibleLanguageViews(lead.ir, familyIr, coversContinuation)
2908
3188
  if (compatible !== undefined) {
2909
3189
  if (compatible.every(view => view.relation === 'equal')
2910
3190
  && familyPredicates.length > 0
@@ -3294,6 +3574,7 @@ function lexicalCapabilityInventory(
3294
3574
  roots: ReadonlyArray<Combinator<unknown>>,
3295
3575
  resolve?: (name: string) => Combinator<unknown> | undefined,
3296
3576
  ): {
3577
+ closureReceipt: LexicalCapabilityClosureReceipt
3297
3578
  capabilities: LexicalCapabilitySite[]
3298
3579
  languages: LexicalCapabilityLanguage[]
3299
3580
  bindingEdges: LexicalBindingEdge[]
@@ -3315,6 +3596,11 @@ function lexicalCapabilityInventory(
3315
3596
  decisionEffectPlan: LexicalDecisionEffectInventory
3316
3597
  } {
3317
3598
  const inventory = lexicalCapabilityCandidates(roots, resolve)
3599
+ // Capture graph occurrence, context, and fixed-edge identity before any
3600
+ // language/decision/effect derivation. The final closure transcript below is
3601
+ // canonicalized independently against this immutable authority.
3602
+ const rawClosureReceipt = capabilityOccurrenceReceipt(inventory)
3603
+ const dynamicScanConsumers = dynamicScanReachability(roots, resolve)
3318
3604
  const occurrenceContexts = new Map<Combinator<unknown>, Set<string>>()
3319
3605
  for (const candidate of inventory.candidates) {
3320
3606
  let contexts = occurrenceContexts.get(candidate.parser)
@@ -3350,7 +3636,7 @@ function lexicalCapabilityInventory(
3350
3636
  }))
3351
3637
  const contextSnapshotIdByKey = new Map(contextSnapshots.map(snapshot => [snapshot.key, snapshot.id]))
3352
3638
  const decisionInventory = lexicalDecisionInventory(
3353
- inventory.candidates, contextSnapshotIdByKey, resolve,
3639
+ inventory.candidates, contextSnapshotIdByKey, dynamicScanConsumers, resolve,
3354
3640
  )
3355
3641
  const transitionDiagnostics: LexicalTransitionDiagnosticPlan[] = []
3356
3642
  const transitionDiagnosticIdByKey = new Map<string, number>()
@@ -3805,7 +4091,11 @@ function lexicalCapabilityInventory(
3805
4091
  )
3806
4092
  return { ...site, obligations, status: derivedCapabilityStatus(obligations) }
3807
4093
  })
3808
- return {
4094
+ const boundaryPlans: LexicalBoundaryPlan[] = boundaryTopologies.length === 0
4095
+ ? [] : [{ id: 0, kind: 'token-context-transaction' }]
4096
+ const materializationPlans: LexicalMaterializationPlan[] = boundaryTopologies.length === 0
4097
+ ? [] : [{ id: 0, kind: 'token-source-range' }]
4098
+ const closureTables = {
3809
4099
  capabilities, languages, bindingEdges, bindingProjections, terminalProjections,
3810
4100
  decisionFamilies: decisionInventory.families,
3811
4101
  decisionOutcomes: decisionInventory.outcomes,
@@ -3813,10 +4103,8 @@ function lexicalCapabilityInventory(
3813
4103
  decisionEffectPlan: decisionEffects,
3814
4104
  transitionDiagnostics,
3815
4105
  controlPlans,
3816
- boundaryPlans: boundaryTopologies.length === 0
3817
- ? [] : [{ id: 0, kind: 'token-context-transaction' }],
3818
- materializationPlans: boundaryTopologies.length === 0
3819
- ? [] : [{ id: 0, kind: 'token-source-range' }],
4106
+ boundaryPlans,
4107
+ materializationPlans,
3820
4108
  grammarWrapperSpecs,
3821
4109
  grammarCaptureTriviaKinds,
3822
4110
  boundaryTopologies,
@@ -3824,6 +4112,10 @@ function lexicalCapabilityInventory(
3824
4112
  ownSkipPlans,
3825
4113
  scanConsumerPolicies,
3826
4114
  }
4115
+ return {
4116
+ closureReceipt: lexicalCapabilityClosureReceipt(rawClosureReceipt, closureTables),
4117
+ ...closureTables,
4118
+ }
3827
4119
  }
3828
4120
 
3829
4121
  /** Re-enumerate the final graph so a dropped/filtered candidate fails closed. */
@@ -3941,17 +4233,19 @@ export function collectLexicalCapabilities(
3941
4233
  roots: ReadonlyArray<Combinator<unknown>>,
3942
4234
  resolve?: (name: string) => Combinator<unknown> | undefined,
3943
4235
  ): LexicalCapabilityInventory {
3944
- const inventory = lexicalCapabilityInventory(roots, resolve)
3945
- const { languages: capabilityLanguages, ...fields } = inventory
3946
- return {
4236
+ const built = lexicalCapabilityInventory(roots, resolve)
4237
+ const { closureReceipt, languages: capabilityLanguages, ...fields } = built
4238
+ const inventory: LexicalCapabilityInventory = {
3947
4239
  ...fields,
3948
4240
  capabilityLanguages,
3949
- capabilityComplete: inventory.capabilities.every(site => site.status.kind !== 'gap')
3950
- && inventory.bindingEdges.every(edge => edge.status.kind !== 'gap')
3951
- && inventory.decisionEffectPlan.decisionEffects.every(effect =>
4241
+ capabilityComplete: built.capabilities.every(site => site.status.kind !== 'gap')
4242
+ && built.bindingEdges.every(edge => edge.status.kind !== 'gap')
4243
+ && built.decisionEffectPlan.decisionEffects.every(effect =>
3952
4244
  effect.phase.executableLowering.kind !== 'gap')
3953
- && inventory.scanConsumerPolicies.every(policy => policy.status.kind !== 'gap'),
4245
+ && built.scanConsumerPolicies.every(policy => policy.status.kind !== 'gap'),
3954
4246
  }
4247
+ assertLexicalCapabilityBijection(closureReceipt, inventory)
4248
+ return inventory
3955
4249
  }
3956
4250
 
3957
4251
  function tokenBoundary(
@@ -6,6 +6,10 @@ export type CstCaptureBuf = {
6
6
  /** Table-host specialisation: collect source-order raw entries without the
7
7
  * duplicate semantic children view. Selected once for the enclosing node. */
8
8
  rawOnly?: true | undefined
9
+ /** Direct reducer proved `rawChildren` unobservable. Preserve only the source-order
10
+ * COUNT needed by trivia insertion/rollback; never retain raw entry values. */
11
+ noRaw?: true | undefined
12
+ rawLen?: number | undefined
9
13
  single?: unknown | undefined
10
14
  ch?: unknown[] | undefined
11
15
  rawSingle?: unknown | undefined
@@ -54,7 +58,9 @@ export function finishCstBuf(buf: CstCaptureBuf | undefined): {
54
58
  } {
55
59
  if (!buf) return { children: EMPTY, rawChildren: EMPTY, triviaLog: EMPTY_TL }
56
60
  const children = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY)
57
- const rawChildren = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY)
61
+ const rawChildren = buf.noRaw === true
62
+ ? EMPTY
63
+ : buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY)
58
64
  const triviaLog = buf.tl ?? EMPTY_TL
59
65
  return { children, rawChildren, triviaLog }
60
66
  }
@@ -94,7 +100,8 @@ export function pushCstChild(ctx: ParseContext, built: unknown, rawEntry: unknow
94
100
  else b.single = built
95
101
  }
96
102
 
97
- if (b.raw) b.raw.push(rawEntry)
103
+ if (b.noRaw === true) b.rawLen = (b.rawLen ?? 0) + 1
104
+ else if (b.raw) b.raw.push(rawEntry)
98
105
  else if (b.rawSingle !== undefined) { b.raw = [b.rawSingle, rawEntry]; b.rawSingle = undefined }
99
106
  else b.rawSingle = rawEntry
100
107
  return
@@ -107,6 +114,7 @@ export function pushCstChild(ctx: ParseContext, built: unknown, rawEntry: unknow
107
114
  export function cstRawLen(ctx: ParseContext): number {
108
115
  const b = ctx._cstBuf
109
116
  if (b) {
117
+ if (b.noRaw === true) return b.rawLen ?? 0
110
118
  if (b.raw) return b.raw.length
111
119
  return b.rawSingle !== undefined ? 1 : 0
112
120
  }
@@ -156,9 +164,10 @@ function rollbackBufList(
156
164
  if (len === 0) b[keyMulti] = undefined
157
165
  else if (len === 1) { b[keySingle] = arr[0]; b[keyMulti] = undefined }
158
166
  // Guarded like every other truncation — see rollbackCstCapture. This is the
159
- // buffered node-capture path, so it is reached only by the INTERPRETER (the
160
- // compiled engine inlines its own capture and never calls here), and it is
161
- // where the redundant-store rate is highest: over a backtracking fixture
167
+ // buffered node-capture path. `demoteCapturedToRaw` is its remaining caller;
168
+ // the table closure's fixed raw/child rollback fields stay inline so its hot
169
+ // speculative path does not rediscover them through dynamic property keys.
170
+ // This is where the redundant-store rate is highest: over a backtracking fixture
162
171
  // this branch ran 32,800 times and 31,198 of those — 95% — restored a
163
172
  // length that had not moved.
164
173
  else if (arr.length !== len) arr.length = len
@@ -221,8 +230,8 @@ export function rollbackCstCaptureAt(
221
230
  raw: number,
222
231
  tlog: number,
223
232
  leaves: number,
224
- fields: number,
225
- errors: number,
233
+ fields: number | undefined,
234
+ errors: number | undefined,
226
235
  ): void {
227
236
  // Truncate the flat recovery-error sink alongside the CST, so a rolled-back
228
237
  // speculative branch leaves no ghost error (see saveCstMark). Guarded on a
@@ -238,19 +247,33 @@ export function rollbackCstCaptureAt(
238
247
  if (ctx._errors && errors !== undefined && ctx._errors.length !== errors) ctx._errors.length = errors
239
248
  const b = ctx._cstBuf
240
249
  if (b) {
241
- rollbackBufList(b, 'raw', 'rawSingle', raw)
242
- rollbackBufList(b, 'ch', 'single', leaves)
250
+ if (b.noRaw === true) {
251
+ if (b.rawLen !== raw) b.rawLen = raw
252
+ } else {
253
+ const rawList = b.raw
254
+ if (rawList) {
255
+ if (raw === 0) b.raw = undefined
256
+ else if (raw === 1) { b.rawSingle = rawList[0]; b.raw = undefined }
257
+ else if (rawList.length !== raw) rawList.length = raw
258
+ } else if (raw === 0) b.rawSingle = undefined
259
+ }
260
+ const childList = b.ch
261
+ if (childList) {
262
+ if (leaves === 0) b.ch = undefined
263
+ else if (leaves === 1) { b.single = childList[0]; b.ch = undefined }
264
+ else if (childList.length !== leaves) childList.length = leaves
265
+ } else if (leaves === 0) b.single = undefined
243
266
  if (b.tl) {
244
267
  if (tlog === 0) b.tl = undefined
245
268
  else if (b.tl.length !== tlog) b.tl.length = tlog
246
269
  }
247
- if (ctx._fields && ctx._fields.length !== fields) ctx._fields.length = fields
270
+ if (ctx._fields && fields !== undefined && ctx._fields.length !== fields) ctx._fields.length = fields
248
271
  return
249
272
  }
250
273
  if (ctx._cstRawChildren && ctx._cstRawChildren.length !== raw) ctx._cstRawChildren.length = raw
251
274
  if (ctx._cstTriviaLog && ctx._cstTriviaLog.length !== tlog) ctx._cstTriviaLog.length = tlog
252
275
  if (ctx._cstLeaves && ctx._cstLeaves.length !== leaves) ctx._cstLeaves.length = leaves
253
- if (ctx._fields && ctx._fields.length !== fields) ctx._fields.length = fields
276
+ if (ctx._fields && fields !== undefined && ctx._fields.length !== fields) ctx._fields.length = fields
254
277
  }
255
278
 
256
279
  export function pushCstTriviaEntry(