@asmlift/core 0.5.0 → 0.6.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 (86) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
@@ -0,0 +1,207 @@
1
+ // L3 re-spelling lever: declare a pointer local that holds a NUMERIC address as pointing to
2
+ // volatile data (`volatile u16 *p = (u16 *)0x3000010;`).
3
+ //
4
+ // A numeric address has no declaration anywhere — the project maps a symbol's volatility, but a
5
+ // raw constant is exactly the case with no symbol — so whether the original source read it
6
+ // through a `volatile` pointer is not derivable from the asm. It is codegen-visible all the
7
+ // same: a volatile MEM is barred from motion and combination, which reorders the loop
8
+ // optimizer's pseudos and lands the register allocator on different homes (on the row this was
9
+ // built for, the counter is copied out of r0 so the loaded value can have it — the target's
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
12
+ // referees.
13
+ //
14
+ // SEMANTICS ARE PRESERVED BY CONSTRUCTION: `volatile` only RESTRICTS what a compiler may do
15
+ // with the accesses; every execution of the qualified spelling is an execution of the plain
16
+ // one. C89 also admits the assignment without a cast change — the qualifier is added on the
17
+ // pointee, and assignment may add pointee qualifiers.
18
+ //
19
+ // GATE: only a local of pointer type assigned a REMATERIALIZABLE address somewhere in the body
20
+ // (l3/ast.ts — any constant expression reading no variable and no memory, so a shift-encoded
21
+ // hardware base qualifies exactly like a pool word); a bare `0` is NULL, never an address. A value
22
+ // CONTAINING a global's address — `&gSym` at ANY depth: under a cast, inside interior-address
23
+ // arithmetic (`(u16 *)((u32)&gSym + 8)`) — VETOES the local, qualifying assignments on other paths
24
+ // notwithstanding, and the veto propagates through assignments to a FIXPOINT (`q` tainted,
25
+ // `p = q` taints `p`; conservatively, `p` assigned ANY expression mentioning a tainted name).
26
+ // The symbol map owns a declared global's volatility, and a mixed-feed local would read the
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.
29
+ import { addrConst, cellAddress, inRange } from './address';
30
+ import { type Expr, type SFn, type Stmt, mapExprChildren, rematerializableAddress, walkExprs } from './ast';
31
+
32
+ const exprHas = (e: Expr, pred: (x: Expr) => boolean): boolean => {
33
+ if (pred(e)) {
34
+ return true;
35
+ }
36
+ let hit = false;
37
+ mapExprChildren(e, (c) => {
38
+ hit ||= exprHas(c, pred);
39
+ return c;
40
+ });
41
+ return hit;
42
+ };
43
+
44
+ function collectAssigns(stmts: Stmt[], out: { name: string; value: Expr }[]): void {
45
+ for (const s of stmts) {
46
+ switch (s.k) {
47
+ case 'assign':
48
+ out.push({ name: s.name, value: s.value });
49
+ break;
50
+ case 'if':
51
+ collectAssigns(s.then, out);
52
+ collectAssigns(s.else, out);
53
+ break;
54
+ case 'while':
55
+ case 'dowhile':
56
+ collectAssigns(s.body, out);
57
+ break;
58
+ case 'for':
59
+ collectAssigns([s.init, s.inc], out);
60
+ collectAssigns(s.body, out);
61
+ break;
62
+ case 'switch':
63
+ for (const c of s.cases) {
64
+ collectAssigns(c.body, out);
65
+ }
66
+ collectAssigns(s.default ?? [], out);
67
+ break;
68
+ default:
69
+ break;
70
+ }
71
+ }
72
+ }
73
+
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.
76
+ *
77
+ * Two spellings assert one: a `volatile` pointer cast over a numeric address (what
78
+ * l3/inlinebase.ts leaves at each use and what l3/volstore.ts mints at a device store), and a
79
+ * pointee-volatile pointer local, which asserts it at whichever numeric address feeds the local.
80
+ * A `volatile` SCALAR local (l3/volatileval.ts) asserts nothing about an address — it is a stack
81
+ * slot — so it is not counted.
82
+ *
83
+ * A claim under a SUBSCRIPT is placed by the whole cell and not by the cast's own operand, which
84
+ * is the disagreement l3/address.ts warns about: `((volatile s32 *)0x03FFFFF0)[8]` denotes
85
+ * BG0HOFS, and asking the base alone reports EWRAM and counts nothing. Either reading landing in
86
+ * the window counts it, so a runtime subscript over an in-window base still does.
87
+ *
88
+ * Nothing outside the window counts. A qualifier on ordinary memory is a claim about the source
89
+ * the differ cannot referee and the range cannot support, and preferring it corpus-wide buys one
90
+ * honest MMIO spelling at the price of many false ones. */
91
+ export function deviceVolatileClaims(sfn: SFn, window?: readonly [number, number]): number {
92
+ if (window === undefined) {
93
+ return 0;
94
+ }
95
+ const inWindow = (e: Expr) => inRange(addrConst(e), window);
96
+ let n = 0;
97
+ const underSubscript = new Set<Expr>();
98
+ for (const e of walkExprs(sfn.body)) {
99
+ if (e.k === 'index' && e.base.k === 'cast') {
100
+ underSubscript.add(e.base);
101
+ }
102
+ }
103
+ const assigns: { name: string; value: Expr }[] = [];
104
+ collectAssigns(sfn.body, assigns);
105
+ for (const l of sfn.locals) {
106
+ if (l.type.kind === 'ptr' && (l.pointeeVolatile !== undefined || l.volatile !== undefined)) {
107
+ n += assigns.filter((a) => a.name === l.name && inWindow(a.value)).length;
108
+ }
109
+ }
110
+ for (const e of walkExprs(sfn.body)) {
111
+ if (e.k === 'index' && e.base.k === 'cast' && e.base.volatile === true) {
112
+ if (inRange(cellAddress(e), window) || inWindow(e.base.e)) {
113
+ n++;
114
+ }
115
+ } else if (e.k === 'cast' && e.volatile === true && !underSubscript.has(e) && inWindow(e.e)) {
116
+ n++;
117
+ }
118
+ }
119
+ return n;
120
+ }
121
+
122
+ /** The locals the lever would qualify — the per-local SUBSET enumeration's input (below):
123
+ * which pointers the original declared volatile is per-pointer knowledge the asm does not
124
+ * carry (an MMIO block and a plain RAM table can sit side by side, and qualifying the table
125
+ * blocks the read collapse its region wants), so each non-empty subset is its own candidate
126
+ * when few enough locals qualify, and the differ referees. */
127
+ function volatileEligibleLocals(sfn: SFn): string[] {
128
+ return sfn.locals.filter(eligibility(sfn)).map((l) => l.name);
129
+ }
130
+
131
+ /** the value-side VETO of the header's GATE: `&gSym` at any depth, under a cast or inside interior
132
+ * address arithmetic. Named because the taint seed and `volatileEligibleValue` are the same test. */
133
+ const feedsSymbolAddress = (e: Expr): boolean => exprHas(e, (x) => x.k === 'addr');
134
+
135
+ /** Whether a pointer local fed exactly this value, and nothing else, would qualify — the header's
136
+ * GATE at its one-assignment case, where the taint fixpoint is its own seed.
137
+ *
138
+ * Exported because a caller asking "would the spelling I am REPLACING have carried the qualifier?"
139
+ * has to ask this model rather than re-derive it. The const-vs-symbol answer is this file's: a
140
+ * numeric address has no declaration anywhere, a symbol's volatility is the map's, and a rule that
141
+ * hard-codes that split drifts the moment the veto moves (l3/homesplit.ts). */
142
+ export const volatileEligibleValue = (e: Expr): boolean => rematerializableAddress(e) && !feedsSymbolAddress(e);
143
+
144
+ /** the shared eligibility predicate (the GATE in the header) for one function's locals */
145
+ function eligibility(sfn: SFn): (l: SFn['locals'][number]) => boolean {
146
+ const assigns: { name: string; value: Expr }[] = [];
147
+ collectAssigns(sfn.body, assigns);
148
+ const numericFed = new Set<string>();
149
+ const tainted = new Set<string>();
150
+ for (const a of assigns) {
151
+ if (rematerializableAddress(a.value)) {
152
+ numericFed.add(a.name);
153
+ }
154
+ if (feedsSymbolAddress(a.value)) {
155
+ tainted.add(a.name);
156
+ }
157
+ }
158
+ for (let grew = true; grew;) {
159
+ grew = false;
160
+ for (const a of assigns) {
161
+ if (!tainted.has(a.name) && exprHas(a.value, (x) => x.k === 'var' && tainted.has(x.name))) {
162
+ tainted.add(a.name);
163
+ grew = true;
164
+ }
165
+ }
166
+ }
167
+ return (l) =>
168
+ l.type.kind === 'ptr' &&
169
+ l.volatile === undefined &&
170
+ l.pointeeVolatile === undefined &&
171
+ numericFed.has(l.name) &&
172
+ !tainted.has(l.name);
173
+ }
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
177
+ * three qualifiers: the arm is capped at 6 extra spellings, and the all-qualifiers form is the
178
+ * plain lever's own candidate. */
179
+ export function volatileSubsetCandidates(sfn: SFn, within?: ReadonlySet<string>): { merged: string; sfn: SFn }[] {
180
+ const elig = volatileEligibleLocals(sfn).filter((n) => within === undefined || within.has(n));
181
+ if (elig.length < 2 || elig.length > 3) {
182
+ return [];
183
+ }
184
+ const out: { merged: string; sfn: SFn }[] = [];
185
+ for (let mask = 1; mask < (1 << elig.length) - 1; mask++) {
186
+ const subset = elig.filter((_, i) => (mask & (1 << i)) !== 0);
187
+ const r = volatilePtrLocals(sfn, new Set(subset));
188
+ if (r) {
189
+ out.push({ merged: subset.join('-'), sfn: r });
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+
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
199
+ * the function's other locals — and the subset enumeration re-uses the same door. */
200
+ export function volatilePtrLocals(sfn: SFn, only?: ReadonlySet<string>): SFn | null {
201
+ const eligible = eligibility(sfn);
202
+ const qualifies = (l: SFn['locals'][number]): boolean => eligible(l) && (only === undefined || only.has(l.name));
203
+ if (!sfn.locals.some(qualifies)) {
204
+ return null;
205
+ }
206
+ return { ...sfn, locals: sfn.locals.map((l) => (qualifies(l) ? { ...l, pointeeVolatile: true } : l)) };
207
+ }
@@ -0,0 +1,130 @@
1
+ // L3 re-spelling lever: declare a stack-homed scalar local `volatile` (`volatile u16 sp0;`).
2
+ //
3
+ // `volatile` on a scalar VALUE local FORCES the value into memory: agbcc's allocator is
4
+ // otherwise free to keep it in a callee-saved register across a call, and no other qualifier
5
+ // or type spelling takes that freedom away. Whether the source spelled it is not derivable from
6
+ // the asm — a slot-homed value can equally come from an address-taken local or from plain
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.
9
+ //
10
+ // SEMANTICS ARE PRESERVED BY CONSTRUCTION, as they are there: `volatile` only RESTRICTS what a
11
+ // compiler may do with the accesses, so every execution of the qualified spelling is an
12
+ // execution of the plain one. The object is a function local whose address does not escape,
13
+ // so nothing outside the function can observe the difference at all.
14
+ //
15
+ // ENVELOPE — narrower than "a source-level volatile scalar": an `laddr`-recovered frame object,
16
+ // which under Thumb is a SUB-WORD one (see the `frame` note on SFn.locals). A `volatile s32`
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
19
+ // set of values a source could have qualified.
20
+ //
21
+ // GATE — VOL_SLOT_GATES holds the rules; the argument behind each is here, where there is room:
22
+ //
23
+ // • The frame record is what gives the qualifier a home to force. Qualifying a register-homed
24
+ // value would enumerate a spelling no source that produced this asm could have had.
25
+ // • SCALAR, because a pointer local declares as `volatile u16 * p` — one prefix, two meanings —
26
+ // and cfamily.ts spells that for `pointeeVolatile`. On a pointer the qualifier would say
27
+ // something about the pointee the tree never claimed.
28
+ // • `volatile` already set is the frontend's stamp for an object whose address was PUBLISHED
29
+ // to memory (frontend/thumb.ts stamps it there, on `published`, not on any escape) — the
30
+ // candidate would duplicate the primary.
31
+ // • An address-TAKEN local already has a memory home in every spelling, so there is no home
32
+ // left for the qualifier to move: EReader_Reset's slot read and written through a pointer
33
+ // local compiles to IDENTICAL assembly with the qualifier and without (agbcc 2.9-arm-000512,
34
+ // `-O2 -mthumb-interwork -Wimplicit -fhex-asm -fprologue-bugfix`). What it can still do
35
+ // there is stop reads collapsing, which frontend/thumb.ts measured turning a byte-exact
36
+ // candidate into a four-instruction nonmatch.
37
+ // • ACCESS-SET EQUALITY is what makes the qualified spelling honest, and it is the one
38
+ // condition the tree alone cannot answer. `volatile` asserts that every access written is an
39
+ // access performed; the passes between the asm and here break that in both directions and
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
42
+ // arrives with one store. And the structurer emits one C read per USE rather than per
43
+ // machine load, so one `ldrh` feeding two uses arrives as two reads. Either way the
44
+ // qualified spelling would declare an access set asmlift did not preserve, so both DECLINE.
45
+ //
46
+ // No qualifying local ⇒ decline (null), so the lever never emits a duplicate of the primary.
47
+ //
48
+ // ALL ELIGIBLE SLOTS OR NONE, where the sibling pointee lever enumerates per-local SUBSETS on the
49
+ // argument that volatility is per-pointer knowledge. It is per-slot knowledge here too; the
50
+ // subsets are simply uninhabited — across the 311 agbcc benchmark rows no function reaches this
51
+ // gate with two eligible slots. How many reach it at all depends on the sweep's configuration, so
52
+ // the subset question is the one to quote: two rows with the callee arities declared, one without.
53
+ import type { SFn } from './ast';
54
+ import { type Gate, firstRejection } from './gates';
55
+ import { localMentions, readsOf } from './mentions';
56
+
57
+ /** One local as the gates read it. */
58
+ interface SlotCtx {
59
+ hasFrame: boolean;
60
+ isScalar: boolean;
61
+ alreadyVolatile: boolean;
62
+ addrTaken: number;
63
+ /** the tree's reads and writes are the machine's loads and stores */
64
+ accessSetKept: boolean;
65
+ }
66
+
67
+ export const VOL_SLOT_GATES: readonly Gate<SlotCtx>[] = [
68
+ {
69
+ id: 'no-frame',
70
+ why: 'the frame record is the memory home the qualifier has to force',
71
+ sound: false,
72
+ rejects: (c) => !c.hasFrame,
73
+ },
74
+ {
75
+ id: 'non-scalar',
76
+ why: 'on a pointer declarator the one `volatile` prefix binds to the pointee, not the object',
77
+ sound: true,
78
+ guardedBy: 'volatileval.test.ts: a non-scalar frame local never qualifies',
79
+ rejects: (c) => !c.isScalar,
80
+ },
81
+ {
82
+ id: 'already-volatile',
83
+ why: 'the primary already declares it, so the candidate would duplicate it',
84
+ sound: false,
85
+ rejects: (c) => c.alreadyVolatile,
86
+ },
87
+ {
88
+ id: 'addr-taken',
89
+ why: 'an address-taken local already has a memory home, so there is none left to force',
90
+ sound: false,
91
+ rejects: (c) => c.addrTaken > 0,
92
+ },
93
+ {
94
+ id: 'access-set',
95
+ why: 'the qualifier asserts every access written is performed, so the tree’s must be the machine’s',
96
+ sound: true,
97
+ guardedBy: 'volatileval.test.ts: a store the tree no longer carries declines',
98
+ rejects: (c) => !c.accessSetKept,
99
+ },
100
+ ];
101
+
102
+ /** the shared eligibility predicate (VOL_SLOT_GATES) for one function's locals */
103
+ function eligibility(sfn: SFn): (l: SFn['locals'][number]) => boolean {
104
+ const uses = localMentions(sfn);
105
+ return (l) => {
106
+ const u = uses.get(l.name);
107
+ if (u === undefined) {
108
+ return false;
109
+ }
110
+ return (
111
+ firstRejection(VOL_SLOT_GATES, {
112
+ hasFrame: l.frame !== undefined,
113
+ isScalar: l.type.kind === 'int',
114
+ alreadyVolatile: l.volatile !== undefined || l.pointeeVolatile !== undefined,
115
+ addrTaken: u.addrTaken,
116
+ accessSetKept: readsOf(u) === l.frame?.loads && u.assigns === l.frame?.stores,
117
+ }) === null
118
+ );
119
+ };
120
+ }
121
+
122
+ /** The `/vol-slot` candidate, or null when no local qualifies. Read-only: returns a fresh SFn
123
+ * sharing the (unmodified) body. */
124
+ export function volatileValueLocals(sfn: SFn): SFn | null {
125
+ const qualifies = eligibility(sfn);
126
+ if (!sfn.locals.some(qualifies)) {
127
+ return null;
128
+ }
129
+ return { ...sfn, locals: sfn.locals.map((l) => (qualifies(l) ? { ...l, volatile: true as const } : l)) };
130
+ }
@@ -0,0 +1,229 @@
1
+ // L3 re-spelling lever: pin a STORE at a fixed DEVICE-REGISTER address `volatile`
2
+ // (`*(volatile s32 *)0x40000d4 = x` rather than `*(s32 *)0x40000d4 = x`).
3
+ //
4
+ // A numeric address has no declaration anywhere, so whether the original source wrote through a
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
7
+ // where there is no local at all, which is the spelling a `#define REG(x) *(vu32 *)(x)` macro
8
+ // produces and the shape structure.ts leaves when the address re-materializes at each use.
9
+ //
10
+ // IT IS CODEGEN-VISIBLE, and the mechanism is one line of the compiler. agbcc's loop optimizer
11
+ // runs `load_mems` (gcc/loop.c:8877) between the invariant hoist and strength reduction; it
12
+ // PROMOTES a loop-invariant MEM into a register for the loop's duration and writes it back once
13
+ // after the exit, and it stands down on exactly two conditions (gcc/loop.c:8934):
14
+ //
15
+ // if (MEM_VOLATILE_P (mem) || invariant_p (XEXP (mem, 0)) != 1)
16
+ // loop_mems[i].optimize = 0;
17
+ //
18
+ // So an unpinned store to a fixed address inside a loop LEAVES THE LOOP — verified in both
19
+ // directions on synthetic:dmafill (unpinned: one store in the body, three after the `ble`, two
20
+ // pool words; pinned: four stores per iteration, four pool words). A decompiler that never spells
21
+ // the qualifier can therefore never reproduce the loop body of any function that drives a device
22
+ // register in a loop, whatever else it gets right.
23
+ //
24
+ // SEMANTICS ARE PRESERVED BY CONSTRUCTION: `volatile` only RESTRICTS what a compiler may do with
25
+ // an access; every execution of the qualified spelling is an execution of the plain one. The
26
+ // qualifier goes on the POINTEE of the deref cast, which is where C puts it and what
27
+ // backend/cfamily.ts already prints for the casts l3/inlinebase.ts mints.
28
+ //
29
+ // GATE (VOL_STORE_GATES, read once per access). THE ADMISSION RULE IS ONE: the access's WHOLE
30
+ // address must be a compile-time constant — a base that rematerializes to a numeric address plus a
31
+ // constant subscript, no `lead` — lying inside a device-register window the target declares
32
+ // (TargetDescription.capabilities.deviceRegisters); and the access must not already be qualified.
33
+ // The first three entries PARTITION the first half rather than adding to it: `inRange` is false
34
+ // when there is no window OR no constant OR the wrong constant, so ablating `no-window` or
35
+ // `non-const-address` changes no admission anywhere in the ctx space. What they buy is a refusal
36
+ // that says WHICH half is missing — "this target declares no device page" and "this address is
37
+ // IWRAM" are different facts about a row, and over the corpus they are 283 and 711 of the 1011
38
+ // refusals against `outside-window`'s 17. Nothing qualifying ⇒ decline (null), never a duplicate
39
+ // of the primary.
40
+ //
41
+ // THE WINDOW IS A REACH GATE, PRICED — not a soundness one, which is why it is `sound: false`.
42
+ // A `volatile` qualifier only restricts the compiler, so widening the range can never make a
43
+ // candidate WRONG; what it would make is a claim about ordinary memory that the target denies
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
46
+ // range and once with one admitting every constant address: 8 trees over 7 rows carry a
47
+ // const-address store the window excludes, and the fan moves on two of them —
48
+ // `synthetic:readarm` 6 candidates → 8 (the extra one TIES its match at 0) and
49
+ // `synthetic:fieldbase` 14 → 20 (best extra 22, losing to 0). No row's score or outcome moves
50
+ // either way. So the range buys candidate discipline and the honesty of the claim, and no match
51
+ // rests on it.
52
+ //
53
+ // WHAT THE WINDOW IS NOT: A CLAIM THAT NOTHING ELSE MAY QUALIFY ORDINARY MEMORY. `/volatile`
54
+ // (l3/volatileptr.ts) does exactly that, and a sweep over 834 corpus trees finds it qualifying an
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
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.
59
+ //
60
+ // THE TWO ARE SEPARATED BY THEIR DERIVATION AND NOT BY WHAT THEY EMIT, which is the trap in
61
+ // reading that spelling as a contradiction. `/inlinebase` alone mints no qualifier at all —
62
+ // enumerated on that row, `unsigned/inlinebase` carries zero `volatile` casts and
63
+ // `unsigned/inlinebase/volatile` carries three. The qualifier comes from `/volatile`, which put it
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
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
68
+ // where the target's permission does.
69
+ //
70
+ // (So the two are not foldable on the window, and the fold is not free: adopting it for `/volatile`
71
+ // would delete the WritePaletteColor spelling. `deviceVolatileClaims` in volatileptr.ts already
72
+ // unifies the COUNT side, which is the half where one answer really is enough. Nor do they
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 —
75
+ // `kleod:SetupBG3WindowOverlay:agbcc`, which neither decompiler scores — and under two DIFFERENT
76
+ // published classifications: asmlift `noncompile` (agbcc rejects its call to `m4aSoundVSyncOff`),
77
+ // m2c `declined`. A pairing whose whole reach is a row asmlift cannot compile is one a row has yet
78
+ // to demand.
79
+ //
80
+ // A lever that MINTS the locals is a different question with a different answer, and `rank.ts`
81
+ // pairs this pass with one: `/regionbase` homes the regions holding two or more direct uses of a
82
+ // device base and leaves every other spelling of the same address inline, so both qualifiers have
83
+ // something to claim in one function. `synthetic:dmascope` is that row.)
84
+ //
85
+ // SCOPE: STORES only. A device READ is a different question with a different answer — the idiom
86
+ // fold's DCE drops a use-less device load outright (synthetic:dmaback), so a read that survives to
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
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
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
93
+ // inhabitant is what "earn the level" forbids.
94
+ import { type IrType, T, scalarTypeForAccess } from '../ir/types';
95
+ import { cellAddress, inRange } from './address';
96
+ import { type Expr, type SFn, type Stmt, stmtChildren } from './ast';
97
+ import { type Gate, firstRejection } from './gates';
98
+
99
+ /** One STORE lvalue as the gates read it. */
100
+ interface AccessCtx {
101
+ /** the target declares a device-register range at all */
102
+ hasWindow: boolean;
103
+ /** the access's whole address as a compile-time constant, or null */
104
+ address: number | null;
105
+ /** that address lies inside the declared window */
106
+ inWindow: boolean;
107
+ /** the lvalue already carries a `volatile` qualifier */
108
+ qualified: boolean;
109
+ }
110
+
111
+ // One rule at three resolutions, most general first: `inRange` fails for all three reasons, so
112
+ // only `outside-window` and `already-qualified` change an admission. See the header's GATE
113
+ // paragraph — what the first two decide is the REASON a row is refused, not whether it is.
114
+ export const VOL_STORE_GATES: readonly Gate<AccessCtx>[] = [
115
+ {
116
+ id: 'no-window',
117
+ why: 'a target that declares no device range has no address this lever may call volatile',
118
+ sound: false,
119
+ rejects: (c) => !c.hasWindow,
120
+ },
121
+ {
122
+ id: 'non-const-address',
123
+ why: 'a runtime address names no cell, so nothing here can say which object it reaches',
124
+ sound: false,
125
+ rejects: (c) => c.address === null,
126
+ },
127
+ {
128
+ // NOT "the target denies this address may be volatile" — `/volatile` qualifies IWRAM on eleven
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.
131
+ id: 'outside-window',
132
+ why: 'the address is the only evidence this lever has, and outside the window it supports nothing',
133
+ sound: false,
134
+ rejects: (c) => !c.inWindow,
135
+ },
136
+ {
137
+ id: 'already-qualified',
138
+ why: 'a second qualifier would nest a cast over a spelling that already asserts volatility',
139
+ sound: false,
140
+ rejects: (c) => c.qualified,
141
+ },
142
+ ];
143
+
144
+ /** Does this base already assert volatility — a `volatile` cast at any depth of the cast chain? */
145
+ const qualifiedBase = (e: Expr): boolean => e.k === 'cast' && (e.volatile === true || qualifiedBase(e.e));
146
+
147
+ /** The pointee the deref cast carries: the access's own scalar type. */
148
+ const pointee = (ix: Extract<Expr, { k: 'index' }>): IrType => scalarTypeForAccess(ix.width, ix.signed);
149
+
150
+ /** What the gates read about one indexed access: its cell address, and whether that address is
151
+ * evidence — a window to place it in, the placement itself, and whether the base already carries
152
+ * the qualifier. */
153
+ function accessCtx(lval: Extract<Expr, { k: 'index' }>, window: readonly [number, number] | undefined): AccessCtx {
154
+ const address = cellAddress(lval);
155
+ return {
156
+ hasWindow: window !== undefined,
157
+ address,
158
+ inWindow: inRange(address, window),
159
+ qualified: qualifiedBase(lval.base),
160
+ };
161
+ }
162
+
163
+ /** The store lvalue the gates admit, rewritten — or the input unchanged. */
164
+ function qualify(lval: Expr, window: readonly [number, number] | undefined): Expr {
165
+ if (lval.k !== 'index') {
166
+ return lval; // a `field` lvalue is a recovered struct view, whose declaration owns volatility
167
+ }
168
+ const ctx = accessCtx(lval, window);
169
+ if (firstRejection(VOL_STORE_GATES, ctx) !== null) {
170
+ return lval;
171
+ }
172
+ // An existing scalar pointer cast takes the qualifier in place; a bare const gets one minted,
173
+ // exactly the node backend/cfamily.ts's own deref legalization would have synthesized.
174
+ const base: Expr =
175
+ lval.base.k === 'cast' && lval.base.to.kind === 'ptr'
176
+ ? { ...lval.base, volatile: true }
177
+ : { k: 'cast', to: T.ptr(pointee(lval)), volatile: true, e: lval.base };
178
+ return { ...lval, base };
179
+ }
180
+
181
+ /** How many stores this tree would qualify — the enumeration gate, so a function with no device
182
+ * store costs one walk and no candidate.
183
+ *
184
+ * A COUNT, so the shared `stmtChildren` walk order (a switch's default at `defaultAt`, not last)
185
+ * cannot reach the answer. */
186
+ export function deviceStoreCount(sfn: SFn, window?: readonly [number, number]): number {
187
+ let n = 0;
188
+ const visit = (stmts: readonly Stmt[]): void => {
189
+ for (const s of stmts) {
190
+ if (s.k === 'store' && qualify(s.lval, window) !== s.lval) {
191
+ n++;
192
+ }
193
+ visit(stmtChildren(s));
194
+ }
195
+ };
196
+ visit(sfn.body);
197
+ return n;
198
+ }
199
+
200
+ /** The `/vol-store` candidate, or null when no store qualifies. Read-only: returns a fresh SFn
201
+ * whose body is rebuilt, leaving the input untouched. */
202
+ export function volatileDeviceStores(sfn: SFn, window?: readonly [number, number]): SFn | null {
203
+ if (deviceStoreCount(sfn, window) === 0) {
204
+ return null;
205
+ }
206
+ const rewrite = (stmts: readonly Stmt[]): Stmt[] =>
207
+ stmts.map((s): Stmt => {
208
+ switch (s.k) {
209
+ case 'store':
210
+ return { ...s, lval: qualify(s.lval, window) };
211
+ case 'if':
212
+ return { ...s, then: rewrite(s.then), else: rewrite(s.else) };
213
+ case 'while':
214
+ case 'dowhile':
215
+ return { ...s, body: rewrite(s.body) };
216
+ case 'for':
217
+ return { ...s, init: rewrite([s.init])[0], inc: rewrite([s.inc])[0], body: rewrite(s.body) };
218
+ case 'switch':
219
+ return {
220
+ ...s,
221
+ cases: s.cases.map((c) => ({ ...c, body: rewrite(c.body) })),
222
+ ...(s.default ? { default: rewrite(s.default) } : {}),
223
+ };
224
+ default:
225
+ return s;
226
+ }
227
+ });
228
+ return { ...sfn, body: rewrite(sfn.body) };
229
+ }
@@ -0,0 +1,62 @@
1
+ // L3 re-spelling lever: spell a negate of a SHARED subtraction as `0 - x`.
2
+ //
3
+ // The two C spellings of a negation are not interchangeable in front of gcc 2.9's folder. `-x` is
4
+ // built through build_unary_op and FOLDED, so fold-const.c's "Convert - (a - b) to (b - a)"
5
+ // (gcc/fold-const.c:4821) rewrites a negated subtraction into the reversed one before CSE ever
6
+ // runs. `0 - x` reaches fold as a MINUS_EXPR with a zero left operand and comes back as a bare
7
+ // `build1 (NEGATE_EXPR, …)` (gcc/fold-const.c:5085) which is never re-folded, so the subtraction
8
+ // survives as itself.
9
+ //
10
+ // With ONE use that difference is invisible: RTL combine folds the negate back in and both
11
+ // spellings emit the same `sub r0, r1, r0`. With the subtraction SHARED — its value also feeding
12
+ // the compare that chose the branch — they diverge by more than the negate, because the reversed
13
+ // subtraction is a second computation whose operands must stay live: `-(a - b)` costs six
14
+ // instructions (one a register copy) where `0 - (a - b)` costs five. Both compiled with agbcc
15
+ // `-O2 -mthumb-interwork -Wimplicit -fhex-asm -fprologue-bugfix`.
16
+ //
17
+ // NOT an agbcc rule, so no target gate: compiled on the shared shape, IDO, KMC GCC and gcc
18
+ // 2.7.2/MIPS each emit a different function for the two spellings as well — IDO the other way
19
+ // round, where `-(a - b)` is the `negu` and `0 - (a - b)` the reversed `subu` — and only mwcc
20
+ // collapses them, where the duplicate scores identically and can neither win nor lose.
21
+ //
22
+ // So which one the source wrote is not recoverable — a `neg` is reachable from `-t` over a named
23
+ // local as well — and the differ referees. Semantics are preserved by construction: `-x` and
24
+ // `0 - x` are the same C expression for every integer type.
25
+ //
26
+ // A RE-SPELLING rather than a fourth value-home axis (docs/level-tower.md's third fork). The fold
27
+ // rule fires at all only because asmlift INLINED a value the source bound to a local, and naming
28
+ // that local reaches the same match by the other route: on `pokeemerald:GetAnchorCoord:agbcc`,
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`
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
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.
35
+ //
36
+ // SCOPE (decline over approximate). Only a `bin('-')` operand, and only a SHARED one. Neither
37
+ // restriction is caution: over any other operand shape the fold rule does not apply and the two
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 —
40
+ // two textually equal calls are two calls, not one shared value, so the premise fails.
41
+ import { type Expr, type SFn, exprEquals, exprHasEffect, mapExprChildren, mapStmtExprs, walkExprs } from './ast';
42
+
43
+ export function zeroSubNegates(sfn: SFn): SFn | null {
44
+ const subs: Expr[] = [];
45
+ for (const e of walkExprs(sfn.body)) {
46
+ if (e.k === 'bin' && e.op === '-') {
47
+ subs.push(e);
48
+ }
49
+ }
50
+ const shared = (x: Expr): boolean => subs.filter((s) => exprEquals(s, x)).length > 1;
51
+ let changed = false;
52
+ const rewrite = (e: Expr): Expr => {
53
+ const m = mapExprChildren(e, rewrite);
54
+ if (m.k === 'un' && m.op === '-' && m.e.k === 'bin' && m.e.op === '-' && !exprHasEffect(m.e) && shared(m.e)) {
55
+ changed = true;
56
+ return { k: 'bin', op: '-', l: { k: 'const', value: 0 }, r: m.e };
57
+ }
58
+ return m;
59
+ };
60
+ const body = sfn.body.map((s) => mapStmtExprs(s, rewrite));
61
+ return changed ? { ...sfn, body } : null;
62
+ }