@asmlift/core 0.5.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 (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -0,0 +1,760 @@
1
+ // asmlift — the candidate-enumeration TABLES, split out of rank.ts so the enumeration driver and
2
+ // the data it walks are two files. Every table here is DATA the driver derives from: adding a
3
+ // structure variation, a stacked variation or a base-CSE hoist is one entry in one of these lists, and the
4
+ // driver reads it without a second hand-edited site.
5
+ //
6
+ // DECLARATION ORDER IS PUBLISHED BEHAVIOUR. `compareScored` breaks a score tie by enumeration
7
+ // order, so the order of `STRUCTURE_VARIATIONS`, `STACKED_VARIATIONS` and every `*_HOISTS` roster
8
+ // decides which of two byte-identical candidates wins and gets its variations into
9
+ // `results.json`. Reordering one of these arrays is a behaviour change wearing a cleanup's
10
+ // clothes — never do it as tidying.
11
+ //
12
+ // A SIBLING MODULE, never a `rank/` directory: a directory named `rank` beside `rank.ts` is a
13
+ // resolver trap, and `@asmlift/core`'s `"./*": "./src/*.ts"` export map addresses these files by
14
+ // name.
15
+ import { globalCellOf } from './ir/alias';
16
+ import type { Fn, Op, Value } from './ir/core';
17
+ import { successorsOf } from './ir/core';
18
+ import type { SFn } from './l3/ast';
19
+ import {
20
+ BASEFOLD_GATES,
21
+ type BaseKey,
22
+ LIVEBASE_BLOCK_GATES,
23
+ LIVEBASE_GATES,
24
+ ORDERBASE_GATES,
25
+ UNFOLDED_GATES,
26
+ } from './l3/basecse';
27
+ import type { Gate } from './l3/gates';
28
+ import type { HoistPlacement } from './l3/hoist';
29
+ import { initFirstGuards } from './l3/initfirst';
30
+ import { pollGuards, pollReads } from './l3/pollguard';
31
+ import { unmergeJoins } from './l3/unmerge';
32
+ // TYPE-ONLY, and deliberately: the structure-variation table types its `options` as
33
+ // `Parameters<typeof structureChecked>[1]`, which needs the binding in a `typeof` position and
34
+ // nothing at runtime. A value import here would make this module depend on the whole pipeline.
35
+ import type { structureChecked } from './pipeline';
36
+ import {
37
+ hasDerivedReadHome,
38
+ hasHomeableSharedAddress,
39
+ hasLoopSharedPureValue,
40
+ hasMergeFeedHome,
41
+ } from './structure/analysis';
42
+ import { edgeCopyOrdersDiffer, hasParamRootedMerge } from './structure/structure';
43
+ import type { VariationName } from './variation-tokens';
44
+
45
+ /** The STRUCTURE VARIATIONS — the boolean candidate dimensions crossed into every enumeration
46
+ * (after signedness/branch-sense/defsite/bitfields, which have their own shapes). One entry per
47
+ * variation; chain construction, the dropped-sibling strip closure, the per-candidate
48
+ * StructureOptions, and the default-setting abort guard all derive from this table, so a new
49
+ * structure variation is one entry — not four hand-edited sites that can drift.
50
+ *
51
+ * `sharedGate` gates the alternative's ENUMERATION on the shared lift (the only thing the
52
+ * variation can change must exist at all); `perLiftGate` re-evaluates per symbol-map setting on
53
+ * that setting's own lifted fn (a map-lifted shared lift spells const bases as gaddr, which would
54
+ * blind the /raw-globals siblings — the /addr-home lesson). `strip` opts the variation into the
55
+ * dropped-sibling closure: a candidate that applies it is skipped when its default sibling (the
56
+ * same setting without it) failed the boundary contracts. Two variations are EXEMPT from structure()'s assertDefaultAccepts invariant:
57
+ * `/reread-globals` only relaxes inlining barriers and `/uns-cmp` only changes spelling and
58
+ * declarations — neither adds materialization or merging, so neither can unlock a function the
59
+ * default declines (reread also skips the strip closure). Both exemptions are stated here
60
+ * rather than left implicit in a missing `||` arm or trigger term. */
61
+ export interface StructureVariation {
62
+ flag:
63
+ | 'reread'
64
+ | 'inplace'
65
+ | 'mergeNames'
66
+ | 'addrHome'
67
+ | 'exprHome'
68
+ | 'derivedHome'
69
+ | 'mergeHome'
70
+ | 'unsCmp'
71
+ | 'freshMerge'
72
+ | 'copyDefPos'
73
+ | 'siteSense';
74
+ name: VariationName;
75
+ options: (on: boolean) => Parameters<typeof structureChecked>[1];
76
+ sharedGate?: (sharedLift: Fn, defs: Map<Value, Op>) => boolean;
77
+ perLiftGate?: (fn: Fn) => boolean;
78
+ strip: boolean;
79
+ }
80
+ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
81
+ // `/reread-globals` — the VALUE-HOME variation (structure/analysis.ts AnalyzeOptions). Whether the
82
+ // source read a global once into a variable or re-read it at each use is not derivable from
83
+ // asm: the compiler CSEs the second spelling back into one load, and the round-5 dogfood
84
+ // watched agbcc land on both sides inside a single function (its highest-cost defect, 25 of
85
+ // 27 points on one klonoa function and 35/50 both ways on another). Gated on the function
86
+ // having a load that resolves to a named global at all.
87
+ {
88
+ flag: 'reread',
89
+ name: 'reread-globals',
90
+ options: (on) => ({ rereadGlobals: on }),
91
+ sharedGate: (sharedLift, defs) =>
92
+ sharedLift.blocks.some((b) =>
93
+ b.ops.some((op) => op.opcode === 'load' && globalCellOf(defs, op.operands[0], op.attrs.off as number) !== null),
94
+ ),
95
+ strip: false,
96
+ },
97
+ // `/inplace` — materialize a load that feeds a `cond_br` join arg (structure.ts
98
+ // materializeJoinFeeds), so the merge homes in the load's own variable and the identity arm
99
+ // elides to a one-sided in-place overwrite (`v = *p; if (v > 31) v = 32;`). The recompiled
100
+ // code differs (the two-sided form needs a second register — at the margin a callee-save
101
+ // push — and the emptied arm flips the branch sense). Gated on a load-fed cond_br arg.
102
+ {
103
+ flag: 'inplace',
104
+ name: 'inplace',
105
+ options: (on) => ({ materializeJoinFeeds: on }),
106
+ sharedGate: (sharedLift, defs) =>
107
+ sharedLift.blocks.some((b) =>
108
+ b.ops.some(
109
+ (op) =>
110
+ op.opcode === 'cond_br' && op.successors.some((sx) => sx.args.some((a) => defs.get(a)?.opcode === 'load')),
111
+ ),
112
+ ),
113
+ strip: true,
114
+ },
115
+ // `/merge-names` — coalesce two variables a merge copy would join when the values under them
116
+ // never interfere (structure/namecoalesce.ts). Whether the source had one variable there is
117
+ // not derivable, and the copies are worth less than they look — agbcc coalesces most of them
118
+ // itself, so which side scores better is per-function. Gated on a merge fed by 2+ edges.
119
+ {
120
+ flag: 'mergeNames',
121
+ name: 'merge-names',
122
+ options: (on) => ({ coalesceMergeNames: on }),
123
+ sharedGate: (sharedLift) =>
124
+ sharedLift.blocks
125
+ .slice(1)
126
+ .some(
127
+ (b) =>
128
+ b.params.length > 0 && new Set(sharedLift.blocks.filter((pr) => successorsOf(pr).includes(b))).size > 1,
129
+ ),
130
+ strip: true,
131
+ },
132
+ // `/addr-home` — the address-home variation (structure/analysis.ts AnalyzeOptions
133
+ // homeSharedAddresses): a pure computed address dereferenced at 2+ sites, and the multi-render
134
+ // loads through it, materialize into locals — the source's pointer-local + scalar-temp
135
+ // spelling, where the default re-derives per use (a pool literal per folded offset). Gated PER
136
+ // SYMBOL-MAP SETTING (see the table doc) on that setting's own lifted fn having a homeable base.
137
+ {
138
+ flag: 'addrHome',
139
+ name: 'addr-home',
140
+ options: (on) => ({ homeSharedAddresses: on }),
141
+ perLiftGate: hasHomeableSharedAddress,
142
+ strip: true,
143
+ },
144
+ // `/expr-home` — the loop-expression-home variation (structure/analysis.ts AnalyzeOptions
145
+ // homeLoopExprs): a pure value defined outside a loop with 2+ distinct consumers, at least one
146
+ // of them inside it, materializes into a local carrying the value's recovered type — the register
147
+ // the compiler holds across the iterations (`u32 size = 16 << t;` driving a loop bound, a product
148
+ // and a shift), where the default re-derives per use. Gated per symbol-map setting like `/addr-home`
149
+ // (the cone refusal reads the setting's own lift).
150
+ {
151
+ flag: 'exprHome',
152
+ name: 'expr-home',
153
+ options: (on) => ({ homeLoopExprs: on }),
154
+ perLiftGate: hasLoopSharedPureValue,
155
+ strip: true,
156
+ },
157
+ // `/derived-home` — the derived-read-home variation (structure/analysis.ts AnalyzeOptions
158
+ // homeDerivedReads): a pure value with 2+ consumers standing on a memory read materializes, and
159
+ // the read then renders once inside it — the register the asm carried the DERIVED value in
160
+ // (`eor r1,r1,r0` keeps `0x3FF ^ REG_KEYINPUT`), where the default homes the read and re-derives
161
+ // the computation at every use. Both spellings compile (agbcc CSEs the re-derivation back), so
162
+ // the differ referees. Gated per symbol-map setting like its `/addr-home` and `/expr-home` siblings,
163
+ // and for the same reason the /addr-home lesson names: the scope refuses a cone holding a
164
+ // standalone address, and a pool constant the map lifts to a `gaddr` is a bare `const` in the
165
+ // `/raw-globals` sibling — so the two settings genuinely answer differently.
166
+ {
167
+ flag: 'derivedHome',
168
+ name: 'derived-home',
169
+ options: (on) => ({ homeDerivedReads: on }),
170
+ perLiftGate: hasDerivedReadHome,
171
+ strip: true,
172
+ },
173
+ // `/merge-home` — the merge-feed-home variation (structure/analysis.ts AnalyzeOptions
174
+ // homeMergeFeeds): a pure value one join's incoming edges render into the SAME parameter slot
175
+ // from 2+ places materializes in the block that dominates them — the value the source computed
176
+ // once above the branch (`s32 m = (b & 1) ? 0x400 : 0;`), where the default has no name to
177
+ // reference on an edge and re-derives the whole expression per arm. Gated per symbol-map setting on
178
+ // the scope itself rather than on an approximation of it.
179
+ //
180
+ // A VARIATION, not a default: forced on, the spelling is REPLACED across the fan rather than
181
+ // added to it, which costs `kleod:MultiplyQ8` (measured on kl-eod-decomp's source, before 2026-09-13) and `pokeemerald:MathUtil_Mul16` their matches
182
+ // (and cost `MultiplyQ4`, its byte-for-byte twin, until the 2026-09 kleod source swap retired that
183
+ // row as a duplicate). As a variation that is unreachable — `compareScored`
184
+ // orders by score and the un-homed sibling rides beside it.
185
+ //
186
+ // Its fan is essentially one row's: over the corpus rows the gate admits, 2790 → 5841
187
+ // candidates map-less and 2538 → 5363 with a map, of which `kleod:UpdateCameraScroll` (outcome
188
+ // `noncompile`, so they buy nothing) is +2944 and +2752, three rows add none at all where
189
+ // `/defsite` already spells the same tree, and the rest pay 107 and 73 between them.
190
+ //
191
+ // THAT CENSUS IS A STATEMENT ABOUT `hasMergeFeedHome`'s ADMITTED SET, and the admitted set is not
192
+ // a property of this file: it moves whenever an upstream pass changes whether a merge feed still
193
+ // EXISTS in the IR. It now reads **20** of the 804 rows that lift (mine, both tiers), three more
194
+ // than the same census over the tree before this branch: `synthetic:sinkacc:agbcc`,
195
+ // `kleod:CountCollectedGems:agbcc` and `kleod:CheckWorldCompletion:agbcc`, which `raise/const.ts`
196
+ // gained by REFUSING to fold an accumulator's `add(const 0, const 1)` and so keeping the feed
197
+ // alive for this gate to see. Re-measure the counts above before tuning off them — the number
198
+ // recorded here has gone stale once already.
199
+ {
200
+ flag: 'mergeHome',
201
+ name: 'merge-home',
202
+ options: (on) => ({ homeMergeFeeds: on }),
203
+ perLiftGate: hasMergeFeedHome,
204
+ strip: true,
205
+ },
206
+ // `/uns-cmp` — spell unsigned compares unsigned (structure.ts unsignedCompareSpelling): an
207
+ // icmp_u* operand takes a (u32) cast where the rendered operands do not guarantee the
208
+ // unsignedness, and a mixed-claimant declaration reconciles to u32 when nothing under the
209
+ // name needs signed. Which side the source spelled is genuinely ambiguous: a signed spelling
210
+ // that byte-matched was PROVED non-negative by the compiler (only then does it emit the
211
+ // unsigned branch from a signed compare), and emission's provable set is smaller than the
212
+ // compiler's. Gated on the function having an unsigned compare at all.
213
+ {
214
+ flag: 'unsCmp',
215
+ name: 'uns-cmp',
216
+ options: (on) => ({ unsignedCompareSpelling: on }),
217
+ sharedGate: (sharedLift) => sharedLift.blocks.some((b) => b.ops.some((op) => op.opcode.startsWith('icmp_u'))),
218
+ strip: true,
219
+ },
220
+ // `/fresh-merge` — the parameter-merge-home variation (structure.ts `freshParamMerge`, whose
221
+ // `FRESH_MERGE_GATES` carry the argument): a merge whose carrier is a parameter takes its own
222
+ // local (`if (a1 < a0) { v0 = a0; } else { v0 = a1; }`) where the default assigns back into the
223
+ // parameter (`if (a1 < a0) a1 = a0;`). Both are ordinary C over the same values, so
224
+ // the differ decides. At TWO arguments they compile to the SAME bytes on agbcc and on mwcc
225
+ // (measured, both directions), which is why `maxi`/`mini` hold under the variation.
226
+ //
227
+ // IT ALSO UNLOCKS `/defsite`. `anchorConstCopies` refuses a merge whose name claims another SSA
228
+ // value, so a merge that adopted its parameter is never anchored, while a minted home is sole by
229
+ // construction and clears that one refusal — a constant arm then writes above the branch, where
230
+ // the remaining placement rules allow it. That pair spells m2c's own
231
+ // `v0 = 0xFF; if (a0 <= 0xFF) v0 = a0;`, which is how `synthetic:clampu8:mwcc_242_81` matches
232
+ // under `signed/defsite/fresh-merge` — `signed/defsite` is inert on the base tree and does not
233
+ // appear in that row's fan at all. Priced at the guard it widens: sole-claimant admissions go
234
+ // 196 → 245, 34 rows gaining 49 merges between them. A DELTA WITHOUT ITS DENOMINATOR, on
235
+ // purpose — the corpus grows, so the row count that census ran over is not today's and quoting
236
+ // it would read as verified. Re-run the sweep before budgeting against the totals.
237
+ //
238
+ // Gated on `hasParamRootedMerge`, which lives beside the rule it over-approximates. Structural,
239
+ // so it cannot answer differently per symbol-map setting.
240
+ {
241
+ flag: 'freshMerge',
242
+ name: 'fresh-merge',
243
+ options: (on) => ({ freshParamMerge: on }),
244
+ sharedGate: (sharedLift) => hasParamRootedMerge(sharedLift),
245
+ strip: true,
246
+ },
247
+ // `/copy-defpos` — the EDGE-COPY ORDER variation (structure.ts `preferDefPosCopyOrder`). The frontend
248
+ // measures the order each predecessor wrote its successors' keys (ir/core.ts `WriteOrder`) and
249
+ // the default lays the edge's copies out in it. That the compiler laid its copies out in the
250
+ // order it wrote them is only licensed for a CYCLIC copy set, where the spill has to be the
251
+ // register whose old value was displaced first; for an acyclic set it is an assumption, and the
252
+ // benchmark answers it BOTH WAYS INSIDE ONE COMPILER: `synthetic:gcd:mwcc_242_81` matches only
253
+ // with the record, while `memcpy1:mwcc_242_81` (19 → 23) and `memset1:mwcc_242_81` (19 → 21)
254
+ // score worse with it, as does `armfall:agbcc` (8 → 11). A per-compiler boolean cannot decide a
255
+ // question with rows on both sides of it inside one compiler, and a variation that emits one tree
256
+ // referees nothing — so the def-position spelling is enumerated beside the record's and the
257
+ // differ picks, exactly as `/fresh-merge` above does for the merge home.
258
+ //
259
+ // THE VARIATION SPANS THE UNLICENSED HALF ONLY: its alternative keeps the record on cyclic sets,
260
+ // so neither spelling puts a cycle against the instruction that names the compiler's temp. Over the
261
+ // whole benchmark corpus that scoping moves nothing — `memcpy1`, `memset1` and `armfall` keep their
262
+ // `/copy-defpos` winners, so the acyclic half is what they needed — and it makes one spelling
263
+ // reachable that neither whole-function spelling has: the record on a cycle and the proxy on an
264
+ // acyclic edge of the same function, which is what `synthetic:gcd:agbcc`'s two edges want.
265
+ //
266
+ // Gated on the two orders actually differing somewhere in this function, so on a row where the
267
+ // record changes nothing the pair is one tree and the fan does not grow. PER SYMBOL-MAP SETTING,
268
+ // on that setting's own fully-raised fn, because that is `structure()`'s own input and only there
269
+ // is withholding provably inert: same fn, same comparators, so a false answer means the
270
+ // alternative would structure the tree the default already produced. Asked any earlier the claim does not
271
+ // follow, and both ways it fails are real —
272
+ // - THE SYMBOL-MAP SETTING. klonoa's `UpdateHUDCollectibleCount` answers false with the kleod map
273
+ // and true on the `/raw-globals` sibling's own lift (fixture: test/corpus/agbcc-hudcount.s).
274
+ // - THE STAGE. A shared lift stopping after `recoverTypes` is asked before `foldEmptyLatches`
275
+ // (raise/latch.ts) repoints edges and rewrites the record the gate reads: klonoa's
276
+ // `EntityGravityAndFloorCheck` answers false there and true after that fold.
277
+ // Neither costs candidates today — over 192 klonoa functions enumerated with the map the fan is
278
+ // 27,847 either way, 1,970 of them `/copy-defpos`. `strip` like its neighbours: a reordering
279
+ // cannot rescue a spelling whose default sibling failed the boundary contracts.
280
+ {
281
+ flag: 'copyDefPos',
282
+ name: 'copy-defpos',
283
+ options: (on) => ({ preferDefPosCopyOrder: on }),
284
+ perLiftGate: edgeCopyOrdersDiffer,
285
+ strip: true,
286
+ },
287
+ // `/site-sense` — spell a folded short-circuit `if` from the FOLD'S own orientation evidence
288
+ // rather than from the per-function branch-sense boolean (structure.ts senseFromFoldEvidence,
289
+ // raise/shortcircuit.ts `scSharedOnFall` + `scSharedIsTaken` + `scEdgeRelayed`). The two sense
290
+ // booleans are per FUNCTION, so a function whose `if`s were written in opposite senses reaches
291
+ // neither spelling: over `synthetic:mixsense`'s four divergent ladder sites the whole 2^4
292
+ // per-site enumeration scores 10 at the source's own mixed configuration against 20 and 27 for
293
+ // the two the booleans reach,
294
+ // and `synthetic:joinsense` MATCHES at a mix the booleans cannot spell.
295
+ //
296
+ // WHY IT IS A VARIATION AND NOT THE DEFAULT. The reading rests on gcc laying a condition's arms out
297
+ // in SOURCE ORDER, so that a shared block the last test FELL INTO is the source's `then`. That is
298
+ // a claim about one compiler's layout, and the differ is what referees it per row — the long
299
+ // branch is the layout that BREAKS it, gcc inverting the last test and laying the `else` arm
300
+ // first (`synthetic:ifand_far` and `synthetic:ifor_far`, whose scores cannot referee anything:
301
+ // both MATCH on `/flip-join` either way, so their spellings are pinned by
302
+ // test/site-sense.test.ts instead).
303
+ //
304
+ // The premise is only ever consulted where it decides something. The fold also stamps the
305
+ // successor SLOT the shared arm landed in, and a shared arm in the FALL slot leaves the source's
306
+ // `then` in the taken slot whichever way the last test went — so at a CHAINED fold, whose outer
307
+ // `^g` is the head's taken edge, the site is positive and does not ask the premise at all
308
+ // (`synthetic:chainsense`, 4/44 when only the source arm is read, MATCH once the slot is). The THIRD stamp
309
+ // is the long-branch trampoline, the layout where the premise is known to be false: it is the one
310
+ // cell that is GUARDED rather than consulted, and without it the long `||` presents the same two
311
+ // booleans as the short `&&` and is spelled its own dual (`synthetic:ifor_far`, the row added for
312
+ // exactly that cell).
313
+ //
314
+ // THE MAPPING IS STILL NOT A FUNCTION, which is the standing reason this is a variation rather
315
+ // than a default: at `(scSharedOnFall=true, scSharedIsTaken=false)` the corpus holds one function with
316
+ // two sites of OPPOSITE source sense (`kleod:CheckWorldCompletion:agbcc`, 45/191 and unmoved
317
+ // because `/site-sense` is not its winner), so no constant is right there. Read the table in
318
+ // structure.ts as the best per-site default, never as a decision procedure — the enumeration that
319
+ // does not have to pick is `rank.ts`'s per-site `/sense-N` measurement.
320
+ //
321
+ // Gated on this lift's own fully-raised fn carrying a stamped branch at all, for
322
+ // `/copy-defpos`'s reason one entry up: the `/connective` lift variation and the symbol-map
323
+ // settings each change which sites fold, and a gate asked on a different lift would govern a fan
324
+ // it did not measure. A function with no fold structures the identical tree either way and the
325
+ // tree dedup collapses the pair before any compile.
326
+ //
327
+ // NOT in the strip closure, like the two per-function sense variations it refines — which are not in
328
+ // this table at all. `negateCond` is total (l3/ast.ts: a relational opcode swaps, a connective
329
+ // distributes, anything else takes a `!`), so re-spelling a sense can neither throw nor let a
330
+ // candidate structure a function the default declines; there is no failing default sibling for
331
+ // an alternative to ride past.
332
+ {
333
+ flag: 'siteSense',
334
+ name: 'site-sense',
335
+ options: (on) => ({ senseFromFoldEvidence: on }),
336
+ // ALL THREE stamps, which is the predicate the consumer admits a site on (structure.ts's
337
+ // `senseFromFoldEvidence` site default): one contract, not two spellings of it in two files.
338
+ // The fold writes the three in one object literal, so this is the same set of functions today —
339
+ // measured, 0 partially-stamped sites over the 1037 committed rows at both `/connective`
340
+ // settings.
341
+ perLiftGate: (fn) =>
342
+ fn.blocks.some((b) =>
343
+ b.ops.some(
344
+ (op) =>
345
+ typeof op.attrs.scSharedOnFall === 'boolean' &&
346
+ typeof op.attrs.scSharedIsTaken === 'boolean' &&
347
+ typeof op.attrs.scEdgeRelayed === 'boolean',
348
+ ),
349
+ ),
350
+ strip: false,
351
+ },
352
+ ];
353
+
354
+ /** The STACKED variations (sanctioned in the POLICY note at rank.ts's respell site): each entry is
355
+ * a statement-order/shape respell variation orthogonal to every other respell variation, derived
356
+ * onto every source. Each fires alone, plus
357
+ * all of them together in table order — not the full subset lattice; the pairs question is
358
+ * settled by applyStacked' skip-on-decline below, and a row demanding a true EXCLUSION pair —
359
+ * all three fire, the match needs exactly two — is what would earn the lattice. */
360
+ export const STACKED_VARIATIONS: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
361
+ { name: 'initfirst', apply: initFirstGuards },
362
+ { name: 'pollguard', apply: pollGuards },
363
+ { name: 'pollread', apply: pollReads },
364
+ ];
365
+
366
+ /** The PRE-RESPELL variations (sanctioned in the POLICY note at rank.ts's respell site): a tree
367
+ * rewrite applied BEFORE the respell set, so the whole set derives from its output instead of
368
+ * composing onto it. Same record type as STACKED_VARIATIONS above, and deliberately so — the only difference is
369
+ * WHERE it is applied, and that is the whole admission bar.
370
+ *
371
+ * ADMITTED on one ground: the spelling a row demands needs a downstream respell variation to run on this
372
+ * rewrite's OUTPUT, and the measured pair shows neither order alone reaches it. For `/unmerge`
373
+ * (l3/unmerge.ts, the dual of the unconditional `tailmerge`) that measurement is
374
+ * `synthetic:dmascope`: the un-merged store has to land inside the arm's own region base
375
+ * (`p0[2] = …`), which only a base-hoisting variation running AFTER the un-merge can spell — hand-compiled,
376
+ * that source is byte-exact where the merged spelling the structurer produces is 9, and applying
377
+ * the un-merge to the WINNER's tree instead measures 14. Every other respell variation derives
378
+ * from the structured tree it is handed, before any other respell variation, so the order can
379
+ * only be had this way.
380
+ *
381
+ * A pre-respell variation only ADDS candidates, so it cannot cost a match; its price is a second
382
+ * respell set on every tree where the rewrite fires, which is why the table is not a place to put
383
+ * a variation that would compose perfectly well as a `respell`.
384
+ *
385
+ * NOT A SECOND ADMISSION GROUND — the bar above is still WHERE the rewrite is applied, and sign
386
+ * base-dependence discriminates nothing (every variation in this file has a base-dependent price).
387
+ * What follows is why the variation is ENUMERATED at all. `/unmerge`'S SIGN IS A PROPERTY OF THE
388
+ * BASE, NOT OF THE VARIATION. One variation, one row (`kleod:CountCollectedGems:agbcc`), with the
389
+ * variation untouched throughout — two MEASUREMENTS and three GRAFTS, kept apart because a graft is
390
+ * a lower bound with an unreliable sign AND magnitude, which this very variation then demonstrated:
391
+ *
392
+ * MEASURED +44 against the #172-era winner (three routes: the fan's own candidate, a graft,
393
+ * a recompile)
394
+ * MEASURED +24 at #169's base
395
+ * GRAFT +2 at a flat ladder
396
+ * GRAFT −3 at flat + no-copies — #172 published this as the prediction for THIS round;
397
+ * the build delivered −17, an under-read of 5.7×
398
+ * GRAFT +4 at flat + no-copies + connective — the rung a connective round makes real, and
399
+ * the ONLY one where the sign goes back POSITIVE at a base strictly CLOSER to
400
+ * the reference. Quote it with the rest: it says the "−17, among the winner's
401
+ * variations" state below may not survive the next rung.
402
+ * MEASURED −17/352 once #184 and #185 made the flat+no-copies base real
403
+ *
404
+ * Six numbers, two signs. Nothing about the tree changed the variation; what changed was which tree the
405
+ * fan derives from, and the fan is itself choosing that tree. So the mapping from a tree to "is the
406
+ * merged tail cheaper" is not a function of anything a gate here could read, and no predicate this
407
+ * pass could evaluate settles it. That is what enumeration is for. The corollary a future author
408
+ * needs more than the rule: MEASURING A VARIATION AT +44 ON TODAY'S WINNER DOES NOT REFUTE IT.
409
+ * `/unmerge` was the most expensive variation on that row for three rounds and is among its
410
+ * winner's variations now, with no change to the variation at all.
411
+ *
412
+ * WHAT ENUMERATION BUYS AND WHAT IT DOES NOT. It buys the whole-function delta above being honest
413
+ * — the differ picks, so the number cannot be WRONG. It buys no REACH: `unmergeJoins` rewrites
414
+ * every site clearing its gates and returns one tree, so the fan carries TWO trees, all-merged
415
+ * and all-un-merged, never 2^k, and a function wanting site A un-merged and site B merged has no
416
+ * candidate. Measured, not assumed — `unmergeAt`'s SUCCESSFUL returns counted per call under an
417
+ * instrumented `unmergeJoins` (`bench gates --pass unmerge` tallies each rule's REFUSALS and never
418
+ * a success), over the synthetic agbcc tier: k = 1 on 23 of the 25 firing rows and on
419
+ * `kleod:CountCollectedGems:agbcc` itself, k = 2 on `synthetic:joinsame` and
420
+ * `synthetic:joinsense` — both MATCH, so the refusal costs nothing today. State it as PR #120
421
+ * states its own: a price, never an immunity.
422
+ *
423
+ * WHAT BREAKS IF THIS TABLE IS EMPTIED, so an author editing here is not measuring it again:
424
+ * twelve MATCH rows fall, `sa3:numToASCII:agbcc` (REAL-TIER) among them, plus
425
+ * `synthetic:dmascope`, `synthetic:dmascope2`, `synthetic:joinsame`, `synthetic:joinsense`,
426
+ * `synthetic:armcb`, `synthetic:armcb2`, `synthetic:ladder4`, `synthetic:ladder5`,
427
+ * `synthetic:ladidx1`, `synthetic:ladidx2`, `synthetic:revlad5s`. `bench regression` is an
428
+ * OUTCOME gate over all tiers, so every one of them turns it red — but `benchmark.yml` is
429
+ * `workflow_dispatch`, manual, with no cron, so nothing runs that gate on a PR. The scores and the
430
+ * method are in `apps/benchmark/dataset/synthetic.ts`'s `/unmerge` block. */
431
+ export const PRE_RESPELL_VARIATIONS: typeof STACKED_VARIATIONS = [{ name: 'unmerge', apply: unmergeJoins }];
432
+
433
+ export const STACKED_SUBSETS: (typeof STACKED_VARIATIONS)[number][][] = [
434
+ ...STACKED_VARIATIONS.map((x) => [x]),
435
+ ...(STACKED_VARIATIONS.length > 1 ? [STACKED_VARIATIONS] : []),
436
+ ];
437
+
438
+ /** The subset applied in table order, SKIP-ON-DECLINE: a member that declines contributes
439
+ * nothing rather than killing the combination — the all-shapes candidate is "everything that
440
+ * fires", so a pair is reachable whenever the third declines. The variations are the members that
441
+ * actually FIRED, so they never name a variation that declined; a fired-set that
442
+ * duplicates a smaller subset emits identical source and the dedup collapses it. Null when
443
+ * nothing fired. */
444
+ export const applyStacked = (
445
+ subset: readonly (typeof STACKED_VARIATIONS)[number][],
446
+ from: SFn,
447
+ ): { out: SFn; variations: VariationName[] } | null => {
448
+ let cur = from;
449
+ const fired: VariationName[] = [];
450
+ for (const sp of subset) {
451
+ const r = sp.apply(cur);
452
+ if (r) {
453
+ cur = r;
454
+ fired.push(sp.name);
455
+ }
456
+ }
457
+ return fired.length > 0 ? { out: cur, variations: fired } : null;
458
+ };
459
+
460
+ /** The locals a variation added — a NAME diff rather than a positional slice, so a pass that ever
461
+ * reorders locals cannot silently empty the set. It is what scopes `/volatile` to the pointers
462
+ * the variation itself created (volatilePtrLocals' `only`), leaving the tree's own locals alone. */
463
+ export const createdLocals = (from: SFn, to: SFn): Set<string> => {
464
+ const before = new Set(from.locals.map((l) => l.name));
465
+ return new Set(to.locals.filter((l) => !before.has(l.name)).map((l) => l.name));
466
+ };
467
+
468
+ /** The base-CSE HOISTS `/livebase` offers the differ, widest first. WHICH of several numeric
469
+ * bases the source named is per-base knowledge the asm does not carry — a DMA register file wants
470
+ * one register held across the whole body while the IWRAM halfword beside it re-materializes — so
471
+ * each hoist rides as its own candidate and the differ referees between them. A new
472
+ * hoist is one entry here, one gate table, and that table's line in the gate-contract
473
+ * roster — not nine hand-edited sites that can drift; whether it also joins the `/livebase`
474
+ * PAIRINGS is the entry's own `pairings`. A MIRROR hoist (bind the scalar cells, leave
475
+ * the register file inline) is that, with the complementary predicate; it is never another entry
476
+ * in LIVEBASE_BLOCK_GATES, which can only reject more.
477
+ *
478
+ * WHAT BOUNDS IT. A hoist declines unless it binds a non-empty set of bases no earlier hoist
479
+ * already bound, and each composition declines wherever its own variation does, so the list widens
480
+ * only where an inhabitant exists — over the corpus the second hoist reaches 8 rows, its
481
+ * `/nearbase` pairing 3, and its `/indexed` and `/coalesce` pairings and its volatile subsets none
482
+ * at all. A function
483
+ * inhabiting them all pays far more, and the fan is not always a win there: the mixpoll dataset
484
+ * entry prices one where the `/coalesce` pairing costs the most candidates of any and scores two
485
+ * points worse than going unpaired. THAT row fans anyway because on the `/livebase` hoists a
486
+ * pairing belongs to the VARIATION rather than to one of its hoists — but it is a per-hoist
487
+ * decision, not a property of the roster: three of the five hoists below are unpaired. `pairings`
488
+ * is the field, and its own doc says how a hoist earns a `true`.
489
+ *
490
+ * `/basefold` is the third and fourth hoist and `/unfolded` the fifth; those three are the
491
+ * conditional set — their registry entries' target gate offers them only where the target declares
492
+ * `compilerBehaviors.foldsConstAddrOffset`. They need no second "did the default already carry
493
+ * this" test: `structureChecked` runs the DEFAULT hoist to its fixpoint before any tree reaches
494
+ * here, so a key still admissible is by construction one `BASECSE_GATES` rejected, and binding
495
+ * nothing is the whole of the decline.
496
+ * WHAT THE EXEMPTION REACHES, over the agbcc rows the artifact carried when the census ran and in
497
+ * BOTH symbol-map configurations — 451 observations, of which 39 do not lift on this one-tree
498
+ * census. The 451 is the census's OWN denominator, quoted so the "0 of 451" below has one; the
499
+ * corpus row count it came from is deliberately not, because that number has moved since.
500
+ * HOW TO REPRODUCE IT: the prototypes live inside `row.scripts.asmlift`'s `PROTO_INPUT`
501
+ * heredoc, and there is no `row.proto` field — a census reaching for one lifts all 451 with
502
+ * `prototypes: {}` while the harness scores every one of them with `--proto proto.json`, and
503
+ * says nothing about it. Numbers below are from the heredoc.
504
+ * 20 observations bind a key the default table refuses, spread over 14 rows in 4 projects (6
505
+ * map-ful, 14 map-less), 25 keys in all. FOUR are numeric — two on `kleod:RollRandomLevelVariant`
506
+ * and one each on `synthetic:basecell` and `synthetic:foldsink`, all map-less, because with a map
507
+ * the pool constant lifts to a `gaddr` and the numeric clause stands down while the symbol clause
508
+ * takes over. The other 21 are SYMBOL keys over 11 rows in three projects (6 of those
509
+ * observations map-ful, 11 map-less), and all 21 are what the symbol half added: on the
510
+ * value-proxy predicate this replaced, the same census binds the 4 numeric keys and nothing else,
511
+ * losing none of them. `admittedBases(sfn, BASECSE_GATES)` — the COMMITTED table — differs on 0
512
+ * of 451, which is the check that says the widening stayed on the roster.
513
+ * A target that declares no fold is offered none of the three — not to protect a score (no roster
514
+ * row can cost one; see LIVEBASE_BLOCK_GATES) but because `unfoldedOffset` would be read as
515
+ * evidence on an instruction that carries the addend by construction, where there is none.
516
+ * On klonoa's `LoadBGTilemapData` — a checkout function rather than a row, so re-run it with the
517
+ * ranked command in docs/ranked-repro.md — the `/basefold` admission declines on every
518
+ * structuring, leaving that fan the size it was, with ZERO candidates carrying `basefold` in the
519
+ * control run. NO FAN TOTAL IS QUOTED HERE ON PURPOSE: that function's fan was 112896 at this
520
+ * commit and two five-figure numbers ago at others, and a DELTA outlives the total it was
521
+ * measured beside — which is what makes a stale paragraph read as verified. Re-run the total
522
+ * before budgeting against it. All floors, though: the ranked path structures each function many ways
523
+ * where this census builds one tree per observation.
524
+ *
525
+ * WHAT THE PAIR COSTS, through the HARNESS's own enumeration and re-runnable from the recipe in
526
+ * the BASEFOLD_HOISTS note below: enumerate every agbcc row with the pair on and off,
527
+ * `ASMLIFT_CANDCACHE=0`, candidates only. The pair adds 3921
528
+ * distinct candidate sources over 14 observations — 3911 over 12 real rows and 10 over 2
529
+ * synthetic ones (`foldsink` 4 → 12, `basecell` 2 → 4) — and every per-row delta equals that
530
+ * row's count of candidates carrying `basefold` exactly, which is both what says the ablation
531
+ * reached and what says these are sources nothing earlier in the roster emits.
532
+ * It is CONCENTRATED, not spread: in the map configuration the harness uses on real rows,
533
+ * `kleod:ProcessInputAndUpdateEntities` takes +2880 (14976 → 17856),
534
+ * `kleod:UpdateCameraScroll` +512 (5968 → 6480), `kleod:CountCollectedGems` +192 (384 → 576),
535
+ * `kleod:UpdateWorldMapNodeAnim` +176 (488 → 664) and nothing else more than 32. Re-run a
536
+ * concentration figure before budgeting against it: a DELTA can reproduce while the fan it was
537
+ * quoted against has moved, and that is what makes a stale paragraph read as verified.
538
+ * `kleod:UpdateCameraScroll` is an `outcome: noncompile` row — `decompileRanked` throws only when
539
+ * EVERY candidate failed to build — so its whole fan is compiled and discarded, and this made
540
+ * that discard 10% bigger. Timed on two full bench runs on a shared box, and not re-timed since
541
+ * the deltas above, so read them as a floor rather than a price: that row 377.6s → 483.0s, the
542
+ * second 238.4s → 313.6s, real tier 416.1s → 529.4s. Priced — and the three rows the pair was
543
+ * bought with DO NOT BUY IT TODAY: ablated, `sa3:sub_803213C` is MATCH with the pair removed,
544
+ * `kleod:ProcessInputAndUpdateEntities` 211 either way and `kleod:CountCollectedGems` 290 either
545
+ * way. A SCORE QUOTED HERE IS THE ARTIFACT'S: it moves whenever anything at all moves the row,
546
+ * a basefold change or not, so re-read it off the artifact rather than off this line. Read the
547
+ * ablation in the note on BASEFOLD_HOISTS, which carries the fan counts that prove it
548
+ * reached. */
549
+ export interface BaseHoist {
550
+ /** the variations this hoist's candidates carry, in name order */
551
+ variations: readonly VariationName[];
552
+ gates: readonly Gate<BaseKey>[];
553
+ /** WHERE the locals this hoist binds are initialized (l3/hoist.ts). Eligibility and placement are
554
+ * two questions and this roster answers both, so a hoist can offer the same bases in the other
555
+ * position without a second gate table — and a variation that wants both offers both, as the
556
+ * `/basefold` pair below does. */
557
+ placement: HoistPlacement;
558
+ /** Whether the hoist joins the `/livebase ×` PAIRINGS in rank.ts. Each of those pairings was added
559
+ * for a row that demanded the joint spelling (see POLICY), and every demanding row so far is served
560
+ * by a `/livebase` hoist — so a new hoist joins them when a row demands it, not by roster
561
+ * membership.
562
+ *
563
+ * ONE BOOLEAN PER HOIST, ANSWERING A QUESTION THAT IS REALLY PER FUNCTION, so a `false`
564
+ * here is a corpus claim and has to be measured like one — on the whole corpus, not on the
565
+ * synthetic row that earned the entry. The measurement is candidates-only and cheap: enumerate
566
+ * every agbcc row twice from `row.scripts.asmlift`'s heredocs and compare the distinct-source
567
+ * sets. For `/unfolded` (see its note) that is +912 sources over 8 rows (+1.92% corpus fan) and
568
+ * the only nonmatch among the 8 scores the same either way, which is what the `false` rests on.
569
+ * Flipping one of these is one character; the gate on doing it is that census plus a score on
570
+ * every row it moves. */
571
+ pairings: boolean;
572
+ }
573
+
574
+ export const LIVEBASE_HOISTS: readonly BaseHoist[] = [
575
+ { variations: ['livebase'], gates: LIVEBASE_GATES, placement: 'head', pairings: true },
576
+ { variations: ['livebase-block'], gates: LIVEBASE_BLOCK_GATES, placement: 'head', pairings: true },
577
+ ];
578
+
579
+ /** Narrower than either `/livebase` hoist, so both go last: they keep both placement heuristics and
580
+ * exempt only `single-use`, and only for a base whose offset survived the compiler's fold.
581
+ *
582
+ * They are ONE eligibility rule at the two placements, because for a base reached ONCE the
583
+ * question the differ has to settle is where the pool load sits, not whether the local exists:
584
+ * the head keeps the address live over everything above the access, the first-use position is
585
+ * where a single access loaded it. Which one the source wrote is per-function knowledge the asm
586
+ * does not carry, so both ride and the differ referees.
587
+ *
588
+ * WHAT EACH HOIST IS WORTH, ablated through the harness rather than read off the winners'
589
+ * variations, because a winner a hoist names can be a TIE another hoist also reaches. THE HEAD
590
+ * HOIST IS BRACKETED AND THE SUNK ONE IS NOT. `synthetic:foldhead` is MATCH at 0 under
591
+ * `unsigned/basefold` and becomes NONMATCH 11 under `unsigned` the moment the HEAD entry is
592
+ * removed — and removing BOTH entries gives the same 11, so the sunk entry is what nothing here
593
+ * brackets. `synthetic:foldsink` and `synthetic:basecell` are unbracketed for a reason worth
594
+ * keeping: they are MATCH at 0 in every configuration because `/offmember` ALSO reaches 0 on
595
+ * them and wins `compareScored`'s line-count tie-break. A TIE IS NOT A SUBSUMPTION — that is
596
+ * why a census over winners' variations reads zero here, and reading that zero as "loses" would
597
+ * delete a pair that no other spelling reaches.
598
+ * (Their fans still move: `foldsink` 12 → 8 → 8 → 4 over control/sunk/head/both, `basecell`
599
+ * 4 → 4 → 4 → 2, the four-number sequence saying that on `basecell` the two entries emit the
600
+ * SAME two sources and `seen` collapses them, so only removing both takes the fan down.)
601
+ * `sa3:sub_803213C` MATCH, and — with the pair removed — `kleod:ProcessInputAndUpdateEntities`
602
+ * 211, `kleod:CountCollectedGems` 290 and `kleod:RollRandomLevelVariant` 18, each of them the
603
+ * number the artifact carried then (all three retired 2026-09-13), and each of them ENTAILED
604
+ * rather than separately scored:
605
+ * the ablated candidate set is a strict SUBSET of the control one on every row here (enumerated
606
+ * both ways, 0 sources ADDED and 0 RENAMED — `ProcessInputAndUpdateEntities` 58752 → 48384
607
+ * with 10368 carrying `basefold`, `CountCollectedGems` 576 → 384 with 192, `RollRandomLevelVariant`
608
+ * 29 → 11 with 18, `sub_803213C` 36 → 20 with 16), and no winner carries `basefold`, so the
609
+ * minimum cannot move. A BRACKET IS A CLAIM ABOUT THE WHOLE TREE, so it expires
610
+ * whenever anything else learns to reach the same spelling more cheaply: re-run one before
611
+ * re-quoting it, including the number that survived the last re-run.
612
+ * The SUNK entry is kept ONLY because it is a real spelling: 3921 distinct candidate sources over
613
+ * 14 observations that nothing else emits (see WHAT THE PAIR COSTS for the per-row split), and
614
+ * a C source that initializes its base pointers where it declares them is the ordinary case.
615
+ * That is a weaker justification than a protected row and should be read as one — a round pricing
616
+ * the agbcc fan may delete it, and the gate on doing so is `bench diff`, not this note. The HEAD
617
+ * entry is NOT in that category: deleting it costs `synthetic:foldhead` its match, which
618
+ * `bench regression` fails on.
619
+ * HOW THE ABLATION IS DONE, since there is no shipped knob: filter this roster at its one use
620
+ * site (the `hoists` const in `enumerateCandidates`) behind a temporary env read, run the
621
+ * rows with `ASMLIFT_CANDCACHE=0`, and revert. Prove the filter REACHED before believing a null
622
+ * result — `synthetic:livepark` MATCH → diff:3 with `/livebase` AND `/unfolded` both removed is
623
+ * the positive control, and a fan count per configuration is the second. Removing `/livebase`
624
+ * alone leaves that row MATCH today, which is a control silently going vacuous rather than a
625
+ * variation going dead: `/unfolded` binds the same base there. Any positive control naming ONE
626
+ * hoist expires the next time a hoist is added — re-run it, and if it no longer moves, widen the
627
+ * ablation until it does before concluding anything from a null. */
628
+ export const BASEFOLD_HOISTS: readonly BaseHoist[] = [
629
+ { variations: ['basefold'], gates: BASEFOLD_GATES, placement: 'head', pairings: false },
630
+ { variations: ['basefold', 'sinkinit'], gates: BASEFOLD_GATES, placement: 'first-use', pairings: false },
631
+ ];
632
+
633
+ /** The fifth hoist: its table requires the fold evidence (l3/basecse.ts, UNFOLDED_GATES), so
634
+ * it binds the reused bases an operand offset says a pointer local strode and leaves the ones the
635
+ * pool already carried folded. `/livebase` and `/livebase-block` are a chain — all the reused
636
+ * bases, or those minus the scalar cells — and a source that parked one numeric base and spelled
637
+ * another inline is at neither end of it. This hoist is not a third link in that chain but beside
638
+ * it: `singleCell` and `unfoldedOffset` are independent fields, so each of the two tables binds
639
+ * keys the other refuses (censused, with its scope, in UNFOLDED_GATES' own note). Read the roster
640
+ * as hand-picked subsets, never as a narrowness ranking.
641
+ *
642
+ * LAST on the roster, so `seen` and `sameBases` between them keep it from restating an earlier
643
+ * HOIST — but only `seen` does any work here. `sameBases` declines a hoist that binds what an
644
+ * EARLIER hoist binds AT THE SAME PLACEMENT, and the only earlier `first-use` hoist is
645
+ * `/basefold/sinkinit`, whose table keeps the two gates this one ablates; instrumented over every
646
+ * agbcc row it fires on 0 of 5541 roster observations for this entry (33 distinct functions),
647
+ * against 3939 for `/livebase-block`. So the shadow is available and vacuous, and what keeps this
648
+ * hoist from restating anything is `seen` — WHICH MAKES IT A RENAMER, and it renames: the roster
649
+ * loop runs before the `/livebase ×` pairing loops, so a source one of those pairings would
650
+ * emit later is claimed under this hoist's variations instead. `synthetic:foldpark` is that case measured —
651
+ * fan 34 with this entry and 34 without, the same source winning at 0 under
652
+ * `signed/unfolded/volatile` here and `signed/livebase-block/volatile/sinkinit` there.
653
+ * Corpus-wide (map-less, candidates only, over the artifact's agbcc rows as they stood) 21 of the 333 rows
654
+ * whose distinct-source set is byte-identical either way carry candidates with `/unfolded`:
655
+ * 21 pure renames against 7 rows that really gain sources, and 0 that lose one. What that costs
656
+ * any census taken over candidates' variations is at the `seen` dedup site below.
657
+ *
658
+ * ONE placement, unlike the `/basefold` pair, and by measurement rather than by symmetry. All
659
+ * four configurations scored on `synthetic:unfoldpark`, cache off — the fan, then that fan's best
660
+ * score:
661
+ * first-use, unpaired 44 0 MATCH — shipped
662
+ * first-use, paired 44 0 no paired candidate emits a source the unpaired row does not
663
+ * head, unpaired 44 9 the score the row already had without any of this
664
+ * head, paired 48 0 reached only through the `/sinkinit` pairing
665
+ * The head is where `/livebase` already offers a spelling for every base this table can bind —
666
+ * these are bases reached 2+ times — so what the hoist adds is the SUNK init, which is where a
667
+ * source that declares its base pointer beside the loop it feeds puts the pool load. On both
668
+ * neighbouring rows the head placement is shadowed outright (`/unfolded` binds set-for-set what
669
+ * `/livebase` binds on `synthetic:livepark` and what `/livebase-block` binds on
670
+ * `synthetic:foldpark`). A second hoist at the head is one line and no new table; add it when a row
671
+ * demands it, which none does today.
672
+ *
673
+ * `pairings: false` for the reason the field's own doc gives — a pairing is added for a row that
674
+ * demands the joint spelling, and the row that earned this entry does not: paired and unpaired
675
+ * are the same 44 candidates above. ONE 15-LINE FUNCTION CANNOT SETTLE A CORPUS QUESTION, so the
676
+ * same knob was censused over every agbcc row the artifact carries, candidates only: `true` adds
677
+ * 912 distinct sources over 8 rows, +1.92% of the agbcc corpus fan (quoted as the DELTA,
678
+ * because the total moves with the corpus and with the roster) — `kleod:UpdateCameraScroll`
679
+ * +608, `synthetic:sizebound` +128, `synthetic:dmascope` +64, `kleod:SetupBG3WindowOverlay` and
680
+ * `synthetic:maskhome` +32 each, and +16 each on `dmafield`, `dmaflat` and `dmapoll`. Five of the
681
+ * eight are MATCH and two are `noncompile`, where extra candidates cannot help; the one that
682
+ * could, `synthetic:sizebound`, scores diff:8 with the pairings on and diff:8 with them off. So
683
+ * the `false` buys 1.92% of the agbcc fan for a measured zero, on the whole corpus rather than on
684
+ * the row that earned the entry. Flip it when a row scores better with it, and re-run that
685
+ * census when one does. */
686
+ export const UNFOLDED_HOISTS: readonly BaseHoist[] = [
687
+ { variations: ['unfolded'], gates: UNFOLDED_GATES, placement: 'first-use', pairings: false },
688
+ ];
689
+
690
+ /** The sixth hoist, and the only one whose evidence is the INSTRUCTION ORDER rather than the
691
+ * shape of the accesses (l3/basecse.ts, ORDERBASE_GATES). It binds a base the assembly says was
692
+ * materialized before the index was scaled — including the `(struct S *)&gSym` of an
693
+ * array-of-struct element, which no other table on this roster can even see.
694
+ *
695
+ * LAST, so `sameBases` can shadow it and it can shadow nothing: on a function whose licensed base
696
+ * is a plain leaf reached twice, `/livebase` already binds exactly that set at this placement and
697
+ * this hoist declines rather than restating it under different variations.
698
+ *
699
+ * TWO PLACEMENTS, and the FLAT second one is a measured zero. `synthetic:bgarr` emits the identical
700
+ * source at `head` and `first-use` (the hoist has nothing to sit above) and that one row
701
+ * generalizes to nothing: over the artifact's agbcc rows the two emit DIFFERENT source on 3 of the
702
+ * 8 rows this hoist binds map-less and 4 of the 10 map-ful — `kleod:SetupBG3WindowOverlay`,
703
+ * `kleod:UpdateCameraScroll`, `pokeemerald:TrySetCantSelectMoveBattleScript`, and map-ful
704
+ * `kleod:StreamCmd_SetBGScroll`. Run through the harness on all four, an entry at
705
+ * `placement: 'first-use'` scores nothing: 146 → 146, noncompile → noncompile, MATCH → MATCH,
706
+ * noncompile → noncompile, against +1129 candidates over those rows' 15167 (+7.4%) and
707
+ * `kleod:UpdateCameraScroll` 224 s → 278 s. That hoist stays withheld.
708
+ *
709
+ * `scope` is a DIFFERENT question and a row demanded it. `first-use` reaches only the top-level
710
+ * statement list, so on a function whose licensed base is used solely inside a guarded loop it
711
+ * spells the same bytes `head` does — the pool word above the branch — while the reference loads
712
+ * it after. Compiled through the benchmark's own agbcc on `synthetic:ereadctl`'s target, with
713
+ * everything else held identical: the init above the `if` differs, the same init INSIDE the arm is
714
+ * instruction-identical. So the two flat placements are one answer here and this is the other, the
715
+ * way `/basefold`'s pair is one eligibility rule at two positions.
716
+ *
717
+ * AND THE WITHHOLDING ABOVE IS ENFORCED BY THE PLACEMENT, not by this row's absence. `scope`
718
+ * reproduces `first-use` on every function where no nested list holds all of a base's uses, so a
719
+ * scoped hoist that answered there would ship the withheld candidate under this hoist's variations — and it
720
+ * is the COMMON case, not the corner: over each project's whole `asm` tree, map-ful, of the 48
721
+ * functions this gate table admits, 41 place every init in the top-level list and only 7 reach a
722
+ * nested one. `hoistBaseLocals` DECLINES at `scope` in exactly that case (l3/basecse.ts) — a
723
+ * WITHDRAWAL and not a dedup, because on 29 of the 41 the flat spelling is one no other row here
724
+ * produces, which that file's header prices. Measured on
725
+ * `kleod:UpdateCameraScroll` map-ful, the row that priced the withheld one: 512 of its 512
726
+ * `/orderbase-scoped` sources placed the init at the top level, and all 512 are gone.
727
+ *
728
+ * `pairings: false` on both for the field's own reason — a pairing is added for a row that demands
729
+ * the joint spelling, and neither row here demands one. */
730
+ export const ORDERBASE_HOISTS: readonly BaseHoist[] = [
731
+ { variations: ['orderbase'], gates: ORDERBASE_GATES, placement: 'head', pairings: false },
732
+ { variations: ['orderbase-scoped'], gates: ORDERBASE_GATES, placement: 'scope', pairings: false },
733
+ ];
734
+
735
+ export const sameBases = (a: readonly string[], b: readonly string[]): boolean =>
736
+ a.length === b.length && a.every((k, i) => k === b[i]);
737
+
738
+ /** The signedness of the entry parameters — the classic ambiguity asm cannot resolve.
739
+ *
740
+ * Struct LAYOUT is recovered structurally (raise/structs.ts) rather than enumerated here, and the
741
+ * reason is REACH, not neutrality. This file used to say `->field_N` and `[idx]` "compile
742
+ * identically, so the differ cannot referee between them"; the second clause is FALSE on agbcc and
743
+ * `synthetic:dmanest` is the counterexample — the same element read scores 0 as
744
+ * `((struct Elem0 *)K)[a1].field_4` and 2 as `((s32 *)((a1 << 3) + K))[1]`, because an index folds
745
+ * the field offset into the pool literal (tree reassociation) where a COMPONENT_REF leaves it in
746
+ * the load displacement. `synthetic:dmaptrsrc` is a second counterexample on the field's TYPE.
747
+ *
748
+ * What is true is that no candidate is enumerated for the question, and none is NEEDED: the recovery
749
+ * reads the base from the observed pool word and the field offset from the observed load
750
+ * displacement, so it reproduces the target's own split by construction. The measurements and the
751
+ * conditions are in `raise/structs.ts`; nothing about them belongs in a roster comment. */
752
+ export const SIGNEDNESS: readonly { variation: VariationName; signed: boolean }[] = [
753
+ { variation: 'unsigned', signed: false },
754
+ { variation: 'signed', signed: true },
755
+ ];
756
+
757
+ // A recovered POINTER/aggregate param must NOT be signedness-pinned: pinning a still-`unknown`
758
+ // pointer param to a scalar int BEFORE recovery blocks pointer recovery and emits uncompilable
759
+ // `*(s32)`. Only genuine scalars carry the signedness variation.
760
+ export const NO_PIN_KINDS = new Set(['ptr', 'struct', 'array']);