@asmlift/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/contracts.ts +60 -11
  5. package/src/frontend/ssa.ts +1 -1
  6. package/src/frontend/thumb.ts +2 -2
  7. package/src/ir/alias.ts +24 -0
  8. package/src/ir/core.ts +8 -0
  9. package/src/ir/opcodes.ts +43 -7
  10. package/src/ir/simplify.ts +1 -1
  11. package/src/l3/address.ts +2 -2
  12. package/src/l3/advance.ts +373 -0
  13. package/src/l3/argbase.ts +4 -4
  14. package/src/l3/ast.ts +65 -21
  15. package/src/l3/basecse.ts +48 -28
  16. package/src/l3/coalesce.ts +9 -9
  17. package/src/l3/gates.ts +75 -1
  18. package/src/l3/hoist.ts +1 -1
  19. package/src/l3/homesplit.ts +13 -13
  20. package/src/l3/initfirst.ts +3 -3
  21. package/src/l3/inlinebase.ts +16 -16
  22. package/src/l3/mentions.ts +68 -5
  23. package/src/l3/mulfirst.ts +3 -3
  24. package/src/l3/nearbase.ts +4 -4
  25. package/src/l3/offmember.ts +5 -5
  26. package/src/l3/parkfirst.ts +6 -6
  27. package/src/l3/pollguard.ts +3 -3
  28. package/src/l3/ptrfield.ts +4 -4
  29. package/src/l3/regspell.ts +8 -8
  30. package/src/l3/reindex.ts +22 -17
  31. package/src/l3/scopebase.ts +28 -25
  32. package/src/l3/sinkinit.ts +7 -7
  33. package/src/l3/slotorder.ts +3 -3
  34. package/src/l3/storage.ts +1 -1
  35. package/src/l3/tailmerge.ts +2 -2
  36. package/src/l3/typing.ts +3 -3
  37. package/src/l3/unmerge.ts +483 -59
  38. package/src/l3/unreduce.ts +13 -13
  39. package/src/l3/volatileptr.ts +11 -11
  40. package/src/l3/volatileval.ts +11 -11
  41. package/src/l3/volstore.ts +16 -16
  42. package/src/l3/zerosub.ts +6 -6
  43. package/src/pattern/engine.ts +4 -4
  44. package/src/pipeline.ts +17 -5
  45. package/src/proto.ts +2 -2
  46. package/src/raise/const.ts +203 -3
  47. package/src/raise/divpow2.ts +2 -2
  48. package/src/raise/extscale.ts +342 -0
  49. package/src/raise/globalshape.ts +32 -12
  50. package/src/raise/gvn.ts +2 -2
  51. package/src/raise/magicdiv.ts +2 -2
  52. package/src/raise/memberarrays.ts +4 -4
  53. package/src/raise/narrowlocal.ts +18 -2
  54. package/src/raise/paramwidth.ts +24 -2
  55. package/src/raise/pre-recovery.ts +90 -25
  56. package/src/raise/retsink.ts +381 -15
  57. package/src/raise/shortcircuit.ts +595 -34
  58. package/src/raise/structs.ts +4 -4
  59. package/src/raise/tailsink.ts +126 -0
  60. package/src/rank-declare.ts +4 -4
  61. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  62. package/src/rank.ts +1148 -803
  63. package/src/structure/analysis.ts +87 -90
  64. package/src/structure/bitfields.ts +130 -30
  65. package/src/structure/globalaccess.ts +30 -4
  66. package/src/structure/namecoalesce.ts +32 -13
  67. package/src/structure/structure.ts +1415 -200
  68. package/src/structure/switch-recover.ts +100 -7
  69. package/src/symbols.ts +127 -6
  70. package/src/target.ts +155 -35
  71. package/src/trace.ts +1 -1
  72. package/src/variation-definitions.ts +1540 -0
  73. package/src/variation-gates.ts +89 -0
  74. package/src/variation-tokens.ts +355 -0
@@ -1,4 +1,4 @@
1
- // L3 re-spelling lever: UN-REDUCE a loop-carried accumulator — delete `v = INIT; … v = v + K;`
1
+ // L3 respell variation: UN-REDUCE a loop-carried accumulator — delete `v = INIT; … v = v + K;`
2
2
  // and spell each read as the closed form `INIT[start := counter]`.
3
3
  //
4
4
  // v0 = (a0 << 6) + a1; while (v1 <= 31) {
@@ -91,8 +91,8 @@
91
91
  // left. Refused function-wide, because a stashed pointer outlives the statement that made it.
92
92
  // • MOVED-EFFECT — a call or a marker would run once per read instead of once. Refused.
93
93
  // • MOVED-VOLATILE — a `volatile` access is one the source pinned precisely so it would not be
94
- // duplicated or moved. Refused. No corpus row reaches it today (nothing on the base spelling
95
- // this lever rides carries a qualifier on a READ), so it is guarded by its unit test alone.
94
+ // duplicated or moved. Refused. No corpus row reaches it today (nothing on the default spelling
95
+ // this variation rides carries a qualifier on a READ), so it is guarded by its unit test alone.
96
96
  // • MOVED-READ-ALIASABLE — an ordinary memory read moved down the region sees whatever the
97
97
  // region wrote. asmlift can only rule that out for writes it can NAME, so a moved read is
98
98
  // admitted on one configuration: every write the region evaluates goes to a compile-time
@@ -111,7 +111,7 @@
111
111
  // is FALSE. "A write to a hardware register is not a write to any object a C program declares" is
112
112
  // true, and it does not finish the argument: a DMA controller READS a control word and then WRITES
113
113
  // ordinary memory on the program's behalf. On the GBA, storing `0x84000020` to `DMA3CNT`
114
- // (0x040000DC) starts a 32-word transfer into `[DMA3DAD]` — and every row this lever reaches
114
+ // (0x040000DC) starts a 32-word transfer into `[DMA3DAD]` — and every row this variation reaches
115
115
  // drives exactly that register. Modelled and executed, the admitted candidate turns a clean walk
116
116
  // over a destination table into wild writes: the first transfer clobbers the table the init reads,
117
117
  // and every later iteration recomputes its destination from the garbage.
@@ -147,12 +147,12 @@
147
147
  // `firstRejection` call site over both — 750 synthetic trees and all 252 real-tier rows, the real
148
148
  // tier being the SYMBOL-MAPPED configuration since every row carries its authored map —
149
149
  // `unitsDisagree` is true on NO row in either, and `/unreduce` fires on no real-tier row at all,
150
- // so no zero-flip gate reaches this lever. Its one known inhabitant is outside the benchmark:
150
+ // so no zero-flip gate reaches this variation. Its one known inhabitant is outside the benchmark:
151
151
  // klonoa's `UpdateHUDTimePanel`, where WITH a symbol map the accumulator lifts `u16 *` against an
152
152
  // integer init and the gate refuses it, and with RAW ADDRESSES the same asm lifts all-integer and
153
153
  // the candidate is correct and survives (`50335396 + (v15 << 6)`, 64 bytes an iteration, which is
154
154
  // the ROM's own `adds r1, #0x40`). Same assembly, same loop, same stride — a raw-address sweep is
155
- // BLIND to the defect and reports the lever as correct. The checkout sweep behind that datum
155
+ // BLIND to the defect and reports the variation as correct. The checkout sweep behind that datum
156
156
  // covered klonoa's 467 functions in BOTH configurations; a second checkout was swept raw-only and
157
157
  // found nothing, and since raw-only is the blind arm that null result carries no weight. Only the
158
158
  // klonoa half of the sweep is evidence, and it is quoted here without the other.
@@ -186,7 +186,7 @@
186
186
  // was actually MISSING from that table (`volatile-counter`) is in it now, which is the part that
187
187
  // could not wait.
188
188
  //
189
- // Nothing qualifying ⇒ decline (null), never a duplicate of the primary.
189
+ // Nothing qualifying ⇒ decline (null), never a duplicate of the default.
190
190
  import { type IrType } from '../ir/types';
191
191
  import { cellAddress, inRange, rootConst } from './address';
192
192
  import {
@@ -264,7 +264,7 @@ export const UNREDUCE_GATES: readonly Gate<AccCtx>[] = [
264
264
  },
265
265
  {
266
266
  id: 'acc-pinned',
267
- why: 'a declaration that carries an asm fact cannot be deleted without dropping the fact',
267
+ why: 'a declaration that carries a fact from the assembly cannot be deleted without dropping the fact',
268
268
  sound: true,
269
269
  guardedBy: 'unreduce.test.ts: a pinned accumulator declines, on every pin a local can carry',
270
270
  rejects: (c) => c.pinned,
@@ -344,14 +344,14 @@ export const UNREDUCE_GATES: readonly Gate<AccCtx>[] = [
344
344
  },
345
345
  {
346
346
  id: 'nonzero-start',
347
- why: 'a counter starting at a nonzero constant leaves a bias term this file does not spell',
347
+ why: 'a counter starting at a nonzero constant leaves a bias term this rewrite does not write',
348
348
  sound: true,
349
349
  guardedBy: 'unreduce.test.ts: a counter-free init declines unless its start is the constant 0',
350
350
  rejects: (c) => c.declined === 'nonzero-start',
351
351
  },
352
352
  {
353
353
  id: 'step-ratio',
354
- why: 'a counter stepping by more than one leaves the ratio K/d, which is not a shift',
354
+ why: 'a counter stepping by more than one leaves a ratio between the two steps, which is not a shift',
355
355
  sound: true,
356
356
  guardedBy: 'unreduce.test.ts: a counter-free init declines when the counter does not step by one',
357
357
  rejects: (c) => c.declined === 'step-ratio',
@@ -426,7 +426,7 @@ function arithScale(t: IrType | undefined): number | null {
426
426
  /** Does this local's DECLARATION pin it against deletion? Every flag `SFn.locals` can carry, because
427
427
  * each is a fact about the ASM that only the declaration states: two qualifiers (deleting a
428
428
  * `volatile u16 *` local re-spells `*p = 0` as a raw cast with no qualifier on it — l3/inlinebase.ts
429
- * carries it onto the minted cast instead, and this lever has no local left to carry anything), a
429
+ * carries it onto the minted cast instead, and this variation has no local left to carry anything), a
430
430
  * frame home, an `undef` whose whole content is the assignment that is MISSING, and the SPILL HOMES.
431
431
  *
432
432
  * WHY `slots` PINS, which is not the obvious reading. Deleting a slot-carrying local does not
@@ -537,7 +537,7 @@ function relate(init: Expr, start: Expr, ctr: string, k: Expr, d: number): Relat
537
537
  return null; // anything else between the root and the counter, and the stride is not `k`
538
538
  };
539
539
  // THE SUBSTITUTIONAL FORM FIRST, unchanged: it is the shipped spelling every corpus inhabitant
540
- // of this lever rides, and trying it first makes the branch below strictly additive — it is
540
+ // of this variation rides, and trying it first makes the branch below strictly additive — it is
541
541
  // reached only where the old rule already declined, so it can admit candidates but never
542
542
  // re-spell one.
543
543
  //
@@ -796,7 +796,7 @@ export interface UnreduceResult {
796
796
 
797
797
  /** The `/unreduce` candidate, or null when no accumulator qualifies. `window` is the target's
798
798
  * declared device-register range (TargetDescription.capabilities.deviceRegisters) — absent, the
799
- * lever still fires on a closed form that reads no memory. `triggers` is
799
+ * variation still fires on a closed form that reads no memory. `triggers` is
800
800
  * `capabilities.deviceMemoryWriters`; absent, EVERY device store is treated as one. */
801
801
  export function unreduceAccumulators(
802
802
  sfn: SFn,
@@ -1,4 +1,4 @@
1
- // L3 re-spelling lever: declare a pointer local that holds a NUMERIC address as pointing to
1
+ // L3 respell variation: declare a pointer local that holds a NUMERIC address as pointing to
2
2
  // volatile data (`volatile u16 *p = (u16 *)0x3000010;`).
3
3
  //
4
4
  // A numeric address has no declaration anywhere — the project maps a symbol's volatility, but a
@@ -8,7 +8,7 @@
8
8
  // optimizer's pseudos and lands the register allocator on different homes (on the row this was
9
9
  // built for, the counter is copied out of r0 so the loaded value can have it — the target's
10
10
  // allocation). So the qualified spellings are emitted alongside the plain one — the all-locals
11
- // form as rank.ts `/volatile`, per-local subsets as its `-name` variants — and the differ
11
+ // form as rank.ts `/volatile`, per-local subsets as its results named by subject — and the differ
12
12
  // referees.
13
13
  //
14
14
  // SEMANTICS ARE PRESERVED BY CONSTRUCTION: `volatile` only RESTRICTS what a compiler may do
@@ -25,7 +25,7 @@
25
25
  // `p = q` taints `p`; conservatively, `p` assigned ANY expression mentioning a tainted name).
26
26
  // The symbol map owns a declared global's volatility, and a mixed-feed local would read the
27
27
  // mapped global through a volatile view the map never granted. No qualifying local ⇒ decline
28
- // (null), so the lever never emits a duplicate of the primary.
28
+ // (null), so the variation never emits a duplicate of the default.
29
29
  import { addrConst, cellAddress, inRange } from './address';
30
30
  import { type Expr, type SFn, type Stmt, mapExprChildren, rematerializableAddress, walkExprs } from './ast';
31
31
 
@@ -72,7 +72,7 @@ function collectAssigns(stmts: Stmt[], out: { name: string; value: Expr }[]): vo
72
72
  }
73
73
 
74
74
  /** How many of this tree's `volatile` claims land on a DEVICE REGISTER — the gate on rank.ts's
75
- * volatility tie-break, so it is counted here beside the lever that mints the claims.
75
+ * volatility tie-break, so it is counted here beside the variation that mints the claims.
76
76
  *
77
77
  * Two spellings assert one: a `volatile` pointer cast over a numeric address (what
78
78
  * l3/inlinebase.ts leaves at each use and what l3/volstore.ts mints at a device store), and a
@@ -119,7 +119,7 @@ export function deviceVolatileClaims(sfn: SFn, window?: readonly [number, number
119
119
  return n;
120
120
  }
121
121
 
122
- /** The locals the lever would qualify — the per-local SUBSET enumeration's input (below):
122
+ /** The locals the variation would qualify — the per-local SUBSET enumeration's input (below):
123
123
  * which pointers the original declared volatile is per-pointer knowledge the asm does not
124
124
  * carry (an MMIO block and a plain RAM table can sit side by side, and qualifying the table
125
125
  * blocks the read collapse its region wants), so each non-empty subset is its own candidate
@@ -172,10 +172,10 @@ function eligibility(sfn: SFn): (l: SFn['locals'][number]) => boolean {
172
172
  !tainted.has(l.name);
173
173
  }
174
174
 
175
- /** The proper non-empty SUBSETS of the qualifying locals (within `within`, when given) as
176
- * alternative outputs — one candidate per subset, labeled by its member names. Empty above
175
+ /** The proper non-empty SUBSETS of the qualifying locals (within `within`, when given) as the
176
+ * results of a multi-result variation — one candidate per subset, its members named as the subject. Empty above
177
177
  * three qualifiers: the arm is capped at 6 extra spellings, and the all-qualifiers form is the
178
- * plain lever's own candidate. */
178
+ * plain variation's own candidate. */
179
179
  export function volatileSubsetCandidates(sfn: SFn, within?: ReadonlySet<string>): { merged: string; sfn: SFn }[] {
180
180
  const elig = volatileEligibleLocals(sfn).filter((n) => within === undefined || within.has(n));
181
181
  if (elig.length < 2 || elig.length > 3) {
@@ -193,9 +193,9 @@ export function volatileSubsetCandidates(sfn: SFn, within?: ReadonlySet<string>)
193
193
  }
194
194
 
195
195
  /** The `/volatile` candidate, or null when no local qualifies. Read-only: returns a fresh SFn
196
- * sharing the (unmodified) body. `only` narrows the lever to the named locals — a /volatile
197
- * PRODUCT (rank.ts) qualifies just the locals its first lever centres on (kept walk bases,
198
- * created hoists), so the product never degenerates into a general /volatile composition over
196
+ * sharing the (unmodified) body. `only` narrows the variation to the named locals — a /volatile
197
+ * composition (rank.ts) qualifies just the locals its first variation centres on (kept walk bases,
198
+ * created hoists), so the composition never degenerates into a general /volatile composition over
199
199
  * the function's other locals — and the subset enumeration re-uses the same door. */
200
200
  export function volatilePtrLocals(sfn: SFn, only?: ReadonlySet<string>): SFn | null {
201
201
  const eligible = eligibility(sfn);
@@ -1,11 +1,11 @@
1
- // L3 re-spelling lever: declare a stack-homed scalar local `volatile` (`volatile u16 sp0;`).
1
+ // L3 respell variation: declare a stack-homed scalar local `volatile` (`volatile u16 sp0;`).
2
2
  //
3
3
  // `volatile` on a scalar VALUE local FORCES the value into memory: agbcc's allocator is
4
4
  // otherwise free to keep it in a callee-saved register across a call, and no other qualifier
5
5
  // or type spelling takes that freedom away. Whether the source spelled it is not derivable from
6
6
  // the asm — a slot-homed value can equally come from an address-taken local or from plain
7
7
  // register pressure — so both spellings are emitted and the differ referees, exactly as the
8
- // sibling pointee lever (volatileptr.ts) does for a numeric-address pointer.
8
+ // sibling pointee variation (volatileptr.ts) does for a numeric-address pointer.
9
9
  //
10
10
  // SEMANTICS ARE PRESERVED BY CONSTRUCTION, as they are there: `volatile` only RESTRICTS what a
11
11
  // compiler may do with the accesses, so every execution of the qualified spelling is an
@@ -15,7 +15,7 @@
15
15
  // ENVELOPE — narrower than "a source-level volatile scalar": an `laddr`-recovered frame object,
16
16
  // which under Thumb is a SUB-WORD one (see the `frame` note on SFn.locals). A `volatile s32`
17
17
  // local spills straight to `[sp,#imm]` and is recovered as an ordinary value with no local of
18
- // its own, so this lever cannot reach it. The flag is the set of slots asmlift PROVED, not the
18
+ // its own, so this variation cannot reach it. The flag is the set of slots asmlift PROVED, not the
19
19
  // set of values a source could have qualified.
20
20
  //
21
21
  // GATE — VOL_SLOT_GATES holds the rules; the argument behind each is here, where there is room:
@@ -27,7 +27,7 @@
27
27
  // something about the pointee the tree never claimed.
28
28
  // • `volatile` already set is the frontend's stamp for an object whose address was PUBLISHED
29
29
  // to memory (frontend/thumb.ts stamps it there, on `published`, not on any escape) — the
30
- // candidate would duplicate the primary.
30
+ // candidate would duplicate the default.
31
31
  // • An address-TAKEN local already has a memory home in every spelling, so there is no home
32
32
  // left for the qualifier to move: EReader_Reset's slot read and written through a pointer
33
33
  // local compiles to IDENTICAL assembly with the qualifier and without (agbcc 2.9-arm-000512,
@@ -38,14 +38,14 @@
38
38
  // condition the tree alone cannot answer. `volatile` asserts that every access written is an
39
39
  // access performed; the passes between the asm and here break that in both directions and
40
40
  // leave no trace. eliminateDeadStores drops a store to a local it can see is dead — licensed
41
- // by the ABSENCE of the flag this lever adds — so a source that stores the slot twice
41
+ // by the ABSENCE of the flag this variation adds — so a source that stores the slot twice
42
42
  // arrives with one store. And the structurer emits one C read per USE rather than per
43
43
  // machine load, so one `ldrh` feeding two uses arrives as two reads. Either way the
44
44
  // qualified spelling would declare an access set asmlift did not preserve, so both DECLINE.
45
45
  //
46
- // No qualifying local ⇒ decline (null), so the lever never emits a duplicate of the primary.
46
+ // No qualifying local ⇒ decline (null), so the variation never emits a duplicate of the default.
47
47
  //
48
- // ALL ELIGIBLE SLOTS OR NONE, where the sibling pointee lever enumerates per-local SUBSETS on the
48
+ // ALL ELIGIBLE SLOTS OR NONE, where the sibling pointee variation enumerates per-local SUBSETS on the
49
49
  // argument that volatility is per-pointer knowledge. It is per-slot knowledge here too; the
50
50
  // subsets are simply uninhabited — across the 311 agbcc benchmark rows no function reaches this
51
51
  // gate with two eligible slots. How many reach it at all depends on the sweep's configuration, so
@@ -67,7 +67,7 @@ interface SlotCtx {
67
67
  export const VOL_SLOT_GATES: readonly Gate<SlotCtx>[] = [
68
68
  {
69
69
  id: 'no-frame',
70
- why: 'the frame record is the memory home the qualifier has to force',
70
+ why: 'without a stack slot in the assembly there is no memory for the qualifier to force the local into',
71
71
  sound: false,
72
72
  rejects: (c) => !c.hasFrame,
73
73
  },
@@ -80,19 +80,19 @@ export const VOL_SLOT_GATES: readonly Gate<SlotCtx>[] = [
80
80
  },
81
81
  {
82
82
  id: 'already-volatile',
83
- why: 'the primary already declares it, so the candidate would duplicate it',
83
+ why: 'the default already declares it, so the candidate would duplicate it',
84
84
  sound: false,
85
85
  rejects: (c) => c.alreadyVolatile,
86
86
  },
87
87
  {
88
88
  id: 'addr-taken',
89
- why: 'an address-taken local already has a memory home, so there is none left to force',
89
+ why: 'an address-taken local already lives in memory, so the qualifier has nothing left to force',
90
90
  sound: false,
91
91
  rejects: (c) => c.addrTaken > 0,
92
92
  },
93
93
  {
94
94
  id: 'access-set',
95
- why: 'the qualifier asserts every access written is performed, so the tree’s must be the machine’s',
95
+ why: 'the qualifier asserts every access written is performed, so the lifted code must write exactly the accesses the machine performed',
96
96
  sound: true,
97
97
  guardedBy: 'volatileval.test.ts: a store the tree no longer carries declines',
98
98
  rejects: (c) => !c.accessSetKept,
@@ -1,9 +1,9 @@
1
- // L3 re-spelling lever: pin a STORE at a fixed DEVICE-REGISTER address `volatile`
1
+ // L3 respell variation: pin a STORE at a fixed DEVICE-REGISTER address `volatile`
2
2
  // (`*(volatile s32 *)0x40000d4 = x` rather than `*(s32 *)0x40000d4 = x`).
3
3
  //
4
4
  // A numeric address has no declaration anywhere, so whether the original source wrote through a
5
5
  // `volatile` lvalue is not derivable from the asm — the same gap l3/volatileptr.ts's header
6
- // argues. That lever answers it for a pointer LOCAL holding the address; this one answers it
6
+ // argues. That variation answers it for a pointer LOCAL holding the address; this one answers it
7
7
  // where there is no local at all, which is the spelling a `#define REG(x) *(vu32 *)(x)` macro
8
8
  // produces and the shape structure.ts leaves when the address re-materializes at each use.
9
9
  //
@@ -36,13 +36,13 @@
36
36
  // that says WHICH half is missing — "this target declares no device page" and "this address is
37
37
  // IWRAM" are different facts about a row, and over the corpus they are 283 and 711 of the 1011
38
38
  // refusals against `outside-window`'s 17. Nothing qualifying ⇒ decline (null), never a duplicate
39
- // of the primary.
39
+ // of the default.
40
40
  //
41
41
  // THE WINDOW IS A REACH GATE, PRICED — not a soundness one, which is why it is `sound: false`.
42
42
  // A `volatile` qualifier only restricts the compiler, so widening the range can never make a
43
43
  // candidate WRONG; what it would make is a claim about ordinary memory that the target denies
44
44
  // (IWRAM, EWRAM, palette, VRAM and OAM are memory a source does not qualify — target.ts) and that
45
- // the differ can only referee by luck. Measured by running the lever twice, once with the declared
45
+ // the differ can only referee by luck. Measured by running the variation twice, once with the declared
46
46
  // range and once with one admitting every constant address: 8 trees over 7 rows carry a
47
47
  // const-address store the window excludes, and the fan moves on two of them —
48
48
  // `synthetic:readarm` 6 candidates → 8 (the extra one TIES its match at 0) and
@@ -53,31 +53,31 @@
53
53
  // WHAT THE WINDOW IS NOT: A CLAIM THAT NOTHING ELSE MAY QUALIFY ORDINARY MEMORY. `/volatile`
54
54
  // (l3/volatileptr.ts) does exactly that, and a sweep over 834 corpus trees finds it qualifying an
55
55
  // address outside this window on 21 (tree, local, address) pairs, 16 of them on agbcc — including
56
- // `kleod:WritePaletteColor:agbcc`, a published byte-exact MATCH whose winning source contains
56
+ // `kleod:WritePaletteColor:agbcc` (retired 2026-09-13), then a published byte-exact MATCH whose winning source contained
57
57
  // `*(volatile s32 *)50351492 = v2 + 5;` at 0x03004D84, which is IWRAM — the same minted-cast form
58
- // this lever produces, at an address this lever's window refuses.
58
+ // this variation produces, at an address this variation's window refuses.
59
59
  //
60
60
  // THE TWO ARE SEPARATED BY THEIR DERIVATION AND NOT BY WHAT THEY EMIT, which is the trap in
61
61
  // reading that spelling as a contradiction. `/inlinebase` alone mints no qualifier at all —
62
62
  // enumerated on that row, `unsigned/inlinebase` carries zero `volatile` casts and
63
63
  // `unsigned/inlinebase/volatile` carries three. The qualifier comes from `/volatile`, which put it
64
64
  // on a pointer LOCAL the asm shows the compiler re-materializing rather than keeping; inlining
65
- // then carries that codegen fact onto each cast it leaves behind. This lever runs where no such
65
+ // then carries that codegen fact onto each cast it leaves behind. This variation runs where no such
66
66
  // local ever existed, so its only input is the number — and outside a range the target has
67
- // declared, a number supports nothing. The window is where THIS lever's evidence runs out, not
67
+ // declared, a number supports nothing. The window is where THIS variation's evidence runs out, not
68
68
  // where the target's permission does.
69
69
  //
70
70
  // (So the two are not foldable on the window, and the fold is not free: adopting it for `/volatile`
71
71
  // would delete the WritePaletteColor spelling. `deviceVolatileClaims` in volatileptr.ts already
72
72
  // unifies the COUNT side, which is the half where one answer really is enough. Nor do they
73
73
  // COMPOSE over the tree's OWN locals: that pairing would qualify a function's existing pointer-local
74
- // homes and its raw-constant stores together, and over 834 corpus trees both levers fire on ONE —
74
+ // homes and its raw-constant stores together, and over 834 corpus trees both variations fire on ONE —
75
75
  // `kleod:SetupBG3WindowOverlay:agbcc`, which neither decompiler scores — and under two DIFFERENT
76
76
  // published classifications: asmlift `noncompile` (agbcc rejects its call to `m4aSoundVSyncOff`),
77
77
  // m2c `declined`. A pairing whose whole reach is a row asmlift cannot compile is one a row has yet
78
78
  // to demand.
79
79
  //
80
- // A lever that MINTS the locals is a different question with a different answer, and `rank.ts`
80
+ // A variation that MINTS the locals is a different question with a different answer, and `rank.ts`
81
81
  // pairs this pass with one: `/regionbase` homes the regions holding two or more direct uses of a
82
82
  // device base and leaves every other spelling of the same address inline, so both qualifiers have
83
83
  // something to claim in one function. `synthetic:dmascope` is that row.)
@@ -85,11 +85,11 @@
85
85
  // SCOPE: STORES only. A device READ is a different question with a different answer — the idiom
86
86
  // fold's DCE drops a use-less device load outright (synthetic:dmaback), so a read that survives to
87
87
  // L3 is one whose value the function consumes, and whether THAT may be CSEd is the question
88
- // `/reread-globals` referees as a structuring axis. The price of pinning one is real and is
88
+ // `/reread-globals` referees as a structure variation. The price of pinning one is real and is
89
89
  // measured on `synthetic:ucmp:agbcc`, a byte-exact match whose loop test READS 0x3001048: qualify
90
- // that read and the row scores 15. This lever does not reach it — ucmp's stores go through a
90
+ // that read and the row scores 15. This variation does not reach it — ucmp's stores go through a
91
91
  // runtime address (`*(u8 *)(v1 + 0x3002000)`), so it declines there on `non-const-address`, in
92
- // both configurations and with any window. No row demands the read spelling, and a lever with no
92
+ // both configurations and with any window. No row demands the read spelling, and a variation with no
93
93
  // inhabitant is what "earn the level" forbids.
94
94
  import { type IrType, T, scalarTypeForAccess } from '../ir/types';
95
95
  import { cellAddress, inRange } from './address';
@@ -114,7 +114,7 @@ interface AccessCtx {
114
114
  export const VOL_STORE_GATES: readonly Gate<AccessCtx>[] = [
115
115
  {
116
116
  id: 'no-window',
117
- why: 'a target that declares no device range has no address this lever may call volatile',
117
+ why: 'a target that declares no device range has no address this variation may call volatile',
118
118
  sound: false,
119
119
  rejects: (c) => !c.hasWindow,
120
120
  },
@@ -127,9 +127,9 @@ export const VOL_STORE_GATES: readonly Gate<AccessCtx>[] = [
127
127
  {
128
128
  // NOT "the target denies this address may be volatile" — `/volatile` qualifies IWRAM on eleven
129
129
  // agbcc rows, one of them a published match, and it is right to: see the header's last
130
- // paragraph. This gate is about what THIS lever has evidence for, which is the address alone.
130
+ // paragraph. This gate is about what THIS variation has evidence for, which is the address alone.
131
131
  id: 'outside-window',
132
- why: 'the address is the only evidence this lever has, and outside the window it supports nothing',
132
+ why: 'the address is the only evidence this variation has, and outside the window it supports nothing',
133
133
  sound: false,
134
134
  rejects: (c) => !c.inWindow,
135
135
  },
package/src/l3/zerosub.ts CHANGED
@@ -1,4 +1,4 @@
1
- // L3 re-spelling lever: spell a negate of a SHARED subtraction as `0 - x`.
1
+ // L3 respell variation: spell a negate of a SHARED subtraction as `0 - x`.
2
2
  //
3
3
  // The two C spellings of a negation are not interchangeable in front of gcc 2.9's folder. `-x` is
4
4
  // built through build_unary_op and FOLDED, so fold-const.c's "Convert - (a - b) to (b - a)"
@@ -23,20 +23,20 @@
23
23
  // local as well — and the differ referees. Semantics are preserved by construction: `-x` and
24
24
  // `0 - x` are the same C expression for every integer type.
25
25
  //
26
- // A RE-SPELLING rather than a fourth value-home axis (docs/level-tower.md's third fork). The fold
26
+ // A RESPELL VARIATION rather than a fourth value-home structure variation (docs/level-tower.md's third fork). The fold
27
27
  // rule fires at all only because asmlift INLINED a value the source bound to a local, and naming
28
28
  // that local reaches the same match by the other route: on `pokeemerald:GetAnchorCoord:agbcc`,
29
29
  // `s32 t = a1 - a0;` scores 0 against the row's own target.o where the inlined body with a plain
30
- // `-` scores 1. asmlift cannot spell it — all three home axes decline on pedigree (`/addr-home`
30
+ // `-` scores 1. asmlift cannot spell it — all three home variations decline on pedigree (`/addr-home`
31
31
  // wants an address, `/expr-home` a loop, `/derived-home` a memory read) and this is a bare pure
32
- // value with three consumers. An axis admitting any such value would reach the use shapes a
32
+ // value with three consumers. A structure variation admitting any such value would reach the use shapes a
33
33
  // substitution cannot, and would double the fan wherever it admits; this costs one candidate per
34
- // distinct tree carrying the shape. Take the axis when a row demands a shape this cannot reach.
34
+ // distinct tree carrying the shape. Take the structure variation when a row demands a shape this cannot reach.
35
35
  //
36
36
  // SCOPE (decline over approximate). Only a `bin('-')` operand, and only a SHARED one. Neither
37
37
  // restriction is caution: over any other operand shape the fold rule does not apply and the two
38
38
  // spellings compile identically (verified for `-(a + b)`, `-(a >> 3)`, `-(a * 3)`, `-a`), so
39
- // firing there could only duplicate the primary. An EFFECTFUL subtraction is out of scope too —
39
+ // firing there could only duplicate the default. An EFFECTFUL subtraction is out of scope too —
40
40
  // two textually equal calls are two calls, not one shared value, so the premise fails.
41
41
  import { type Expr, type SFn, exprEquals, exprHasEffect, mapExprChildren, mapStmtExprs, walkExprs } from './ast';
42
42
 
@@ -67,7 +67,7 @@ export interface RewritePattern {
67
67
  // `applies` is DATA, consumed generically by patternApplies — NOT an `arch ==` branch.
68
68
  // `isa` pins the ISA; `compilers` pins which COMPILERS emit this idiom (the same shift-sequence
69
69
  // for `/2` is produced by agbcc AND gcc, so a compiler LIST, not a single arch, is the honest
70
- // predicate); `capabilities` is a hardware predicate. An absent axis means "don't constrain on it".
70
+ // predicate); `capabilities` is a hardware predicate. An absent field means "don't constrain on it".
71
71
  applies: { isa?: string; compilers?: string[]; capabilities?: Partial<{ hwDivide: boolean; hwFloat: boolean }> };
72
72
  match: MatchNode; // rooted at the op result to replace
73
73
  // NOTE: a RELATIONAL guard (a `where` clause constraining the bound immediates, e.g. "two shift
@@ -93,10 +93,10 @@ export interface RewritePattern {
93
93
  unsequencedRightFirst?: [string, string];
94
94
  }
95
95
 
96
- /** Does this pattern apply to `target`? Every DECLARED axis must match: the ISA (so an idiom can be
96
+ /** Does this pattern apply to `target`? Every DECLARED field must match: the ISA (so an idiom can be
97
97
  * pinned to one frontend), the compiler set (so an idiom fires only for the compilers that emit
98
98
  * it — the reason MIPS+IDO and MIPS+GCC are distinguishable despite one frontend), and every
99
- * declared capability. An omitted axis is unconstrained. */
99
+ * declared capability. An omitted field is unconstrained. */
100
100
  export function patternApplies(
101
101
  p: RewritePattern,
102
102
  target: { id: string; compiler: string; capabilities: { hwDivide: boolean; hwFloat: boolean } },
@@ -172,7 +172,7 @@ export const SDIV_POW2_2: RewritePattern = {
172
172
  // out as its own definition. Folding the triple back to one `smod`/`umod` gives recovery and the
173
173
  // structurer the operator the source wrote, and re-emitting `%` reproduces the triple byte-exact.
174
174
  //
175
- // This is NOT the `capabilities.hwDivide` axis: MIPS also divides in hardware and needs no fold at
175
+ // This is NOT the `capabilities.hwDivide` field: MIPS also divides in hardware and needs no fold at
176
176
  // all, because `div` leaves the remainder in `hi` and the frontend reads it straight out. The
177
177
  // narrower fact is a hardware divide that yields the QUOTIENT ONLY; `isa: 'ppc'` STANDS IN for it
178
178
  // until a second ISA earns the capability, and `compilers` carries the measured half, that mwcc's
package/src/pipeline.ts CHANGED
@@ -261,7 +261,7 @@ export function raiseRecovered(
261
261
  self?: FnProto,
262
262
  pre: PreRecoveryOptions = {},
263
263
  ): void {
264
- runPreRecovery(
264
+ const lifted = runPreRecovery(
265
265
  fn,
266
266
  target,
267
267
  (pass, result) => {
@@ -276,7 +276,15 @@ export function raiseRecovered(
276
276
  verify(fn);
277
277
  assertTypesRecovered(fn);
278
278
  hooks.afterRecover?.();
279
- if (sinkReturns(fn)) {
279
+ // `lifted.mergeShapes` is the CFG as it ENTERED pre-recovery, and retsink's `pre-diamond` needs
280
+ // exactly that: `raise/shortcircuit.ts` manufactures two-armed diamonds out of condition trees the
281
+ // ROM never merged, and a diamond this pass reads at its own turn may be one of those.
282
+ if (
283
+ sinkReturns(fn, {
284
+ hoistsSingleSetArm: target.compilerBehaviors.hoistsSingleSetArm,
285
+ mergeShapes: lifted.mergeShapes,
286
+ })
287
+ ) {
280
288
  verify(fn);
281
289
  hooks.afterRetsink?.();
282
290
  }
@@ -333,7 +341,7 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
333
341
  if (!names.size || /unmodelled instruction/.test(e.message)) {
334
342
  throw e;
335
343
  }
336
- // Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written variant
344
+ // Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written spelling
337
345
  // misses the mnemonic-anchored classes and every attributed decline lands in the generic bucket.
338
346
  const list = [...names].sort().map(gapReasonFor).join(', ');
339
347
  throw new StructureError(`${e.message} — and the function carries ${list}, which is the more likely cause`);
@@ -341,8 +349,12 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
341
349
  }
342
350
 
343
351
  /** Stage 4 — structure + its boundary contracts, always as a pair. */
344
- export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]): SFn {
345
- const raw = attributeOpaques(fn, () => structure(fn, opts));
352
+ export function structureChecked(
353
+ fn: Fn,
354
+ opts: Parameters<typeof structure>[1],
355
+ hooks?: Parameters<typeof structure>[2],
356
+ ): SFn {
357
+ const raw = attributeOpaques(fn, () => structure(fn, opts, hooks));
346
358
  // The boundary contracts run on the pre-DCE tree: the readability pass must never be able to
347
359
  // hide a structuring defect by dropping the dead statement that carries it. assertResolved
348
360
  // catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
package/src/proto.ts CHANGED
@@ -16,7 +16,7 @@ import type { SymbolMap, SymbolTypeFacts } from './symbols';
16
16
  * the extension is an inference off an encoding two different C sources produce. Where the asm
17
17
  * carries no extension, this list is NOT consulted: pinning there would type every parameter of
18
18
  * every row from the declaration, and a declared `u32` kills rank.ts's signed arm before the
19
- * differ ever sees it. That half is an axis question and is not answered here. */
19
+ * differ ever sees it. That half is a variation question and is not answered here. */
20
20
  export type ParamType = string;
21
21
 
22
22
  /** What the headers know about one function. All fields optional: a partial table (only
@@ -143,7 +143,7 @@ export function validatePrototypes(value: unknown): string[] {
143
143
  * determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
144
144
  * makes every stride explicit — so nothing is guessed about what it points at. A richer spelling
145
145
  * would also be INERT: `declaredWidth` answers 32 for every `*`, and a CALLEE's parameter types
146
- * are read for the list's length alone (test/param-pointee-axis.test.ts). */
146
+ * are read for the list's length alone (test/param-pointee-variation.test.ts). */
147
147
  function typeSpelling(t: SymbolTypeFacts): ParamType | null {
148
148
  if (t.pointer) {
149
149
  return 'void *';