@asmlift/core 0.6.0 → 0.7.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 (74) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/contracts.ts +60 -11
  5. package/src/frontend/ssa.ts +1 -1
  6. package/src/frontend/thumb.ts +2 -2
  7. package/src/ir/alias.ts +24 -0
  8. package/src/ir/core.ts +8 -0
  9. package/src/ir/opcodes.ts +43 -7
  10. package/src/ir/simplify.ts +1 -1
  11. package/src/l3/address.ts +2 -2
  12. package/src/l3/advance.ts +373 -0
  13. package/src/l3/argbase.ts +4 -4
  14. package/src/l3/ast.ts +65 -21
  15. package/src/l3/basecse.ts +48 -28
  16. package/src/l3/coalesce.ts +9 -9
  17. package/src/l3/gates.ts +75 -1
  18. package/src/l3/hoist.ts +1 -1
  19. package/src/l3/homesplit.ts +13 -13
  20. package/src/l3/initfirst.ts +3 -3
  21. package/src/l3/inlinebase.ts +16 -16
  22. package/src/l3/mentions.ts +68 -5
  23. package/src/l3/mulfirst.ts +3 -3
  24. package/src/l3/nearbase.ts +4 -4
  25. package/src/l3/offmember.ts +5 -5
  26. package/src/l3/parkfirst.ts +6 -6
  27. package/src/l3/pollguard.ts +3 -3
  28. package/src/l3/ptrfield.ts +4 -4
  29. package/src/l3/regspell.ts +8 -8
  30. package/src/l3/reindex.ts +22 -17
  31. package/src/l3/scopebase.ts +28 -25
  32. package/src/l3/sinkinit.ts +7 -7
  33. package/src/l3/slotorder.ts +3 -3
  34. package/src/l3/storage.ts +1 -1
  35. package/src/l3/tailmerge.ts +2 -2
  36. package/src/l3/typing.ts +3 -3
  37. package/src/l3/unmerge.ts +483 -59
  38. package/src/l3/unreduce.ts +13 -13
  39. package/src/l3/volatileptr.ts +11 -11
  40. package/src/l3/volatileval.ts +11 -11
  41. package/src/l3/volstore.ts +16 -16
  42. package/src/l3/zerosub.ts +6 -6
  43. package/src/pattern/engine.ts +4 -4
  44. package/src/pipeline.ts +17 -5
  45. package/src/proto.ts +2 -2
  46. package/src/raise/const.ts +203 -3
  47. package/src/raise/divpow2.ts +2 -2
  48. package/src/raise/extscale.ts +342 -0
  49. package/src/raise/globalshape.ts +32 -12
  50. package/src/raise/gvn.ts +2 -2
  51. package/src/raise/magicdiv.ts +2 -2
  52. package/src/raise/memberarrays.ts +4 -4
  53. package/src/raise/narrowlocal.ts +18 -2
  54. package/src/raise/paramwidth.ts +24 -2
  55. package/src/raise/pre-recovery.ts +90 -25
  56. package/src/raise/retsink.ts +381 -15
  57. package/src/raise/shortcircuit.ts +595 -34
  58. package/src/raise/structs.ts +4 -4
  59. package/src/raise/tailsink.ts +126 -0
  60. package/src/rank-declare.ts +4 -4
  61. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  62. package/src/rank.ts +1148 -803
  63. package/src/structure/analysis.ts +87 -90
  64. package/src/structure/bitfields.ts +130 -30
  65. package/src/structure/globalaccess.ts +30 -4
  66. package/src/structure/namecoalesce.ts +32 -13
  67. package/src/structure/structure.ts +1415 -200
  68. package/src/structure/switch-recover.ts +100 -7
  69. package/src/symbols.ts +127 -6
  70. package/src/target.ts +155 -35
  71. package/src/trace.ts +1 -1
  72. package/src/variation-definitions.ts +1540 -0
  73. package/src/variation-gates.ts +89 -0
  74. package/src/variation-tokens.ts +355 -0
package/src/l3/basecse.ts CHANGED
@@ -13,14 +13,14 @@
13
13
  // question the source answered per BASE — one register file spelled as a pointer local beside
14
14
  // scalar cells spelled as bare derefs. The `single-cell` gate is what makes the narrower answer
15
15
  // reachable: under `LIVEBASE_BLOCK_GATES` a base every access of which is ONE fixed offset stays
16
- // inline, and rank's LIVEBASE_ADMISSIONS roster emits each table's hoist — and every product of
16
+ // inline, and rank's LIVEBASE_HOISTS roster emits each table's hoist — and every composition onto
17
17
  // it — as its own candidate family, for the differ to referee between them. The unit is
18
18
  // the (base, width, signedness) KEY, not the base — a base read at two widths is two keys, and the
19
19
  // gate can leave one of them inline while the other binds.
20
20
  //
21
21
  // COVERAGE: the roster (rank.ts) is SEVEN rows over FIVE gate tables — two PAIRS share a table and
22
22
  // differ only in placement, `/basefold` with `/basefold/sinkinit` and `/orderbase` with
23
- // `/orderbase/scoped` — and it is a set of hand-picked SUBSETS rather than a narrowness ranking;
23
+ // `/orderbase-scoped` — and it is a set of hand-picked SUBSETS rather than a narrowness ranking;
24
24
  // only `/livebase` ⊇ `/livebase-block` are ordered by inclusion. A table
25
25
  // whose predicate cuts across the others therefore carves out a PARTIAL answer, which is what
26
26
  // `UNFOLDED_GATES` does. Measured at ONE stated scope, `decompile()`'s default structuring,
@@ -61,7 +61,7 @@
61
61
  // and `ldrb r0, [r4, #0x3]` — agbcc CSEs the symbol reference where it re-materializes the integer
62
62
  // — so the inline subscript spelling produces exactly the shape this rule reads as evidence
63
63
  // against it. asmlift lifts that asm back to the correct `((u8 *)&gS)[3]` and then offers the
64
- // named-base respelling anyway. Measured reach: of the 21 keys the symbol half newly admits over
64
+ // spelling through a named base anyway. Measured reach: of the 21 keys the symbol half newly admits over
65
65
  // the artifact's agbcc rows in both symbol-map configurations, 4 are on a base whose address the
66
66
  // tree also uses as a value (2 distinct keys, on `kleod:ProcessInputAndUpdateEntities` and
67
67
  // `pokeemerald:TrySetCantSelectMoveBattleScript`).
@@ -74,14 +74,14 @@
74
74
  // `synthetic:foldhead` its match — which is why `index.operandOff` is carried from the lift
75
75
  // instead of re-derived, and why a committed pass that can drop it is worth a test
76
76
  // (test/basecse.test.ts, the "`operandOff` is provenance" describe). WHAT EACH ROW IS WORTH is
77
- // measured in rank.ts's note on `BASEFOLD_ADMISSIONS`, not here, and the two rows are not worth
77
+ // measured in rank.ts's note on `BASEFOLD_HOISTS`, not here, and the two rows are not worth
78
78
  // the same thing. Promoting the hint to a default would need this paragraph to say something it
79
79
  // does not.
80
80
  //
81
- // It is EVIDENCE and not proof, which is why `BASEFOLD_GATES` below is a lever rather than a
81
+ // It is EVIDENCE and not proof, which is why `BASEFOLD_GATES` below backs a variation rather than a
82
82
  // relaxation of the default table. agbcc folds a subscript but keeps an aggregate MEMBER offset in
83
83
  // the memory operand: `((struct S *)0x3001100)->b` emits `.word 0x3001100` + `ldr [r0, #0x4]`,
84
- // byte-identical to the named-base spelling, and the same holds for a union member and for a
84
+ // byte-identical to the spelling through a named base, and the same holds for a union member and for a
85
85
  // store. So the shape has two sources and asmlift can spell only one of them; rank.ts offers both
86
86
  // and the differ referees.
87
87
  //
@@ -97,7 +97,7 @@
97
97
  // AGGREGATE base (F9 spells a SCALAR global as a bare `var`, which is never an `index`-of-leaf, so
98
98
  // scalar recovery is untouched). Non-leaf bases (a local, a struct-element `p[a0]`,
99
99
  // arithmetic) are excluded: agbcc may re-derive those, so hoisting them can
100
- // MISMATCH (empirically confirmed) — the differ-refereed `/addr-home` axis
100
+ // MISMATCH (empirically confirmed) — the differ-refereed `/addr-home` variation
101
101
  // (structure/analysis.ts homeSharedAddresses) serves the shared gaddr-free ARITHMETIC bases
102
102
  // instead.
103
103
  // The hoisted local carries the access's pointer type, so the
@@ -149,8 +149,8 @@ const keyOf = (base: HoistableBase, width: number, signed: boolean): string => `
149
149
  /** The key's own grammar, read back — `<leafId>[ <type>] <width> <signed>`.
150
150
  *
151
151
  * IT LIVES BESIDE `keyOf` BECAUSE THAT IS THE ONLY THING THAT MAKES IT SAFE. The key is a string
152
- * and its readers are elsewhere — `l3/homesplit.ts` builds a candidate LABEL out of it, and a
153
- * label is a candidate's identity — so a second file knowing this grammar is a collision waiting
152
+ * and its readers are elsewhere — `l3/homesplit.ts` builds a candidate's VARIATION out of it, and a
153
+ * candidate's variations are its identity — so a second file knowing this grammar is a collision waiting
154
154
  * for the next base kind (`homeSplitTag` states the one the cast form causes).
155
155
  *
156
156
  * The one space inside a cast's base id is this grammar's own separator, not the type's: every
@@ -357,7 +357,7 @@ export interface BaseKey {
357
357
  * what a pointer local's own initializer STATEMENT produces (see raise/globalshape.ts's header
358
358
  * for the compile that separates the two), while the inline cast produces the other order — so
359
359
  * it is evidence a home is what the source wrote. Read only by `ORDERBASE_GATES` (rank.ts);
360
- * false for every base a compiler that has not opted in produced, which is what keeps the axis
360
+ * false for every base a compiler that has not opted in produced, which is what keeps the variation
361
361
  * off those targets. */
362
362
  orderLicensed: boolean;
363
363
  }
@@ -380,13 +380,13 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
380
380
  // Censused at `decompile()`'s default structuring, map-less, one tree per row over the
381
381
  // artifact's 404 agbcc rows: `ORDERBASE_GATES` admits 12 keys on 10 rows, 11 of them cast keys.
382
382
  id: 'cast-base',
383
- why: 'a struct element’s reinterpret cast is the inline spelling unless the assembly says the base had a home',
383
+ why: 'a struct element’s reinterpret cast is the inline spelling unless the assembly says the base was held in a local',
384
384
  sound: false,
385
385
  rejects: (c) => c.castBase,
386
386
  },
387
387
  {
388
388
  id: 'single-use',
389
- why: 'one access re-materializes as cheaply as a named local',
389
+ why: 'a base accessed once is as cheap to load again as to hold in a named local',
390
390
  sound: false,
391
391
  rejects: reachedOnce,
392
392
  },
@@ -397,8 +397,27 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
397
397
  rejects: (c) => c.inLoop,
398
398
  },
399
399
  {
400
+ // A KNOWN COUNTEREXAMPLE, recorded here rather than fixed. `LIVEBASE_GATES` already names it —
401
+ // an MMIO wait or poll stores and re-reads ONE fixed offset through ONE register the whole
402
+ // time — so this gate rejects a base the target demonstrably held in a register, and the row
403
+ // it costs is carried by `/livebase`, which is this table minus the placement gates.
404
+ //
405
+ // NOT FIXED BY EXCLUDING A VALUELESS READ FROM THE CENSUS: that hides a real access to make
406
+ // the gate right for the wrong reason — the premise is falsified by the function's assembly,
407
+ // not by how the access is spelled. NOT FIXED BY EXEMPTING DEVICE-REGISTER BASES EITHER: that
408
+ // is the sound rule, and its blast radius is every MMIO row in the corpus against a gate this
409
+ // comment records was bought with a real match. It wants its own round and its own zero-flip
410
+ // gate.
411
+ //
412
+ // THE STRUCTURER'S DEAD-READ SPELLING (structure.ts `unreadResult`) INTERACTS, but not on the
413
+ // map-fed default: that spelling requires a qualifier to reach the access, a CAST spelling
414
+ // (`((s32 *)&REG_DMA3SAD)[2]`) carries none, and a map-fed DMA tree spells it exactly that
415
+ // way — so no statement is emitted there, offset 8 is touched once, and the base local
416
+ // survives. The inhabitant is the `/raw-globals` subtree, where the read IS spelled and this
417
+ // gate DOES demote; that subtree's own winner is a `/livebase` candidate, so nothing on the
418
+ // ranked path loses by it. Stated because the demotion is invisible from either file alone.
400
419
  id: 'repeated-const-offset',
401
- why: 'a fixed offset touched twice is a scalar RMW, which the compiler re-materializes',
420
+ why: 'a fixed offset read and then written is one scalar update, and the compiler loads its address again for it',
402
421
  sound: false,
403
422
  rejects: (c) => c.repeatedConstOffset,
404
423
  },
@@ -427,14 +446,14 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
427
446
  export const BASEFOLD_GATES: readonly Gate<BaseKey>[] = [
428
447
  {
429
448
  id: 'single-use-unfolded',
430
- why: 'one access re-materializes as cheaply as a named local, unless its offset survived the fold',
449
+ why: 'a base accessed once is as cheap to load again as to hold in a named local, unless its offset survived the fold',
431
450
  sound: false,
432
451
  rejects: (c) => reachedOnce(c) && !c.unfoldedOffset,
433
452
  },
434
453
  ...ablateHeuristic(BASECSE_GATES, 'single-use'),
435
454
  ];
436
455
 
437
- /** The `/livebase` lever's admission (rank.ts): the default rules with both PLACEMENT heuristics
456
+ /** The `/livebase` variation's admission (rank.ts): the default rules with both PLACEMENT heuristics
438
457
  * ablated, keeping only `single-use`. `loop` and `repeated-const-offset` predict which spelling
439
458
  * the compiler chose, and both predictions have a counterexample — an MMIO poll (`p[2] = go;
440
459
  * while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register the whole
@@ -447,7 +466,7 @@ export const LIVEBASE_GATES: readonly Gate<BaseKey>[] = ablateHeuristic(
447
466
 
448
467
  /** `/livebase-block`'s admission (rank.ts): `/livebase` plus `single-cell`. The two tables differ
449
468
  * by exactly one gate, so `without(LIVEBASE_BLOCK_GATES, 'single-cell')` is `/livebase`'s own
450
- * admission and this selectivity axis prices by ablation like every other.
469
+ * admission and this selectivity rule prices by ablation like every other.
451
470
  *
452
471
  * `single-cell` GENERATES a narrower candidate; it does not classify, and taking it for a compiler
453
472
  * fact is the way to misuse it. Its counterexample is in this corpus: `synthetic:sizebound`'s
@@ -470,13 +489,13 @@ export const LIVEBASE_GATES: readonly Gate<BaseKey>[] = ablateHeuristic(
470
489
  * whenever an admission is added to or removed from the roster.
471
490
  * HOW: prefer the edit-free form — import this array and `splice` the gate out of it before the
472
491
  * first `enumerateCandidates` call, since the roster holds a reference to this very object. The
473
- * env-read recipe on BASEFOLD_ADMISSIONS edits files instead, and a tap reverted underneath a
492
+ * env-read recipe on BASEFOLD_HOISTS edits files instead, and a tap reverted underneath a
474
493
  * running process reports ZEROES rather than crashing, which reads exactly like "the rule never
475
494
  * fires"; if you use it, hash the tree either side of the window and quote both hashes.
476
495
  *
477
- * A CENSUS OVER WINNING LABELS CANNOT STAND IN FOR THAT — "only a row whose winner carries
496
+ * A CENSUS OVER WINNERS' VARIATIONS CANNOT STAND IN FOR THAT — "only a row whose winner carries
478
497
  * `/livebase-block` can move" is unsound for the reason rank.ts's `seen` dedup spells out. This
479
- * table's own winning-label census reads 5 rows and read 7 before `/unfolded` shipped, and the
498
+ * table's own census over winners' variations reads 5 rows and read 7 before `/unfolded` shipped, and the
480
499
  * two that left differ: `synthetic:foldpark` by RENAME (byte-identical source, MATCH either
481
500
  * side), `synthetic:unfoldpark` because its winning SPELLING changed, 402 bytes at diff:9 to 397
482
501
  * at MATCH.
@@ -597,7 +616,8 @@ export const UNFOLDED_GATES: readonly Gate<BaseKey>[] = [
597
616
  * reading the population: where `raise/globalshape.ts` shapes a name the structurer usually spells
598
617
  * it bare and no key exists here at all — but a shape is ONE element type for the whole name, so an
599
618
  * access that strides something else keeps its cast and its key. `kleod:SetupBG3WindowOverlay`'s
600
- * `gBgInfo` derives `elemSize 4` and still reaches this table at stride 28, in both arms.
619
+ * `gBgInfo` (a row retired 2026-09-13) derived `elemSize 4` and still reached this table at stride
620
+ * 28, in both arms.
601
621
  *
602
622
  * What this table admits, censused over the artifact's 370 agbcc rows: map-less 8 rows / 10 keys,
603
623
  * map-ful 10 rows / 12 keys. TWO shapes, and the arms differ:
@@ -624,8 +644,8 @@ export const UNFOLDED_GATES: readonly Gate<BaseKey>[] = [
624
644
  * exemption cannot have.
625
645
  *
626
646
  * `loop` and `repeated-const-offset` STAY. Neither is about the base's identity and both are fan
627
- * control; ablating them is `/livebase`'s axis, already on the roster, and a row that wants the
628
- * product is one roster line. THE PRICE OF THAT IS A HOLE, and it is named rather than left for a
647
+ * control; ablating them is `/livebase`'s variation, already on the roster, and a row that wants the
648
+ * pairing is one hoist. THE PRICE OF THAT IS A HOLE, and it is named rather than left for a
629
649
  * reader to find: a licensed base with a use inside a loop is admitted by NO table on the roster —
630
650
  * this one refuses it on `loop`, and every table that ablates `loop` refuses it on `cast-base` or
631
651
  * `single-use` — which is the "a base set that is no row's stays unreachable" debt
@@ -652,7 +672,7 @@ export const ORDERBASE_GATES: readonly Gate<BaseKey>[] = [
652
672
  ...ablateHeuristic(ablateHeuristic(BASECSE_GATES, 'cast-base'), 'single-use'),
653
673
  {
654
674
  id: 'order-licensed',
655
- why: 'nothing in the assembly says this base had a home: the index was scaled first, or the order says nothing',
675
+ why: 'nothing in the assembly says this base was held in a local: the index was scaled first, or the order says nothing',
656
676
  sound: false,
657
677
  rejects: (c) => !c.orderLicensed,
658
678
  },
@@ -718,13 +738,13 @@ function admit(sfn: SFn, gates: readonly Gate<BaseKey>[]): { c: Collected; keys:
718
738
  * `scope` DECLINES, and the overload is how a caller is told: `null` means the placement had
719
739
  * nothing to say about this function, because no init landed inside a nested list. That tree is
720
740
  * byte-for-byte the `first-use` spelling (l3/hoist.ts's `nested`), and the roster withholds the
721
- * `first-use` row for this table deliberately (rank.ts, ORDERBASE_ADMISSIONS) — so returning it
741
+ * `first-use` row for this table deliberately (rank.ts, ORDERBASE_HOISTS) — so returning it
722
742
  * ships the withheld candidate under the scoped row's name.
723
743
  *
724
744
  * IT WITHDRAWS A SPELLING RATHER THAN COLLAPSING A DUPLICATE, which is what the decline costs.
725
- * `ORDERBASE_ADMISSIONS` holds exactly two rows, `head` and `scope`, so nothing is ever enumerated
745
+ * `ORDERBASE_HOISTS` holds exactly two rows, `head` and `scope`, so nothing is ever enumerated
726
746
  * at `first-use` for this table and the refused tree has no twin to fold into — its shape and
727
- * `/volatile` products go with it. Over each project's whole `asm` tree, map-ful: of the 48
747
+ * `/volatile` compositions go with it. Over each project's whole `asm` tree, map-ful: of the 48
728
748
  * functions `ORDERBASE_GATES` admits, 7 place an init inside a nested list and 41 do not, and for
729
749
  * 29 of the 41 the refused spelling is one the `head` row does not already produce. Instrumented
730
750
  * on two of those, both map-ful — the `kleod:StreamCmd_SetBGScroll` row (fan 11), and
@@ -785,13 +805,13 @@ export function hoistBaseLocals(
785
805
  }
786
806
  const out = { ...sfn, body, locals };
787
807
  // The two FLAT placements can only put the run in the top-level list, above every use of it by
788
- // construction. `scope` puts an init inside a nested list, which is where a placing lever can ship
808
+ // construction. `scope` puts an init inside a nested list, which is where a placing variation can ship
789
809
  // the one failure the byte differ rewards — a read of a local whose assignment does not reach it —
790
810
  // so the tree it emits is checked rather than argued (contracts.ts).
791
811
  //
792
812
  // THE POPULATION IS THE MOTION, and `moved` is what the placer says it moved rather than what this
793
813
  // function minted. The leading run this pass inherits is the DEFAULT hoist's, committed by
794
- // `structureChecked` before rank's levers see the tree (pipeline.ts), and `scope` moves those
814
+ // `structureChecked` before rank's variations see the tree (pipeline.ts), and `scope` moves those
795
815
  // inits too — so `newLocals` names less than half of what has to be judged. Real inhabitants, in the
796
816
  // CHECKOUTS rather than in a benchmark row — `DecompressAndLoadLevel` in klonoa and `sub_8052474`
797
817
  // in sa3, both map-ful — each sink one inherited `p0` beside the minted `p1`.
@@ -217,7 +217,7 @@ export interface MergePair {
217
217
  * `const-fed` is what keeps three of them (ablate it and the span path offers 273). A rule
218
218
  * refusing every in-loop local would make that bound redundant; this one does not, so any further
219
219
  * relaxation of `const-fed` is a multiplier, and two call sites pay it (`/coalesce` and
220
- * `/scopebase-coalesce`; the `/livebase` pairings enumerate the ARM path and pay
220
+ * `/scopebase/coalesce`; the `/livebase` pairings enumerate the ARM path and pay
221
221
  * ARM_DISJOINT_GATES' `arm-init` instead). */
222
222
  export const COALESCE_GATES: readonly Gate<MergePair>[] = [
223
223
  {
@@ -234,7 +234,7 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
234
234
  },
235
235
  {
236
236
  id: 'volatile',
237
- why: 'a volatile qualifier (object or pointee) is observable and typeToString does not spell it merging strips or adds it',
237
+ why: 'a `volatile` qualifier, on the variable or on what it points to, is observable, and merging would drop or add it',
238
238
  sound: true,
239
239
  guardedBy: 'coalesce.test.ts: a volatile pair never merges',
240
240
  rejects: (c) => c.eitherIsVolatile,
@@ -252,7 +252,7 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
252
252
  },
253
253
  {
254
254
  id: 'const-fed',
255
- why: 'a load-fed local other than a for induction variable, whose feeds are its own is one the compiler had a reason to keep where it was',
255
+ why: 'a local set from a memory load, other than a `for` loop counter, is one the compiler had a reason to keep where it was',
256
256
  sound: false,
257
257
  rejects: (c) => !c.x.constFed || !c.y.constFed,
258
258
  },
@@ -265,20 +265,20 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
265
265
  },
266
266
  {
267
267
  id: 'first-is-write',
268
- why: 'a survivor first MENTIONED by a read would see the absorbed value there',
268
+ why: 'a survivor whose first mention is a read would see the absorbed value there',
269
269
  sound: false,
270
270
  rejects: (c) => !c.y.firstIsWrite,
271
271
  },
272
272
  ];
273
273
 
274
- /** Every legal single merge, each as its own tree — NOT one committed choice.
274
+ /** Every legal single merge, each as its own tree — NOT one committed decision.
275
275
  *
276
276
  * Which pair a register allocator coalesced is not derivable from the L3 tree, and first-fit gets
277
277
  * it wrong. Run kleod:UpdateHUDCounterDisplay's published repro script (results.json carries it)
278
278
  * and read the candidate table: of its two legal merges, one scores WORSE than not merging at all
279
279
  * and declaration order is the one that picks it. Emitting no merges at all costs that row its
280
280
  * match, which is what guards this file. `rank.ts` already has the idiom for exactly this —
281
- * `/regcopy`'s "the tail choice is allocator-ambiguous, so both are ranked" — so every candidate is
281
+ * `/regcopy`'s "the tail decision is allocator-ambiguous, so both are ranked" — so every candidate is
282
282
  * emitted and the differ referees.
283
283
  *
284
284
  * ACCEPTED, NOT FIXED: a survivor assigned only on SOME paths still absorbs the other's value on
@@ -356,7 +356,7 @@ export const ARM_DISJOINT_GATES: readonly Gate<ArmPair>[] = [
356
356
  },
357
357
  {
358
358
  id: 'volatile',
359
- why: 'a volatile qualifier (object or pointee) is observable merging strips or adds it',
359
+ why: 'a `volatile` qualifier, on the variable or on what it points to, is observable, and merging would drop or add it',
360
360
  sound: true,
361
361
  guardedBy: 'coalesce.test.ts: a volatile pair never merges',
362
362
  rejects: (c) => c.eitherIsVolatile,
@@ -370,7 +370,7 @@ export const ARM_DISJOINT_GATES: readonly Gate<ArmPair>[] = [
370
370
  },
371
371
  {
372
372
  id: 'arm-init',
373
- why: 'a local not const-initialized at its arm’s first mention is one the compiler had a reason to keep — the growth bound const-fed gives the span table',
373
+ why: 'a local its arm does not first set to a constant is one the compiler had a reason to keep apart',
374
374
  sound: false,
375
375
  rejects: (c) => !c.bothArmConstInit,
376
376
  },
@@ -378,7 +378,7 @@ export const ARM_DISJOINT_GATES: readonly Gate<ArmPair>[] = [
378
378
 
379
379
  /** The arm-disjoint merges alone — the class the livebase pairings enumerate (rank.ts): the
380
380
  * demanding row's shared counter is arm-disjoint, and the span-model merges already ride the
381
- * plain /coalesce label, so pairing them too would multiply candidates with no row behind it. */
381
+ * plain /coalesce variation, so pairing them too would multiply candidates with no row behind it. */
382
382
  export function armDisjointCandidates(sfn: SFn): { merged: string; sfn: SFn }[] {
383
383
  return armDisjointUnder(ARM_DISJOINT_GATES, sfn).candidates;
384
384
  }
package/src/l3/gates.ts CHANGED
@@ -7,10 +7,14 @@
7
7
  //
8
8
  // `why` is a LABEL, one line. The argument for why the rule is correct belongs in the file header,
9
9
  // which has room; duplicating it here is how a table stops paying for itself.
10
+ //
11
+ // A table a variation's definition names (`variation-gates.ts`) shows its `why` to a reader, in the
12
+ // webapp's variation drawer, so there the label is plain prose: no tag, file or function name, or
13
+ // shouted word. `variation-offers.test.ts` holds it to that.
10
14
  export interface Gate<Ctx> {
11
15
  /** stable, kebab-case; appears in test names and in the contract report */
12
16
  readonly id: string;
13
- /** one line: the reason the rule exists */
17
+ /** one line: the reason the rule exists, readable without the code */
14
18
  readonly why: string;
15
19
  /** Remove it and some candidate is WRONG, not merely worse. Everything else is a codegen
16
20
  * heuristic the differ still referees. This flag is what makes `guardedBy` mandatory. */
@@ -86,3 +90,73 @@ export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
86
90
  }
87
91
  return out;
88
92
  }
93
+
94
+ /** A gate table that counts its own refusals — {@link tallying}'s return. */
95
+ export interface Tallied<Ctx> {
96
+ /** Hand this to the pass, in place of the table it wraps. */
97
+ readonly gates: readonly Gate<Ctx>[];
98
+ /** The census so far, most-refused first, ties in table order. A snapshot: counts keep
99
+ * accumulating across every later call, which is what a corpus-wide census wants.
100
+ *
101
+ * THERE IS NO RESET, deliberately — a per-row census is two snapshots DIFFED, not a fresh
102
+ * wrapper per row, because a wrapper is a new table IDENTITY and a reader that keys on one
103
+ * (`rank.ts`'s `censuses` memo) sees a fresh key every row. Counting is keyed by `g.id`, so a
104
+ * table COMPOSED from several should be run past `gateTableDefects` first: two rules sharing an
105
+ * id sum into one number, and the contract test only checks the tables on its own roster. */
106
+ readonly refusals: () => readonly (readonly [string, number])[];
107
+ }
108
+
109
+ /** The same table, wrapping each `rejects` in a counter — so a caller OUTSIDE core can obtain the
110
+ * per-id census that `l3/coalesce.ts` and `structure/namecoalesce.ts` hand-roll into their return
111
+ * type (`l3/scopebase.ts` reports the same attribution per KEY), from any pass that takes its
112
+ * table as a parameter:
113
+ *
114
+ * const t = tallying(UNMERGE_SITE_GATES);
115
+ * unmergeJoins(sfn, { site: t.gates });
116
+ * console.log(t.refusals()); // [['empty-arm', 168], ['no-merge-name', 80]]
117
+ *
118
+ * THAT IS THE API AND NOT YET A CENSUS: nothing exports a corpus of trees to loop over, and a
119
+ * tabled pass's only shipped caller is normally inside core. Taking the census off a REAL
120
+ * enumeration is `pnpm bench gates --pass <id>`, whose header
121
+ * (`apps/benchmark/src/run/gate-census.ts`) holds the measured reasons it is a subcommand rather
122
+ * than a script to copy, and what a SECOND censusable pass costs.
123
+ *
124
+ * WHAT IT COUNTS IS AN EVALUATION THAT ANSWERED TRUE, not a site. Under `firstRejection` — which
125
+ * short-circuits — that is the FIRST rejecter, so this produces the same census the hand-rolled
126
+ * maps do, with the same reading: an id absent from it is starved OR REDUNDANT WITH an earlier
127
+ * rule, and telling the two apart takes the same rule run with the rest of the table empty
128
+ * (`grep -n "ON ITS OWN" packages/core/src/raise/globalshape.ts`, whose dated table ships three
129
+ * rules of the second kind). A consumer that asks the table something else — `.some`, `.filter` —
130
+ * gets one count per evaluation instead, which is a different question and rarely the one wanted.
131
+ *
132
+ * AND IT COUNTS REFUSALS, WHICH IS NOT REACH. A rule can refuse hundreds of times and still change
133
+ * no output, because a later rule or a narrowing outside the table would have refused the same
134
+ * sites: that is the MOVED column, it costs an ablation rather than a census, and `l3/unmerge.ts`'s
135
+ * header carries the worked example of the two disagreeing.
136
+ *
137
+ * IT CHANGES NO BEHAVIOUR: each wrapper's predicate IS the original's, `id`/`why`/`sound`/
138
+ * `guardedBy` are carried, so `without`, `ablateHeuristic` and `gateTableDefects` all still hold
139
+ * over the result. What it does change is the table's IDENTITY, and some readers key on that —
140
+ * `rank.ts`'s `censuses` memo is a `Map` over `Gate<BaseKey>[]` instances — so wrap once and reuse
141
+ * `gates`, rather than per call. */
142
+ export function tallying<Ctx>(gates: readonly Gate<Ctx>[]): Tallied<Ctx> {
143
+ const counts = new Map<string, number>();
144
+ const order = new Map(gates.map((g, i) => [g.id, i]));
145
+ return {
146
+ gates: gates.map((g) => ({
147
+ ...g,
148
+ rejects: (c: Ctx) => {
149
+ const r = g.rejects(c);
150
+ if (r) {
151
+ counts.set(g.id, (counts.get(g.id) ?? 0) + 1);
152
+ }
153
+ return r;
154
+ },
155
+ })),
156
+ refusals: () =>
157
+ [...counts].sort((a, b) => b[1] - a[1] || (order.get(a[0]) ?? 0) - (order.get(b[0]) ?? 0)) as readonly (readonly [
158
+ string,
159
+ number,
160
+ ])[],
161
+ };
162
+ }
package/src/l3/hoist.ts CHANGED
@@ -208,7 +208,7 @@ function scopeSite(list: Stmt[], name: string): { list: Stmt[]; at: number } | n
208
208
  * different bytes. Where no nested list holds every mention this IS `first-use`, which is what
209
209
  * makes it a placement rather than a second policy.
210
210
  *
211
- * These three are the axis a roster admission may state (rank.ts) and the only values
211
+ * These three are the positions a hoist may state (rank.ts) and the only values
212
212
  * `hoistBaseLocals` accepts. */
213
213
  export type HoistPlacement = 'head' | 'first-use' | 'scope';
214
214
 
@@ -1,7 +1,7 @@
1
1
  // The `/livebase-block × /regionbase` PAIRING (rank.ts `/livebase-block/homesplit`): one base kept
2
2
  // at the function head and a SECOND base split into one local per region, in the same function.
3
3
  //
4
- // WHY IT IS NOT REACHABLE FROM EITHER LEVER. Both are whole-FUNCTION policies over the bases they
4
+ // WHY IT IS NOT REACHABLE FROM EITHER VARIATION. Both are whole-FUNCTION policies over the bases they
5
5
  // bind: `hoistBaseLocals` homes every key its table admits at one placement, and
6
6
  // `hoistScopedBases` splits every key its region rule admits. A function whose two bases want
7
7
  // OPPOSITE answers is spelled by neither. `synthetic:dmapoll` is that function and its endpoint is
@@ -19,9 +19,9 @@
19
19
  // over ONE input is the shape that would collide, and nothing here does it.
20
20
  //
21
21
  // WHICH KEY IS WITHHELD IS NOT DERIVABLE, so every admitted key is offered as its own candidate,
22
- // LABELLED WITH THAT KEY, and the differ referees — the same posture `/scopebase` and `/regionbase`
23
- // take toward each other. The label carries `homeSplitTag(key)` because a candidate label is an
24
- // IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by it, so one label over two
22
+ // NAMED BY THAT KEY, and the differ referees — the same posture `/scopebase` and `/regionbase`
23
+ // take toward each other. The variation carries `homeSplitTag(key)` because a candidate's variations are its
24
+ // IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by them, so one name over two
25
25
  // withholds would hide a program swap from both. The withhold itself is DATA: one rejection
26
26
  // prepended to the caller's own admission table, in the `Gate<BaseKey>` type that table already
27
27
  // has, so `firstRejection` attributes a refusal to it, `without` ablates it, and the composed table
@@ -30,7 +30,7 @@
30
30
  // EXACTLY ONE KEY IS WITHHELD, and the arity is a claim rather than an oversight. A two-key withhold
31
31
  // exists only where the caller's table binds three — `homesplit-fan-cap` admits no more — and there
32
32
  // it is the three further pairs, each carrying the three respells rank.ts derives from one pipe:
33
- // NINE more candidates per axis point on those functions alone, before any shape product, and no
33
+ // NINE more candidates per structure setting on those functions alone, before any stacked variation, and no
34
34
  // row asks for one. `l3/volatileptr.ts`'s `volatileSubsetCandidates` enumerates every proper subset
35
35
  // under the same cap; it does that because a row demanded each of them. Widen this the same way,
36
36
  // on a row.
@@ -59,7 +59,7 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
59
59
  ...gates,
60
60
  ];
61
61
 
62
- /** The withheld key as a LABEL token: `c:67109076 4 true` → `0x40000d4.4s`. Width and signedness
62
+ /** The withheld key as a variation's SUBJECT: `c:67109076 4 true` → `0x40000d4.4s`. Width and signedness
63
63
  * ride because they are part of the key — two keys over one address are two different spellings.
64
64
  *
65
65
  * PARSED BY THE KEY'S PRODUCER (`l3/basecse.ts`'s `parseBaseKey`, beside `keyOf`), because a base
@@ -68,9 +68,9 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
68
68
  * separator rather than the type's. Split from the FRONT, the type reads as the width and the
69
69
  * width as the signedness; split from the END, the `a:` form comes out right and the `c:` form
70
70
  * runs `Number` over `67109076 <u16*>`, tagging every cast over a numeric base `0xNaN` and
71
- * collapsing distinct keys onto one label. No shipped table admits a cast base outside
71
+ * collapsing distinct keys onto one name. No shipped table admits a cast base outside
72
72
  * `/orderbase`, and `/orderbase` carries `pairings: false`, so no such key reaches this function
73
- * today — but a candidate LABEL is an identity (`bench diff` and docs/ranked-repro.md compare
73
+ * today — but a candidate's variations are its identity (`bench diff` and docs/ranked-repro.md compare
74
74
  * candidates by it), and one roster line is all that stands between the two. The sibling half of
75
75
  * the same hazard is already guarded in `splitHomeBases`, which translates a cast base to no
76
76
  * region key and declines. */
@@ -79,7 +79,7 @@ export function homeSplitTag(key: string): string {
79
79
  const base = leaf.startsWith('c:') ? `0x${Number(leaf.slice(2)).toString(16)}` : leaf.slice(leaf.indexOf(':') + 1);
80
80
  // The cast's element type stays in the token — it is part of the key's identity, since two casts
81
81
  // over one symbol are two locals that stride differently. Whitespace is squeezed defensively
82
- // rather than because any type spells one: a label is one whitespace-free word everywhere it is
82
+ // rather than because any type spells one: a variation is one whitespace-free word everywhere it is
83
83
  // read, and that has to hold whatever `typeToString` grows.
84
84
  const type = castType === null ? '' : `<${castType}>`;
85
85
  return `${`${base}${type}`.replace(/\s+/g, '')}.${width}${signed ? 's' : 'u'}`;
@@ -98,13 +98,13 @@ export interface HomeSplitFanCtx {
98
98
  export const HOMESPLIT_FAN_GATES: readonly Gate<HomeSplitFanCtx>[] = [
99
99
  {
100
100
  id: 'homesplit-degenerate',
101
- why: 'withholding the only hoistable key is `/regionbase`, and withholding none is `/livebase-block`',
101
+ why: 'with one hoistable base there is nothing new: withholding it is `regionbase`, and withholding none is `livebase-block`',
102
102
  sound: false,
103
103
  rejects: (c) => c.hoistableKeys < 2,
104
104
  },
105
105
  {
106
106
  id: 'homesplit-fan-cap',
107
- why: 'one candidate per hoistable key, times the volatile products the whole cost of the axis',
107
+ why: 'more than three hoistable bases would add a candidate for each, times the volatile pairings',
108
108
  sound: false,
109
109
  rejects: (c) => c.hoistableKeys > 3,
110
110
  },
@@ -143,13 +143,13 @@ export interface HomeSplitCtx {
143
143
  export const HOMESPLIT_GATES: readonly Gate<HomeSplitCtx>[] = [
144
144
  {
145
145
  id: 'homesplit-no-region',
146
- why: 'a withheld key the region rule declines to split leaves the spelling the primary carries',
146
+ why: 'a base held back from the function-top local that no per-region local takes instead leaves the spelling the default carries',
147
147
  sound: false,
148
148
  rejects: (c) => !c.withheldSplits,
149
149
  },
150
150
  {
151
151
  id: 'homesplit-drops-device-volatile',
152
- why: 'a device read left inline is qualified by neither /volatile nor /vol-store',
152
+ why: 'a device read left inline would carry neither `volatile` nor `vol-store`',
153
153
  sound: false,
154
154
  rejects: (c) => c.inlineDeviceRead,
155
155
  },
@@ -1,10 +1,10 @@
1
- // L3 re-spelling lever: a loop INIT moves above the guard that encloses it, and the guard reads
1
+ // L3 respell variation: a loop INIT moves above the guard that encloses it, and the guard reads
2
2
  // the initialized variable.
3
3
  //
4
4
  // `for (i = 0; i < n; i++)` compiles with the init BEFORE the zero-trip test (`mov r3,#0` then
5
5
  // `cmp r3, r5`), while `if (0 < n) { i = 0; do … }` compiles with the init behind the branch.
6
6
  // Both source forms lift to the SAME IR — a const has no position — so which the original spelled
7
- // is not recoverable; this lever emits the init-first sibling and the differ referees:
7
+ // is not recoverable; this variation emits the init-first sibling and the differ referees:
8
8
  //
9
9
  // if (0 < n) { v = 0; … } → v = 0; if (v < n) { … }
10
10
  //
@@ -61,7 +61,7 @@ const stripWideIntCast = (e: Expr): Expr =>
61
61
 
62
62
  /** Every deref rooted at a var through casts only. The var-root rule is a TWO-WORLD argument, not a
63
63
  * volatility proof: a deref through a plain-declared pointer local may still be MMIO, but the
64
- * /volatile axis enumerates the qualified sibling — where this lever refuses — so both worlds reach
64
+ * /volatile variation enumerates the qualified sibling — where this variation refuses — so both worlds reach
65
65
  * the differ and collapsing reads here is the plain world's own premise. A raw `*(u16 *)CONST` deref
66
66
  * has NO local for /volatile to qualify, so no sibling carries the volatile world and the collapse
67
67
  * would silently discard it. */
@@ -1,4 +1,4 @@
1
- // L3 re-spelling lever: DELETE a pointer local holding a CONSTANT address and spell each access
1
+ // L3 respell variation: DELETE a pointer local holding a CONSTANT address and spell each access
2
2
  // through it as the cast constant (`*(u16 *)0x4000208` rather than `p = (u16 *)0x4000208; *p`).
3
3
  //
4
4
  // The local is structure/analysis.ts's value-home spelling for a `const` with 2+ consumers that
@@ -25,34 +25,34 @@
25
25
  // separate at 11 against 12 on their own, and are BYTE-IDENTICAL once the slot qualifier is
26
26
  // there too — the shape that matches (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit
27
27
  // -fhex-asm -fprologue-bugfix`; `.s` diff empty, `.o` identical under cmp). So rank.ts emits
28
- // the qualified spelling as a second OUTPUT of this lever, `/volatile` narrowed to the locals
28
+ // the qualified spelling as a second OUTPUT of this variation, `/volatile` narrowed to the locals
29
29
  // it deletes. Each cast this mints carries the qualifier; a use whose width does not stride the
30
30
  // declared pointee renders through the C-family printer's reinterpret cast instead, which
31
31
  // carries it too (backend/cfamily.ts) — in C the access takes the OUTER type, so a plain cast
32
32
  // there would spell exactly the silent drop this paragraph exists to prevent.
33
33
  //
34
34
  // GATE (INLINEBASE_GATES) — the local must be all of: pointer-typed; initialized by a bare
35
- // NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own lever
35
+ // NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own variation
36
36
  // family owns that question; `0` is NULL, never an address, which is also the sibling qualifier
37
- // lever's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
37
+ // variation's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
38
38
  // statement's mention precedes; never address-taken; used only as the base of an `index`, at 2+
39
39
  // sites (one use is not the reused address this exists for); not object-`volatile` (a
40
40
  // `T *volatile p` has no inhabitant, and cfamily.ts prints that flag in the pointee's position);
41
41
  // and not `frame` (a slot is an asm fact — see the SFn.locals doc). Anything else, and nothing
42
42
  // qualifying at all, DECLINES (null) rather than approximating.
43
43
  //
44
- // A RE-SPELLING RATHER THAN A STRUCTURING AXIS, which is a cost choice and not the
45
- // underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked population").
44
+ // A RESPELL VARIATION RATHER THAN A STRUCTURE VARIATION, which is a cost decision and not the
45
+ // underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked variations").
46
46
  // The question — does a `const` with 2+ consumers live across a call in a named local — is the one
47
47
  // `/reread-globals`, `/addr-home`, `/expr-home` and `/derived-home` each answer as a
48
- // STRUCTURING_AXES entry. An axis here would double the enumeration on every function it admits;
48
+ // STRUCTURE_VARIATIONS entry. A structure variation here would double the enumeration on every function it admits;
49
49
  // substituting on the already-homed tree costs 766 candidates over 47058 (+1.6%) across the 33 of
50
50
  // 69 klonoa functions that lift with no symbol map.
51
51
  //
52
52
  // KNOWN GAP, and it is the price of that choice rather than an oversight: only `index` bases are
53
53
  // re-spelled, so the same L2 home passed to a callee or standing as a `field` base is out of
54
54
  // reach — `otherUses` refuses it. Reaching those needs the un-homed tree, which is
55
- // structure/analysis.ts's decision; the day a row demands one, this becomes the axis.
55
+ // structure/analysis.ts's decision; the day a row demands one, this becomes a structure variation.
56
56
  //
57
57
  // The idiom it recovers is every GBA project's register macro: `*(vu16 *)0x4000208` is what
58
58
  // `REG_IME` expands to, so the deleted local is not merely an undone home.
@@ -71,20 +71,20 @@ interface BaseCtx {
71
71
  export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
72
72
  {
73
73
  id: 'non-pointer',
74
- why: 'the lever re-spells an address; a scalar value home is a different question',
74
+ why: 'only a local holding an address is substituted; a local holding a scalar value is a different question',
75
75
  sound: false,
76
76
  rejects: (c) => !c.isPointer,
77
77
  },
78
78
  {
79
79
  id: 'object-volatile',
80
- why: 'the substitution carries the POINTEE flag, so an object-volatile pointer would lose its own',
80
+ why: 'the substitution keeps only the `volatile` of what the pointer points to, so a pointer that is itself volatile would lose its own',
81
81
  sound: true,
82
82
  guardedBy: 'inlinebase.test.ts: an object-volatile or frame local declines',
83
83
  rejects: (c) => c.objectVolatile,
84
84
  },
85
85
  {
86
86
  id: 'frame',
87
- why: 'a slot the asm materialized is an asm fact, not a spelling to undo',
87
+ why: 'a stack slot the assembly used is a fact about the function, not a spelling to undo',
88
88
  sound: false,
89
89
  rejects: (c) => c.hasFrame,
90
90
  },
@@ -104,7 +104,7 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
104
104
  },
105
105
  {
106
106
  id: 'null-base',
107
- why: '`0` is NULL, never an address the sibling qualifier lever (volatileptr.ts) refuses it too',
107
+ why: 'a base of `0` is a null pointer, never an address, and `volatile` refuses it too',
108
108
  sound: false,
109
109
  rejects: (c) => c.m.constValue === 0,
110
110
  },
@@ -126,12 +126,12 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
126
126
  id: 'other-uses',
127
127
  why: 'a use the substitution cannot reach would name the deleted local',
128
128
  sound: true,
129
- guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the lever re-spells',
129
+ guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the variation re-spells',
130
130
  rejects: (c) => c.m.otherUses !== 0,
131
131
  },
132
132
  {
133
133
  id: 'single-use',
134
- why: 'one use is not the reused address this lever exists for',
134
+ why: 'one use is not the reused address this variation exists for',
135
135
  sound: false,
136
136
  rejects: (c) => c.m.baseUses < 2,
137
137
  },
@@ -164,8 +164,8 @@ function plan(sfn: SFn): Map<string, Extract<Expr, { k: 'cast' }>> {
164
164
  return out;
165
165
  }
166
166
 
167
- /** Which locals this lever would delete — rank.ts narrows `/volatile` to exactly these before
168
- * pairing, so the qualified output never qualifies a pointer the lever leaves standing. */
167
+ /** Which locals this variation would delete — rank.ts narrows `/volatile` to exactly these before
168
+ * pairing, so the qualified output never qualifies a pointer the variation leaves standing. */
169
169
  export function inlinableConstBases(sfn: SFn): string[] {
170
170
  return [...plan(sfn).keys()];
171
171
  }