@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
package/src/rank.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  // from asm alone (is this value signed or unsigned? which branch sense did the source spell?).
3
3
  // Rather than guess, asmlift emits a small set of CANDIDATES and lets an external differ score
4
4
  // pick the winner — the differ is the fitness function; types/branch-sense are differ-ranked
5
- // levers, not asserted truths.
5
+ // variations, not asserted truths.
6
6
  //
7
- // This module owns only the PURE half: producing the distinct candidate spellings. It has NO
7
+ // This module owns only the PURE half: producing the distinct candidates. It has NO
8
8
  // scorer (that stays out of @asmlift/core, which is browser-pure). `rankBy` takes an INJECTED
9
9
  // scoreFn, so the same enumeration feeds the cli's Node/objdiff scorer and the webapp's
10
10
  // wasm/objdiff scorer alike.
@@ -12,21 +12,29 @@
12
12
  // THREE SIBLING FILES, one job each — never a `rank/` directory, which beside `rank.ts` is a
13
13
  // resolver trap:
14
14
  // rank.ts this file: the enumeration DRIVER and the two ranking drivers over it.
15
- // rank-axes.ts the TABLES the driver walks — structuring axes, shape/pre-fan products, the
16
- // base-CSE admission rosters. Their DECLARATION ORDER is published behaviour
15
+ // rank-variations.ts the TABLES the driver walks — structure variations, stacked and pre-respell
16
+ // variations, the base-CSE hoist rosters. Their DECLARATION ORDER is published behaviour
17
17
  // (`compareScored` breaks a score tie by enumeration order), so reordering one
18
18
  // is a behaviour change and never a tidy-up.
19
19
  // rank-declare.ts the DECLARATION half: what a candidate's own asm says about the globals it
20
20
  // names, and which of those names a declaration must refuse to claim.
21
21
  import { cBackend } from './backend/c';
22
- import { assertDerefsTyped, assertLocalsWritten, assertPlacementSurvives, assertResolved } from './contracts';
22
+ import {
23
+ assertDerefsTyped,
24
+ assertLocalsWritten,
25
+ assertNoOrphanedLocals,
26
+ assertPlacementSurvives,
27
+ assertResolved,
28
+ } from './contracts';
23
29
  import type { AsmData } from './frontend/asmdata';
24
30
  import { frontendFor } from './frontend/registry';
25
31
  import { hasSetupArgsNarrowing, narrowToSetupArgs } from './frontend/ssa';
26
32
  import { Fn, defOpMap } from './ir/core';
27
33
  import { T } from './ir/types';
28
34
  import { verify } from './ir/verify';
35
+ import { advancedBases } from './l3/advance';
29
36
  import { materializeArgBases } from './l3/argbase';
37
+ import { argCopyCandidates } from './l3/argcopy';
30
38
  import type { LanguageBackend, SFn } from './l3/ast';
31
39
  import { type BaseKey, admittedBases, hoistBaseLocals } from './l3/basecse';
32
40
  import { armDisjointCandidates, coalesceCandidates } from './l3/coalesce';
@@ -55,39 +63,49 @@ import { type Prototypes, prototypesFromSymbols } from './proto';
55
63
  import { inferGlobalArrays, orderLicensedGlobals, sameDerivedShape } from './raise/globalshape';
56
64
  import { runPreRecovery } from './raise/pre-recovery';
57
65
  import { recoverTypes } from './raise/recover';
58
- import {
59
- BASEFOLD_ADMISSIONS,
60
- type BaseAdmission,
61
- LIVEBASE_ADMISSIONS,
62
- NO_PIN_KINDS,
63
- ORDERBASE_ADMISSIONS,
64
- PRE_FAN_PRODUCTS,
65
- SHAPE_SUBSETS,
66
- SIGN_CANDS,
67
- STRUCTURING_AXES,
68
- type StructuringAxis,
69
- UNFOLDED_ADMISSIONS,
70
- applyShapes,
71
- createdLocals,
72
- sameBases,
73
- } from './rank-axes';
66
+ import { sinkStoreTails } from './raise/tailsink';
74
67
  import {
75
68
  type RefusedDeclarationReason,
76
69
  bareGlobalAccessFacts,
77
70
  bareGlobalSymbols,
78
71
  makeRefCollector,
79
72
  } from './rank-declare';
80
- import { type SymbolInfo, type SymbolMap, arrayInnerExtents, isPtrField, symbolsByName } from './symbols';
73
+ import {
74
+ BASEFOLD_HOISTS,
75
+ type BaseHoist,
76
+ LIVEBASE_HOISTS,
77
+ NO_PIN_KINDS,
78
+ ORDERBASE_HOISTS,
79
+ PRE_RESPELL_VARIATIONS,
80
+ SIGNEDNESS,
81
+ STACKED_SUBSETS,
82
+ STRUCTURE_VARIATIONS,
83
+ type StructureVariation,
84
+ UNFOLDED_HOISTS,
85
+ applyStacked,
86
+ createdLocals,
87
+ sameBases,
88
+ } from './rank-variations';
89
+ import { hasDivergentSharedRet } from './structure/structure';
90
+ import {
91
+ type SymbolInfo,
92
+ type SymbolMap,
93
+ arrayInnerExtents,
94
+ declaresBitfields,
95
+ isPtrField,
96
+ symbolsByName,
97
+ } from './symbols';
81
98
  import { type TargetDescription, structureOptionsFor } from './target';
99
+ import { type SubjectVariationName, type Variation, offeredOn, withSubject } from './variation-tokens';
82
100
 
83
101
  /** Pin every SCALAR entry param (index not in `ptrIdx`) to the candidate signedness, before
84
102
  * recovery. Answers whether any param was PINNABLE — not whether its type moved: which of the
85
- * two passes writes first is an accident of enumeration order, and the arms differ exactly where
86
- * a param can be written at all.
103
+ * two passes writes first is an accident of enumeration order, and the two signedness candidates
104
+ * differ exactly where a param can be written at all.
87
105
  *
88
106
  * A param NARROWED by raise/paramwidth.ts is not pinnable: the extension it was narrowed at states
89
107
  * the signedness as well as the width — agbcc's shift pair by its `asr`/`lsr`, PPC's `extsb`/`extsh`
90
- * by the opcode — so there is no question for the axis to put to the differ, and pinning would
108
+ * by the opcode — so there is no question for the signedness variation to put to the differ, and pinning would
91
109
  * widen it back to 32 bits. */
92
110
  function pinScalarParams(fn: Fn, signed: boolean, ptrIdx: Set<number>): boolean {
93
111
  let pinnable = false;
@@ -115,21 +133,29 @@ export interface EnumerateOptions {
115
133
  asmData?: AsmData;
116
134
  /** address→symbol map (symbols.ts) — same contract as DecompileOptions.symbols */
117
135
  symbols?: SymbolMap;
118
- /** Called when a re-spelling lever THROWS or fails a boundary contract, so the failure is visible
119
- * instead of the candidate silently not existing. Enumeration continues either way — the primary
120
- * spelling is unaffected — but a lever that never fires because it always throws is a defect, and
121
- * without this it looks identical to a lever that correctly declined. */
122
- onLeverError?: (label: string, error: string) => void;
136
+ /** Called when a respell variation THROWS or fails a boundary contract, so the failure is visible
137
+ * instead of the candidate silently not existing. Enumeration continues either way — the default
138
+ * candidate is unaffected — but a variation that never fires because it always throws is a defect,
139
+ * and without this it looks identical to a variation that correctly declined.
140
+ *
141
+ * `variations` is NOT a candidate's name: it lists what the throwing step itself applied, in
142
+ * name order. A lift or structure setting reports its lift and structure variations
143
+ * (`['defsite']`). A respell or pre-respell step reports only its pre-respell and respell
144
+ * variations (`['unmerge', 'vol-slot']`), never the lift, structure, signedness or symbol-map
145
+ * variations of the tree it ran on. So one failure is reported once per signedness, symbol-map
146
+ * setting and structured tree it recurs on, each time with the same list, and `[]` means a
147
+ * structured tree's own source could not be spelled. */
148
+ onEnumerationError?: (variations: readonly string[], error: string) => void;
123
149
  /** Called once per (name, reason) when the declaration synthesis REFUSES a name the tree
124
150
  * references (see `RefusedDeclarationReason`). The name then stays undeclared and the
125
151
  * candidate fails loudly in a self-declared world — this is what lets the consumer say which
126
152
  * undeclared name was asmlift's own refusal rather than a symbol it never saw. */
127
153
  onRefusedDeclaration?: (name: string, reason: RefusedDeclarationReason) => void;
128
- /** Called with the axis suffix each time a STRUCTURING axis's shared probe gate says this
129
- * function has no inhabitant for it, so the arm is never enumerated.
154
+ /** Called with the variation's name (`defsite`) each time a STRUCTURE variation's shared gate says this
155
+ * function has no inhabitant for it, so the alternative is never enumerated.
130
156
  *
131
157
  * The two callbacks below report the enumeration's two SILENT candidate-deleting sites, and
132
- * they exist for `onLeverError`'s reason read one level up: a candidate that was never
158
+ * they exist for `onEnumerationError`'s reason read one level up: a candidate that was never
133
159
  * enumerated is indistinguishable, from outside, from one the differ simply did not pick, and
134
160
  * nothing else in the pipeline reports it. A gate that has stopped firing and a gate that
135
161
  * correctly declines on every corpus row look identical without this.
@@ -140,22 +166,29 @@ export interface EnumerateOptions {
140
166
  * Nothing shipped passes either one, so a channel that had stopped firing would be invisible in
141
167
  * exactly the way the channel exists to prevent. `test/enumerate-signals.test.ts` pins that both
142
168
  * reach a caller. */
143
- onAxisGated?: (suffix: string) => void;
144
- /** Called once per axis point whose structured tree an earlier point already spelled the tree
145
- * dedup, which is where most of the cross's factors of two go. See `onAxisGated` for why both
169
+ onVariationGated?: (variation: string) => void;
170
+ /** Called once per structure setting whose structured tree an earlier setting already produced
171
+ * the tree dedup, which is where most of the cross's factors of two go. See `onVariationGated` for why both
146
172
  * are here rather than on the result. */
147
173
  onTreeDeduped?: () => void;
174
+ /** PROBE (`ASMLIFT_PERSITE_SENSE`, wired in the cli): fork the two per-FUNCTION branch-sense
175
+ * booleans into one bit per SITE, crossing every sense setting with all 2^n masks over the first
176
+ * `n` sense sites. Costs a factor of 2^n on the whole fan, which is the measurement — see the
177
+ * per-site sense measurement in the enumeration below. 0/absent = the shipped per-function sense. */
178
+ perSiteSenseBits?: number;
148
179
  }
149
180
 
150
- /** One distinct candidate spelling a point in the axis cross (signedness × branch sense ×
151
- * def-site anchoring × bitfield spelling × symbol-map variant, plus the L3 re-spellings) —
181
+ /** One distinct candidate — one combination of variations (signedness × branch sense ×
182
+ * def-site anchoring × bitfield spelling × symbol map, plus the respell variations) —
152
183
  * emitted to source. */
153
184
  export interface Candidate {
154
- label: string;
185
+ /** the variations this candidate applied, in enumeration order — signedness first. This list IS
186
+ * the candidate's name; `joinVariations` prints it. */
187
+ variations: readonly string[];
155
188
  source: string;
156
- /** Which PREFERENCE GROUP this spelling belongs to — the symbol-variant index (0 = the map's own
157
- * named spellings, 1 = their `/raw-globals` siblings). Enumeration emits the groups in
158
- * preference order, and a lower group WINS a score tie: when both compile to the same bytes the
189
+ /** Which PREFERENCE this candidate carries — the symbol-map setting's index (0 = the map's own
190
+ * named spellings, 1 = their `/raw-globals` siblings). Enumeration emits the settings in
191
+ * preference order, and a lower preference WINS a score tie: when both compile to the same bytes the
159
192
  * reader should get `gCounter.field`, not a byte offset off a hoisted `(u8 *)` base.
160
193
  *
161
194
  * Carried structurally rather than left to enumeration order because the readability tie-break
@@ -163,12 +196,12 @@ export interface Candidate {
163
196
  * thing. Ranking a named spelling against a raw-address one on cast count is not a readability
164
197
  * comparison at all — the raw form's `(u8 *)` base is not counted, so it would win by
165
198
  * construction, trading named struct fields for anonymous byte offsets. */
166
- group: number;
199
+ preference: number;
167
200
  /** the DECLARABLE VALUE references this candidate's tree contains — what the scoring layer's
168
201
  * declaration synthesis renders. DERIVED, never carried: computed once from the exact tree
169
202
  * this candidate's source was emitted from, at the moment the candidate is finalized
170
203
  * (l3/symbol-refs.ts — no pipeline stage caches refs, so they cannot go stale). Present on
171
- * EVERY spelling variant that names such symbols — including '/raw-globals', whose tree still
204
+ * EVERY candidate that names such symbols — including '/raw-globals', whose tree still
172
205
  * names pool/reloc-derived globals (it only drops the map's shaped SPELLINGS).
173
206
  *
174
207
  * PRESENT WITHOUT A MAP TOO: a name is read out of the asm's own literal pool or relocation,
@@ -185,7 +218,7 @@ export interface Candidate {
185
218
  deviceVolatile?: number;
186
219
  /** PUBLISHABLE ONLY WHERE THE DIFFER PROVES IT — a byte-exact score, nothing else.
187
220
  *
188
- * The third admission ground, and the narrowest. A lever must preserve semantics by
221
+ * The third admission ground, and the narrowest. A respell variation must preserve semantics by
189
222
  * construction (the POLICY note at the respell site), because on a nonmatch row the best
190
223
  * spelling is what the user is shown. One spelling cannot meet that bar from inside the pass:
191
224
  * `l3/unreduce.ts` moves a memory read into a loop whose stores are all device registers, and
@@ -206,7 +239,7 @@ export interface Scored<S> extends Candidate {
206
239
  * scoring harness that shows only the surviving sibling reports a clean win over a hidden
207
240
  * failure. */
208
241
  export interface DroppedCandidate {
209
- label: string;
242
+ variations: readonly string[];
210
243
  /** the scorer's first error line (a compiler diagnostic, usually) */
211
244
  error: string;
212
245
  }
@@ -216,18 +249,59 @@ export interface DroppedCandidate {
216
249
  * compiled fine and simply did not earn publication is a different fact, and folding the two
217
250
  * would make the `[dropped]` line report compile failures that never happened. */
218
251
  export interface WithheldCandidate {
219
- label: string;
252
+ variations: readonly string[];
220
253
  score: number;
254
+ /** the denominator that score was measured against — objdiff's row count for THIS candidate's
255
+ * alignment, so it moves with the spelling. Present whenever the injected scorer supplies one
256
+ * (the cli and webapp objdiff scorers both do); a scorer whose result carries no `rows` leaves
257
+ * it absent rather than inventing a scale. A withheld score is read across runs exactly like a
258
+ * published one, and a bare numerator there invites the same subtraction on a scale that moved. */
259
+ rows?: number;
221
260
  /** one line: why publication needed a proof this score did not supply */
222
261
  why: string;
223
262
  }
224
263
 
264
+ /** EVERY candidate refused — `rankBy` has no ranked result to return, so it throws this.
265
+ *
266
+ * The two lists ride on the error, and that is the point of having a class at all: on a row where
267
+ * nothing scored, `dropped` IS the whole fan, and a bare `Error` discards it. A caller that
268
+ * prints one candidate's failure (the `cause`) is showing the LAST spelling the scorer refused,
269
+ * which is neither the first nor a representative one — the benchmark's own noncompile rows have
270
+ * up to a thousand siblings behind that single line. The MESSAGE is load-bearing and must stay
271
+ * byte-identical: `bench fidelity` matches it verbatim to recognise a reproduced noncompile
272
+ * row. */
273
+ export class NoScorableCandidateError extends Error {
274
+ readonly dropped: DroppedCandidate[];
275
+ readonly withheld: WithheldCandidate[];
276
+ constructor(message: string, dropped: DroppedCandidate[], withheld: WithheldCandidate[], options?: ErrorOptions) {
277
+ super(message, options);
278
+ this.name = 'NoScorableCandidateError';
279
+ this.dropped = dropped;
280
+ this.withheld = withheld;
281
+ }
282
+ }
283
+
284
+ /** EVERY spelling refused BY THE BACKEND, before anything was compiled — `enumerateCandidates`
285
+ * has no fan to return, so it throws this.
286
+ *
287
+ * It is a sibling of `NoScorableCandidateError` and a DIFFERENT fact, which is the whole reason
288
+ * it is a class: nothing was scored there because nothing COMPILED, and nothing was scored here
289
+ * because nothing was ever spelled. A surface that cannot tell the two apart prints one of them
290
+ * under the other's name. The message is load-bearing too
291
+ * (`packages/core/test/rank-backend-decline.test.ts` matches it). */
292
+ export class NoSpellableCandidateError extends Error {
293
+ constructor(message: string, options?: ErrorOptions) {
294
+ super(message, options);
295
+ this.name = 'NoSpellableCandidateError';
296
+ }
297
+ }
298
+
225
299
  export interface RankedResult<S> {
226
- best: Scored<S>; // lowest score
300
+ winner: Scored<S>; // lowest score
227
301
  candidates: Scored<S>[]; // sorted best (lowest) first
228
302
  /** candidates whose scoreFn threw — empty when every spelling built */
229
303
  dropped: DroppedCandidate[];
230
- /** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` lever fired */
304
+ /** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` variation fired */
231
305
  withheld: WithheldCandidate[];
232
306
  }
233
307
 
@@ -243,15 +317,15 @@ export function withheldReason<S extends { score: number }>(c: Candidate, score:
243
317
  : null;
244
318
  }
245
319
 
246
- /** What a re-spelling lever hands `respell`: its tree, or — when the lever cannot establish the
320
+ /** What a respell variation hands `respell`: its tree, or — when the variation cannot establish the
247
321
  * candidate's semantics from inside the pass — the tree paired with that fact. `undefined`/`null`
248
322
  * is a decline. */
249
- type LeverResult = SFn | { sfn: SFn; needsProof: boolean } | null | undefined;
323
+ type RespellResult = SFn | { sfn: SFn; needsProof: boolean } | null | undefined;
250
324
 
251
- /** REQUIRE-ALL composition of re-spelling levers, and the ONE place a proof obligation crosses
252
- * from one lever to the next.
325
+ /** REQUIRE-ALL composition of respell variations, and the ONE place a proof obligation crosses
326
+ * from one variation to the next.
253
327
  *
254
- * `LeverResult` is a union, so a hand-written composition can spell the obligation away by
328
+ * `RespellResult` is a union, so a hand-written composition can spell the obligation away by
255
329
  * accident and stay type-correct: `return pointerFields(u.sfn);` in place of
256
330
  * `return { sfn: t, needsProof: u.needsProof };` compiles, passes tsc and passes every suite,
257
331
  * and publishes as asmlift's answer a spelling that was supposed to be withheld unless byte-exact.
@@ -259,14 +333,14 @@ type LeverResult = SFn | { sfn: SFn; needsProof: boolean } | null | undefined;
259
333
  * touches the flag.
260
334
  *
261
335
  * The obligation is MONOTONE, which is what lets it be an `or`: it says "no gate over this C can
262
- * settle the fact this spelling rests on", and a later re-spelling cannot settle a fact about an
336
+ * settle the fact this spelling rests on", and a later respell variation cannot settle a fact about an
263
337
  * earlier one. `/ptr-field` re-types a field and never moves a read, so it carries `/unreduce`'s
264
338
  * obligation through unchanged rather than discharging it.
265
339
  *
266
340
  * REQUIRE-ALL, never skip-on-decline: one declining stage declines the whole composition, so the
267
- * label always names exactly the levers that fired. That is the property the pairing site turns
268
- * on, and the reason it rejects `applyShapes` — see the POLICY note there. */
269
- export function composeLevers(sfn: SFn, stages: readonly ((s: SFn) => LeverResult)[]): LeverResult {
341
+ * candidate's variations always name exactly the ones that fired. That is the property the pairing site turns
342
+ * on, and the reason it rejects `applyStacked` — see the POLICY note there. */
343
+ export function composeRespellVariations(sfn: SFn, stages: readonly ((s: SFn) => RespellResult)[]): RespellResult {
270
344
  let cur = sfn;
271
345
  let needsProof = false;
272
346
  for (const stage of stages) {
@@ -280,42 +354,43 @@ export function composeLevers(sfn: SFn, stages: readonly ((s: SFn) => LeverResul
280
354
  return needsProof ? { sfn: cur, needsProof } : cur;
281
355
  }
282
356
 
283
- /** What one `fanOut` call produced: its spellings, plus the PRIMARY emit's refusal where the
284
- * backend declined the tree it was handed.
357
+ /** What one `respellTree` call produced: the sources it emitted, plus the DEFAULT source's emit refusal
358
+ * where the backend declined the tree it was handed.
285
359
  *
286
360
  * RETURNED rather than written to the enumeration's shared `lastEmitError`, because only ONE
287
- * caller may record one. `fanOut` runs over the row's own tree and over each PRE-FAN product's
361
+ * caller may record one. `respellTree` runs over the row's own tree and over each pre-respell variation's
288
362
  * REWRITTEN tree, and a backend refusal of a rewrite is not a refusal of the row's spelling —
289
363
  * letting it reach `lastEmitError` would put the wrong cause on the row's "no spellable
290
- * candidate" throw. With the value returned, the primary caller records and the pre-fan caller
291
- * does not, which is the rule made structural instead of saved and restored around the call.
364
+ * candidate" throw. With the value returned, the caller over the row's own tree records and the
365
+ * pre-respell caller does not, which is the rule made structural instead of saved and restored around the call.
292
366
  *
293
- * A DISCRIMINATED FIELD, not a nullable error: a lever that throws a falsy value is still
367
+ * A DISCRIMINATED FIELD, not a nullable error: a variation that throws a falsy value is still
294
368
  * recorded, where `?? ` would read it as "nothing was thrown". */
295
- interface FanResult {
296
- spellings: Spelling[];
369
+ interface TreeSources {
370
+ sources: TreeSource[];
297
371
  emit?: { error: unknown };
298
372
  }
299
373
 
300
- /** One emitted spelling of a structured tree: the label suffix naming the lever that produced it,
374
+ /** One source emitted from a structured tree: the variations that produced it,
301
375
  * the rendered source, and the tree-derived facts `compareScored` ranks by. */
302
- interface Spelling {
303
- suffix: string;
376
+ interface TreeSource {
377
+ variations: readonly Variation[];
304
378
  source: string;
305
379
  symbolRefs?: SymbolRef[];
306
380
  deviceVolatile?: number;
307
- /** see `Candidate.matchOnly` — set by a lever that cannot establish its own semantics */
381
+ /** see `Candidate.matchOnly` — set by a variation that cannot establish its own semantics */
308
382
  matchOnly?: true;
309
383
  }
310
384
 
311
- /** Emit the DISTINCT type/branch-sense candidate spellings for `name` — PURE, no scoring.
385
+ /** Emit the DISTINCT type/branch-sense candidates for `name` — PURE, no scoring.
312
386
  * It differs from `decompile()` in exactly two arguments to the shared spine — the signedness
313
387
  * pin, injected between pre-recovery and recoverTypes via the `beforeRecover` hook, and the
314
388
  * `pre.shortCircuit` connective owner (the raiseRecovered call below states both).
315
389
  * Duplicate sources are collapsed so the scorer never
316
- * recompiles an identical spelling, and the fan runs once per distinct STRUCTURED TREE rather
317
- * than once per axis pointan axis inert on this function reaches a tree an earlier point
318
- * already spelled, and every re-spelling is a pure function of that tree. */
390
+ * recompiles an identical spelling, and the respell set runs once per distinct STRUCTURED TREE
391
+ * rather than once per structure settinga structure variation inert on this function reaches a
392
+ * tree an earlier setting already produced, and every respell variation is a pure function of that
393
+ * tree. */
319
394
  export function enumerateCandidates(
320
395
  name: string,
321
396
  asm: string,
@@ -323,6 +398,10 @@ export function enumerateCandidates(
323
398
  opts: EnumerateOptions = {},
324
399
  ): Candidate[] {
325
400
  const backend = opts.backend ?? cBackend;
401
+ /** Report a setting that THREW through `onEnumerationError`, as the variations it names (none for
402
+ * the function's own tree). */
403
+ const reportThrow = (variations: readonly Variation[], e: unknown): void =>
404
+ opts.onEnumerationError?.(variations, firstLine(e));
326
405
  /** The last refusal from a backend asked to spell a tree — what the empty-enumeration check
327
406
  * below reports, so "this backend can spell nothing here" names its reason. */
328
407
  let lastEmitError: unknown = null;
@@ -338,7 +417,7 @@ export function enumerateCandidates(
338
417
  spellSwitchFallthrough: backend.spellsSwitchFallthrough,
339
418
  ...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
340
419
  };
341
- // Branch-sense is a differ-ranked LEVER, the same class as param signedness: a divergent `if`
420
+ // Branch-sense is a differ-ranked VARIATION, the same class as param signedness: a divergent `if`
342
421
  // can be spelled with either sense (`if (c) A else B` vs `if (!c) B else A`), and which one the
343
422
  // source compiler emitted is genuinely ambiguous from asm. There is no safe global heuristic
344
423
  // (`ifor` wants positive, `simpleif` wants negated, `diamond` wants positive) — emit BOTH senses
@@ -346,134 +425,165 @@ export function enumerateCandidates(
346
425
  // worse; it only wins where the flip matches.
347
426
  const defSense = baseOpts.preserveDivergentBranchSense ?? true;
348
427
  // `/defsite` — def-site-anchored constant merge copies (structure.ts anchorConstCopies) — is a
349
- // structuring axis on the same footing as branch sense: where the asm materialized a merge
428
+ // structure variation on the same footing as branch sense: where the asm materialized a merge
350
429
  // constant is placement evidence, but whether the SOURCE spelled it there is genuinely
351
430
  // ambiguous, so both placements are emitted and the differ referees. Crossed with branch sense
352
431
  // (an anchored copy empties an arm, which is exactly what changes which sense wins); the dedup
353
- // below collapses every variant the anchoring left unchanged.
432
+ // below collapses every candidate the anchoring left unchanged.
354
433
  //
355
434
  // `/defsite/loop-entry` widens it to a LOOP HEADER's entry const (`int s = 0;` above the guard
356
- // rather than on the edge into the loop). Its own point rather than a widening of `/defsite`
435
+ // rather than on the edge into the loop). Its own setting rather than a widening of `/defsite`
357
436
  // because it is a SECOND placement decision: a function carrying both kinds of anchorable const
358
437
  // has THREE spellings, and folding the two decisions into one boolean would delete the middle
359
438
  // one — measured on klonoa's TransitionSelfRemoveFadeIn, where 448 of the 896 sources `/defsite`
360
439
  // reaches became unreachable. Enumerated as a CHAIN (none ⊂ plain ⊂ plain + entry) rather than a
361
- // 2×2 cross: the fourth point costs another quarter of the whole fan — the anchor dimension
440
+ // 2×2 cross: the fourth setting costs another quarter of the whole fan — the anchor dimension
362
441
  // multiplies everything below it — and no row has been shown to need it.
363
442
  //
364
- // The four spelling booleans that PREDATE `STRUCTURING_AXES` and are still hand-carried
365
- // (`bitfields`, `ptrElems`, `declRank` and the anchor pair) start from one record, so a base
366
- // point's default lives in one place instead of six literals that can disagree. Each entry
443
+ // The spelling booleans hand-carried outside `STRUCTURE_VARIATIONS`
444
+ // (`bitfields`, `ptrElems`, `declRank` and the anchor pair) start from one record, so the
445
+ // default setting lives in one place instead of six literals that can disagree. Each entry
367
446
  // states only what it VARIES — which is the whole content of the chain above.
368
- const SPELLING_DEFAULTS = { anchor: false, entry: false, bitfields: true, ptrElems: true, declRank: true };
369
- const senseAnchor = [
370
- { ...SPELLING_DEFAULTS, suffix: '', sense: defSense },
371
- { ...SPELLING_DEFAULTS, suffix: '/flip-branch', sense: !defSense },
372
- { ...SPELLING_DEFAULTS, suffix: '/defsite', sense: defSense, anchor: true },
373
- { ...SPELLING_DEFAULTS, suffix: '/flip-branch/defsite', sense: !defSense, anchor: true },
374
- { ...SPELLING_DEFAULTS, suffix: '/defsite/loop-entry', sense: defSense, anchor: true, entry: true },
375
- { ...SPELLING_DEFAULTS, suffix: '/flip-branch/defsite/loop-entry', sense: !defSense, anchor: true, entry: true },
447
+ const STRUCTURE_DEFAULTS = { anchor: false, entry: false, bitfields: true, ptrElems: true, declRank: true };
448
+ const senseAnchor: (typeof STRUCTURE_DEFAULTS & { variations: readonly Variation[]; sense: boolean })[] = [
449
+ { ...STRUCTURE_DEFAULTS, variations: [], sense: defSense },
450
+ { ...STRUCTURE_DEFAULTS, variations: ['flip-branch'], sense: !defSense },
451
+ { ...STRUCTURE_DEFAULTS, variations: ['defsite'], sense: defSense, anchor: true },
452
+ { ...STRUCTURE_DEFAULTS, variations: ['flip-branch', 'defsite'], sense: !defSense, anchor: true },
453
+ { ...STRUCTURE_DEFAULTS, variations: ['defsite', 'loop-entry'], sense: defSense, anchor: true, entry: true },
454
+ {
455
+ ...STRUCTURE_DEFAULTS,
456
+ variations: ['flip-branch', 'defsite', 'loop-entry'],
457
+ sense: !defSense,
458
+ anchor: true,
459
+ entry: true,
460
+ },
376
461
  ];
377
462
  // `/flip-join` — the JOINED-if sibling of `/flip-branch` (structure.ts
378
463
  // negateJoinedBranchSense): a reconverging two-armed if reads the same fall-through-is-then
379
464
  // layout evidence the divergent case does, so the DEFAULT sense is the divergent one's and
380
- // this axis emits the other. Read off the TARGET's sense, not this candidate's `s.sense`, so
381
- // `/flip-branch` still moves only divergent ifs and the two axes stay independent. The suffix
382
- // therefore names a sense RELATIVE to the target's default: a label quoted from a log identifies
383
- // a spelling only together with the tree that produced it, which is what the `[asmlift source
465
+ // this variation emits the other. Read off the TARGET's sense, not this candidate's `s.sense`, so
466
+ // `/flip-branch` still moves only divergent ifs and the two variations stay independent. The name
467
+ // therefore names a sense RELATIVE to the target's default: a candidate's variations quoted from a
468
+ // log identify a candidate only together with the tree that produced it, which is what the `[asmlift source
384
469
  // <commit>]` stamp on the `[ranked]` line is for (docs/ranked-repro.md).
385
470
  // Crossed with the pair above. The two senses are two different sources wherever a two-armed
386
471
  // joined `if` exists at all — agbcc emits different bytes for the arms-swapped spelling — and
387
- // all three things that invert the polarity are per-SITE where this lever is per-function, so no
472
+ // all three things that invert the polarity are per-SITE where this variation is per-function, so no
388
473
  // per-function predicate decides it: a short-circuit fold choosing the orientation, a
389
474
  // conditional branch relayed past Thumb's ±256-byte reach, and a rotated loop's zero-trip guard,
390
- // where the `if` is the compiler's own and no source sense exists to be faithful to. The third
391
- // is what keeps the residue on targets that have neither: rows still win on the axis under
475
+ // where the `if` is the compiler's own and no source sense exists to be faithful to. The FIRST
476
+ // of the three is now decided per site rather than enumerated `/site-sense` (rank-variations.ts)
477
+ // reads the orientation the fold records — and this variation stays because the other two are not.
478
+ // The third
479
+ // is what keeps the residue on targets that have neither: rows still win on the variation under
392
480
  // gcc2.7.2 / gcc2.7.2kmc / mwcc, with no `short-circuit` tag and no Thumb branch range to
393
481
  // explain them, and most of those carry `loop`. A function with no two-armed joined if emits identical
394
482
  // source and the dedup collapses it before any compile.
395
- const baseSense = [
483
+ const senseOnly = [
396
484
  ...senseAnchor.map((s) => ({ ...s, join: false })),
397
- ...senseAnchor.map((s) => ({ ...s, suffix: `${s.suffix}/flip-join`, join: true })),
485
+ ...senseAnchor.map((s): typeof s & { join: boolean } => ({
486
+ ...s,
487
+ variations: [...s.variations, 'flip-join'],
488
+ join: true,
489
+ })),
398
490
  ];
491
+ // THE PER-SITE SENSE MEASUREMENT, a structure variation never enumerated by default: every mask
492
+ // over the sense sites, crossed with the whole fan. It exists to price the fork the two booleans above cannot express
493
+ // — 2^n on a function with n sites, which is the number a decidable predicate would replace —
494
+ // and to say whether the target's configuration is REACHABLE at all. Not enumerated unless the
495
+ // caller asks; `structure()`'s own `branchSenseFlipSites` is the seam it drives.
496
+ const maskSites = (m: number): ReadonlySet<number> => new Set([...Array(32).keys()].filter((i) => (m >> i) & 1));
497
+ const senseMasks = Array.from({ length: 1 << (opts.perSiteSenseBits ?? 0) }, (_, m) => m);
498
+ const baseSense = senseMasks.flatMap((m) =>
499
+ senseOnly.map((s) => ({
500
+ ...s,
501
+ ...(m === 0 ? {} : { variations: [...s.variations, withSubject('sense', String(m))] }),
502
+ // Always present, `undefined` at mask 0: an optional key added on one arm of a ternary would
503
+ // make the two arms different object TYPES, and the list is what the whole fan spreads from.
504
+ flipSites: m === 0 ? undefined : maskSites(m),
505
+ })),
506
+ );
399
507
  // `/no-bitfield` — keep the honest shift spelling where the map would name a bitfield member.
400
508
  // The named read recompiles at the DECLARATION's access width; where that diverges from the
401
509
  // asm's load width, the shifts are the spelling that matches — so both are emitted and the
402
510
  // differ referees. Enumerated only when the map carries any bitfield member at all (checked
403
511
  // below), so the 2× cross is paid exactly by the functions it can help; the dedup collapses
404
- // every variant where no fold fired.
405
- const mapHasBitfields =
406
- opts.symbols !== undefined &&
407
- [...opts.symbols.values()].some((infos) =>
408
- infos.some((i) => [...(i.layout ?? []), ...(i.pointee?.layout ?? [])].some((f) => f.bitWidth !== undefined)),
409
- );
410
- const bitfieldCands = mapHasBitfields
411
- ? [...baseSense, ...baseSense.map((s) => ({ ...s, suffix: `${s.suffix}/no-bitfield`, bitfields: false }))]
412
- : baseSense;
512
+ // every candidate where no fold fired.
513
+ const bitfieldSettings =
514
+ opts.symbols !== undefined && declaresBitfields(opts.symbols)
515
+ ? [
516
+ ...baseSense,
517
+ ...baseSense.map((s): typeof s => ({ ...s, variations: [...s.variations, 'no-bitfield'], bitfields: false })),
518
+ ]
519
+ : baseSense;
413
520
  // `/connective`'s enumeration gate, read off the pass's OWN refusal rather than from a second
414
521
  // copy of its matcher: the fold reports every site where the PAIRWISE comparison-tree refusal is
415
522
  // the ONE thing stopping it — asked after `sameArgs` and the negatability check, so a report
416
523
  // means a candidate that DIFFERS, not a refusal merely reached — and a function with none has no
417
- // inhabitant for the axis.
524
+ // inhabitant for the variation.
418
525
  //
419
- // PER SYMBOL VARIANT, on a lift of its OWN, for the reason the `/setup-args` gate below states
526
+ // PER SYMBOL-MAP SETTING, on a lift of its OWN, for the reason the `/setup-args` gate below states
420
527
  // for itself: no lift may be governed by a fact measured on a different one. The pin and
421
528
  // `/setup-args` cannot move this answer — neither a parameter's type nor a call's argument list
422
529
  // moves a `cond_br` — but a SYMBOL MAP can, by lifting a pool-loaded comparison constant as a
423
530
  // `gaddr` the const-test test then does not read. Measured once and NOT re-derived since: over
424
531
  // the real rows that lifted, 21 sites mapped and 21 raw with no per-row divergence. Read that as
425
- // the REASON the gate is asked per variant, not as a fact about today's corpus — it carries no
532
+ // the REASON the gate is asked per symbol-map setting, not as a fact about today's corpus — it carries no
426
533
  // commit stamp. The SYNTHETIC tier is inside that comparison rather than exempt from it: 9 of
427
534
  // its 770 rows carry a map (`SynthSpec.symbols`) in the committed artifact, and that count moves
428
535
  // every time a map row is added, so re-derive it rather than carrying this one forward. So this
429
536
  // buys no candidate; what
430
- // it buys is that a lift-time change which splits them enumerates both arms rather than
537
+ // it buys is that a lift-time change which splits them enumerates both settings rather than
431
538
  // silently dropping one, the failure nothing reports.
432
- let probeTreeOwned = false;
433
- // Probe: recover ONCE with no signedness pin, to learn which entry params are pointers/aggregates
434
- // so they are excluded from the signedness axis (see NO_PIN_KINDS). One extra lift+recover, no
435
- // compile. (The probe deliberately stops after recoverTypes — it only reads the param KINDS, so
539
+ let sharedLiftTreeOwned = false;
540
+ // Shared lift: recover ONCE with no signedness pin, to learn which entry params are
541
+ // pointers/aggregates so they are excluded from the signedness variation (see NO_PIN_KINDS). One
542
+ // extra lift+recover, no compile. (The shared lift deliberately stops after recoverTypes — it only reads the param KINDS, so
436
543
  // the totality contract / return-sinking of the full spine are not run on it.)
437
- const probe = frontend.lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
438
- verify(probe);
544
+ const sharedLift = frontend.lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
545
+ verify(sharedLift);
439
546
  // The ARRAY SHAPES the input assembly evidences (raise/globalshape.ts), for the DECLARATION
440
- // half. Read off the probe's LIFTED form — before the fold below and the tower rewrite it —
547
+ // half. Read off the shared lift's LIFTED form — before the fold below and the tower rewrite it —
441
548
  // because the base-materialization order the derivation's licence reads does not survive them.
442
- // Probe-derived like `accessFacts` beside it, and for the same reason: it is a lift-time fact.
443
- // The candidate half reads its OWN lift (each symbol variant lifts differently), just as the
444
- // spelling axes do.
445
- const probeShapes = inferGlobalArrays(probe, target);
446
- applyIdiomPatterns(probe, target, opts.patterns);
447
- runPreRecovery(probe, target, () => verify(probe), prototypes[name], {
549
+ // Derived from the shared lift like `accessFacts` beside it, and for the same reason: it is a
550
+ // lift-time fact. The candidate half reads its OWN lift (each symbol-map setting lifts
551
+ // differently), just as the structure variations do.
552
+ const sharedLiftShapes = inferGlobalArrays(sharedLift, target);
553
+ applyIdiomPatterns(sharedLift, target, opts.patterns);
554
+ runPreRecovery(sharedLift, target, () => verify(sharedLift), prototypes[name], {
448
555
  shortCircuit: {
449
556
  onTreeOwned: () => {
450
- probeTreeOwned = true;
557
+ sharedLiftTreeOwned = true;
451
558
  },
452
559
  },
453
560
  });
454
- recoverTypes(probe);
455
- const ptrIdx = new Set<number>(probe.blocks[0].params.flatMap((p, i) => (NO_PIN_KINDS.has(p.type.kind) ? [i] : [])));
561
+ recoverTypes(sharedLift);
562
+ const ptrIdx = new Set<number>(
563
+ sharedLift.blocks[0].params.flatMap((p, i) => (NO_PIN_KINDS.has(p.type.kind) ? [i] : [])),
564
+ );
456
565
  // Access facts for name-only symbol declarations (see bareGlobalAccessFacts) — derived once
457
- // from the probe: widths/offsets are lift-time facts, identical across every candidate.
566
+ // from the shared lift: widths/offsets are lift-time facts, identical across every candidate.
458
567
  // Ungated on `opts.symbols`: map-less candidates now carry name-only refs too (see
459
568
  // `bareGlobalSymbols`), and these facts are their declarations' WIDTH AUTHORITY — without them
460
569
  // every map-less decl would be the `extern u32` fallback and a bare `gCell = x` would compile
461
570
  // to `str` where the target says `strh`. One IR walk; on a function with no `gaddr` at all
462
571
  // (every synthetic corpus row) it returns the same empty map the gate used to hand back.
463
- const accessFacts = bareGlobalAccessFacts(probe);
572
+ const accessFacts = bareGlobalAccessFacts(sharedLift);
464
573
  //
465
- // The MAPPED variant reads it off the probe below, itself a lift in exactly that configuration —
466
- // reuse, not inheritance. Only a variant lifting under DIFFERENT symbols pays a lift of its own,
467
- // so the price is one per `/raw-globals` arm and zero on a map-less row, never per candidate.
574
+ // The MAPPED setting reads it off the shared lift below, itself a lift in exactly that
575
+ // configuration — reuse, not inheritance. Only a setting lifting under DIFFERENT symbols pays a
576
+ // lift of its own, so the price is one per `/raw-globals` setting and zero on a map-less row,
577
+ // never per candidate.
468
578
  //
469
- // DECLARED AFTER THE PROBE'S OWN `runPreRecovery` ON PURPOSE, and that placement is the memo's
470
- // precondition: the map arm returns `probeTreeOwned`, which is only the answer once the probe's
471
- // `onTreeOwned` hook has had its chance to fire. Called any earlier it would report a confident
579
+ // DECLARED AFTER THE SHARED LIFT'S OWN `runPreRecovery` ON PURPOSE, and that placement is the
580
+ // memo's precondition: the mapped case returns `sharedLiftTreeOwned`, which is only the answer once the
581
+ // shared lift's `onTreeOwned` hook has had its chance to fire. Called any earlier it would report a confident
472
582
  // `false` for a function that owns a tree. As a `const` below that call, an early call is a TDZ
473
583
  // ReferenceError instead — a wrong answer traded for a loud one.
474
584
  const treeOwnedIn = (symbols: typeof opts.symbols): boolean => {
475
585
  if (symbols === opts.symbols) {
476
- return probeTreeOwned;
586
+ return sharedLiftTreeOwned;
477
587
  }
478
588
  const p = frontend.lift(name, asm, target, prototypes, opts.asmData, symbols);
479
589
  verify(p);
@@ -505,66 +615,66 @@ export function enumerateCandidates(
505
615
  // (`/no-bitfield` above still asks the MAP rather than the function; narrowing it would be this
506
616
  // same edit against a different measurement. Its cross is censused below beside this one.)
507
617
  //
508
- // Where it DOES reach, the cross is the honest price of an arm the differ has to referee, and on
509
- // the corpus's largest fan it is large: `kleod:ProcessInputAndUpdateEntities` enumerates 58,752
510
- // candidates of which 23,040 carry this arm, so removing it leaves 35,712 — a factor of 1.65,
618
+ // Where it DOES reach, the cross is the honest price of an alternative the differ has to referee,
619
+ // and on the corpus's largest fan it is large: `kleod:ProcessInputAndUpdateEntities` enumerates
620
+ // 58,752 candidates of which 23,040 carry this variation, so removing it leaves 35,712 — a factor of 1.65,
511
621
  // not a doubling. A ROUNDER NUMBER IS NOT A SAFER ONE: re-measure rather than reaching for a
512
622
  // vaguer word. The instrument is `decompileRanked`'s own enumeration, and a direct
513
623
  // `enumerateCandidates` call from a standalone script is NOT it (an ESM/CJS duplicate of this
514
624
  // module answers 544 where the harness answers 952 on `SetupBG3WindowOverlay`).
515
625
  //
516
- // The two arms are also NESTED rather than independent — `ptrElemCands` is built by doubling
517
- // `bitfieldCands`, so this arm's candidates include the `/no-bitfield` ones and adding the two
518
- // families' counts double-counts the overlap: on that same row 12,672 of the 23,040 carry BOTH
519
- // tokens, which is half of `/no-bitfield`'s own 25,344. A per-family price read off either
520
- // label alone therefore double-counts more than half of this row's cross.
626
+ // The two variations are also NESTED rather than independent — `ptrElemSettings` is built by doubling
627
+ // `bitfieldSettings`, so this variation's candidates include the `/no-bitfield` ones and adding the
628
+ // two families' counts double-counts the overlap: on that same row 12,672 of the 23,040 carry BOTH
629
+ // variations, which is half of `/no-bitfield`'s own 25,344. A per-family price read off either
630
+ // variation alone therefore double-counts more than half of this row's cross.
521
631
  //
522
- // EXACTLY ONE WINNING LABEL IN THE ARTIFACT CARRIES `/no-ptr-elem` — `synthetic:ptrelem:agbcc`,
523
- // match at 0. READ THAT ONE, NOT A ZERO: the axis is two-sided where it fires. Compile the byte
632
+ // EXACTLY ONE WINNER IN THE ARTIFACT CARRIES `/no-ptr-elem` — `synthetic:ptrelem:agbcc`,
633
+ // match at 0. READ THAT ONE, NOT A ZERO: the variation is two-sided where it fires. Compile the byte
524
634
  // spelling and the element spelling of the SAME address with the klonoa checkout's own agbcc,
525
- // lift each back with that project's own map, and the arm is the ONLY candidate that matches the
635
+ // lift each back with that project's own map, and the `/no-ptr-elem` candidate is the ONLY one that matches the
526
636
  // byte target while the default is the only one that matches the element target — on a constant
527
637
  // element offset, at one element in, at a pointee width of 1, and on a STORE.
528
- // `cli/test/matching/ptr-elem-axis.test.ts` is that measurement, and deleting the `ptrElemCands`
638
+ // `cli/test/matching/ptr-elem-variation.test.ts` is that measurement, and deleting the `ptrElemSettings`
529
639
  // cross turns 8 of its 13 assertions red — the four BYTE-target ones (each scoring 1
530
- // rather than 0) and the four that check the arm is enumerated at all — while its four
640
+ // rather than 0) and the four that check the variation is enumerated at all — while its four
531
641
  // ELEMENT-target ones stay green, which is the two-sidedness itself. A low count over the REAL
532
642
  // tier counts something else: klonoa's map declares a sized pointer member at ONE address, and
533
643
  // every decompiled caller of it happens to be written in the element form.
534
644
  //
535
645
  // WHERE IT GENUINELY DOES NOT REACH, measured on the same probes: at element offset ZERO the two
536
- // arms emit the IDENTICAL source (`((u16 *)gSym.pMap)[a0]`, because with no constant left there
646
+ // spellings emit the IDENTICAL source (`((u16 *)gSym.pMap)[a0]`, because with no constant left there
537
647
  // is nothing for the byte form to spell differently), the tree dedup collapses the pair — 10
538
- // candidates, not 12 — and neither arm matches a byte-form target. That is an open gap in the
539
- // spelling, not a refusal of this axis.
648
+ // candidates, not 12 — and neither matches a byte-form target. That is an open gap in the
649
+ // spelling, not a refusal of this variation.
540
650
  //
541
651
  // STATE THE DENOMINATOR, AND DERIVE IT FROM REACH RATHER THAN FROM CO-OCCURRENCE — the same
542
652
  // distinction the paragraph above draws about the gate, applied to the gate's own price.
543
653
  // "Offered only where a symbol map exists" is true and useless: all 252 real rows carry a map,
544
- // so that framing hands back 151 labelled rows, which is the map-wide `some` this gate was
654
+ // so that framing hands back 151 rows with a winner, which is the map-wide `some` this gate was
545
655
  // written to avoid. The gate is per-FUNCTION, so census the FUNCTIONS. Enumerating every real
546
656
  // case (candidates only, `ASMLIFT_CANDCACHE=0`, the harness's own inputs) and counting rows with
547
657
  // any surviving `/no-ptr-elem` candidate: TWO — `kleod:ProcessInputAndUpdateEntities` (23040 of
548
- // its 58752) and `kleod:SetupBG3WindowOverlay` (128 of 952), and only the first carries a
549
- // winning label at all, the second being `noncompile`. THE TWO-ROW REACH IS STABLE AND THE
550
- // COUNTS ARE NOT — they move with every fan-widening axis, so re-run the census rather than
551
- // quoting these. So the REAL tier's "0 winning labels" is 0 of ONE here, not 0 of 151 and not
658
+ // its 58752) and `kleod:SetupBG3WindowOverlay` (128 of 952), and only the first has a
659
+ // winner at all, the second being `noncompile`. THE TWO-ROW REACH IS STABLE AND THE
660
+ // COUNTS ARE NOT — they move with every fan-widening variation, so re-run the census rather than
661
+ // quoting these. So the REAL tier's "0 winners carry it" is 0 of ONE here, not 0 of 151 and not
552
662
  // 0 of the artifact's row count. That census enumerates 155 of the 252 real rows — the
553
- // 151 the artifact labels plus its 4 `noncompile` rows — and the 97 it cannot enumerate are
663
+ // 151 with a winner plus its 4 `noncompile` rows — and the 97 it cannot enumerate are
554
664
  // exactly the rows the artifact declines. What makes "no row is LOST" a proof rather than a
555
- // sample is the soundness rule instead: this axis only ADDS candidates, so a row whose winner
665
+ // sample is the soundness rule instead: this variation only ADDS candidates, so a row whose winner
556
666
  // does not carry it cannot move when it is removed. The same census prices `/no-bitfield`: it
557
667
  // survives dedup on FIVE real rows — `ProcessInputAndUpdateEntities` 25344, `CountCollectedGems`
558
668
  // 192, `UpdateWorldMapNodeAnim` 168, `UpdateHUDCounterDisplay` 96, `CopyBGScrollTiles` 4 — every
559
- // one of them a row the artifact labels, and none of the five wins under it. So its map-wide
669
+ // one of them a row with a winner, and none of the five wins under it. So its map-wide
560
670
  // enumeration gate buys a candidate cross on 5 functions and the dedup collapses it everywhere
561
671
  // else.
562
672
  //
563
- // WHERE THE WINNING LABELS ARE, since the REAL tier has none for either arm: the SYNTHETIC tier
564
- // carries rows that hand asmlift a map (`SynthSpec.symbols`), and both arms win on one —
673
+ // WHERE THE WINNERS ARE, since the REAL tier has none for either variation: the SYNTHETIC tier
674
+ // carries rows that hand asmlift a map (`SynthSpec.symbols`), and both variations win on one —
565
675
  // `/no-bitfield` on `bfwordread` and `bfwordwrite`, `/no-ptr-elem` on `ptrelem`, each a match at
566
- // 0 that becomes a NONMATCH when its own arm is ablated. A LABEL CENSUS IS SCOPED TO ITS TIER
567
- // AND ITS COMMIT: say which tier a count is over, and re-derive it rather than carrying it
676
+ // 0 that becomes a NONMATCH when its own variation is ablated. A CENSUS OF WINNERS' VARIATIONS IS
677
+ // SCOPED TO ITS TIER AND ITS COMMIT: say which tier a count is over, and re-derive it rather than carrying it
568
678
  // forward.
569
679
  // The name-keyed map `baseOpts` already built, not a second `symbolsByName` walk over the same
570
680
  // input: the function is deterministic and unmemoized, nothing in packages/core mutates a
@@ -573,7 +683,7 @@ export function enumerateCandidates(
573
683
  const byName = baseOpts.symbols;
574
684
  const fnHasSizedPtrFields =
575
685
  byName !== undefined &&
576
- [...bareGlobalSymbols(probe).keys()].some((n) => {
686
+ [...bareGlobalSymbols(sharedLift).keys()].some((n) => {
577
687
  const i = byName.get(n);
578
688
  return (
579
689
  i !== undefined &&
@@ -582,9 +692,16 @@ export function enumerateCandidates(
582
692
  )
583
693
  );
584
694
  });
585
- const ptrElemCands = fnHasSizedPtrFields
586
- ? [...bitfieldCands, ...bitfieldCands.map((s) => ({ ...s, suffix: `${s.suffix}/no-ptr-elem`, ptrElems: false }))]
587
- : bitfieldCands;
695
+ const ptrElemSettings = fnHasSizedPtrFields
696
+ ? [
697
+ ...bitfieldSettings,
698
+ ...bitfieldSettings.map((s): typeof s => ({
699
+ ...s,
700
+ variations: [...s.variations, 'no-ptr-elem'],
701
+ ptrElems: false,
702
+ })),
703
+ ]
704
+ : bitfieldSettings;
588
705
  // `/flat-rank` — spell a multidimensional global's access as the FLAT byte arithmetic
589
706
  // (`*(u16 *)((r << 11) + (i << 1) + (u32)&g)`) where the default recovers the map's declared
590
707
  // subscripts (`g[r][i]`). The recovery's evidence is a term at the declared ROW stride, and that
@@ -600,7 +717,7 @@ export function enumerateCandidates(
600
717
  // reaches the IR as a `gaddr`. `arrayInnerExtents` is the recovery's own rank test, called here
601
718
  // rather than re-spelled, so the gate cannot be narrower than the rule it gates. It is still a
602
719
  // superset — it does not know the access WIDTH, and it cannot know whether any residual carries
603
- // a row term — so where the axis changes nothing the tree dedup below collapses the pair and the
720
+ // a row term — so where the variation changes nothing the tree dedup below collapses the pair and the
604
721
  // fan does not grow. OVER THE ARTIFACT'S 957 ROWS: 10 name such a symbol at all — 9 of them in
605
722
  // their winning `symbolsUsed`, the tenth (`kleod:SetupBG3WindowOverlay`) in a source its row
606
723
  // cannot compile, which is why the count is taken off the emitted sources and not off
@@ -613,74 +730,90 @@ export function enumerateCandidates(
613
730
  // project map and the shapes the asm evidences, so a MAP-LESS function whose own strides nest
614
731
  // (`synthetic:tblrank2:agbcc`) now spells `gPtrTbl[a0][a1]` by default while its flat sibling
615
732
  // `*(s32 *)((a1 << 2) + (a0 << 3) + (u32)&gPtrTbl)` — a genuinely different tree — was
616
- // enumerated nowhere. An axis exists BECAUSE the asm underdetermines the choice; supplying the
733
+ // enumerated nowhere. A variation exists BECAUSE the asm underdetermines the question; supplying the
617
734
  // rank from a new place does not make it determined, and nothing reports a candidate that was
618
735
  // never enumerated. Map first, exactly as everywhere else: a name the map knows is answered by
619
736
  // the map.
620
- const derivedOrMapped = (n: string): SymbolInfo | undefined => byName?.get(n) ?? probeShapes.get(n);
621
- const fnNamesMultidimArray = [...bareGlobalSymbols(probe).keys()].some((n) => {
737
+ const derivedOrMapped = (n: string): SymbolInfo | undefined => byName?.get(n) ?? sharedLiftShapes.get(n);
738
+ const fnNamesMultidimArray = [...bareGlobalSymbols(sharedLift).keys()].some((n) => {
622
739
  const i = derivedOrMapped(n);
623
740
  return i !== undefined && i.shape === 'array' && (arrayInnerExtents(i)?.length ?? 0) > 0;
624
741
  });
625
- const declRankCands = fnNamesMultidimArray
626
- ? [...ptrElemCands, ...ptrElemCands.map((s) => ({ ...s, suffix: `${s.suffix}/flat-rank`, declRank: false }))]
627
- : ptrElemCands;
628
- // The axis chain, derived from STRUCTURING_AXES: each admitted axis doubles the list, OFF arm
629
- // first — order is load-bearing for the dropped-primary skip below (every OFF sibling
630
- // enumerates before its ON twin, so a twin's stripped-key lookup always finds a sibling that
631
- // has already run or been condemned). Each per-axis rationale lives on its table entry; both
632
- // arms are always emitted and the differ referees, never a default — the dedup below collapses
633
- // a pair wherever the axis changed nothing.
634
- const probeDefs = defOpMap(probe);
635
- type AxisCand = (typeof ptrElemCands)[number] & Record<StructuringAxis['flag'], boolean>;
636
- /** Every axis OFF seeded from the table so an added axis is one table entry and not a second
742
+ const declRankSettings = fnNamesMultidimArray
743
+ ? [
744
+ ...ptrElemSettings,
745
+ ...ptrElemSettings.map((s): typeof s => ({
746
+ ...s,
747
+ variations: [...s.variations, 'flat-rank'],
748
+ declRank: false,
749
+ })),
750
+ ]
751
+ : ptrElemSettings;
752
+ // The structure-variation chain, derived from STRUCTURE_VARIATIONS: each admitted variation doubles
753
+ // the list, the settings without it first order is load-bearing for the dropped-default skip
754
+ // below (every default sibling enumerates before the alternative that applies the variation, so
755
+ // an alternative's stripped-key lookup always finds a sibling that has already run or been
756
+ // condemned). Each variation's rationale lives on its table
757
+ // entry; both settings are always emitted and the differ referees, never a fixed default — the
758
+ // dedup below collapses a pair wherever the variation changed nothing.
759
+ const sharedLiftDefs = defOpMap(sharedLift);
760
+ type StructureSetting = (typeof ptrElemSettings)[number] & Record<StructureVariation['flag'], boolean>;
761
+ /** Every structure variation OFF — seeded from the table so an added variation is one table entry and not a second
637
762
  * hand-edited literal, in table order like everything else derived from it.
638
763
  *
639
- * WHAT THE CAST CANNOT CATCH: a `StructuringAxis['flag']` union member with NO table entry.
764
+ * WHAT THE CAST CANNOT CATCH: a `StructureVariation['flag']` union member with NO table entry.
640
765
  * `Object.fromEntries` types its result by the key type it was handed, not by the union, so the
641
766
  * assertion is taken on trust where the hand-written literal was checked. Such a member is
642
- * inert either way — every reader of these flags iterates `STRUCTURING_AXES`, so a flag with no
767
+ * inert either way — every reader of these flags iterates `STRUCTURE_VARIATIONS`, so a flag with no
643
768
  * entry is never read — but it stops being a type error and becomes an absent field. */
644
- const axisFlagsOff = Object.fromEntries(STRUCTURING_AXES.map((ax) => [ax.flag, false])) as Record<
645
- StructuringAxis['flag'],
646
- boolean
647
- >;
648
- let axisCands: AxisCand[] = declRankCands.map((s) => ({ ...s, ...axisFlagsOff }));
649
- for (const ax of STRUCTURING_AXES) {
650
- if (ax.probeGate !== undefined && !ax.probeGate(probe, probeDefs)) {
651
- opts.onAxisGated?.(ax.suffix);
769
+ const allStructureVariationsOff = Object.fromEntries(
770
+ STRUCTURE_VARIATIONS.map((variation) => [variation.flag, false]),
771
+ ) as Record<StructureVariation['flag'], boolean>;
772
+ let structureSettings: StructureSetting[] = declRankSettings.map((s) => ({ ...s, ...allStructureVariationsOff }));
773
+ for (const variation of STRUCTURE_VARIATIONS) {
774
+ if (variation.sharedGate !== undefined && !variation.sharedGate(sharedLift, sharedLiftDefs)) {
775
+ opts.onVariationGated?.(variation.name);
652
776
  continue;
653
777
  }
654
- axisCands = [
655
- ...axisCands,
656
- ...axisCands.map((s) => ({ ...s, suffix: `${s.suffix}${ax.suffix}`, [ax.flag]: true }) as AxisCand),
778
+ structureSettings = [
779
+ ...structureSettings,
780
+ ...structureSettings.map((s) => {
781
+ const on: StructureSetting = { ...s, variations: [...s.variations, variation.name] };
782
+ on[variation.flag] = true;
783
+ return on;
784
+ }),
657
785
  ];
658
786
  }
659
- /** Is this the point where NO structuring lever is on? The base-axes abort guard's other half:
660
- * at the base LIFT variant a failure here aborts the row, because it says the lift is broken
661
- * rather than that one axis cannot spell this tree.
787
+ /** Is this a setting where no structure variation is on, other than `/flip-branch`? The default-setting abort guard's
788
+ * other half: at the default LIFT setting a failure here aborts the row, because it says the lift
789
+ * is broken rather than that one variation cannot spell this tree.
662
790
  *
663
- * `s.suffix === ''` IS NOT THE SAME TEST, which is why this is a named predicate rather than
664
- * the string compare it looks like. `/flip-branch` and `/flip-join` name a branch sense
665
- * RELATIVE to the target's default, so BOTH senses are base axis points the flipped one
666
- * carries a suffix and still has every lever off. The table's own flags decide, plus the four
667
- * shape booleans that predate the table. */
668
- const isBaseAxisPoint = (s: AxisCand): boolean =>
669
- !s.anchor && !s.join && s.bitfields && s.ptrElems && s.declRank && STRUCTURING_AXES.every((ax) => !s[ax.flag]);
791
+ * `s.variations.length === 0` IS NOT THE SAME TEST, which is why this is a named predicate rather than
792
+ * the length check it looks like. `/flip-branch` names a branch sense RELATIVE to the target's
793
+ * default, so both of its senses pass this test: the flipped one carries a variation, and `sense` is
794
+ * not among the flags read here. `/flip-join` does not pass, because `join` is one of the five
795
+ * hand-carried booleans below. The table's own flags decide, plus those five. */
796
+ const isDefaultSetting = (s: StructureSetting): boolean =>
797
+ !s.anchor &&
798
+ !s.join &&
799
+ s.bitfields &&
800
+ s.ptrElems &&
801
+ s.declRank &&
802
+ STRUCTURE_VARIATIONS.every((variation) => !s[variation.flag]);
670
803
 
671
804
  const seen = new Map<string, Candidate>();
672
805
  const seenTrees = new Set<string>();
673
- /** the PRE-FAN products' own tree dedup — see the pre-fan loop for why it is not `seenTrees` */
674
- const seenPreFan = new Set<string>();
806
+ /** the pre-respell variations' own tree dedup — see their loop for why it is not `seenTrees` */
807
+ const seenPreRespell = new Set<string>();
675
808
  const out: Candidate[] = [];
676
809
  // The map-derived VALUE references one emitted tree contains, applied at every point a candidate
677
810
  // is finalized and derived from the tree that candidate emitted. No pipeline stage carries refs
678
811
  // (SFn has no such field), so a future l3 pass that rewrites the tree can never leave a stale ref
679
812
  // behind: whatever tree reaches emit is the tree the refs describe, by construction. Collected
680
- // against the FULL name-keyed map for EVERY spelling variant — the '/raw-globals' sibling drops
813
+ // against the FULL name-keyed map for EVERY candidate — the '/raw-globals' sibling drops
681
814
  // the map's shaped SPELLINGS, but its tree still NAMES pool/reloc-derived globals (ARM
682
815
  // `.word gSym`, MIPS `%lo(gSym)`), and those references need declarations in the self-declared
683
- // scoring world exactly like the named variant's (without them every raw sibling fails to compile
816
+ // scoring world exactly like the named candidate's (without them every raw sibling fails to compile
684
817
  // there, and the eval-winning raw candidate becomes unreproducible outside project headers).
685
818
  // The volatility tie-break's input, derived at the same moment as the refs and for the same
686
819
  // reason: whatever tree reaches emit is the tree it describes. Absent on a target that declares
@@ -690,8 +823,8 @@ export function enumerateCandidates(
690
823
  return n > 0 ? { deviceVolatile: n } : {};
691
824
  };
692
825
  // Every refusal is reported at most once per (name, reason): `refsOf` runs per CANDIDATE over
693
- // the same probe-derived dictionary, so without this the caller would hear the same refusal
694
- // once per spelling in the fan (hundreds of times on a wide row).
826
+ // the same dictionary derived from the shared lift, so without this the caller would hear the
827
+ // same refusal once per candidate in the fan (hundreds of times on a wide row).
695
828
  const refusalsSeen = new Set<string>();
696
829
  const refuse = (name: string, reason: RefusedDeclarationReason): void => {
697
830
  if (refusalsSeen.has(`${name}\u0000${reason}`)) {
@@ -708,159 +841,174 @@ export function enumerateCandidates(
708
841
  // worse. A union cannot — each name is declared by whichever half knows more about it.
709
842
  // SCOPE: `declSymbols` is used ONLY here. It must never reach `opts.symbols`/`baseOpts.symbols`
710
843
  // or `frontend.lift` — feeding it to the lift would turn on pool promotion, interior
711
- // attribution and the `/raw-globals` variant, which is a different (and source-moving) change.
844
+ // attribution and the `/raw-globals` variation, which is a different (and source-moving) change.
712
845
  // THREE halves now, in increasing authority: the name-only pool/reloc symbols, the array shapes
713
846
  // the asm evidences for them (raise/globalshape.ts — an `extern u16 gTbl[];` where the bare
714
847
  // spelling needs one, and the declaration a candidate spelling `gTbl[i]` cannot compile
715
848
  // without), and the project map, which knows more than either.
716
849
  const mapSymbols = baseOpts.symbols;
717
- const declSymbols = new Map<string, SymbolInfo>([...bareGlobalSymbols(probe), ...probeShapes, ...(mapSymbols ?? [])]);
850
+ const targetSymbols = new Map<string, SymbolInfo>([...bareGlobalSymbols(sharedLift), ...sharedLiftShapes]);
851
+ const declSymbols = new Map<string, SymbolInfo>([...targetSymbols, ...(mapSymbols ?? [])]);
718
852
  // The four per-enumeration constants named at the seam rather than captured across 60 lines of
719
853
  // closure (rank-declare.ts states why they belong on one object).
720
- const refsOf = makeRefCollector({ declSymbols, accessFacts, mapSymbols, refuse });
721
- // THE RE-SPELLING FAN, as a function whose PARAMETER LIST is the invariant the tree skip below
722
- // rests on: every spelling here is a pure function of the structured tree and this call's own
723
- // constants, so a tree an earlier axis point already spelled can only re-emit sources `seen`
724
- // already holds. Inline in that loop the invariant would be a comment asking future levers not to
725
- // read `fn` or the axis flags; as a signature, a lever that needs one has to widen it in front of
726
- // a reviewer. The same argument l3/ast.ts's `walkExprs` header makes for its own shape, and it
727
- // counts for more here: a lever reading `fn` would not misprint a candidate, it would DELETE one,
854
+ const refsOf = makeRefCollector({
855
+ declSymbols,
856
+ accessFacts,
857
+ mapSymbols,
858
+ targetNames: new Set(targetSymbols.keys()),
859
+ refuse,
860
+ });
861
+ // THE RESPELL SET, as a function whose PARAMETER LIST is the invariant the tree skip below
862
+ // rests on: every source here is a pure function of the structured tree and this call's own
863
+ // constants, so a tree an earlier structure setting already produced can only re-emit sources
864
+ // `seen` already holds. Inline in that loop the invariant would be a comment asking future respell
865
+ // variations not to read `fn` or the structure flags; as a signature, a variation that needs one
866
+ // has to widen it in front of a reviewer. The same argument l3/ast.ts's `walkExprs` header makes
867
+ // for its own shape, and it counts for more here: a variation reading `fn` would not misprint a
868
+ // candidate, it would DELETE one,
728
869
  // and nothing in the harness reports a candidate that was never enumerated.
729
- // `leverLabel` names the SPELLING this call is fanning, and it is a diagnostic argument only: it
730
- // reaches `onLeverError` and nothing else, so the invariant the parameter list states above —
731
- // every spelling is a pure function of the tree and this call's own constants — is untouched by
732
- // it. It exists because the pre-fan products call this on a REWRITTEN tree, where a refusal of
733
- // the primary spelling is a refusal of the rewrite, not of the row's own spelling.
870
+ // `preRespellVariations` names the tree this call re-spells, and it is a diagnostic argument only: it
871
+ // reaches `onEnumerationError` and nothing else, so the invariant the parameter list states above —
872
+ // every source is a pure function of the tree and this call's own constants — is untouched by
873
+ // it. It exists because the pre-respell variations call this on a REWRITTEN tree, where a refusal
874
+ // of the default source is a refusal of the rewrite, not of the row's own tree.
734
875
  //
735
- // IT PREFIXES EVERY `onLeverError` IN THIS FUNCTION, not just the primary emit's, and that is
736
- // the whole point rather than a detail: every one of them is reachable from both fans, and the
737
- // suffix each already carries names a LEVER, which on a pre-fan tree is a lever applied to the
738
- // rewrite. Reported without this prefix, a refusal of `/unmerge/volatile` reads as a refusal of
739
- // `/volatile` — a spelling that did not fail and is still in the fan. The order
740
- // is the candidate labels' own (`${pf.suffix}${sp.suffix}`), so a reported label and an
741
- // enumerated one name the same spelling the same way.
742
- const fanOut = (sfn: SFn, leverLabel = ''): FanResult => {
743
- // The walk→index re-spelling (l3/reindex.ts) is a THIRD lever on the same footing as
876
+ // IT PREFIXES EVERY `onEnumerationError` IN THIS FUNCTION, not just the default emit's, and that is
877
+ // the whole point rather than a detail: every one of them is reachable from both calls, and the
878
+ // variations each one already carries are, on a pre-respell tree, variations applied to the
879
+ // rewrite. Reported without this prefix, a refusal of `/unmerge/volatile` reads as a refusal
880
+ // of `/volatile` — a candidate that did not fail and is still in the fan. The order
881
+ // is the candidates' own (`[pf.name, ...sp.variations]`), so a reported name and an enumerated
882
+ // candidate's variations name the same candidate the same way.
883
+ const respellTree = (sfn: SFn, preRespellVariations: readonly Variation[] = []): TreeSources => {
884
+ // The walk→index respell variation (l3/reindex.ts) is a THIRD variation on the same footing as
744
885
  // signedness and branch sense: whether the source spelled `*p; p++` or `arr[i]` is
745
886
  // genuinely ambiguous from asm (compilers strength-reduce the latter into the former), so
746
887
  // when a loop re-spells, BOTH representations are emitted and the differ referees. The
747
- // re-spelling passes the same boundary contracts as the primary; one that fails them is
888
+ // respelled tree passes the same boundary contracts as the default; one that fails them is
748
889
  // dropped here — never scored, never able to win.
749
- const spellings: Spelling[] = [];
750
- // The PRIMARY spelling takes the same posture as every re-spelling below: a backend that
751
- // declines by throwing costs this tree — its primary and the re-spellings built from it —
752
- // never the row. The opposite posture from the STRUCTURING refusal below, which aborts the
753
- // row at the base point, and for the reason that separates them: that one says the lift is
890
+ const sources: TreeSource[] = [];
891
+ // The DEFAULT source takes the same posture as every respell variation below: a backend that
892
+ // declines by throwing costs this tree — its default source and the respelled sources built
893
+ // from it — never the row. The opposite posture from the STRUCTURE refusal below, which aborts
894
+ // the row at the default setting, and for the reason that separates them: that one says the lift is
754
895
  // broken, this one that the target language has no spelling for a tree the lift got right
755
896
  // (structuring is language-neutral, and the signedness pins it inserts are `cast` nodes the
756
897
  // Pascal backend loud-declines). Refusing EVERY tree is still loud — the empty-enumeration
757
898
  // check at the end raises the last refusal.
758
899
  try {
759
- spellings.push({ suffix: '', source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn) });
900
+ sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn) });
760
901
  } catch (e) {
761
- opts.onLeverError?.(name + leverLabel, firstLine(e));
762
- return { spellings, emit: { error: e } };
902
+ reportThrow(preRespellVariations, e);
903
+ return { sources, emit: { error: e } };
763
904
  }
764
- // Representation re-spellings — each a lever on the same footing as signedness/branch sense,
765
- // each guarded: it must pass the same boundary contracts as the primary AND emit (a backend
766
- // that declines by throwing — Pascal loud-fails unspellable shapes — drops the candidate,
767
- // never aborts the enumeration). A dropped re-spelling loses nothing: the primary remains.
905
+ // Respell variations — each on the same footing as signedness/branch sense, each guarded:
906
+ // it must pass the same boundary contracts as the default AND emit (a backend that declines
907
+ // by throwing — Pascal loud-fails unspellable shapes — drops the candidate, never aborts the
908
+ // enumeration). A dropped respell variation loses nothing: the default remains.
768
909
  //
769
- // POLICY: re-spellings derive from the BASE spelling only levers do not compose by
770
- // default. FOUR product mechanisms are sanctioned, each with its own admission bar — the
771
- // three below, which all derive from or compose onto a spelling, plus the PRE-FAN products
772
- // (PRE_FAN_PRODUCTS, applied to the TREE before this fan runs over it; its admission bar is
773
- // stated at the table)
774
- // plus ALTERNATIVE OUTPUTS: one lever whose single application has several legitimate
910
+ // POLICY: respell variations derive from the tree they are handed, un-respelled (its default
911
+ // source), only they do not compose unless
912
+ // one of these compositions sanctions it, each with its own admission bar: `/volatile`
913
+ // narrowed onto a variation's own locals, the STACKED variations and the PAIRINGS, which all
914
+ // derive from or compose onto a source, plus the PRE-RESPELL variations (PRE_RESPELL_VARIATIONS,
915
+ // applied to the TREE before this respell set runs over it; its admission bar is stated at the
916
+ // table) —
917
+ // plus MULTI-RESULT variations: one variation whose single application has several legitimate
775
918
  // results (which locals a coalesce merges, which pointers /volatile qualifies) emits
776
- // each as its own candidate via `enumerate`, capped at the lever, with the base spelling
777
- // retained; outputs may also ride an already-sanctioned product (the /livebase/volatile
778
- // subsets), since they add no new lever to the composition.
779
- // Products with /volatile go only onto a lever whose re-spelling CENTRES ON a
780
- // numeric-address pointer local — the joint spelling is reachable from neither lever
781
- // alone, each product narrows /volatile to the lever's own locals (volatilePtrLocals'
782
- // `only`), and each needed a row to demand it. The SHAPE products (SHAPE_PRODUCTS) are
783
- // derived onto EVERY spelling: statement order/shape is orthogonal to what any
784
- // representation lever changes — the same kind of independent dimension as signedness —
785
- // so they compose as an axis rather than a pairing; a third blanket product needs the
786
- // same argument, not just a row. And a specific LEVER PAIRING is admitted on one of two
919
+ // each as its own candidate via `respellEach`, capped at the variation, with the default
920
+ // retained; the results may also ride an already-sanctioned composition (the /livebase/volatile
921
+ // subsets), since they add no new variation to it.
922
+ // `/volatile` composes only onto a variation whose output CENTRES ON a
923
+ // numeric-address pointer local — the joint spelling is reachable from neither variation
924
+ // alone, each composition narrows /volatile to that variation's own locals (volatilePtrLocals'
925
+ // `only`), and each needed a row to demand it. The STACKED variations (STACKED_VARIATIONS) are
926
+ // derived onto EVERY source: statement order/shape is orthogonal to what any other respell
927
+ // variation changes — the same kind of independent dimension as signedness —
928
+ // so they are crossed with every source rather than paired; a third blanket composition needs
929
+ // the same argument, not just a row. And a specific PAIRING is admitted on one of two
787
930
  // grounds, never on "it might help". FIRST, a row demands the joint spelling AND that
788
- // spelling is reachable from neither lever alone: /livebase × /indexed, × /sinkinit,
789
- // × /nearbase and × /coalesce, plus /vol-store × /unreduce and that pair × /ptr-field —
931
+ // spelling is reachable from neither variation alone: /livebase × /indexed, × /sinkinit,
932
+ // × /nearbase and × /coalesce, /scopebase × /coalesce, plus /vol-store × /unreduce and that
933
+ // pair × /ptr-field —
790
934
  // each with its demanding row at the respell site. (A TRIPLE is admitted on the same ground
791
935
  // and no weaker one: it is one joint spelling with one demanding row, and the pairs BELOW it
792
936
  // are not thereby admitted — on synthetic:dmaptrsrc the two intermediate pairs measure 27 and
793
937
  // 32 against the triple's 0, and neither is in the fan.) SECOND, a
794
- // lever COMMITS a policy the differ would otherwise never see — /nearbase × /sinkinit,
938
+ // variation COMMITS a policy the differ would otherwise never see — /nearbase × /sinkinit,
795
939
  // where `l3/nearbase.ts` picks one of two init orderings inside the pass, so without the
796
- // pairing that choice decides a match with no candidate beside it to lose to. The second
797
- // ground is narrower than it looks: it needs a committed choice INSIDE a lever with an
798
- // existing lever that expresses the alternative, not a lever one could imagine wanting
799
- // twice. Anything else stays un-composed. A pairing is admitted for a LEVER, so it fans
800
- // over that lever's whole admission table (LIVEBASE_ADMISSIONS): a roster row changes
801
- // which bases the same hoist binds, not what pairing it with /coalesce means.
802
- // And a lever must PRESERVE SEMANTICS by construction: the differ referees byte-exactness
803
- // (a wrong candidate can never fake a score-0 match), but on a NONMATCH row the best-
804
- // scoring source is shown to the user — a semantically-wrong re-spelling there is
940
+ // pairing that decision decides a match with no candidate beside it to lose to. The second
941
+ // ground is narrower than it looks: it needs a committed decision INSIDE a variation with an
942
+ // existing variation that expresses the alternative, not a variation one could imagine wanting
943
+ // twice. Anything else stays un-composed. A pairing is admitted for the hoist mechanism, not per
944
+ // entry, so it runs over every LIVEBASE_HOISTS entry that sets `pairings`: adding an entry
945
+ // changes which bases get bound, not what pairing a hoist with /coalesce means.
946
+ // And a respell variation must PRESERVE SEMANTICS by construction: the differ referees
947
+ // byte-exactness (a wrong candidate can never fake a score-0 match), but on a NONMATCH row the
948
+ // best-scoring source is shown to the user — a semantically-wrong respelled source there is
805
949
  // plausible-but-wrong output, the defect class this project exists to avoid. THE ONE
806
- // EXCEPTION IS THE SAME RULE READ FORWARD: where a lever cannot establish its semantics from
950
+ // EXCEPTION IS THE SAME RULE READ FORWARD: where a variation cannot establish its semantics from
807
951
  // inside the pass — `l3/unreduce.ts` moving a read into a loop whose device stores may make
808
952
  // the DEVICE write memory — the spelling is marked `Candidate.matchOnly` and published ONLY
809
953
  // at a byte-exact score, which is the clause in brackets above used as a licence instead of a
810
954
  // consolation. It is never shown as a best-effort answer, so the nonmatch case the sentence
811
955
  // is about cannot arise. Hence each
812
- // lever's decline-over-approximate gates, adversarially audited.
813
- // Takes a THUNK, so the lever's own computation is inside the try too. A lever that threw
814
- // from the pass itself — rather than from the contracts or the backend — would escape and
815
- // abort the whole enumeration for this row, primary included: the one way a lever can cost
816
- // a match. Making that structural rather than per-call-site means no lever can opt out.
956
+ // variation's decline-over-approximate gates, adversarially audited.
957
+ // Takes a THUNK, so the variation's own computation is inside the try too. A variation that
958
+ // threw from the pass itself — rather than from the contracts or the backend — would escape and
959
+ // abort the whole enumeration for this row, default included: the one way a variation can cost
960
+ // a match. Making that structural rather than per-call-site means no variation can opt out.
817
961
  //
818
- // WHICH boundary contracts run here, and why it is three of the four. A lever gets the
819
- // tree `structureChecked` already validated, so what these re-check is what a LEVER can
962
+ // WHICH boundary contracts run here, and why it is three of the four. A respell variation gets
963
+ // the tree `structureChecked` already validated, so what these re-check is what a VARIATION can
820
964
  // break, not what structuring can. `assertResolved` and `assertDerefsTyped` catch an
821
965
  // unspellable tree — a candidate the compiler would reject, which the harness would report
822
- // as a dropped spelling with no cause. `assertLocalsWritten` catches the one wrongness the
966
+ // as a dropped candidate with no cause. `assertLocalsWritten` catches the one wrongness the
823
967
  // differ REWARDS: a pass that moves or suppresses an assignment and never emits it leaves
824
968
  // the reads standing over whatever the allocator left behind, and that candidate compiles,
825
- // scores, and can win (the shape #106 shipped). Levers that place a def — l3/sinkinit.ts,
826
- // l3/basecse.ts's first-use policy, l3/nearbase.ts, l3/scopebase.ts, l3/argbase.ts — are
827
- // exactly the population that can produce it, so the check belongs on every lever tree
828
- // rather than on theirs. It cost nothing when measured: 0 violations over the 34357 trees the
969
+ // scores, and can win (the shape #106 shipped). Respell variations that place a def —
970
+ // l3/sinkinit.ts, l3/basecse.ts's first-use policy, l3/nearbase.ts, l3/scopebase.ts,
971
+ // l3/argbase.ts — are exactly the ones that can produce it, so the check belongs on every
972
+ // respelled tree rather than on theirs. It cost nothing when measured: 0 violations over the 34357 trees the
829
973
  // artifact's agbcc rows enumerated in both symbol-map configurations. A count with no commit
830
974
  // stamp — re-run it rather than reading it as today's.
831
975
  // `assertEffectsPreserved` is the fourth and is NOT here: it needs the L1 `fn`, and
832
- // `fanOut`'s parameter list is the invariant the tree-dedup skip rests on (see its header).
976
+ // `respellTree`'s parameter list is the invariant the tree-dedup skip rests on (see its header).
833
977
  // Widening it for a contract is a defensible change and an argued one — not a silent import.
834
- // A lever returns its tree, or `{ sfn, needsProof }` when it cannot establish its own
978
+ // A respell variation returns its tree, or `{ sfn, needsProof }` when it cannot establish its own
835
979
  // semantics from inside the pass (Candidate.matchOnly carries the argument).
836
- const respell = (suffix: string, make: () => LeverResult, alreadyShaped = false): void => {
980
+ const respell = (variations: readonly Variation[], make: () => RespellResult, alreadyShaped = false): void => {
981
+ if (!offeredOn(target, variations)) {
982
+ return;
983
+ }
837
984
  try {
838
985
  const made = make();
839
986
  if (!made) {
840
- return; // the lever declined to fire — no candidate, not a duplicate of the primary
987
+ return; // the variation declined to fire — no candidate, not a duplicate of the default
841
988
  }
842
989
  const alt = 'sfn' in made ? made.sfn : made;
843
990
  const proof: { matchOnly?: true } = 'sfn' in made && made.needsProof ? { matchOnly: true } : {};
844
991
  assertResolved(alt);
845
992
  assertDerefsTyped(alt);
846
993
  assertLocalsWritten(alt);
847
- spellings.push({ suffix, source: backend.emit(alt), ...refsOf(alt), ...volOf(alt), ...proof });
848
- // STATEMENT-SHAPE products, derived onto EVERY spelling the second sanctioned
849
- // product mechanism (the POLICY note above carries the admission argument). Each is
850
- // a statement-order/shape fact orthogonal to representation; subsets compose in the
851
- // fixed order below. A shape that never fires declines and costs nothing.
994
+ assertNoOrphanedLocals(sfn, alt);
995
+ sources.push({ variations, source: backend.emit(alt), ...refsOf(alt), ...volOf(alt), ...proof });
996
+ // STACKED variations, derived onto EVERY source (the POLICY note above carries the
997
+ // admission argument). Each is a statement-order/shape fact orthogonal to
998
+ // representation; subsets compose in the fixed order below. A stacked variation that
999
+ // never fires declines and costs nothing.
852
1000
  if (!alreadyShaped) {
853
- // A shape REORDERS statements, and it is derived after a lever has placed its defs — so
854
- // the placement is re-checked on the shaped tree (contracts.ts). Differential: judged
855
- // only where the unshaped tree already satisfied the walk, so a lever whose placement it
856
- // never described is not dropped on the strength of a model that does not apply.
1001
+ // A shape REORDERS statements, and it is derived after a variation has placed its defs —
1002
+ // so the placement is re-checked on the shaped tree (contracts.ts). Differential: judged
1003
+ // only where the unshaped tree already satisfied the walk, so a variation whose placement
1004
+ // it never described is not dropped on the strength of a model that does not apply.
857
1005
  //
858
- // `minted` is a NAME DIFF, so for a RENAMING lever (`/regspell`, `/merge-names`) it also
859
- // holds locals the lever never PLACED. Harmless and deliberate: the differential's
1006
+ // `minted` is a NAME DIFF, so for a RENAMING variation (`/regspell`, `/merge-names`) it
1007
+ // also holds locals the variation never PLACED. Harmless and deliberate: the differential's
860
1008
  // early return absorbs a name the unshaped tree already fails on, and a renamed local
861
1009
  // whose def a shape moved below a read is the same wrongness as a placed one.
862
1010
  //
863
- // KNOWN GAP on the other side of the same diff: a lever that RELOCATES a local it did not
1011
+ // KNOWN GAP on the other side of the same diff: a variation that RELOCATES a local it did not
864
1012
  // mint contributes no name, so the shape differential does not judge it. The `scope`
865
1013
  // placement is the one that does this — it sinks the run `structureChecked` already
866
1014
  // committed (l3/basecse.ts judges those itself, over the placer's own report of the
@@ -870,20 +1018,21 @@ export function enumerateCandidates(
870
1018
  // which moves only const or pure-read assigns and so cannot lift a read of a base local
871
1019
  // above its init.
872
1020
  const minted = createdLocals(sfn, alt);
873
- for (const subset of SHAPE_SUBSETS) {
1021
+ for (const subset of STACKED_SUBSETS) {
874
1022
  // ONE TRY PER SHAPE — a shape is its own candidate and fails as its own candidate.
875
- // Sharing the lever's outer try would let a throw deriving one subset discard every
876
- // later one, under a label (the base lever's suffix) that names no shape at all.
877
- const shapeSuffix = subset.map((x) => x.suffix).join('');
1023
+ // Sharing the respell variation's outer try would let a throw deriving one subset
1024
+ // discard every later one, under that variation's name, which names no shape.
1025
+ const shapeVariations = subset.map((x) => x.name);
878
1026
  try {
879
- const shaped = applyShapes(subset, alt);
1027
+ const shaped = applyStacked(subset, alt);
880
1028
  if (shaped !== null) {
881
1029
  assertResolved(shaped.out);
882
1030
  assertDerefsTyped(shaped.out);
883
1031
  assertLocalsWritten(shaped.out);
1032
+ assertNoOrphanedLocals(alt, shaped.out);
884
1033
  assertPlacementSurvives(alt, shaped.out, minted);
885
- spellings.push({
886
- suffix: `${suffix}${shaped.suffix}`,
1034
+ sources.push({
1035
+ variations: [...variations, ...shaped.variations],
887
1036
  source: backend.emit(shaped.out),
888
1037
  ...refsOf(shaped.out),
889
1038
  ...volOf(shaped.out),
@@ -892,61 +1041,60 @@ export function enumerateCandidates(
892
1041
  });
893
1042
  }
894
1043
  } catch (e) {
895
- opts.onLeverError?.(name + leverLabel + suffix + shapeSuffix, firstLine(e));
1044
+ reportThrow([...preRespellVariations, ...variations, ...shapeVariations], e);
896
1045
  }
897
1046
  }
898
1047
  }
899
1048
  } catch (e) {
900
- // A throwing lever, a contract failure, or an unspellable re-spelling: keep the primary.
901
- // REPORTED, not swallowed. `dropped` (below) records only spellings the SCORER refused,
902
- // so without this a lever that fails here vanishes with no trace — indistinguishable
1049
+ // A throwing variation, a contract failure, or an unspellable respelled tree: keep the
1050
+ // default. REPORTED, not swallowed. `dropped` (below) records only candidates the SCORER
1051
+ // refused, so without this a variation that fails here vanishes with no trace — indistinguishable
903
1052
  // from one that correctly declined, which is exactly the hidden failure
904
1053
  // DroppedCandidate exists to surface.
905
- opts.onLeverError?.(name + leverLabel + suffix, firstLine(e));
1054
+ reportThrow([...preRespellVariations, ...variations], e);
906
1055
  }
907
1056
  };
908
- // `/argbase` — name a call's argument bases before the call (l3/argbase.ts). A lever on the
909
- // same footing as the others: the primary inline spelling stays in the list, so the differ
1057
+ // `/argbase` — name a call's argument bases before the call (l3/argbase.ts). A variation on the
1058
+ // same footing as the others: the default inline spelling stays in the list, so the differ
910
1059
  // referees and this can never cost a match.
911
- for (const subset of SHAPE_SUBSETS) {
912
- // the truthful suffix needs the pass to RUN first, so this bypasses respell's
913
- // label-then-thunk shape: same try posture, label from the fired members
1060
+ for (const subset of STACKED_SUBSETS) {
1061
+ // the truthful variations need the pass to RUN first, so this bypasses respell's
1062
+ // variations-then-thunk shape: same try posture, variations from the fired members
914
1063
  try {
915
- const shaped = applyShapes(subset, sfn);
1064
+ const shaped = applyStacked(subset, sfn);
916
1065
  if (shaped !== null) {
917
- // the ONE call whose suffix already names shapes — say so, rather than making `respell`
918
- // read it back out of the label it was handed
919
- respell(shaped.suffix, () => shaped.out, true);
1066
+ // the ONE call whose variations already name shapes — say so, rather than making `respell`
1067
+ // read them back out of the variations it was handed
1068
+ respell(shaped.variations, () => shaped.out, true);
920
1069
  }
921
1070
  } catch (e) {
922
- // the error label falls back to the full subset — the fired set is unknown mid-throw
923
- const label = subset.map((x) => x.suffix).join('');
924
- opts.onLeverError?.(name + leverLabel + label, firstLine(e));
1071
+ // the report names the full subset — the fired set is unknown mid-throw
1072
+ reportThrow([...preRespellVariations, ...subset.map((x) => x.name)], e);
925
1073
  }
926
1074
  }
927
- respell('/argbase', () => materializeArgBases(sfn));
1075
+ respell(['argbase'], () => materializeArgBases(sfn));
928
1076
  // `/zerosub` — spell a negate of a SHARED subtraction as `0 - x` (l3/zerosub.ts). gcc 2.9
929
1077
  // folds `-(a - b)` into `(b - a)` before CSE but leaves `0 - (a - b)` as a negate of the
930
1078
  // subtraction itself, so over a value the function also uses elsewhere the two spellings are
931
1079
  // a computation and a register apart — and both are reachable from a real source. The differ
932
1080
  // referees; its gate keeps it off every shape where the fold rule does not apply, which is
933
1081
  // every operand but a shared subtraction.
934
- respell('/zerosub', () => zeroSubNegates(sfn));
1082
+ respell(['zerosub'], () => zeroSubNegates(sfn));
935
1083
  // `/volatile` — declare a pointer local holding a NUMERIC address as pointing to volatile
936
1084
  // data (l3/volatileptr.ts). A raw constant has no declaration anywhere, so the original
937
1085
  // qualifier is not derivable — and it is codegen-visible (a volatile MEM is barred from
938
1086
  // motion, which lands the allocator on different homes). Both spellings are emitted and
939
1087
  // the differ referees.
940
- respell('/volatile', () => volatilePtrLocals(sfn));
1088
+ respell(['volatile'], () => volatilePtrLocals(sfn));
941
1089
  // `/vol-slot` — declare a STACK-HOMED scalar local volatile (l3/volatileval.ts). The
942
1090
  // qualifier takes away the allocator's freedom to keep the value in a callee-saved
943
1091
  // register across a call, and which of the three ways a slot can arise (a volatile local,
944
1092
  // an address-taken one, plain register pressure) the source used is not derivable from
945
- // the asm. A DECLARATION lever, not a structuring axis (docs/level-tower.md's third
946
- // fork): it changes nothing structure() decides, so it rides the base spelling like its
947
- // `/volatile` sibling rather than doubling every enumeration, and its frame-flag gate
1093
+ // the asm. A DECLARATION respell variation, not a structure variation (docs/level-tower.md's
1094
+ // third fork): it changes nothing structure() decides, so it rides each structured tree as `structure()`
1095
+ // produced it, like its `/volatile` sibling, rather than doubling every enumeration, and its frame-flag gate
948
1096
  // costs nothing on a function with no slot.
949
- respell('/vol-slot', () => volatileValueLocals(sfn));
1097
+ respell(['vol-slot'], () => volatileValueLocals(sfn));
950
1098
  /** `/vol-store`'s pass with the target's device-register window handed over — the window that
951
1099
  * keeps it off ordinary memory. Written once because five call sites take it. */
952
1100
  const volStore = (from: SFn): SFn | null => volatileDeviceStores(from, target.capabilities.deviceRegisters);
@@ -957,7 +1105,7 @@ export function enumerateCandidates(
957
1105
  // hoists an unpinned fixed-address store clean out of a loop (gcc/loop.c:8934), so the pinned
958
1106
  // spelling is the only one that reproduces a device-driving loop body at all. Its window gate
959
1107
  // is the target's own `deviceRegisters` range, which is what keeps it off ordinary memory.
960
- respell('/vol-store', () => volStore(sfn));
1108
+ respell(['vol-store'], () => volStore(sfn));
961
1109
  /** `/unreduce` with both halves of the device model handed over — the SPELLING range and the
962
1110
  * MEMORY-MODEL trigger list (target.ts). Written once because three call sites take it. */
963
1111
  const unreduced = (from: SFn): UnreduceResult | null =>
@@ -968,177 +1116,199 @@ export function enumerateCandidates(
968
1116
  // reaches a preheader slot no C statement can (a compiler-created giv init is inserted after
969
1117
  // the invariant hoist, gcc/loop.c:1151 then :1173). The scalar-value sibling of `/indexed`,
970
1118
  // which makes the same argument for a pointer walk.
971
- respell('/unreduce', () => unreduced(sfn));
1119
+ respell(['unreduce'], () => unreduced(sfn));
972
1120
  // `/ptr-field` — declare a recovered WORD field a pointer (l3/ptrfield.ts). raise/structs.ts
973
1121
  // types a field from the access width alone, and on a 32-bit target `void *` fits that
974
1122
  // evidence exactly — but not the compiler's alias analysis, which is what lets a pointer
975
1123
  // field's load leave a loop an `s32` store pins it inside. Both are enumerated.
976
- respell('/ptr-field', () => pointerFields(sfn));
1124
+ respell(['ptr-field'], () => pointerFields(sfn));
977
1125
  // `/offmember` — spell a leaf base's constant subscript as a struct MEMBER (l3/offmember.ts),
978
1126
  // so the offset stays in the load's displacement instead of folding into the pool literal.
979
1127
  // The SECOND source of the shape `/basefold` already reads: that row answers the same
980
1128
  // evidence with a named base, this one with an aggregate member, and the two are different C
981
- // and different register pressure. Offered only where the target declares the fold MIPS and
982
- // PPC put the addend in the instruction by construction, so nothing there says a member put
983
- // it there, exactly as with BASEFOLD_ADMISSIONS above.
984
- if (target.compilerBehaviors.foldsConstAddrOffset) {
985
- respell('/offmember', () => spellOperandMembers(sfn));
986
- }
1129
+ // and different register pressure. Offered only where the target declares the fold (its registry
1130
+ // entry's target gate, which `respell` asks): MIPS and PPC put the addend in the instruction by
1131
+ // construction, so nothing there says a member put it there, exactly as with BASEFOLD_HOISTS.
1132
+ respell(['offmember'], () => spellOperandMembers(sfn));
987
1133
  // The `/vol-store` × `/unreduce` PAIRING — row-demanded (synthetic:dmafill), and the joint
988
- // spelling is reachable from neither lever alone: pinning the stores keeps three of them in
1134
+ // spelling is reachable from neither variation alone: pinning the stores keeps three of them in
989
1135
  // the loop body, which is what makes the loop's register pressure — and so the placement of
990
1136
  // the induction init — observable at all. Alone the two score 19 and 34 against the row's own
991
1137
  // 30; together, 0. The TRIPLE adds `/ptr-field` for synthetic:dmaptrsrc, whose closed form
992
1138
  // reads a struct field the un-reduce puts back inside the loop: 27 · 35 · 42 alone, 0
993
1139
  // together. The intermediate pairs are not admitted, and the reason is that NO ROW DEMANDS
994
1140
  // ONE — neither could win where they are reachable: compiled on synthetic:dmaptrsrc, VT TIES
995
- // `/vol-store`'s 27 and RT LOSES to it at 32. (An earlier version of this note said "each
996
- // scores worse than a lever already on the roster", which is loose twice over: VT ties rather
997
- // than loses, and RT's 32 beats the ADMITTED standalone `/unreduce`'s 35. Ranking the pairs
998
- // against the BEST already-admitted spelling is the comparison that holds.)
1141
+ // `/vol-store`'s 27 and RT LOSES to it at 32. Rank a pair against the BEST already-admitted
1142
+ // candidate (on synthetic:dmaptrsrc the triple, at 0), not against any admitted variation: VT
1143
+ // only ties `/vol-store`'s 27, and RT's 32 beats the admitted standalone `/unreduce`'s 35, so
1144
+ // "worse than an admitted variation" would exclude neither.
999
1145
  //
1000
- // WHAT THE STANDALONE LINES COST, since neither of the two levers ever wins an artifact row
1146
+ // WHAT THE STANDALONE LINES COST, since neither of the two variations ever wins an artifact row
1001
1147
  // ALONE — every `/unreduce` and `/ptr-field` winner rides inside a `/vol-store` pairing, which
1002
- // is the property `apps/benchmark/test/census.test.ts` asserts rather than the count it used
1003
- // to quote here. They are kept because a lever has to be able to LOSE on its own terms: the
1004
- // admission posture (compareScored orders by score) is what makes a wrong re-spelling
1005
- // harmless, and it is only observable when the single-lever spelling is in the fan —
1148
+ // is the property `apps/benchmark/test/census.test.ts` asserts. They are kept because a
1149
+ // variation has to be able to LOSE on its own terms: the admission posture (compareScored orders
1150
+ // by score) is what makes a wrong respelled source harmless, and it is only observable when the
1151
+ // single-variation candidate is in the fan —
1006
1152
  // `synthetic:dmastride` exists to show exactly that for `/unreduce`, at 33 against its match.
1007
1153
  //
1008
1154
  // AND THE SUBSET APPLIER IS NOT THE RIGHT MECHANISM HERE, though it looks like it: rebuilding
1009
- // this as a SHAPE_SUBSETS-style table would admit VT and RT by construction, because
1010
- // `applyShapes` is SKIP-ON-DECLINE and would emit "everything that fired" on any tree where
1011
- // one of the three declines. That is the property the shape products are designed around and
1155
+ // this as a STACKED_SUBSETS-style table would admit VT and RT by construction, because
1156
+ // `applyStacked` is SKIP-ON-DECLINE and would emit "everything that fired" on any tree where
1157
+ // one of the three declines. That is the property the stacked variations are designed around and
1012
1158
  // the one the pairing policy forbids — a pair reaches the fan only when a row demands it.
1013
1159
  //
1014
- // Both compose through `composeLevers`, which carries `/unreduce`'s proof obligation across
1160
+ // Both compose through `composeRespellVariations`, which carries `/unreduce`'s proof obligation across
1015
1161
  // the stages after it — hand-writing that carry made dropping it a type-correct edit.
1016
- respell('/vol-store/unreduce', () => composeLevers(sfn, [volStore, unreduced]));
1017
- respell('/vol-store/unreduce/ptr-field', () => composeLevers(sfn, [volStore, unreduced, pointerFields]));
1162
+ respell(['vol-store', 'unreduce'], () => composeRespellVariations(sfn, [volStore, unreduced]));
1163
+ respell(['vol-store', 'unreduce', 'ptr-field'], () =>
1164
+ composeRespellVariations(sfn, [volStore, unreduced, pointerFields]),
1165
+ );
1018
1166
  // `/inlinebase` — spell a CONSTANT-address pointer local at its uses instead
1019
1167
  // (l3/inlinebase.ts). The local is structure/analysis.ts's value home for a `const` the
1020
1168
  // asm kept in a callee-saved register across a call; the register is real, but a constant
1021
1169
  // re-spelled per use is CSEd back into that same one, so which the source had is not
1022
1170
  // derivable. Its own bare-`const`-initializer gate keeps it off l3/basecse.ts's reuse
1023
- // hoists, whose placement levers already answer that question.
1171
+ // hoists, whose placement variations already answer that question.
1024
1172
  //
1025
- // TWO ALTERNATIVE OUTPUTS, not a product: deleting the local also deletes the only place
1173
+ // TWO RESULTS, not a composition: deleting the local also deletes the only place
1026
1174
  // a `volatile` POINTEE could be written, and a raw address has no declaration anywhere
1027
1175
  // else to carry it. So the qualified spelling is emitted too, `/volatile` narrowed to
1028
- // exactly the locals this lever deletes. Usually the bytes separate them and the score
1176
+ // exactly the locals this variation deletes. Usually the bytes separate them and the score
1029
1177
  // decides (11 against 12 on pokeemerald:EReader_Reset), but where the compiler was not
1030
1178
  // exploiting the non-volatility they are byte-identical — as they are on that row's
1031
1179
  // WINNING shape, the one that also qualifies the slot — and `compareScored`'s device-
1032
- // volatility term picks the qualified twin, 0x4000208 being REG_IME.
1180
+ // volatility term picks the qualified candidate, 0x4000208 being REG_IME.
1033
1181
  //
1034
1182
  // COST — it fires broadly: on 33 of the 69 klonoa functions that lift with no symbol map
1035
1183
  // (a symbol-map sweep sees fewer, since an absolute pool constant lifts to a `gaddr`
1036
1184
  // there). Both outputs together add 766 candidates over 47058, +1.6%, and up to +67% on
1037
1185
  // one function (EntityPositionFromLevelTable) — the same class of price the enumeration
1038
- // already pays for `/volatile`, and cheaper than the axis over the same question would
1039
- // be — the choice the lever's header argues. `/vol-slot` adds nothing at all there: no
1186
+ // already pays for `/volatile`, and cheaper than a structure variation over the same question
1187
+ // would be — the decision the variation's header argues. `/vol-slot` adds nothing at all there: no
1040
1188
  // klonoa function reaches its frame gate.
1041
1189
  const inlineVolatile = (): SFn | null => {
1042
1190
  const only = new Set(inlinableConstBases(sfn));
1043
1191
  const q = only.size ? volatilePtrLocals(sfn, only) : null;
1044
1192
  return q ? inlineConstBases(q) : null;
1045
1193
  };
1046
- respell('/inlinebase/volatile', inlineVolatile);
1047
- respell('/inlinebase', () => inlineConstBases(sfn));
1194
+ respell(['inlinebase', 'volatile'], inlineVolatile);
1195
+ respell(['inlinebase'], () => inlineConstBases(sfn));
1048
1196
  // The `/inlinebase` × `/vol-slot` PAIRING — row-demanded, and the joint spelling is
1049
- // reachable from neither lever alone: on pokeemerald:EReader_Reset the primary scores 11,
1197
+ // reachable from neither variation alone: on pokeemerald:EReader_Reset the default scores 11,
1050
1198
  // `/inlinebase` alone 11 and `/vol-slot` alone 2, and the pair 0. The two touch disjoint
1051
1199
  // locals (one pointer-typed, one a scalar frame slot), so applying them in either order
1052
1200
  // gives the same spelling — and each of `/inlinebase`'s two outputs carries it.
1053
- respell('/inlinebase/volatile/vol-slot', () => {
1201
+ respell(['inlinebase', 'volatile', 'vol-slot'], () => {
1054
1202
  const r = inlineVolatile();
1055
1203
  return r ? volatileValueLocals(r) : null;
1056
1204
  });
1057
- respell('/inlinebase/vol-slot', () => {
1205
+ respell(['inlinebase', 'vol-slot'], () => {
1058
1206
  const r = inlineConstBases(sfn);
1059
1207
  return r ? volatileValueLocals(r) : null;
1060
1208
  });
1061
1209
  // `/scopebase` — name a reused global base at the INNERMOST scope holding its uses
1062
- // (l3/scopebase.ts). Distinct from basecse's function-top hoist, which the primary already
1210
+ // (l3/scopebase.ts). Distinct from basecse's function-top hoist, which the default already
1063
1211
  // carries: this one fires exactly where that placement would extend a live range the
1064
1212
  // original never had.
1065
- // `/scopebase`, and its COALESCED variants. Which locals a register allocator shared is not
1213
+ // `/scopebase`, and its COALESCED results. Which locals a register allocator shared is not
1066
1214
  // derivable from the tree — on the row this was built for the two legal merges score 18 and
1067
1215
  // 40 against a no-merge 21, so committing to one by declaration order costs 19 points and
1068
- // discards the winner. Every variant is emitted and the differ referees, exactly as
1069
- // `/regcopy` does for its allocator-ambiguous tail choice.
1216
+ // discards the winner. Every result is emitted and the differ referees, exactly as
1217
+ // `/regcopy` does for its allocator-ambiguous tail decision.
1070
1218
  //
1071
- // POLICY NOTE: rank.ts's rule is that re-spellings derive from the BASE spelling only —
1072
- // levers do not compose. These are not a second lever composed onto the first: coalescing is
1073
- // enumerated as alternative OUTPUTS of the base hoist, in the one place that knows the hoist
1074
- // just happened. The un-coalesced `/scopebase` stays in the list, so nothing is lost.
1219
+ // POLICY NOTE: this is a PAIRING, `/scopebase` × `/coalesce`, admitted for the row above, which
1220
+ // demands the joint spelling (18 against the no-merge 21). `respellEach` makes the coalesce's
1221
+ // results from the tree `/scopebase` produced, in the one place that knows the hoist happened.
1222
+ // Unlike the `/livebase` × `/coalesce` pairings it takes `coalesceCandidates`' whole merge set,
1223
+ // not the arm-disjoint subset. The name lists both variations because both were applied. A
1224
+ // candidate's variations name what was applied, not a route a deletion must remove: these
1225
+ // results are minted by their own `respellEach` call, so deleting `respell(['scopebase'], …)` does
1226
+ // not delete them. The un-coalesced `/scopebase` stays in the list, so nothing is lost.
1075
1227
  //
1076
1228
  // EVERY pass invocation stays INSIDE a thunk — see the paragraph above on why a pass that
1077
- // runs outside `respell`'s try is the one way a lever can cost a match. `enumerate` re-runs
1229
+ // runs outside `respell`'s try is the one way a variation can cost a match. `respellEach` re-runs
1078
1230
  // the hoist per candidate, which is pure and cheap, rather than caching it outside the guard.
1079
- respell('/scopebase', () => hoistScopedBases(sfn));
1231
+ respell(['scopebase'], () => hoistScopedBases(sfn));
1080
1232
  // `/regionbase` — the same pass under its second region rule: a base the source spells inside N
1081
- // disjoint regions becomes N locals, one per region, rather than one at function scope. A LEVER
1082
- // beside `/scopebase`, not a replacement for it: both spellings and the un-hoisted primary stay
1233
+ // disjoint regions becomes N locals, one per region, rather than one at function scope. A VARIATION
1234
+ // beside `/scopebase`, not a replacement for it: both spellings and the un-hoisted default stay
1083
1235
  // in the list, so the differ settles which allocation the original had.
1084
1236
  const regionbase = (): SFn | null => hoistScopedBases(sfn, { regions: 'per-region' });
1085
- respell('/regionbase', regionbase);
1086
- // …and its `/volatile` PRODUCT, narrowed to exactly the locals this lever mints — the same
1087
- // pairing `/livebase` and `/inlinebase` already carry, for the same reason. The shape this
1088
- // lever exists for is a DEVICE base (the DMA block at 0x040000D4), and the project's own
1089
- // reference spells it `vu32 *dmaRegs`; without the product every region local this lever wins
1090
- // with is published UNqualified, and `compareScored`'s `deviceVolatile` term — which prefers
1091
- // the qualified twin on a tie — never sees a qualified twin to prefer. It is a candidate like
1237
+ respell(['regionbase'], regionbase);
1238
+ // …and its `/volatile` composition, narrowed to exactly the locals this variation mints — the
1239
+ // same composition `/livebase` and `/inlinebase` already carry, for the same reason. The shape
1240
+ // this variation exists for is a DEVICE base (the DMA block at 0x040000D4), and the project's own
1241
+ // reference spells it `vu32 *dmaRegs`; without the composition every region local this variation
1242
+ // wins with is published UNqualified, and `compareScored`'s `deviceVolatile` term — which prefers
1243
+ // the qualified candidate on a tie — never sees a qualified candidate to prefer. It is a candidate like
1092
1244
  // any other where the qualifier costs bytes, and the differ referees.
1093
1245
  const regionVolatile = (): SFn | null => {
1094
1246
  const r = regionbase();
1095
1247
  return r ? volatilePtrLocals(r, createdLocals(sfn, r)) : null;
1096
1248
  };
1097
- respell('/regionbase/volatile', regionVolatile);
1098
- // …and the `/vol-store` triple, the pairing this lever is the first to inhabit (see
1249
+ respell(['regionbase', 'volatile'], regionVolatile);
1250
+ // …and the `/vol-store` triple, the pairing this variation is the first to inhabit (see
1099
1251
  // l3/volstore.ts, where the two qualifiers' reach over a tree's OWN locals is disjoint).
1100
- // `/volatile` qualifies a pointer LOCAL and `/vol-store` a STORE SITE, and this lever leaves
1252
+ // `/volatile` qualifies a pointer LOCAL and `/vol-store` a STORE SITE, and this variation leaves
1101
1253
  // both in one function: it homes the regions holding two or more direct uses and leaves every
1102
1254
  // other spelling of the same device address inline. On `synthetic:dmascope` that residue is
1103
1255
  // the write to REG_DMA0CNT that STARTS the transfer, and without the triple it is published
1104
1256
  // bare beside three `volatile s32 *` region locals.
1105
- respell('/regionbase/volatile/vol-store', () => {
1257
+ respell(['regionbase', 'volatile', 'vol-store'], () => {
1106
1258
  const v = regionVolatile();
1107
1259
  return v ? volStore(v) : null;
1108
1260
  });
1109
- const enumerate = (
1110
- label: string,
1261
+ /** The results of a multi-result variation, under the same guard `respell` gives a
1262
+ * single-result one: a throw costs this variation and nothing else. Run at statement level
1263
+ * instead, a throwing source takes the WHOLE row with it — `reportThrow` never runs, so the
1264
+ * DEFAULT candidate is lost too and the row goes `noncompile`. A multi-result source is a
1265
+ * `for`-loop subject rather than a thunk, which is why the guard is a helper and not a try
1266
+ * inside `respell`. */
1267
+ const candidatesOf = (
1268
+ variations: readonly Variation[],
1111
1269
  from: () => SFn | null | undefined,
1112
- variantsOf: (s: SFn) => { merged: string; sfn: SFn }[] = coalesceCandidates,
1113
- ): void => {
1114
- let variants: { merged: string; sfn: SFn }[] = [];
1270
+ resultsOf: (s: SFn) => { merged: string; sfn: SFn }[],
1271
+ ): { merged: string; sfn: SFn }[] => {
1115
1272
  try {
1116
1273
  const base = from();
1117
- variants = base ? variantsOf(base) : [];
1274
+ return base ? resultsOf(base) : [];
1118
1275
  } catch (e) {
1119
- opts.onLeverError?.(name + leverLabel + label, firstLine(e));
1276
+ reportThrow([...preRespellVariations, ...variations], e);
1277
+ return [];
1278
+ }
1279
+ };
1280
+ /** One candidate per result of a multi-result variation, `name` applied to the result's own
1281
+ * subject after `prefix`. */
1282
+ const respellEach = (
1283
+ prefix: readonly Variation[],
1284
+ name: SubjectVariationName,
1285
+ from: () => SFn | null | undefined,
1286
+ resultsOf: (s: SFn) => { merged: string; sfn: SFn }[] = coalesceCandidates,
1287
+ ): void => {
1288
+ if (!offeredOn(target, [...prefix, name])) {
1120
1289
  return;
1121
1290
  }
1122
- for (const c of variants) {
1123
- respell(`${label}-${c.merged}`, () => c.sfn);
1291
+ for (const c of candidatesOf([...prefix, name], from, resultsOf)) {
1292
+ respell([...prefix, withSubject(name, c.merged)], () => c.sfn);
1124
1293
  }
1125
1294
  };
1126
- enumerate('/scopebase-coalesce', () => hoistScopedBases(sfn));
1127
- enumerate('/coalesce', () => sfn);
1295
+ respellEach(['scopebase'], 'coalesce', () => hoistScopedBases(sfn));
1296
+ respellEach([], 'coalesce', () => sfn);
1128
1297
  // `/volatile`'s per-local SUBSETS: which pointers the source declared volatile is
1129
1298
  // per-pointer knowledge (an MMIO block and a plain RAM table sit side by side, and
1130
1299
  // qualifying the table blocks the read collapse its region wants), so each proper
1131
1300
  // non-empty subset is its own candidate — the same alternative-OUTPUTS mechanism as the
1132
- // coalesce merges, not a product (l3/volatileptr.ts volatileSubsetCandidates carries the
1133
- // ≤3 cap). The all-qualifiers form is plain `/volatile` above; the livebase product's
1134
- // subsets ride below with the product's own `only` scope.
1135
- enumerate(
1136
- '/volatile',
1301
+ // coalesce merges, not a pairing (l3/volatileptr.ts volatileSubsetCandidates carries the
1302
+ // ≤3 cap). The all-qualifiers form is plain `/volatile` above; the `/livebase/volatile`
1303
+ // composition's subsets ride below with that composition's own `only` scope.
1304
+ respellEach(
1305
+ [],
1306
+ 'volatile',
1137
1307
  () => sfn,
1138
1308
  (s) => volatileSubsetCandidates(s),
1139
1309
  );
1140
- respell('/indexed', () => reindexWalks(sfn));
1141
- respell('/indexed/volatile', () => {
1310
+ respell(['indexed'], () => reindexWalks(sfn));
1311
+ respell(['indexed', 'volatile'], () => {
1142
1312
  const kept = new Set<string>();
1143
1313
  const r = reindexWalks(sfn, kept);
1144
1314
  return r ? volatilePtrLocals(r, kept) : null;
@@ -1147,47 +1317,46 @@ export function enumerateCandidates(
1147
1317
  // LIVEBASE_GATES): its `loop` and `repeated-const-offset` rules predict re-materialization,
1148
1318
  // and an MMIO poll (store then re-read the same fixed offset while it spins) is the shape
1149
1319
  // where the prediction is wrong — the compiler holds ONE base register across stores, the
1150
- // loop, and the read-back. The primary already carries every base those rules admit, so a
1151
- // hoist-nothing result means the lever has nothing to add and declines.
1152
- // One family per admission row; a row binding exactly what an earlier row bound is the same
1153
- // spelling under a different label, so it declines for that too. `/basefold`'s TWO rows and
1154
- // `/unfolded` join the roster where the target declares the fold, and `/orderbase` where it
1155
- // declares the array-shape fork, so a target with neither is offered the two `/livebase` rows
1156
- // and nothing else. The same fact is stated at the POLICY sites above; a roster change repairs
1157
- // all of them or none.
1158
- const admissions: readonly BaseAdmission[] = [
1159
- ...LIVEBASE_ADMISSIONS,
1160
- ...(target.compilerBehaviors.foldsConstAddrOffset ? [...BASEFOLD_ADMISSIONS, ...UNFOLDED_ADMISSIONS] : []),
1161
- // …and the ORDER row where the compiler's subscript expansion forks on the base's array-ness,
1162
- // which is the same opt-in raise/globalshape.ts carries: with it off nothing is stamped, so
1163
- // `order-licensed` would refuse every key anyway and this only saves the census.
1164
- ...(target.compilerBehaviors.arrayShapeFromStride ? ORDERBASE_ADMISSIONS : []),
1165
- ];
1320
+ // loop, and the read-back. The default already carries every base those rules admit, so a
1321
+ // hoist-nothing result means the variation has nothing to add and declines.
1322
+ // One family per hoist; a hoist binding exactly what an earlier hoist bound is the same
1323
+ // source under different variations, so it declines for that too. `/basefold`'s TWO hoists and
1324
+ // `/unfolded` stay on the roster where the target declares the fold, and `/orderbase` where it
1325
+ // declares the array-shape fork (each registry entry's target gate), so a target with neither is
1326
+ // offered the two `/livebase` hoists and nothing else. The array-shape fork is the opt-in
1327
+ // raise/globalshape.ts carries: with it off nothing is stamped, so `order-licensed` would refuse
1328
+ // every key anyway and the filter only saves the census.
1329
+ const hoists: readonly BaseHoist[] = [
1330
+ ...LIVEBASE_HOISTS,
1331
+ ...BASEFOLD_HOISTS,
1332
+ ...UNFOLDED_HOISTS,
1333
+ ...ORDERBASE_HOISTS,
1334
+ ].filter((h) => offeredOn(target, h.variations));
1166
1335
  // AND THE SAME SKIP KEYED ON THE LICENCE ITSELF WOULD BUY NOTHING, which is worth a paragraph
1167
- // because this row is where the next reader will propose it. `orderLicensedGlobals` is decidable
1168
- // on the lifted fn, so the row could also be dropped wherever THAT set is empty. It would be
1336
+ // because this hoist is where the next reader will propose it. `orderLicensedGlobals` is decidable
1337
+ // on the lifted fn, so the hoist could also be dropped wherever THAT set is empty. It would be
1169
1338
  // sound, and it would be inert, for the same one reason: an empty licence stamps no
1170
1339
  // `baseOrdered` (structure.ts `stampOrderedBases`), so `order-licensed` refuses every key, so
1171
- // `hoist` returns null and this row's three emission sites — two `respell`s and the `enumerate`
1340
+ // `hoist` returns null and this hoist's three emission sites — two `respell`s and the `respellEach`
1172
1341
  // whose generator fans over volatile SUBSETS, so the third is a set and not one spelling — emit
1173
- // nothing. Which GENERALIZES to every axis carrying a licence: a skip like it is sound exactly
1174
- // where the axis would have emitted no candidate, so a sound one shrinks the fan by zero, so it
1342
+ // nothing. Which GENERALIZES to every variation carrying a licence: a skip like it is sound exactly
1343
+ // where the variation would have emitted no candidate, so a sound one shrinks the fan by zero, so it
1175
1344
  // removes no COMPILE, and one compile per candidate is where a ranked run's cost is; what it
1176
1345
  // saves is one `admittedBases` walk per tree. `docs/level-tower.md` carries the general form.
1177
1346
  // Measured on klonoa's `LoadBGTilemapData`, the checkout function whose 112,896-candidate fan
1178
- // raises the question: the licence is empty on every lift variant of BOTH symbol-map arms
1347
+ // raises the question: the licence is empty on every lift setting of BOTH symbol-map configurations
1179
1348
  // four named symbols DO reach the licence table map-ful and the ADDRESS gates refuse all four,
1180
1349
  // so "the pool spells no names" is not the reason — and the skip fires on every tree there and
1181
1350
  // removes not one candidate.
1182
1351
  //
1183
1352
  // IF IT IS EVER BUILT ANYWAY, IT IS `orderLicensedGlobals(fn, target)` READ AT THE SITE BELOW
1184
- // that hands `orderLicensed` to the structuring call, PER LIFT VARIANT — never per function,
1353
+ // that hands `orderLicensed` to the structuring call, PER LIFT SETTING — never per function,
1185
1354
  // and never either of the two predicates standing beside it in that same loop. All three wrong
1186
1355
  // readings delete the SAME four live candidates on `sub_806800C` in the sa3 checkout, in BOTH
1187
- // arms: `unsigned/setup-args/orderbase` and its `/flip-join`, `/derived-home` and
1356
+ // symbol-map configurations: `unsigned/setup-args/orderbase` and its `/flip-join`, `/derived-home` and
1188
1357
  // `/flip-join/derived-home` siblings.
1189
- // · PER FUNCTION — `/setup-args` narrows the lift and can license a name the base lift does
1190
- // not, so the first variant's answer is not the function's.
1358
+ // · PER FUNCTION — `/setup-args` narrows the lift and can license a name the default lift does
1359
+ // not, so the first lift setting's answer is not the function's.
1191
1360
  // · `inferGlobalArrays`, seven lines above the licence call and off the same `fn` — a
1192
1361
  // documented strict SUBSET (`raise/globalshape.ts`), and measurably empty on functions
1193
1362
  // where the licence is not, several of them carrying `/orderbase` candidates.
@@ -1197,17 +1366,17 @@ export function enumerateCandidates(
1197
1366
  // tower destroys the order evidence, but the tempting next step — "so it reads empty
1198
1367
  // everywhere and the skip is free" — is FALSE: it reads NON-empty on a function whose
1199
1368
  // `/orderbase` candidates it then deletes anyway. Firing less often is not a defence.
1200
- // A per-row label/source diff catches the last two, and CANNOT catch the first. Both of those
1369
+ // A per-row variations/source diff catches the last two, and CANNOT catch the first. Both of those
1201
1370
  // delete `unsigned/orderbase` off `synthetic:bgarr:agbcc`, the exact source that row publishes
1202
- // as its score-0 MATCH — a row carrying no symbol map, so its single arm is the one the gate
1203
- // actually runs. The per-function reading needs `/setup-args` AND `/orderbase` in ONE label,
1204
- // and NO published winner label carries both — so a green corpus gate is evidence about two of
1371
+ // as its score-0 MATCH — a row carrying no symbol map, so its single configuration is the one the
1372
+ // gate actually runs. The per-function reading needs `/setup-args` AND `/orderbase` in ONE
1373
+ // candidate's variations, and NO published winner carries both — so a green corpus gate is evidence about two of
1205
1374
  // these readings and none at all about the third. That property is the gate, not a count:
1206
1375
  // apps/benchmark/test/census.test.ts asserts it over the committed artifact.
1207
1376
 
1208
- // The CENSUS is a pure function of (this tree, that table) and every row asks for every
1209
- // earlier row's, from thunks each product re-invokes — quadratic in the roster, times the
1210
- // number of products. Memoized on the gate table's identity. The value is a list of key
1377
+ // The CENSUS is a pure function of (this tree, that table) and every hoist asks for every
1378
+ // earlier hoist's, from thunks each composition re-invokes — quadratic in the roster, times the
1379
+ // number of compositions. Memoized on the gate table's identity. The value is a list of key
1211
1380
  // STRINGS whose two readers here only compare and count it, so a memo hit shares no tree.
1212
1381
  const censuses = new Map<readonly Gate<BaseKey>[], readonly string[]>();
1213
1382
  const census = (g: readonly Gate<BaseKey>[]): readonly string[] => {
@@ -1219,14 +1388,14 @@ export function enumerateCandidates(
1219
1388
  censuses.set(g, v);
1220
1389
  return v;
1221
1390
  };
1222
- /** Does an EARLIER roster row already bind exactly `bound` at this placement? Then this row is
1223
- * that row's spelling under a second label and declines.
1391
+ /** Does an EARLIER hoist already bind exactly `bound` at this placement? Then this hoist is
1392
+ * that hoist's source under different variations and declines.
1224
1393
  *
1225
1394
  * Same bases in the same POSITION is the same spelling; the same bases somewhere else is not,
1226
1395
  * which is why the placement is a conjunct and not an afterthought. `rows` is the slice's
1227
1396
  * own list, so the two readers scope it differently — the roster hoist asks over the whole
1228
- * admissions roster, the homesplit pairing over the PAIRED rows only, because a skip there
1229
- * must never drop a withhold no other row enumerates. Captures `census`, so a repeated table
1397
+ * hoist roster, the homesplit pairing over the PAIRED hoists only, because a skip there
1398
+ * must never drop a withhold no other hoist enumerates. Captures `census`, so a repeated table
1230
1399
  * costs one memo lookup rather than a second walk. */
1231
1400
  const shadowedByEarlier = (
1232
1401
  rows: readonly { placement: HoistPlacement; gates: readonly Gate<BaseKey>[] }[],
@@ -1234,32 +1403,33 @@ export function enumerateCandidates(
1234
1403
  placement: HoistPlacement,
1235
1404
  bound: readonly string[],
1236
1405
  ): boolean => rows.slice(0, i).some((r) => r.placement === placement && sameBases(bound, census(r.gates)));
1237
- const livebases = admissions.map(({ suffix, gates, placement, pairings }, i) => {
1406
+ const livebases = hoists.map(({ variations, gates, placement, pairings }, i) => {
1238
1407
  const hoist = (): SFn | null => {
1239
1408
  const bound = census(gates);
1240
1409
  if (bound.length === 0) {
1241
1410
  return null;
1242
1411
  }
1243
- return shadowedByEarlier(admissions, i, placement, bound) ? null : hoistBaseLocals(sfn, gates, placement);
1412
+ return shadowedByEarlier(hoists, i, placement, bound) ? null : hoistBaseLocals(sfn, gates, placement);
1244
1413
  };
1245
1414
  const volatiles = (): SFn | null => {
1246
1415
  const r = hoist();
1247
1416
  return r ? volatilePtrLocals(r, createdLocals(sfn, r)) : null;
1248
1417
  };
1249
- return { suffix, hoist, volatiles, pairings, gates, placement };
1418
+ return { variations, hoist, volatiles, pairings, gates, placement };
1250
1419
  });
1251
- // THE PLACEMENT DIFFERENTIAL, one composition inwards. `respell` re-checks a lever's
1252
- // placement across the statement SHAPES derived onto it; the lever-on-lever products below
1253
- // are the same hazard in the same file and are outside it, because the composition happens
1254
- // INSIDE one `make()` thunk and the intermediate tree never reaches `respell`'s check. A
1255
- // def-MOVING pass (`sinkInitsToFirstUse`, `nearBaseClusters`, `reindexWalks`) running on a
1256
- // tree a PLACING lever built can move a def below a use exactly as a shape can. Same
1420
+ // THE PLACEMENT DIFFERENTIAL, one composition inwards. `respell` re-checks a variation's
1421
+ // placement across the stacked variations derived onto it; the variation-on-variation
1422
+ // compositions below are the same hazard in the same file and are outside it, because the
1423
+ // composition happens INSIDE one `make()` thunk and the intermediate tree never reaches
1424
+ // `respell`'s check. A def-MOVING pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
1425
+ // `reindexWalks`) running on a tree a PLACING variation built can move a def below a use exactly
1426
+ // as a shape can. Same
1257
1427
  // differential, so a placement neither pass can model is not judged either way, and the throw
1258
1428
  // lands inside the thunk — a reported, dropped candidate.
1259
1429
  //
1260
1430
  // BOTH SIDES' minted locals, because the mover MINTS TOO: `nearBaseClusters` creates the
1261
1431
  // cluster base it then places, and `reindexWalks` creates the induction variable, so the
1262
- // outer lever's name diff alone is empty for a standalone mover and a strict subset for a
1432
+ // outer variation's name diff alone is empty for a standalone mover and a strict subset for a
1263
1433
  // composition — the mover's own stranding of its own local walks straight through. Judging a
1264
1434
  // name the BEFORE tree does not carry keeps the differential honest rather than turning it
1265
1435
  // absolute: a name absent from `before` is never read there, so that walk passes and only the
@@ -1270,59 +1440,59 @@ export function enumerateCandidates(
1270
1440
  }
1271
1441
  return after;
1272
1442
  };
1273
- // Every product below fans over the rows a demanding row earned, never the whole roster.
1443
+ // Every composition below runs over the hoists a demanding row earned, never the whole roster.
1274
1444
  const paired = livebases.filter((l) => l.pairings);
1275
- for (const { suffix, hoist, volatiles } of livebases) {
1276
- respell(suffix, hoist);
1277
- respell(`${suffix}/volatile`, volatiles);
1278
- enumerate(`${suffix}/volatile`, hoist, (r) => volatileSubsetCandidates(r, createdLocals(sfn, r)));
1445
+ for (const { variations, hoist, volatiles } of livebases) {
1446
+ respell(variations, hoist);
1447
+ respell([...variations, 'volatile'], volatiles);
1448
+ respellEach(variations, 'volatile', hoist, (r) => volatileSubsetCandidates(r, createdLocals(sfn, r)));
1279
1449
  }
1280
- // The livebase × indexed PAIRINGS the third sanctioned product kind (see POLICY):
1281
- // row-demanded, and the joint spelling is reachable from neither lever alone (the
1450
+ // The livebase × indexed PAIRINGS (see POLICY): row-demanded, and the joint spelling is
1451
+ // reachable from neither variation alone (the
1282
1452
  // frame-copy + DMA shape).
1283
- for (const { suffix, hoist, volatiles } of paired) {
1284
- respell(`${suffix}/indexed`, () => {
1453
+ for (const { variations, hoist, volatiles } of paired) {
1454
+ respell([...variations, 'indexed'], () => {
1285
1455
  const r = hoist();
1286
1456
  return r ? survives(r, reindexWalks(r)) : null;
1287
1457
  });
1288
- respell(`${suffix}/volatile/indexed`, () => {
1458
+ respell([...variations, 'volatile', 'indexed'], () => {
1289
1459
  const r = volatiles();
1290
1460
  return r ? survives(r, reindexWalks(r)) : null;
1291
1461
  });
1292
1462
  }
1293
1463
  // The livebase × sinkinit PAIRINGS — the same admission again: row-demanded
1294
- // (kleod:DecompressDma), and the joint spelling is reachable from neither lever alone. The
1295
- // bases whose placement moves the row are the ones only this lever's ablation binds, and
1464
+ // (kleod:DecompressDma, on kl-eod-decomp's source, before 2026-09-13), and the joint spelling is reachable from neither variation alone. The
1465
+ // bases whose placement moves the row are the ones only this variation's ablation binds, and
1296
1466
  // `/sinkinit` alone reads the DEFAULT hoist's head, which does not carry them.
1297
- for (const { suffix, hoist, volatiles } of paired) {
1298
- respell(`${suffix}/sinkinit`, () => {
1467
+ for (const { variations, hoist, volatiles } of paired) {
1468
+ respell([...variations, 'sinkinit'], () => {
1299
1469
  const r = hoist();
1300
1470
  return r ? survives(r, sinkInitsToFirstUse(r)) : null;
1301
1471
  });
1302
- respell(`${suffix}/volatile/sinkinit`, () => {
1472
+ respell([...variations, 'volatile', 'sinkinit'], () => {
1303
1473
  const r = volatiles();
1304
1474
  return r ? survives(r, sinkInitsToFirstUse(r)) : null;
1305
1475
  });
1306
1476
  }
1307
- // The livebase x homesplit PAIRINGS the fourth sanctioned product kind, and row-demanded
1308
- // (synthetic:dmapoll): ONE base kept at the head and a SECOND split per region, which neither
1309
- // lever spells alone because each applies its own policy to every base it binds: compiled
1477
+ // The livebase x homesplit PAIRINGS, row-demanded (see POLICY; synthetic:dmapoll): ONE base
1478
+ // kept at the head and a SECOND split per region, which neither variation spells alone because
1479
+ // each applies its own policy to every base it binds: compiled
1310
1480
  // against that row's own object, the score reaches 0 only where the two policies land on
1311
1481
  // DIFFERENT bases, and every uniform choice is worse. The endpoint figures live in
1312
1482
  // l3/homesplit.ts, which is the measurement's one home, along with why this is a PIPE and
1313
1483
  // never a merge.
1314
1484
  //
1315
- // WHICH key is withheld is not derivable, so every admitted key is its own candidate, LABELLED
1316
- // with that key — a label is an identity, and one label over two withholds names two programs.
1317
- // `HOMESPLIT_FAN_GATES`' `homesplit-fan-cap` is what bounds the product.
1318
- // ADDITIVE, like every lever here: `/livebase-block`, `/regionbase`, `/scopebase` and the
1319
- // un-hoisted primary all stay in the list, which is what keeps `synthetic:dmaflat` — where the
1485
+ // WHICH key is withheld is not derivable, so every admitted key is its own candidate, with that
1486
+ // key as the variation's SUBJECT — a candidate's variations are its identity, and one name over
1487
+ // two withholds names two programs. `HOMESPLIT_FAN_GATES`' `homesplit-fan-cap` is what bounds the
1488
+ // pairing. ADDITIVE, like every variation here: `/livebase-block`, `/regionbase`, `/scopebase`
1489
+ // and the un-hoisted default all stay in the list, which is what keeps `synthetic:dmaflat` — where the
1320
1490
  // composed spelling scores 13 against its own 0 — at MATCH.
1321
- for (const [i, { suffix, gates, placement }] of paired.entries()) {
1491
+ for (const [i, { variations, gates, placement }] of paired.entries()) {
1322
1492
  const bound = census(gates);
1323
- // The ROSTER's dedup, which `hoist` applies to every other product and this loop has to ask
1324
- // for itself: every pairing piped from a shadowed row is that row's spelling under a second
1325
- // label too. Asked over the PAIRED rows only — the earlier row runs the identical pipe and
1493
+ // The ROSTER's dedup, which `hoist` applies to every other composition and this loop has to
1494
+ // ask for itself: every pairing piped from a shadowed hoist is that hoist's source under
1495
+ // different variations too. Asked over the PAIRED hoists only — the earlier hoist runs the identical pipe and
1326
1496
  // emits the identical source. Without it both run and `seen` collapses the pair afterwards,
1327
1497
  // having paid a head hoist, region plan, rewrite and emit for each.
1328
1498
  if (shadowedByEarlier(paired, i, placement, bound)) {
@@ -1332,7 +1502,7 @@ export function enumerateCandidates(
1332
1502
  // rules read the key count and nothing else, so inside the pipe they would cost that whole
1333
1503
  // pipe to report a fact this loop already holds.
1334
1504
  for (const key of homeSplitWithholds(bound)) {
1335
- const lever = `${suffix}/homesplit-${homeSplitTag(key)}`;
1505
+ const homesplitVariations = [...variations, withSubject('homesplit', homeSplitTag(key))];
1336
1506
  const homesplit = (): SFn | null => {
1337
1507
  const p = splitHomeBases(sfn, {
1338
1508
  gates,
@@ -1346,9 +1516,9 @@ export function enumerateCandidates(
1346
1516
  const r = homesplit();
1347
1517
  return r ? volatilePtrLocals(r, createdLocals(sfn, r)) : null;
1348
1518
  };
1349
- respell(lever, homesplit);
1350
- respell(`${lever}/volatile`, homesplitVolatile);
1351
- respell(`${lever}/volatile/vol-store`, () => {
1519
+ respell(homesplitVariations, homesplit);
1520
+ respell([...homesplitVariations, 'volatile'], homesplitVolatile);
1521
+ respell([...homesplitVariations, 'volatile', 'vol-store'], () => {
1352
1522
  const v = homesplitVolatile();
1353
1523
  return v ? volStore(v) : null;
1354
1524
  });
@@ -1357,7 +1527,7 @@ export function enumerateCandidates(
1357
1527
  // `/mulfirst` — product-first commutative sums (l3/mulfirst.ts): IDO/mwcc schedule the
1358
1528
  // independent operand's load above the product's mflo/mullw, so def order re-spells a
1359
1529
  // product-first source as load-first. Both orders are emitted; the differ referees.
1360
- respell('/mulfirst', () => mulFirstSums(sfn));
1530
+ respell(['mulfirst'], () => mulFirstSums(sfn));
1361
1531
  // `/nearbase` — neighbor absolute addresses derive from one shared base local
1362
1532
  // (l3/nearbase.ts): one object's cells anchored as separate pool constants re-spell as
1363
1533
  // offsets off its lowest address, within the target's declared derivation reach. Both
@@ -1380,144 +1550,227 @@ export function enumerateCandidates(
1380
1550
  const r = near(base);
1381
1551
  return r ? survives(r, sinkInitsToFirstUse(r)) : null;
1382
1552
  };
1383
- respell('/nearbase', () => near(sfn));
1384
- respell('/nearbase/sinkinit', () => nearSunk(sfn));
1553
+ respell(['nearbase'], () => near(sfn));
1554
+ respell(['nearbase', 'sinkinit'], () => nearSunk(sfn));
1555
+ // `/advance` — a pointer local the source MOVED between two accesses (l3/advance.ts), read off
1556
+ // the `add` the target performed on an address register that already held an address it used.
1557
+ //
1558
+ // THE `/volatile` COMPOSITION IS THE ONE THAT PAYS, and both halves are measured on
1559
+ // `kleod:StreamCmd_SetWindowRegs:agbcc`. Against the INDEXED spelling of the same minted local
1560
+ // the advance buys nothing — agbcc folds `p = p + 1; *p` back into `strh [r3, #2]`, so
1561
+ // `/advance` and `/nearbase` both score 15/23 there — and against the qualified one it is the
1562
+ // match: `/advance/volatile` 0/22, because `volatile` bars that fold and leaves the `add` the
1563
+ // target records.
1564
+ //
1565
+ // WHY THE CONJUNCTION IS A VARIATION AND NOT A COMPILER BEHAVIOR, since the four corners in
1566
+ // test/advance.test.ts's header read as a FUNCTION from the asm: on agbcc a surviving
1567
+ // `adds r3,#2` between two accesses through one address register is produced by exactly one of
1568
+ // the four sources, so volatility and the advance are both determined once the stamp is there.
1569
+ // What the mapping is a function OF is one compiler — agbcc — and the accesses that carry the
1570
+ // stamp: every access agbcc DID fold carries no stamp, so an always-on rewrite gated by a compiler
1571
+ // behavior would never see them, but nothing says the next compiler's fold has the same shape. It stays a variation for
1572
+ // as long as agbcc is the only target that reaches the stamp (the corpus census below), and the
1573
+ // day a second one does, `compilerBehaviors` is where this belongs rather than a variation.
1574
+ //
1575
+ // THE PLAIN `/advance` IS GATED ON THE COMPILER BEHAVIOUR IT IS INERT UNDER — its registry entry's
1576
+ // target gate names a `compilerBehaviors` flag, as `foldsConstAddrOffset` keys `/offmember` above,
1577
+ // but with the POLARITY REVERSED: that flag admits a variation where the compiler folds, this one
1578
+ // withholds one. agbcc FOLDS the advance back (`compilerBehaviors.foldsPointerAdvance`, its four
1579
+ // compiled corners in test/advance.test.ts's header), so the plain spelling emits the same stores
1580
+ // as the indexed one this roster already offers, and it never wins on a row that reaches it:
1581
+ // `/advance` 15/23 against this row's 0/22 match, and it LOSES outright on the other four — `offhi_split`
1582
+ // 33/64 vs 12/61 · `offhi_fused` 31/63 vs 0/58 · `dma_fill_uninit` 76/114 vs 0/103 ·
1583
+ // `volwalk` 5/7 vs 0/7 (2026-09-12).
1584
+ //
1585
+ // THOSE `/advance` HALVES ARE NOT REPRODUCIBLE BY A BARE `bench fan`, because this withhold is
1586
+ // what removes them from every agbcc fan. The falsifying command is the ablation: flip
1587
+ // `foldsPointerAdvance` to `false` in target.ts and re-run `pnpm bench fan <sym>` on the five
1588
+ // rows — the plain `/advance` reappears at the scores above, or these numbers are wrong.
1589
+ //
1590
+ // ABSENT ⇒ FALSY ⇒ THE VARIATION SHIPS, so every compiler whose pair nobody has compiled keeps
1591
+ // exactly the coverage it had: a compiler that does not fold `p = p + 1; *p` back would score
1592
+ // the spelling apart, and that is the case the variation exists for. `/advance/volatile` is not
1593
+ // gated — `volatile` is what bars the fold, and that composition is this row's match.
1594
+ //
1595
+ // NO `/vol-store` COMPOSITION. That variation pins a store whose WHOLE ADDRESS is a device constant,
1596
+ // and this one has just replaced those constants with a local — so on the shape `/advance`
1597
+ // fires for, the pair reaches only whatever OTHER const-addressed device store the function
1598
+ // still has, which no row on the corpus has beside an advanced chain. A pairing with no
1599
+ // inhabitant is candidates without a row behind them.
1600
+ //
1601
+ // AND NO `/nearbase` OR `/livebase` PAIRING, which is a different answer from the one those two
1602
+ // give each other ("each variation's constants are invisible to the other's model"). Here they are
1603
+ // not: all three mint a local for a const address, and `l3/advance.ts` needs its members to
1604
+ // still BE const-addressed accesses (`cellAddress`), which is exactly what a nearbase cluster
1605
+ // or a livebase hoist has already replaced. Running `/advance` on such a tree is a NO-REACH,
1606
+ // not a decline — the same hazard `l3/nearbase.ts` records for committed base-CSE, which
1607
+ // `/advance` inherits: where `structureChecked` has already hoisted the chain's pool word, the
1608
+ // members arrive as a `var` base and this pass enumerates nothing at all.
1609
+ //
1610
+ // AND IT IS MAP-LESS ONLY, which is what caps its reach: with a symbol map the pool word
1611
+ // promotes to `&REG_WININ` and `l3/address.ts`'s `cellAddress` answers null, so every
1612
+ // `/advance` candidate on this row carries `/raw-globals` (`bench fan
1613
+ // kleod:StreamCmd_SetWindowRegs:agbcc --enumerate`, 2026-09-12: 17 candidates, the one
1614
+ // advanced candidate `unsigned/advance/volatile/raw-globals`). A capability that reads a CONST
1615
+ // address does not survive the symbol-map direction unless `cellAddress` learns the promoted
1616
+ // form; test/advance.test.ts records that at the row it exists for.
1617
+ const advance = (): SFn | null => survives(sfn, advancedBases(sfn));
1618
+ respell(['advance'], advance);
1619
+ respell(['advance', 'volatile'], () => {
1620
+ const a = advance();
1621
+ return a ? volatilePtrLocals(a, createdLocals(sfn, a)) : null;
1622
+ });
1385
1623
  // The livebase × nearbase PAIRINGS — the same admission as livebase × indexed above:
1386
1624
  // the volatile triple is the row-demanded one, and the joint spelling is reachable from
1387
- // neither lever alone (a neighbor-cell object and a multi-index MMIO block in one
1388
- // function — each lever's constants are invisible to the other's model); the plain
1625
+ // neither variation alone (a neighbor-cell object and a multi-index MMIO block in one
1626
+ // function — each variation's constants are invisible to the other's model); the plain
1389
1627
  // sibling rides for symmetry with /livebase/indexed.
1390
- for (const { suffix, hoist, volatiles } of paired) {
1391
- respell(`${suffix}/nearbase`, () => near(hoist()));
1392
- respell(`${suffix}/volatile/nearbase`, () => near(volatiles()));
1393
- respell(`${suffix}/nearbase/sinkinit`, () => nearSunk(hoist()));
1394
- respell(`${suffix}/volatile/nearbase/sinkinit`, () => nearSunk(volatiles()));
1628
+ for (const { variations, hoist, volatiles } of paired) {
1629
+ respell([...variations, 'nearbase'], () => near(hoist()));
1630
+ respell([...variations, 'volatile', 'nearbase'], () => near(volatiles()));
1631
+ respell([...variations, 'nearbase', 'sinkinit'], () => nearSunk(hoist()));
1632
+ respell([...variations, 'volatile', 'nearbase', 'sinkinit'], () => nearSunk(volatiles()));
1395
1633
  }
1396
1634
  // The livebase × coalesce PAIRINGS — same admission again: the volatile triple is the
1397
- // row-demanded one, the joint spelling reachable from neither lever alone (an MMIO base
1635
+ // row-demanded one, the joint spelling reachable from neither variation alone (an MMIO base
1398
1636
  // worth homing and a counter shared across both arms of one if, in one function); the
1399
1637
  // plain sibling rides for symmetry.
1400
1638
  // ARM-DISJOINT merges only: the demanding row's shared counter is that class, and the
1401
- // span-model merges already ride the plain /coalesce label — pairing them too would
1639
+ // span-model merges already ride the plain /coalesce variation — pairing them too would
1402
1640
  // multiply candidates with no row behind it.
1403
- for (const { suffix, hoist, volatiles } of paired) {
1404
- enumerate(`${suffix}/coalesce`, hoist, armDisjointCandidates);
1405
- enumerate(`${suffix}/volatile/coalesce`, volatiles, armDisjointCandidates);
1641
+ for (const { variations, hoist, volatiles } of paired) {
1642
+ respellEach(variations, 'coalesce', hoist, armDisjointCandidates);
1643
+ respellEach([...variations, 'volatile'], 'coalesce', volatiles, armDisjointCandidates);
1644
+ }
1645
+ // `/argcopy` — a pointer parameter copied into a local for ONE region (l3/argcopy.ts): the
1646
+ // copy frees the parameter's incoming register for that region, and every legal region is its
1647
+ // own candidate.
1648
+ //
1649
+ // PAIRED WITH `/coalesce`, because a freed register is worth nothing until something takes it:
1650
+ // on pokeemerald:SetMauvilleOldManLanguage the copy frees r5 and the counter shared between two
1651
+ // switch arms is what moves into it, and neither spelling alone reaches the bytes.
1652
+ if (offeredOn(target, ['argcopy'])) {
1653
+ for (const c of candidatesOf(['argcopy'], () => sfn, argCopyCandidates)) {
1654
+ const copied = withSubject('argcopy', c.merged);
1655
+ respell([copied], () => c.sfn);
1656
+ respellEach([copied], 'coalesce', () => c.sfn);
1657
+ }
1406
1658
  }
1407
1659
  // `/parkfirst` — incoming-argument parks lead the entry prefix (l3/parkfirst.ts): the
1408
1660
  // park's `mov` lifts to pure SSA aliasing, so its position is unrecoverable and the
1409
1661
  // default order is emission's. Both orders are emitted; the differ referees.
1410
- respell('/parkfirst', () => parkParamsFirst(sfn));
1662
+ respell(['parkfirst'], () => parkParamsFirst(sfn));
1411
1663
  // `/sinkinit` — each leading pointer-base init sinks to its own first use (l3/sinkinit.ts):
1412
1664
  // the base hoist places every init at the head of the body, which keeps the base live across
1413
1665
  // everything above its first use and can cost a callee-saved register the original avoided.
1414
1666
  // Which placement the source used is not derivable from the asm, so both are emitted and the
1415
1667
  // differ referees.
1416
- respell('/sinkinit', () => sinkInitsToFirstUse(sfn));
1417
- // the register-copy spelling (l3/regspell.ts): 0–3 variants (base; tail assign-back reusing
1418
- // the dead value var; tail assign-back into a fresh var — the tail choice is allocator-
1668
+ respell(['sinkinit'], () => sinkInitsToFirstUse(sfn));
1669
+ // the register-copy variation (l3/regspell.ts): 0–3 results (base; tail assign-back reusing
1670
+ // the dead value var; tail assign-back into a fresh var — the tail decision is allocator-
1419
1671
  // ambiguous, so both are ranked).
1420
1672
  //
1421
- // LABELLED BY THE TAIL THE VARIANT CARRIES, NEVER BY ITS INDEX. The reuse tail exists only
1673
+ // NAMED BY THE TAIL THE RESULT CARRIES, NEVER BY ITS INDEX. The reuse tail exists only
1422
1674
  // where R1 fired, so the list is 1, 2 or 3 long and the fresh tail sits at no fixed position;
1423
- // an index-keyed label table names the fresh spelling `/regcopy-ret` on every R1-less function
1424
- // — the dead-var-reuse name on the one spelling that has no dead var — and `candidateLabel` is
1675
+ // an index-keyed table names the fresh spelling `/regcopy-ret` on every R1-less function
1676
+ // — the dead-var-reuse name on the one spelling that has no dead var — and `winnerVariations` is
1425
1677
  // what every census in this repo counts, `bench diff` included. The exhaustive record is the
1426
1678
  // pin: a new tail kind is a type error here rather than a silent `/regcopy-3`.
1427
1679
  // `cli/test/matching/regspell-candidate.test.ts` holds the correspondence.
1428
- const REGCOPY_LABEL: Record<RegcopyTail, string> = {
1429
- none: '/regcopy',
1430
- reuse: '/regcopy-ret',
1431
- fresh: '/regcopy-ret-fresh',
1680
+ const REGCOPY_VARIATION: Record<RegcopyTail, Variation> = {
1681
+ none: 'regcopy',
1682
+ reuse: withSubject('regcopy', 'ret'),
1683
+ fresh: withSubject('regcopy', 'ret-fresh'),
1432
1684
  };
1433
- registerishSpellings(sfn).forEach((alt) => respell(REGCOPY_LABEL[alt.tail], () => alt.sfn));
1434
- return { spellings };
1685
+ registerishSpellings(sfn).forEach((alt) => respell([REGCOPY_VARIATION[alt.tail]], () => alt.sfn));
1686
+ return { sources };
1435
1687
  };
1436
- // The SYMBOL-MAP spelling is itself a ranked LEVER on the same footing as signedness/branch
1688
+ // The SYMBOL-MAP spelling is itself a ranked VARIATION on the same footing as signedness/branch
1437
1689
  // sense: naming a global changes agbcc's codegen (the eager-load effect), and which side
1438
1690
  // byte-wins is genuinely per-function — the dogfood's landed matches split between extern
1439
1691
  // spellings and raw-address macros. So when a map is present the raw-global spelling is ALSO
1440
1692
  // enumerated ('/raw-globals') and the differ referees; the dedup below collapses the pair
1441
1693
  // wherever the map changed nothing, so this never scores worse than either side alone.
1442
1694
  //
1443
- // Does the `/raw-globals` arm have a RANK of its own to spell? Read off the DERIVED shapes the
1444
- // map does not answer for — the only ones a map-less structuring ever sees — because that is
1445
- // exactly the population `/flat-rank`'s decline just below is about. A superset of what the raw
1446
- // arm's own lift derives (it is read off the probe's), for the reason the axis gate above is one
1447
- // too: this only ADDS an OFF arm, and where the arm changes nothing the tree dedup collapses it.
1448
- const rawDerivesRank = [...probeShapes].some(
1695
+ // Does the `/raw-globals` setting have a RANK of its own to spell? Read off the DERIVED shapes the
1696
+ // map does not answer for — the only ones a map-less structuring ever sees — because those are
1697
+ // exactly the shapes `/flat-rank`'s decline just below is about. A superset of what the raw
1698
+ // setting's own lift derives (it is read off the shared lift's), for the reason the
1699
+ // structure-variation gate above is one too: this only ADDS an alternative, and where it changes
1700
+ // nothing the tree dedup collapses it.
1701
+ const rawDerivesRank = [...sharedLiftShapes].some(
1449
1702
  ([n, i]) => byName?.get(n) === undefined && (arrayInnerExtents(i)?.length ?? 0) > 0,
1450
1703
  );
1451
- const symbolVariants: { suffix: string; symbols?: typeof opts.symbols }[] = opts.symbols
1704
+ const symbolSettings: { variations: readonly Variation[]; symbols?: typeof opts.symbols }[] = opts.symbols
1452
1705
  ? [
1453
- { suffix: '', symbols: opts.symbols },
1454
- { suffix: '/raw-globals', symbols: undefined },
1706
+ { variations: [], symbols: opts.symbols },
1707
+ { variations: ['raw-globals'], symbols: undefined },
1455
1708
  ]
1456
- : [{ suffix: '' }];
1457
- for (const [svIndex, sv] of symbolVariants.entries()) {
1458
- const svOpts = sv.symbols ? baseOpts : { ...baseOpts, symbols: undefined };
1709
+ : [{ variations: [] }];
1710
+ for (const [symbolIndex, symbolSetting] of symbolSettings.entries()) {
1711
+ const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined };
1459
1712
  // `/no-bitfield` names a spelling the MAP makes available, so it has no inhabitant on the
1460
- // variant that structures without one: structure() normalizes `spellBitfieldMembers` to false
1461
- // when `symbols` is absent, so both arms structure the identical tree whatever reads it. This
1462
- // declines to build the second arm rather than leaving the tree skip to collapse it, which is
1713
+ // symbol-map setting that structures without one: structure() normalizes `spellBitfieldMembers`
1714
+ // to false when `symbols` is absent, so both settings structure the identical tree whatever
1715
+ // reads it. This declines to build the second one rather than leaving the tree skip to collapse it, which is
1463
1716
  // worth 512 of LoadBGTilemapData's 1536 structurings under docs/ranked-repro.md's flags.
1464
1717
  // Declining is not pruning — same posture as the signedness decline below, and the same
1465
1718
  // candidate list; bitfield-members.test.ts pins the normalization the decline rests on.
1466
1719
  // …and `/no-ptr-elem` names a spelling only the MAP makes available, for the same reason:
1467
- // structure() normalizes `spellPtrMemberElements` to false without `symbols`, so both arms
1468
- // structure the identical tree on the raw variant. `/flat-rank` IS NOT a third such spelling,
1720
+ // structure() normalizes `spellPtrMemberElements` to false without `symbols`, so both settings
1721
+ // structure the identical tree on the raw setting. `/flat-rank` IS NOT a third such spelling,
1469
1722
  // and the difference is why its decline is asked of the EVIDENCE and not of the map: the
1470
1723
  // declared subscripts come off a render context structure() builds from the UNION of the map
1471
- // and the shapes this function's own strides evidence (raise/globalshape.ts), so the raw arm
1724
+ // and the shapes this function's own strides evidence (raise/globalshape.ts), so the raw setting
1472
1725
  // derives a rank of its own. The decline therefore stands only where no derived shape carries
1473
- // a rank for that arm to spell — the condition under which both arms really do structure the
1474
- // identical tree.
1475
- const svCands = sv.symbols
1476
- ? axisCands
1477
- : axisCands.filter((s) => s.bitfields && s.ptrElems && (s.declRank || rawDerivesRank));
1478
- const treeOwnedFold = treeOwnedIn(sv.symbols);
1479
- // The signedness axis DECLINES where the pin has nothing to pin. `pinScalarParams` writes only
1726
+ // a rank for that setting to spell — the condition under which both settings really do structure
1727
+ // the identical tree.
1728
+ const usableStructureSettings = symbolSetting.symbols
1729
+ ? structureSettings
1730
+ : structureSettings.filter((s) => s.bitfields && s.ptrElems && (s.declRank || rawDerivesRank));
1731
+ const treeOwnedFold = treeOwnedIn(symbolSetting.symbols);
1732
+ // The signedness variation DECLINES where the pin has nothing to pin. `pinScalarParams` writes only
1480
1733
  // over an entry param still `unknown`/`int` that is not one of the recovered pointers/
1481
1734
  // aggregates `ptrIdx` excludes; where no param is left, the second pass re-lifts, re-raises and
1482
1735
  // re-structures a function BYTE-IDENTICAL to the first, reaching a tree the first pass already
1483
1736
  // spelled. Declining is not pruning: the candidate list is the same list, reached without
1484
1737
  // building the duplicates. What the decline saves is therefore invisible in the candidates —
1485
- // sign-axis.test.ts counts LIFTS, the one reading of the enumeration that it moves.
1738
+ // signedness-variation.test.ts counts LIFTS, the one reading of the enumeration that it moves.
1486
1739
  //
1487
- // Read off the pin's OWN call, per symbol variant — the `/raw-globals` arm lifts without the
1488
- // map and answers for itself, so no lift is governed by a fact measured on a different one.
1740
+ // Read off the pin's OWN call, per symbol-map setting — the `/raw-globals` setting lifts without
1741
+ // the map and answers for itself, so no lift is governed by a fact measured on a different one.
1489
1742
  let pinnable = false;
1490
- for (const cand of SIGN_CANDS) {
1743
+ for (const cand of SIGNEDNESS) {
1491
1744
  if (cand.signed && !pinnable) {
1492
1745
  break;
1493
1746
  }
1494
- const base = frontend.lift(name, asm, target, prototypes, opts.asmData, sv.symbols);
1747
+ const base = frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
1495
1748
  // `/setup-args` — pass a prototype-less callee only what the CALLING BLOCK set up; which of
1496
1749
  // the two readings the source spelled is genuinely ambiguous, and frontend/ssa.ts
1497
1750
  // narrowToSetupArgs carries the argument for why the differ is what settles it.
1498
1751
  //
1499
- // A LIFT VARIANT, in the same product position as the signedness pin and the symbol-map
1500
- // spelling — not a re-spelling lever under the POLICY note below. Dropping an argument
1501
- // changes the IR every structuring axis then reads: the value the argument carried loses a
1752
+ // A LIFT VARIATION, in the same position in a candidate's variations as the signedness pin and
1753
+ // the symbol-map setting — not a respell variation under the POLICY note below. Dropping an
1754
+ // argument changes the IR every structure variation then reads: the value the argument carried loses a
1502
1755
  // consumer, so what materializes changes with it, and a row whose callee arities are GUESSED
1503
1756
  // can need the narrowed lift to reach a spelling neither side reaches alone —
1504
1757
  // `kleod:ReadKeyInput` did, until its manifest declared those arities to asmlift as its own
1505
- // `ctx` already declared them to m2c; it now matches on the base lift, at
1506
- // `unsigned/derived-home`, enumerating no variant at all.
1507
- // Only spellings the narrowing actually changed reach a compiler: one that changes nothing
1508
- // downstream emits the base spelling's source and the dedup collapses it, and a DECLARED
1509
- // arity records nothing and enumerates no variant at all. What survives the dedup is the
1510
- // product's real price, and it is not free: this arm added 1201 distinct candidates, all of
1758
+ // `ctx` already declared them to m2c; it now matches on the default lift, at
1759
+ // `unsigned/derived-home`, enumerating no alternative lift at all.
1760
+ // Only sources the narrowing actually changed reach a compiler: one that changes nothing
1761
+ // downstream emits the default source and the dedup collapses it, and a DECLARED
1762
+ // arity records nothing and enumerates no alternative lift at all. What survives the dedup is
1763
+ // the variation's real price, and it is not free: it added 1201 distinct candidates, all of
1511
1764
  // them in the 13 rows whose narrowing changes anything downstream. Quoted as a DELTA with no
1512
1765
  // denominator, because the agbcc row count it was taken over has moved since — measured
1513
1766
  // before those six kleod rows declared their callee arities,
1514
- // and declaring one takes its row out of this population.
1767
+ // and declaring one takes its row out of that count.
1515
1768
  //
1516
1769
  // `/connective` — spell a same-scrutinee const-test chain as `x == 0 || x == 2` rather than
1517
1770
  // leaving it to switch recovery. They are mutually exclusive within one raise
1518
1771
  // (raise/shortcircuit.ts's REFUSALS note has the mechanism: a folded `logic_or` is not the
1519
1772
  // `icmp` switch-recover.ts requires), so no predicate settles it — the differ does.
1520
- // Enumerated only where THIS VARIANT's lift reports the PAIRWISE refusal, which a handful of
1773
+ // Enumerated only where THIS LIFT SETTING's lift reports the PAIRWISE refusal, which a handful of
1521
1774
  // corpus rows do.
1522
1775
  //
1523
1776
  // WHAT IT IS *NOT* FOR: the shared-arm spelling `switch (x) { case 0: case 2: … }`. That is
@@ -1527,71 +1780,75 @@ export function enumerateCandidates(
1527
1780
  // IDO 64 bytes each and one md5). A second group parts them: agbcc 20 against 16, the switch
1528
1781
  // building a balanced `bgt` dispatch where the chain tests sequentially; IDO 80 bytes each,
1529
1782
  // different bytes. So on a recovered MULTI-GROUP switch the connective is a genuine second
1530
- // spelling, and this axis is the only thing that reaches it.
1783
+ // spelling, and this variation is the only thing that reaches it.
1531
1784
  //
1532
- // WHERE IT IS WORTH 0 POINTS IT IS STILL NOT WORTH NOTHING, and the two populations differ.
1785
+ // WHERE IT IS WORTH 0 POINTS IT IS STILL NOT WORTH NOTHING, and the two kinds of row differ.
1533
1786
  // On `kleod:ProcessInputAndUpdateEntities` the grouping alone reaches the same score the
1534
- // axis reaches with it, yet the published winner there carries `/connective` and spells its
1535
- // site `gUnk_030034C0 == 0 || gUnk_030034C0 == 2` — so deleting the axis moves that row's
1536
- // SOURCE. It moves the SCORE on the other population, where switch recovery declined
1787
+ // variation reaches with it, yet the published winner there carries `/connective` and spells its
1788
+ // site `gUnk_030034C0 == 0 || gUnk_030034C0 == 2` — so deleting the variation moves that row's
1789
+ // SOURCE. It moves the SCORE on the other kind of row, where switch recovery declined
1537
1790
  // ENTIRELY and the tree came out as nested `if`s: `kleod:CountCollectedGems` and
1538
1791
  // `kleod:CheckWorldCompletion`, neither with a `switch` at all. Telling the two apart needs
1539
1792
  // an L3 fact (did recovery produce a grouped arm?) at a raise-level hook, which is a level
1540
1793
  // inversion; the fan is the price instead. NO ABLATION PAIR IS QUOTED HERE: the artifact
1541
- // carries only the with-axis score, so half a refreshed pair would manufacture a delta
1794
+ // carries only the with-variation score, so half a refreshed pair would manufacture a delta
1542
1795
  // across two bases — re-run the ablation to price it.
1543
1796
  //
1544
- // It rides the LIFT variants because the raise mutates in place: a second raise policy needs
1797
+ // It is a LIFT variation because the raise mutates in place: a second raise policy needs
1545
1798
  // its own copy of the lifted fn, exactly as `/setup-args` needs one to narrow. Crossed with
1546
1799
  // `/setup-args` rather than nested under it — dropping a call argument and choosing this
1547
1800
  // shape are independent, and the four combinations dedup down to whatever the trees differ on.
1548
- const connectiveVariants = treeOwnedFold
1801
+ const connectiveSettings: { variations: readonly Variation[]; connective: boolean }[] = treeOwnedFold
1549
1802
  ? [
1550
- { suffix: '', connective: false },
1551
- { suffix: '/connective', connective: true },
1803
+ { variations: [], connective: false },
1804
+ { variations: ['connective'], connective: true },
1552
1805
  ]
1553
- : [{ suffix: '', connective: false }];
1554
- const liftVariants: { suffix: string; narrow: boolean; connective: boolean }[] = (
1555
- hasSetupArgsNarrowing(base)
1556
- ? [
1557
- { suffix: '', narrow: false },
1558
- { suffix: '/setup-args', narrow: true },
1559
- ]
1560
- : [{ suffix: '', narrow: false }]
1561
- ).flatMap((l) => connectiveVariants.map((c) => ({ ...l, ...c, suffix: `${l.suffix}${c.suffix}` })));
1562
- for (const lv of liftVariants) {
1806
+ : [{ variations: [], connective: false }];
1807
+ const narrowSettings: { variations: readonly Variation[]; narrow: boolean }[] = hasSetupArgsNarrowing(base)
1808
+ ? [
1809
+ { variations: [], narrow: false },
1810
+ { variations: ['setup-args'], narrow: true },
1811
+ ]
1812
+ : [{ variations: [], narrow: false }];
1813
+ const liftSettings = narrowSettings.flatMap((l) =>
1814
+ connectiveSettings.map((c) => ({ ...l, ...c, variations: [...l.variations, ...c.variations] })),
1815
+ );
1816
+ for (const liftSetting of liftSettings) {
1563
1817
  let fn: Fn;
1564
1818
  let inferredSymbols = new Map<string, SymbolInfo>();
1565
1819
  let orderLicensed: ReadonlySet<string> = new Set<string>();
1566
1820
  try {
1567
- // A NON-EMPTY SUFFIX IS WHAT NEEDS ITS OWN COPY, the catch below's spelling: naming the
1568
- // flags here would leave a fourth axis sharing the primary's already-mutated `base`.
1569
- fn = lv.suffix === '' ? base : frontend.lift(name, asm, target, prototypes, opts.asmData, sv.symbols);
1570
- if (lv.narrow && !narrowToSetupArgs(fn)) {
1571
- continue; // nothing to cut after all — the base lift's own candidates already cover it
1821
+ // A SETTING THAT NAMES ANY VARIATION IS WHAT NEEDS ITS OWN COPY, the catch below's spelling: naming the
1822
+ // flags here would leave a fourth lift variation sharing the default's already-mutated `base`.
1823
+ fn =
1824
+ liftSetting.variations.length === 0
1825
+ ? base
1826
+ : frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
1827
+ if (liftSetting.narrow && !narrowToSetupArgs(fn)) {
1828
+ continue; // nothing to cut after all — the default lift's own candidates already cover it
1572
1829
  }
1573
1830
  verify(fn);
1574
- // This variant's OWN array-shape evidence, off its own lifted fn (a symbol map promotes
1575
- // numeric pool words to `gaddr`, so the `/raw-globals` arm genuinely answers differently).
1831
+ // This lift's OWN array-shape evidence, off its own lifted fn (a symbol map promotes
1832
+ // numeric pool words to `gaddr`, so the `/raw-globals` setting genuinely answers differently).
1576
1833
  //
1577
1834
  // NEVER A NAME THE PROJECT MAP KNOWS, and the filter is here rather than left to
1578
- // structure()'s map-first lookup because THE `/raw-globals` ARM STRUCTURES WITH NO MAP
1579
- // AND DECLARES WITH ONE. `declSymbols` is probe-derived and map-last (the map wins every
1580
- // name it knows), so on an asm whose pool NAMES its globals the raw arm could spell a
1835
+ // structure()'s map-first lookup because THE `/raw-globals` SETTING STRUCTURES WITH NO MAP
1836
+ // AND DECLARES WITH ONE. `declSymbols` is derived from the shared lift and map-last (the map
1837
+ // wins every name it knows), so on an asm whose pool NAMES its globals the raw setting could spell a
1581
1838
  // subscript off THIS function's strides — `gFoo[i][j]`, inner extent 2 — while the
1582
1839
  // declaration beside it came from the map — `extern u32 gFoo[][8];` — and the emitted C
1583
1840
  // would stride by 8. Compiling, and the wrong address. One name the map describes is
1584
- // one name this derivation does not claim, on either arm.
1841
+ // one name this derivation does not claim, on either setting.
1585
1842
  //
1586
1843
  // AND NEVER A SHAPE THE DECLARATION BLOCK WILL NOT CARRY, which is the same hazard one
1587
- // step further out. `declSymbols` is built ONCE, off the probe's lift; this map is built
1588
- // per variant, off the variant's own. Where the two lifts disagree about a name the map
1589
- // does NOT know, the map-precedence delete above says nothing and the raw arm would
1844
+ // step further out. `declSymbols` is built ONCE, off the shared lift; this map is built
1845
+ // per lift, off that lift's own. Where the two lifts disagree about a name the map
1846
+ // does NOT know, the map-precedence delete above says nothing and the raw setting would
1590
1847
  // again spell from one shape and declare from another. So the test is not "the map
1591
1848
  // knows this name" but "whatever will be DECLARED for this name says the same thing" —
1592
1849
  // a name the two answer differently keeps the cast form, which needs no declaration.
1593
1850
  inferredSymbols = inferGlobalArrays(fn, target);
1594
- // The ORDER half, off the same variant lift. NO map-precedence filter, and the
1851
+ // The ORDER half, off the same lift. NO map-precedence filter, and the
1595
1852
  // asymmetry is the point: a shape is a DECLARATION, so a name the map describes must
1596
1853
  // not be spelled from this function's strides — a licence declares nothing, and the
1597
1854
  // spelling it enables keeps the cast (`(T *)&gSym`), which is byte-correct under any
@@ -1618,205 +1875,311 @@ export function enumerateCandidates(
1618
1875
  },
1619
1876
  },
1620
1877
  prototypes[name],
1621
- { shortCircuit: { foldTreeOwned: lv.connective } },
1878
+ { shortCircuit: { foldTreeOwned: liftSetting.connective } },
1622
1879
  );
1623
1880
  } catch (e) {
1624
- // THE PRIMARY IS THE EMPTY SUFFIX, by construction: every lift axis appends a non-empty
1625
- // one, so `suffix === ''` is the only spelling of "no lever is on" that stays correct
1626
- // when a fourth is added — the same reason the structuring half below reads its table
1881
+ // THE DEFAULT CARRIES NO VARIATION, by construction: every lift variation appends one, so
1882
+ // `variations.length === 0` is the only spelling of "no lift variation is on" that stays correct
1883
+ // when a fourth is added — the same reason the structure half below reads its table
1627
1884
  // instead of naming its flags.
1628
- if (lv.suffix === '') {
1629
- throw e; // the base lift keeps its behavior: a raising failure aborts the row
1885
+ if (liftSetting.variations.length === 0) {
1886
+ throw e; // the default lift keeps its behavior: a raising failure aborts the row
1630
1887
  }
1631
- // A dropped lever, never an aborted enumeration — the same posture as `respell`.
1632
- opts.onLeverError?.(name + lv.suffix, firstLine(e));
1888
+ // A dropped variation, never an aborted enumeration — the same posture as `respell`.
1889
+ reportThrow(liftSetting.variations, e);
1633
1890
  continue;
1634
1891
  }
1635
- // the per-variant axis gates, on THIS variant's lifted fn see the table doc
1636
- const variantOff = STRUCTURING_AXES.filter((ax) => ax.variantGate !== undefined && !ax.variantGate(fn));
1637
- const variantCands = svCands.filter((s) => variantOff.every((ax) => !s[ax.flag]));
1638
- // `/merge-names` combinations whose un-merged sibling was DROPPED. `structure()` already
1639
- // refuses to let the axis unlock a function the primary declines, but it can only see its own
1640
- // refusals a boundary contract fails out here, in `structureChecked`. Without this a
1641
- // `/reread-globals/merge-names` candidate could ship where plain `/reread-globals` did not,
1642
- // which is the same trade one level up. `senseCands` puts each `mergeNames:false` sibling
1643
- // first, so the entry is always recorded before its merged twin is reached.
1644
- const droppedPrimary = new Set<string>();
1645
- for (const s of variantCands) {
1646
- if (
1647
- STRUCTURING_AXES.some((ax) => ax.strip && s[ax.flag] && droppedPrimary.has(s.suffix.replace(ax.suffix, '')))
1648
- ) {
1649
- // A SKIPPED variant is recorded exactly like a dropped one, or the closure would not be
1650
- // transitive: with plain X dropped and X/inplace skipped-but-unrecorded,
1651
- // X/inplace/merge-names would find neither stripped key and run — shipping a
1652
- // double-lever candidate where its ancestor failed the boundary contracts.
1653
- droppedPrimary.add(s.suffix);
1892
+ // THE SHARED-TAIL VARIATIONS: the same raised fn, structured again with `followEarlyReturns`,
1893
+ // in two passes after the default one.
1894
+ // - `/shared-ret` is the follow ALONE, on the fn as raised. Some divergent `if` shares a
1895
+ // `ret` the compiler left in place (`synthetic:gcseinner`).
1896
+ // - `/shared-tail` is the follow after `sinkStoreTails` has rewritten the fn in place, which
1897
+ // is safe because `structure()` never mutates `fn`, so the earlier passes are done with it.
1898
+ // It is enumerated only where the sink changed something AND some divergent `if` of the
1899
+ // SUNK fn shares a `ret`. That is the sink's price gate: without it, a tail copied into
1900
+ // arms no `if` shares adds a candidate spelling it in each 204 synthetic candidates on 8
1901
+ // rows (`gcsedup`, `gcseinnerdup`, `armexpr`, `mergeu16`, `ladder4`, `ladder5`,
1902
+ // `ladidx2`, `revlad5s`), every one of them MATCH today.
1903
+ // They are lift variations rather than structure variations because the sink is an IR
1904
+ // rewrite, and they run here rather than in pipeline.ts's spine because that costs no second
1905
+ // lift. Neither is the default: the same IR comes from both sources (raise/tailsink.ts), and
1906
+ // the follow alone as a default costs `synthetic:sw_fallguard:ido7.1` 13/19 19/23 and
1907
+ // `synthetic:gcseflat:agbcc` 19/53 22/54.
1908
+ //
1909
+ // TWO BITS, NOT ONE, because the sink can DELETE the follow. A store tail that is itself the
1910
+ // `ret` both sides of an `if` reach is copied into its `br` sources, and then no shared `ret`
1911
+ // is left. `synthetic:gcsejoin:agbcc` is ordinary loop-free C of that shape, and it scored
1912
+ // 7/37 while the follow was tried only behind the sink. No per-tail predicate on this IR picks
1913
+ // between the two: refusing to sink a tail that is already a follow costs
1914
+ // `synthetic:gcseflat:agbcc` 0/49 → 19/53 and `synthetic:gcsearms6:agbcc` 0/84 → 23/87, and
1915
+ // those two need a tail that is BOTH. So the follow-alone candidate is enumerated wherever
1916
+ // the fn as raised has a follow, sunk or not. Its price over a single bundled follow-and-sink
1917
+ // pass is +76 synthetic candidates, on those two rows, and +0 real: where the sink does not
1918
+ // fire only the `/shared-ret` pass runs, and on every real row where it fires the fn as
1919
+ // raised has no follow.
1920
+ //
1921
+ // EACH BIT IS PER LIFT, not per site: the `/shared-tail` pass sinks every store tail
1922
+ // and follows every divergent `if` at once, so a function with two sites gets the both-on
1923
+ // candidate only. Counted over both tiers (`ProcessInputAndUpdateEntities` excepted), three
1924
+ // functions carry two of something: `synthetic:maskchain:agbcc` two follow sites,
1925
+ // `synthetic:gcsearms6:agbcc` two sunk tails — both MATCH — and
1926
+ // `pokeemerald:SetMauvilleOldManLanguage:agbcc` two sunk tails after which no `if` shares a
1927
+ // `ret`, so the price gate above refuses and its fan is unchanged. Every other function
1928
+ // carries at most one of each.
1929
+ //
1930
+ // `droppedDefault` is the DEFAULT pass's drops. Each shared-tail pass reads it and keeps its
1931
+ // own drops in a copy, so one pass's structuring failure never removes the other's candidate.
1932
+ const droppedDefault = new Set<string>();
1933
+ for (const pass of ['default', 'follow', 'sink'] as const) {
1934
+ if (pass === 'follow' && !hasDivergentSharedRet(fn)) {
1654
1935
  continue;
1655
1936
  }
1656
- // structure() reads `fn` and produces a fresh SFn (it does not mutate `fn`), so both branch
1657
- // senses structure the same recovered function without re-lifting.
1658
- let sfn: SFn;
1659
- try {
1660
- sfn = structureChecked(fn, {
1661
- ...svOpts,
1662
- ...(inferredSymbols.size ? { inferredSymbols } : {}),
1663
- ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
1664
- preserveDivergentBranchSense: s.sense,
1665
- negateJoinedBranchSense: s.join ? !defSense : defSense,
1666
- anchorConstCopies: s.anchor,
1667
- anchorLoopEntryConsts: s.entry,
1668
- spellBitfieldMembers: s.bitfields,
1669
- spellPtrMemberElements: s.ptrElems,
1670
- spellDeclaredSubscripts: s.declRank,
1671
- ...STRUCTURING_AXES.reduce((acc, ax) => ({ ...acc, ...ax.options(s[ax.flag]) }), {}),
1672
- });
1673
- } catch (e) {
1674
- if (lv.suffix === '' && isBaseAxisPoint(s)) {
1675
- throw e; // the base lift's base axes keep their behavior: a failure aborts the row
1937
+ if (pass === 'sink') {
1938
+ let sunk: boolean;
1939
+ try {
1940
+ sunk = sinkStoreTails(fn);
1941
+ if (sunk) {
1942
+ verify(fn);
1943
+ }
1944
+ } catch (e) {
1945
+ reportThrow([...liftSetting.variations, 'shared-tail'], e);
1946
+ break;
1947
+ }
1948
+ // Unsunk, this fn is the `/shared-ret` pass's again.
1949
+ if (!sunk || !hasDivergentSharedRet(fn)) {
1950
+ break;
1676
1951
  }
1677
- // Recorded for EVERY dropped variant: a candidate with more axes on looks its siblings
1678
- // up by stripping one axis at a time, and the stripped key can itself carry the other.
1679
- droppedPrimary.add(s.suffix);
1680
- // an anchored variant that fails structuring or its contracts is a dropped lever, never
1681
- // an aborted enumeration — same rule as respell below
1682
- opts.onLeverError?.(name + lv.suffix + s.suffix, firstLine(e));
1683
- continue;
1684
1952
  }
1685
- // A TREE another axis point already spelled. `fanOut` reads the tree and this call's own
1686
- // constants, nothing that varies per axis point — its signature is the argument — so a
1687
- // repeated tree can only re-emit sources `seen` already holds: the candidate list, its
1688
- // order and its labels are exactly the ones the whole fan produces, reached without
1689
- // re-deriving forty passes. An axis is INERT on most functions (nothing to re-read, no
1690
- // bitfield member, no joined if), and an inert axis is a factor of two in the cross that
1691
- // changes nothing: on the klonoa checkout's `LoadBGTilemapData` under
1692
- // docs/ranked-repro.md's flags, 640 of 1024 axis points (62.5%) re-derive a tree an
1693
- // earlier one already emitted.
1694
- //
1695
- // Keyed on the JSON text, in a Set of STRINGS — a value comparison, so it can never
1696
- // merge two trees the way a hash could. Its one direction of error is a MISS (a
1697
- // differing key order re-runs a fan whose spellings then dedup as they do today), and
1698
- // the property that rules the other direction out — that the text determines the tree —
1699
- // is pinned by rank-tree-key.test.ts rather than assumed.
1953
+ const alternative = pass !== 'default';
1954
+ const dropped = alternative ? new Set(droppedDefault) : droppedDefault;
1955
+ const liftVariations: readonly Variation[] =
1956
+ pass === 'follow'
1957
+ ? [...liftSetting.variations, 'shared-ret']
1958
+ : pass === 'sink'
1959
+ ? [...liftSetting.variations, 'shared-tail']
1960
+ : liftSetting.variations;
1961
+ // the per-lift gates, on THIS lift's fn — see the table doc
1962
+ const offForThisLift = STRUCTURE_VARIATIONS.filter(
1963
+ (variation) => variation.perLiftGate !== undefined && !variation.perLiftGate(fn),
1964
+ );
1965
+ const settingsForLift = usableStructureSettings.filter((s) =>
1966
+ offForThisLift.every((variation) => !s[variation.flag]),
1967
+ );
1968
+ // `/merge-names` combinations whose un-merged sibling was DROPPED. `structure()` already
1969
+ // refuses to let the variation unlock a function the default declines, but it can only see its own
1970
+ // refusals — a boundary contract fails out here, in `structureChecked`. Without this a
1971
+ // `/reread-globals/merge-names` candidate could ship where plain `/reread-globals` did not,
1972
+ // which is the same trade one level up. `structureSettings` puts each `mergeNames:false` sibling
1973
+ // first, so the entry is always recorded before its merged sibling is reached.
1700
1974
  //
1701
- // The key therefore spans EVIDENCE fields too, `index.operandOff` among them, which
1702
- // `exprEquals` deliberately ignores (l3/ast.ts). The two are right to disagree: two
1703
- // trees identical but for that field denote the same cells, so a CSE may collapse them,
1704
- // and they admit different bases under `BASEFOLD_GATES`, so a fan may not. Dropping it
1705
- // from the key would be the direction the paragraph above rules out. It carries a
1706
- // DISPLACEMENT rather than a presence flag, so it can split two trees that print the
1707
- // same subscript off different addends — re-priced when it widened, over klonoa's
1708
- // `LoadBGTilemapData` under docs/ranked-repro.md's flags: 66816 candidates either way,
1709
- // and all 66816 `[score]` lines identical. It splits 0 keys, so the miss it can cause
1710
- // has no inhabitant.
1711
- const treeKey = JSON.stringify(sfn);
1712
- if (seenTrees.has(treeKey)) {
1713
- opts.onTreeDeduped?.();
1714
- continue;
1715
- }
1716
- seenTrees.add(treeKey);
1717
- // The row's OWN tree, so this is the one call whose backend refusal is the row's cause.
1718
- const primary = fanOut(sfn);
1719
- const spellings = primary.spellings;
1720
- if (primary.emit) {
1721
- lastEmitError = primary.emit.error;
1722
- }
1723
- // The PRE-FAN products (PRE_FAN_PRODUCTS, the fourth mechanism the POLICY note names):
1724
- // rewrite the TREE, then fan the whole re-spelling set over the result, so every lever
1725
- // below derives from the rewrite instead of composing onto it. The gate is the pass's
1726
- // own decline; the contracts are `respell`'s three, for `respell`'s reasons.
1727
- for (const pf of PRE_FAN_PRODUCTS) {
1975
+ // The shared-tail passes read the DEFAULT pass's set as well: neither `X/shared-ret` nor
1976
+ // `X/shared-tail` ever ships where `X` was dropped. The follow and the sink each give the
1977
+ // structurer a shape it can accept where the default declined sound, but the same
1978
+ // trade one level up again.
1979
+ for (const s of settingsForLift) {
1980
+ const key = s.variations.join('/');
1981
+ if (alternative && droppedDefault.has(key)) {
1982
+ continue;
1983
+ }
1984
+ if (
1985
+ STRUCTURE_VARIATIONS.some(
1986
+ (variation) =>
1987
+ variation.strip &&
1988
+ s[variation.flag] &&
1989
+ dropped.has(s.variations.filter((v) => v !== variation.name).join('/')),
1990
+ )
1991
+ ) {
1992
+ // A SKIPPED setting is recorded exactly like a dropped one, or the closure would not be
1993
+ // transitive: with plain X dropped and X/inplace skipped-but-unrecorded,
1994
+ // X/inplace/merge-names would find neither stripped key and run — shipping a
1995
+ // candidate carrying two variations where its ancestor failed the boundary contracts.
1996
+ dropped.add(key);
1997
+ continue;
1998
+ }
1999
+ // structure() reads `fn` and produces a fresh SFn (it does not mutate `fn`), so both branch
2000
+ // senses structure the same recovered function without re-lifting.
2001
+ let sfn: SFn;
1728
2002
  try {
1729
- const made = pf.apply(sfn);
1730
- if (made === null) {
1731
- continue;
1732
- }
1733
- // The SAME tree dedup the primary above gets, and for the same reason: `fanOut` is a
1734
- // pure function of the tree, so re-fanning one already fanned buys nothing and makes
1735
- // the row's quoted fan cost a number that is partly duplicates. A SEPARATE set, not
1736
- // `seenTrees`: adding a rewritten tree there would let it skip a later PRIMARY tree
1737
- // that happens to equal it, and that primary's own pre-fan output — which nothing
1738
- // has computed — would go with it.
1739
- const madeKey = JSON.stringify(made);
1740
- if (seenPreFan.has(madeKey)) {
1741
- continue;
1742
- }
1743
- seenPreFan.add(madeKey);
1744
- assertResolved(made);
1745
- assertDerefsTyped(made);
1746
- assertLocalsWritten(made);
1747
- // A backend refusal on this REWRITTEN tree is not a refusal of the row's own
1748
- // spelling, so it never becomes the row's stated cause: `FanResult.emit` is dropped
1749
- // here and only the primary call above records one.
1750
- //
1751
- // It is reported instead through `onLeverError` under `pf.suffix`, which is what
1752
- // `fanOut`'s second argument is for: a primary emit refusal does not THROW —
1753
- // `fanOut` returns it — so the `catch` below never sees it, and under the bare
1754
- // function name it would read as a refusal of the primary spelling while the lever's
1755
- // whole half of the fan was deleted.
1756
- const fanned = fanOut(made, pf.suffix).spellings;
1757
- for (const sp of fanned) {
1758
- spellings.push({ ...sp, suffix: `${pf.suffix}${sp.suffix}` });
1759
- }
2003
+ sfn = structureChecked(fn, {
2004
+ ...symbolSettingOpts,
2005
+ ...(inferredSymbols.size ? { inferredSymbols } : {}),
2006
+ ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
2007
+ preserveDivergentBranchSense: s.sense,
2008
+ negateJoinedBranchSense: s.join ? !defSense : defSense,
2009
+ ...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
2010
+ anchorConstCopies: s.anchor,
2011
+ anchorLoopEntryConsts: s.entry,
2012
+ spellBitfieldMembers: s.bitfields,
2013
+ spellPtrMemberElements: s.ptrElems,
2014
+ spellDeclaredSubscripts: s.declRank,
2015
+ ...STRUCTURE_VARIATIONS.reduce(
2016
+ (acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
2017
+ {},
2018
+ ),
2019
+ ...(alternative ? { followEarlyReturns: true } : {}),
2020
+ });
1760
2021
  } catch (e) {
1761
- opts.onLeverError?.(`${name}${pf.suffix}`, firstLine(e));
2022
+ if (liftVariations.length === 0 && isDefaultSetting(s)) {
2023
+ throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
2024
+ }
2025
+ // Recorded for EVERY dropped setting: a candidate with more variations on looks its siblings
2026
+ // up by stripping one variation at a time, and the stripped key can itself carry the other.
2027
+ dropped.add(key);
2028
+ // an anchored setting that fails structuring or its contracts is a dropped variation, never
2029
+ // an aborted enumeration — same rule as respell below
2030
+ reportThrow([...liftVariations, ...s.variations], e);
2031
+ continue;
1762
2032
  }
1763
- }
1764
- for (const sp of spellings) {
1765
- const source = sp.source;
1766
- // Collapse a spelling that produced identical source (a function with no divergent `if`
1767
- // structures the same either way): no point scoring a duplicate spelling. Deduping the
1768
- // WHOLE emitted set (not just scored survivors) is equivalent an identical source
1769
- // scores identically, so it can never change `best` — and it keeps the candidate set to
1770
- // the genuinely distinct spellings.
1771
- //
1772
- // THE PUBLISHED LABEL IS THEREFORE NOT AN ATTRIBUTION, and every argument in this tree
1773
- // that counts winning labels is unsound to exactly that extent. The label kept is the
1774
- // FIRST route's; the later routes are discarded, silently and by design. Adding one
1775
- // roster row renamed 21 agbcc rows whose emitted source sets were byte-identical —
1776
- // a CANDIDATE-SET census (whole fan unchanged, some candidate relabelled), which is a
1777
- // different population from a WINNING-label census: over published winners the same
1778
- // row moved 5 labels, 2 of them renames.
2033
+ // A TREE another structure setting already produced. `respellTree` reads the tree and this
2034
+ // call's own constants, nothing that varies per setting — its signature is the argument —
2035
+ // so a repeated tree can only re-emit sources `seen` already holds: the candidate list,
2036
+ // its order and its variations are exactly the ones the whole fan produces, reached
2037
+ // without re-deriving forty passes. A structure variation is INERT on most functions
2038
+ // (nothing to re-read, no bitfield member, no joined if), and an inert one is a factor of
2039
+ // two in the cross that changes nothing: on the klonoa checkout's `LoadBGTilemapData`
2040
+ // under docs/ranked-repro.md's flags, 640 of 1024 structure settings (62.5%) re-derive a
2041
+ // tree an earlier one already emitted.
1779
2042
  //
1780
- // AND A LABEL CENSUS CANNOT EVEN SEPARATE A RENAME FROM A RESPELLING. Of those 5
1781
- // winners, THREE changed the source they publish `synthetic:unfoldpark`
1782
- // (402 397 bytes, score 9 0), `kleod:ConfigureEntityBehavior` (3677 3993,
1783
- // 233 230) and `synthetic:livepark` (337 346, both MATCH) while
1784
- // `synthetic:foldpark` and `kleod:DecompressDma` are byte-identical renames. The two
1785
- // look the same from here; only the emitted SOURCE tells them apart (`bench diff`
1786
- // publishes that field, `bench regression` does not).
2043
+ // Keyed on the JSON text, in a Set of STRINGS a value comparison, so it can never
2044
+ // merge two trees the way a hash could. Its one direction of error is a MISS (a
2045
+ // differing key order re-runs a respell set whose sources then dedup as they do today), and
2046
+ // the property that rules the other direction out that the text determines the tree
2047
+ // is pinned by rank-tree-key.test.ts rather than assumed.
1787
2048
  //
1788
- // So "N rows win under this family" bounds nothing: a family can win zero labels and
1789
- // still be the only route to a source, and a family can win five and have introduced
1790
- // three. Price a family by ABLATING it and re-running the rows
1791
- // (LIVEBASE_BLOCK_GATES carries the recipe); a zero census is not a death certificate,
1792
- // and a nonzero one is not a mechanism.
1793
- // THE SEAM FIX IS BOOKED AND NOT BUILT: keep the losing producers on the surviving
1794
- // candidate (`label` plus an `alsoReachedBy: string[]`) and a census by mechanism
1795
- // becomes one. It is not free — every consumer that reads `label` as the derivation
1796
- // would have to say which it means, and the published `candidateLabel` must not
1797
- // change so build it when a round needs the census, not before. Until then the only
1798
- // sound census is an ablation.
1799
- const dup = seen.get(source);
1800
- if (dup !== undefined) {
1801
- // The same TEXT, reached twice. `matchOnly` is a property of the DERIVATION and the
1802
- // published artifact is the text, so a spelling some sound route also produces is a
1803
- // proven one however the first route reached it — clear the flag rather than keeping
1804
- // whichever route the enumeration happened to walk first.
1805
- if (sp.matchOnly === undefined) {
1806
- delete dup.matchOnly;
1807
- }
2049
+ // The key therefore spans EVIDENCE fields too, `index.operandOff` among them, which
2050
+ // `exprEquals` deliberately ignores (l3/ast.ts). The two are right to disagree: two
2051
+ // trees identical but for that field denote the same cells, so a CSE may collapse them,
2052
+ // and they admit different bases under `BASEFOLD_GATES`, so a fan may not. Dropping it
2053
+ // from the key would be the direction the paragraph above rules out. It carries a
2054
+ // DISPLACEMENT rather than a presence flag, so it can split two trees that print the
2055
+ // same subscript off different addends re-priced when it widened, over klonoa's
2056
+ // `LoadBGTilemapData` under docs/ranked-repro.md's flags: 66816 candidates either way,
2057
+ // and all 66816 `[score]` lines identical. It splits 0 keys, so the miss it can cause
2058
+ // has no inhabitant.
2059
+ const treeKey = JSON.stringify(sfn);
2060
+ if (seenTrees.has(treeKey)) {
2061
+ opts.onTreeDeduped?.();
1808
2062
  continue;
1809
2063
  }
1810
- const made: Candidate = {
1811
- label: `${cand.label}${lv.suffix}${s.suffix}${sp.suffix}${sv.suffix}`,
1812
- source,
1813
- group: svIndex,
1814
- ...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
1815
- ...(sp.deviceVolatile ? { deviceVolatile: sp.deviceVolatile } : {}),
1816
- ...(sp.matchOnly ? { matchOnly: sp.matchOnly } : {}),
1817
- };
1818
- seen.set(source, made);
1819
- out.push(made);
2064
+ seenTrees.add(treeKey);
2065
+ // The row's OWN tree, so this is the one call whose backend refusal is the row's cause.
2066
+ const own = respellTree(sfn);
2067
+ const sources = own.sources;
2068
+ if (own.emit) {
2069
+ lastEmitError = own.emit.error;
2070
+ }
2071
+ // The PRE-RESPELL variations (PRE_RESPELL_VARIATIONS, the last composition the POLICY note
2072
+ // names): rewrite the TREE, then run the whole respell set over the result, so every
2073
+ // respell variation derives from the rewrite instead of composing onto it. The gate is the pass's
2074
+ // own decline; the contracts are `respell`'s three, for `respell`'s reasons.
2075
+ for (const pf of PRE_RESPELL_VARIATIONS) {
2076
+ try {
2077
+ const made = pf.apply(sfn);
2078
+ if (made === null) {
2079
+ continue;
2080
+ }
2081
+ // The SAME tree dedup the row's own tree above gets, and for the same reason: `respellTree`
2082
+ // is a pure function of the tree, so running it again on a tree it has already run on buys nothing
2083
+ // and makes the row's quoted fan cost a number that is partly duplicates. A SEPARATE
2084
+ // set, not `seenTrees`: adding a rewritten tree there would let it skip a later
2085
+ // STRUCTURED tree that happens to equal it, and that tree's own pre-respell output —
2086
+ // which nothing has computed — would go with it.
2087
+ const madeKey = JSON.stringify(made);
2088
+ if (seenPreRespell.has(madeKey)) {
2089
+ continue;
2090
+ }
2091
+ seenPreRespell.add(madeKey);
2092
+ assertResolved(made);
2093
+ assertDerefsTyped(made);
2094
+ assertLocalsWritten(made);
2095
+ assertNoOrphanedLocals(sfn, made);
2096
+ // A backend refusal on this REWRITTEN tree is not a refusal of the row's own
2097
+ // tree, so it never becomes the row's stated cause: `TreeSources.emit` is dropped
2098
+ // here and only the call over the row's own tree above records one.
2099
+ //
2100
+ // It is reported instead through `onEnumerationError` under `pf.name`, which is what
2101
+ // `respellTree`'s second argument is for: a default emit refusal does not THROW —
2102
+ // `respellTree` returns it — so the `catch` below never sees it, and reported with no
2103
+ // variations it would read as a refusal of the row's default source while the
2104
+ // variation's whole half of the fan was deleted.
2105
+ const respelled = respellTree(made, [pf.name]).sources;
2106
+ for (const sp of respelled) {
2107
+ sources.push({ ...sp, variations: [pf.name, ...sp.variations] });
2108
+ }
2109
+ } catch (e) {
2110
+ reportThrow([pf.name], e);
2111
+ }
2112
+ }
2113
+ for (const sp of sources) {
2114
+ const source = sp.source;
2115
+ // Collapse a candidate whose source is identical (a function with no divergent `if`
2116
+ // structures the same either way): no point scoring a duplicate. Deduping the
2117
+ // WHOLE emitted set (not just scored survivors) is equivalent — an identical source
2118
+ // scores identically, so it can never change the winner — and it keeps the candidate set to
2119
+ // the genuinely distinct sources.
2120
+ //
2121
+ // A CANDIDATE'S PUBLISHED VARIATIONS ARE THEREFORE NOT AN ATTRIBUTION, and every argument
2122
+ // in this tree that counts winners' variations is unsound to exactly that extent. The
2123
+ // variations kept are the FIRST route's; the later routes are discarded, silently and by
2124
+ // design. Adding one hoist renamed candidates on 21 agbcc rows whose emitted source sets
2125
+ // were byte-identical — a CANDIDATE-SET census (whole fan unchanged, some candidate
2126
+ // renamed), which counts something different from a census of WINNERS: over published
2127
+ // winners the same hoist moved 5 names, 2 of them renames.
2128
+ //
2129
+ // AND A CENSUS OF VARIATIONS CANNOT EVEN SEPARATE A RENAME FROM A CHANGED SOURCE. Of those 5
2130
+ // winners, THREE changed the source they publish — `synthetic:unfoldpark`
2131
+ // (402 → 397 bytes, score 9 → 0), `kleod:ConfigureEntityBehavior` (3677 → 3993,
2132
+ // 233 → 230) and `synthetic:livepark` (337 → 346, both MATCH) — while
2133
+ // `synthetic:foldpark` and `kleod:DecompressDma` (kl-eod-decomp's source, before 2026-09-13) were byte-identical renames. The two
2134
+ // look the same from here; only the emitted SOURCE tells them apart (`bench diff`
2135
+ // publishes that field, `bench regression` does not).
2136
+ //
2137
+ // So "N rows win under this family" bounds nothing: a family can win zero winners and
2138
+ // still be the only route to a source, and a family can win five and have introduced
2139
+ // three. Price a family by ABLATING it and re-running the rows
2140
+ // (LIVEBASE_BLOCK_GATES carries the recipe); a zero census is not a death certificate,
2141
+ // and a nonzero one is not a mechanism.
2142
+ // THE SEAM FIX IS BOOKED AND NOT BUILT: keep the losing routes on the surviving
2143
+ // candidate (`variations` plus an `alsoReachedBy`) and a census by mechanism
2144
+ // becomes one. It is not free — every consumer that reads `variations` as the derivation
2145
+ // would have to say which it means, and the published `winnerVariations` must not
2146
+ // change — so build it when a round needs the census, not before. Until then the only
2147
+ // sound census is an ablation.
2148
+ const dup = seen.get(source);
2149
+ if (dup !== undefined) {
2150
+ // The same TEXT, reached twice. `matchOnly` is a property of the DERIVATION and the
2151
+ // published artifact is the text, so a spelling some sound route also produces is a
2152
+ // proven one however the first route reached it — clear the flag rather than keeping
2153
+ // whichever route the enumeration happened to walk first.
2154
+ if (sp.matchOnly === undefined) {
2155
+ delete dup.matchOnly;
2156
+ }
2157
+ continue;
2158
+ }
2159
+ const variations: readonly Variation[] = [
2160
+ cand.variation,
2161
+ ...liftVariations,
2162
+ ...s.variations,
2163
+ ...sp.variations,
2164
+ ...symbolSetting.variations,
2165
+ ];
2166
+ // `respell`, `respellEach` and the hoist roster ask `offeredOn` before a pass runs; this
2167
+ // asks it of the whole name, so a mint site that skips the question is an enumeration
2168
+ // error and the drawer's target line stays what enumeration does.
2169
+ if (!offeredOn(target, variations)) {
2170
+ throw new Error(`'${variations.join('/')}' is withheld on ${target.compiler} but was enumerated`);
2171
+ }
2172
+ const made: Candidate = {
2173
+ variations,
2174
+ source,
2175
+ preference: symbolIndex,
2176
+ ...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
2177
+ ...(sp.deviceVolatile ? { deviceVolatile: sp.deviceVolatile } : {}),
2178
+ ...(sp.matchOnly ? { matchOnly: sp.matchOnly } : {}),
2179
+ };
2180
+ seen.set(source, made);
2181
+ out.push(made);
2182
+ }
1820
2183
  }
1821
2184
  }
1822
2185
  }
@@ -1826,9 +2189,10 @@ export function enumerateCandidates(
1826
2189
  // candidate; all of them together is the row, and it stays LOUD — the alternative is a caller
1827
2190
  // ranking an empty list and reporting no match for a function nothing ever tried to spell.
1828
2191
  if (out.length === 0) {
1829
- throw new Error(`no spellable candidate for '${name}': ${firstLine(lastEmitError ?? 'no candidate produced')}`, {
1830
- cause: lastEmitError,
1831
- });
2192
+ throw new NoSpellableCandidateError(
2193
+ `no spellable candidate for '${name}': ${firstLine(lastEmitError ?? 'no candidate produced')}`,
2194
+ { cause: lastEmitError },
2195
+ );
1832
2196
  }
1833
2197
  return out;
1834
2198
  }
@@ -1839,27 +2203,32 @@ export function enumerateCandidates(
1839
2203
  * the scorer must be sync (the cli/Node objdiff path). The webapp scores asynchronously and does
1840
2204
  * its own await-loop over `enumerateCandidates`, reusing this module's `Candidate`/`RankedResult`
1841
2205
  * types but not this driver. */
1842
- export function rankBy<S extends { score: number }>(
2206
+ export function rankBy<S extends { score: number; rows?: number }>(
1843
2207
  candidates: Candidate[],
1844
2208
  symbol: string,
1845
2209
  scoreFn: (source: string, symbol: string, candidate: Candidate) => S,
1846
2210
  ): RankedResult<S> {
1847
2211
  const results: (Scored<S> & { order: number })[] = [];
1848
- const dropped: DroppedCandidate[] = []; // spellings that failed to build; only fatal if ALL do
1849
- const withheld: WithheldCandidate[] = []; // spellings that built but did not earn publication
2212
+ const dropped: DroppedCandidate[] = []; // candidates that failed to build; only fatal if ALL do
2213
+ const withheld: WithheldCandidate[] = []; // candidates that built but did not earn publication
1850
2214
  let lastScoreErr: unknown = null;
1851
2215
  candidates.forEach((c, order) => {
1852
2216
  try {
1853
2217
  const score = scoreFn(c.source, symbol, c);
1854
2218
  const why = withheldReason(c, score);
1855
2219
  if (why !== null) {
1856
- withheld.push({ label: c.label, score: score.score, why });
2220
+ withheld.push({
2221
+ variations: c.variations,
2222
+ score: score.score,
2223
+ ...(score.rows === undefined ? {} : { rows: score.rows }),
2224
+ why,
2225
+ });
1857
2226
  return;
1858
2227
  }
1859
2228
  results.push({ ...c, order, score });
1860
2229
  } catch (e) {
1861
2230
  lastScoreErr = e;
1862
- dropped.push({ label: c.label, error: firstLine(e) });
2231
+ dropped.push({ variations: c.variations, error: firstLine(e) });
1863
2232
  }
1864
2233
  });
1865
2234
  if (results.length === 0) {
@@ -1867,22 +2236,24 @@ export function rankBy<S extends { score: number }>(
1867
2236
  // scorer failure, and a list that was entirely proof-gated is a different thing entirely.
1868
2237
  const why =
1869
2238
  lastScoreErr !== null ? firstLine(lastScoreErr) : `${withheld.length} candidate(s) withheld, none scored`;
1870
- throw new Error(`no scorable candidate for '${symbol}': ${why}`, { cause: lastScoreErr });
2239
+ throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, {
2240
+ cause: lastScoreErr,
2241
+ });
1871
2242
  }
1872
2243
  results.sort(compareScored);
1873
- return { best: results[0], candidates: results.map(({ order: _order, ...c }) => c), dropped, withheld };
2244
+ return { winner: results[0], candidates: results.map(({ order: _order, ...c }) => c), dropped, withheld };
1874
2245
  }
1875
2246
 
1876
- /** THE candidate ordering — score, then preference group, then readability, then enumeration
2247
+ /** THE candidate ordering — score, then preference, then readability, then enumeration
1877
2248
  * order. Exported because there are TWO drivers over the same enumeration (this module's sync
1878
2249
  * `rankBy` for the Node/objdiff scorer, and the webapp's async await-loop for the wasm one), and
1879
2250
  * a per-driver copy would let the same input produce two different winners.
1880
2251
  *
1881
- * SCORE dominates absolutely: the differ is the fitness function, and a tie means the axis that
1882
- * separates these two spellings did not change the bytes — so everything below only chooses what
2252
+ * SCORE dominates absolutely: the differ is the fitness function, and a tie means the variation
2253
+ * that separates these two candidates did not change the bytes — so everything below only chooses what
1883
2254
  * the READER sees, and can never cost a match.
1884
2255
  *
1885
- * GROUP next: a named symbol-map spelling beats its `/raw-globals` sibling at equal bytes.
2256
+ * PREFERENCE next: a named symbol-map spelling beats its `/raw-globals` sibling at equal bytes.
1886
2257
  *
1887
2258
  * DEVICE VOLATILITY next: at equal bytes, the spelling that qualifies a DEVICE REGISTER
1888
2259
  * (`capabilities.deviceRegisters`) is the one to publish. A dropped `volatile` on an MMIO cell is
@@ -1891,17 +2262,17 @@ export function rankBy<S extends { score: number }>(
1891
2262
  * window rather than counting the word, because outside it the qualifier is a claim about
1892
2263
  * ordinary memory that the asm does not support — over the bench, counting the word
1893
2264
  * alone decides twelve rows and only two of them touch a device address. A declared term rather
1894
- * than an enumeration order, which an unrelated lever's spellings can slide between.
2265
+ * than an enumeration order, which an unrelated variation's candidates can slide between.
1895
2266
  *
1896
2267
  * IT IS A PREFERENCE, AND EVERY NEW MINTER INHERITS IT. `deviceVolatileClaims` only ever ADDS a
1897
- * claim, so any lever that qualifies a device access wins its own tie by construction: when
1898
- * `/vol-store` joined the roster, six rows changed their published `candidateLabel` and `source`
2268
+ * claim, so any variation that qualifies a device access wins its own tie by construction: when
2269
+ * `/vol-store` joined the roster, six rows changed their published `winnerVariations` and `source`
1899
2270
  * with no score and no outcome moving. That is a judgement about the source rather than a
1900
2271
  * measurement of it — the differ never refereed those six — and it is the same judgement this
1901
2272
  * term was declared to make, taken on the same evidence. What it must never do is change WHICH
1902
- * candidates exist; that stays an admission question, one lever at a time.
2273
+ * candidates exist; that stays an admission question, one variation at a time.
1903
2274
  *
1904
- * CAST COUNT next, and only WITHIN a group. A wrong signedness pin is what manufactures casts —
2275
+ * CAST COUNT next, and only WITHIN a preference. A wrong signedness pin is what manufactures casts —
1905
2276
  * the C backend has to cast a shift operand back to the signedness the machine op needs, so
1906
2277
  * pinning `u32` on a genuinely-signed parameter buys `s32 f(u32 a0) { return (s32)a0 >> a1; }`
1907
2278
  * for the same bytes as `s32 f(s32 a0) { return a0 >> a1; }`. Before the backend synthesized that
@@ -1918,10 +2289,10 @@ export function rankBy<S extends { score: number }>(
1918
2289
  * result deterministic. Spelled explicitly rather than leaning on Array#sort's stability, which
1919
2290
  * would make each preference an accident of two unrelated decisions.
1920
2291
  *
1921
- * WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. `/uns-cmp`'s whole product is
2292
+ * WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. `/uns-cmp`'s whole output is
1922
2293
  * that polarity, and it carries in a DECLARED TYPE rather than a cast — `castCount` reads 0 on
1923
2294
  * both sides of the tie it loses — so at equal bytes enumeration order decides, and a rival
1924
- * axis's spelling can publish a signed compare where the asm's is unsigned. Weighing it needs
2295
+ * variation's candidate can publish a signed compare where the asm's is unsigned. Weighing it needs
1925
2296
  * the candidate's own SFn and the icmp facts it was structured from, neither of which this
1926
2297
  * comparator carries; `deviceVolatile` is the shape such a term would take. */
1927
2298
  export function compareScored<S extends { score: number }>(
@@ -1930,7 +2301,7 @@ export function compareScored<S extends { score: number }>(
1930
2301
  ): number {
1931
2302
  return (
1932
2303
  a.score.score - b.score.score ||
1933
- a.group - b.group ||
2304
+ a.preference - b.preference ||
1934
2305
  (b.deviceVolatile ?? 0) - (a.deviceVolatile ?? 0) ||
1935
2306
  castCount(a.source) - castCount(b.source) ||
1936
2307
  lineCount(a.source) - lineCount(b.source) ||
@@ -1946,7 +2317,7 @@ function lineCount(source: string): number {
1946
2317
 
1947
2318
  /** Scalar casts in a candidate's rendered source — the readability tie-break above.
1948
2319
  *
1949
- * A WITHIN-GROUP tie-break over two spellings of ONE function, and deliberately NARROWER than
2320
+ * A WITHIN-PREFERENCE tie-break over two spellings of ONE function, and deliberately NARROWER than
1950
2321
  * the published readability metric (apps/benchmark/src/eval/quality.ts `casts`): it counts the
1951
2322
  * decomp SCALAR typedef vocabulary only — `(u8)` … `(s32)` — so a pointer, struct or C-keyword
1952
2323
  * cast is not read as noise, those being structural spellings a candidate does not choose.
@@ -1966,7 +2337,7 @@ function castCount(source: string): number {
1966
2337
  return all - addr;
1967
2338
  }
1968
2339
 
1969
- /** First line of whatever a lever, a backend or the scorer threw — the compiler's own diagnostic,
2340
+ /** First line of whatever a variation, a backend or the scorer threw — the compiler's own diagnostic,
1970
2341
  * not a stack. TOTAL on any value, including a non-Error throw, so no caller has to re-spell the
1971
2342
  * `instanceof` test; a caller wanting a word for "nothing was thrown" supplies it at the call. */
1972
2343
  function firstLine(e: unknown): string {