@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
@@ -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 {
@@ -196,6 +196,7 @@ import {
196
196
  exprEquals,
197
197
  exprHasEffect,
198
198
  exprReadsVolatile,
199
+ isLoop,
199
200
  mapExprChildren,
200
201
  stmtChildren,
201
202
  stmtExprs,
@@ -264,7 +265,7 @@ export const UNREDUCE_GATES: readonly Gate<AccCtx>[] = [
264
265
  },
265
266
  {
266
267
  id: 'acc-pinned',
267
- why: 'a declaration that carries an asm fact cannot be deleted without dropping the fact',
268
+ why: 'a declaration that carries a fact from the assembly cannot be deleted without dropping the fact',
268
269
  sound: true,
269
270
  guardedBy: 'unreduce.test.ts: a pinned accumulator declines, on every pin a local can carry',
270
271
  rejects: (c) => c.pinned,
@@ -344,14 +345,14 @@ export const UNREDUCE_GATES: readonly Gate<AccCtx>[] = [
344
345
  },
345
346
  {
346
347
  id: 'nonzero-start',
347
- why: 'a counter starting at a nonzero constant leaves a bias term this file does not spell',
348
+ why: 'a counter starting at a nonzero constant leaves a bias term this rewrite does not write',
348
349
  sound: true,
349
350
  guardedBy: 'unreduce.test.ts: a counter-free init declines unless its start is the constant 0',
350
351
  rejects: (c) => c.declined === 'nonzero-start',
351
352
  },
352
353
  {
353
354
  id: 'step-ratio',
354
- why: 'a counter stepping by more than one leaves the ratio K/d, which is not a shift',
355
+ why: 'a counter stepping by more than one leaves a ratio between the two steps, which is not a shift',
355
356
  sound: true,
356
357
  guardedBy: 'unreduce.test.ts: a counter-free init declines when the counter does not step by one',
357
358
  rejects: (c) => c.declined === 'step-ratio',
@@ -426,7 +427,7 @@ function arithScale(t: IrType | undefined): number | null {
426
427
  /** Does this local's DECLARATION pin it against deletion? Every flag `SFn.locals` can carry, because
427
428
  * each is a fact about the ASM that only the declaration states: two qualifiers (deleting a
428
429
  * `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
430
+ * carries it onto the minted cast instead, and this variation has no local left to carry anything), a
430
431
  * frame home, an `undef` whose whole content is the assignment that is MISSING, and the SPILL HOMES.
431
432
  *
432
433
  * WHY `slots` PINS, which is not the obvious reading. Deleting a slot-carrying local does not
@@ -537,7 +538,7 @@ function relate(init: Expr, start: Expr, ctr: string, k: Expr, d: number): Relat
537
538
  return null; // anything else between the root and the counter, and the stride is not `k`
538
539
  };
539
540
  // 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
541
+ // of this variation rides, and trying it first makes the branch below strictly additive — it is
541
542
  // reached only where the old rule already declined, so it can admit candidates but never
542
543
  // re-spell one.
543
544
  //
@@ -796,7 +797,7 @@ export interface UnreduceResult {
796
797
 
797
798
  /** The `/unreduce` candidate, or null when no accumulator qualifies. `window` is the target's
798
799
  * declared device-register range (TargetDescription.capabilities.deviceRegisters) — absent, the
799
- * lever still fires on a closed form that reads no memory. `triggers` is
800
+ * variation still fires on a closed form that reads no memory. `triggers` is
800
801
  * `capabilities.deviceMemoryWriters`; absent, EVERY device store is treated as one. */
801
802
  export function unreduceAccumulators(
802
803
  sfn: SFn,
@@ -813,7 +814,7 @@ export function unreduceAccumulators(
813
814
 
814
815
  for (let li = 0; li < body.length; li++) {
815
816
  const loop = body[li];
816
- if (loop.k !== 'while' && loop.k !== 'dowhile' && loop.k !== 'for') {
817
+ if (!isLoop(loop)) {
817
818
  continue;
818
819
  }
819
820
  // the counter: one name stepped by a constant, whose start stands above the loop
@@ -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
 
package/src/mangle.ts CHANGED
@@ -129,6 +129,55 @@ export function demangle(sym: string): CppSig | null {
129
129
  return { name, cls, params };
130
130
  }
131
131
 
132
+ /** A class qualifier off the front of `s` — `12RefCountable`, or `Q23zen17particleGenerator` for a
133
+ * nested one — as its scope names and the remaining string; null when `s` does not open with one. */
134
+ function parseQualifier(s: string): { scopes: string[]; rest: string } | null {
135
+ const nested = /^Q(\d)/.exec(s);
136
+ let count = nested === null ? 1 : Number(nested[1]);
137
+ let rest = nested === null ? s : s.slice(2);
138
+ const scopes: string[] = [];
139
+ for (; count > 0; count--) {
140
+ const digits = /^(\d+)/.exec(rest);
141
+ if (digits === null) {
142
+ return null;
143
+ }
144
+ const len = Number(digits[1]);
145
+ const id = rest.slice(digits[1].length, digits[1].length + len);
146
+ if (len === 0 || id.length !== len) {
147
+ return null;
148
+ }
149
+ scopes.push(id);
150
+ rest = rest.slice(digits[1].length + len);
151
+ }
152
+ return { scopes, rest };
153
+ }
154
+
155
+ /** The name a CodeWarrior function symbol has in its SOURCE — `PlayerState::getStartHour` for
156
+ * `getStartHour__11PlayerStateFv`, `zen::particleGenerator::RotAxisX` for a nested class, and the
157
+ * constructor and destructor spelled as they are declared (`RefCountable::RefCountable`,
158
+ * `System::~System`). Only the name: unlike `demangle` it reads no parameter code, so it answers
159
+ * for the references, arrays and const qualifiers that one refuses. Null where `sym` is not a
160
+ * mangled function, and for an operator, whose source name is not an identifier. */
161
+ export function demangledName(sym: string): string | null {
162
+ for (let at = sym.indexOf('__', 1); at > 0; at = sym.indexOf('__', at + 1)) {
163
+ const name = sym.slice(0, at);
164
+ const rest = sym.slice(at + 2);
165
+ const qualified = /^[\dQ]/.test(rest) ? parseQualifier(rest) : { scopes: [], rest };
166
+ if (qualified === null || !/^C?F/.test(qualified.rest)) {
167
+ continue;
168
+ }
169
+ const { scopes } = qualified;
170
+ const cls = scopes.at(-1);
171
+ // every other special name — `__as`, `__nw`, `__pl` — is an operator
172
+ const member = name === '__ct' ? cls : name === '__dt' && cls !== undefined ? `~${cls}` : name;
173
+ if (member === undefined || member.startsWith('__')) {
174
+ return null;
175
+ }
176
+ return [...scopes, member].join('::');
177
+ }
178
+ return null;
179
+ }
180
+
132
181
  /** Spell a CppType as C++ source (`Vec *`, `unsigned int`). */
133
182
  export function spellType(t: CppType): string {
134
183
  return t.base + (t.ptr ? ' ' + '*'.repeat(t.ptr) : '');