@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,301 @@
1
+ // L3 re-spelling lever: a loop INIT moves above the guard that encloses it, and the guard reads
2
+ // the initialized variable.
3
+ //
4
+ // `for (i = 0; i < n; i++)` compiles with the init BEFORE the zero-trip test (`mov r3,#0` then
5
+ // `cmp r3, r5`), while `if (0 < n) { i = 0; do … }` compiles with the init behind the branch.
6
+ // Both source forms lift to the SAME IR — a const has no position — so which the original spelled
7
+ // is not recoverable; this lever emits the init-first sibling and the differ referees:
8
+ //
9
+ // if (0 < n) { v = 0; … } → v = 0; if (v < n) { … }
10
+ //
11
+ // Two rewrites (the guard re-spelling gated on the moved write being dead — see SCOPE):
12
+ // • common-arm hoist — both arms of an `if` begin with the SAME pure-const assign
13
+ // (`if (c) { v = 0; } else { v = 0; … }`, gcc's inverted-guard shape): the assign moves above
14
+ // the `if`, and an emptied then-arm flips into its negated else form.
15
+ // • guard re-spelling — an else-less `if` whose then-arm begins with `v = X` and whose
16
+ // condition carries X ITSELF as a comparison side: the assign moves above and that side
17
+ // becomes `v`. X is a const (`for (j = 0; j < n;…)`'s shape) or a pure NON-VOLATILE read
18
+ // (`for (j = *p; j < size;…)` — guard and init read the same cell back to back, which the
19
+ // local-spelling source reads ONCE; collapsing the adjacent pair is what the compiler saw).
20
+ //
21
+ // SCOPE (decline over approximate): both rewrites touch PRIVATE locals only — a bare-global
22
+ // assign stores memory other code observes, a VOLATILE local's store is itself observable (the
23
+ // escaped DMA scratch), and a local whose ADDRESS is taken anywhere can be read through the
24
+ // pointer where no name betrays it — all three stay put. The common-arm hoist carries pure
25
+ // CONST values only. A guard re-spell's X must be call-free and marker-free, may mention only
26
+ // the function's own non-volatile params/locals (a global or `&gSym` could be project-declared
27
+ // volatile, and a volatile read may not be deduplicated), and the substitution must PRESERVE
28
+ // THE COMPARE'S MEANING: the variable's declared type can differ from X's rendered type, so the
29
+ // swap is admitted only when both sides were provably non-negative (every signedness reading
30
+ // agrees there) or the compare's rendered signedness is unchanged — anything indeterminate
31
+ // refuses. The condition must not read the variable (its pre-assign value dies in the move)
32
+ // and, for a READ X, must be effect-free as a whole (the hoist moves X's read above it). The
33
+ // guard re-spelling
34
+ // mints a write on a previously write-free path, so it needs that write to be DEAD there: the
35
+ // variable must be untouched after the `if` in its own list and in the tail of every ancestor
36
+ // list (a sibling arm of an ancestor `if` is not "after" — it never runs in the same entry).
37
+ // Under a loop or switch ancestor the tails stop describing what runs next (a back edge
38
+ // re-enters everything, a case can fall through), so there the variable must appear nowhere
39
+ // outside the rewritten `if` at all. Declines (null) when nothing changes.
40
+ import type { Expr, SFn, Stmt } from './ast';
41
+ import { NEGATE_REL, exprChildren, exprEquals, exprHasEffect, stmtChildren, stmtExprs, walkExprs } from './ast';
42
+ import { arithConversionSignedness, declaredTypes, provablyNonNegative } from './typing';
43
+
44
+ const readsVar = (e: Expr, name: string): boolean =>
45
+ ((e.k === 'var' || e.k === 'addr') && e.name === name) || exprChildren(e).some((c) => readsVar(c, name));
46
+
47
+ // READS only — a pure write in a tail is benign (it overwrites the minted value on every path,
48
+ // and any read after it is that write's business); touchesOutside below is TOTAL because strong
49
+ // mode must know the name is absent, presence of any kind included.
50
+ const stmtTouches = (s: Stmt, name: string): boolean =>
51
+ stmtExprs(s).some((e) => readsVar(e, name)) || stmtChildren(s).some((x) => stmtTouches(x, name));
52
+
53
+ const isConstAssign = (s: Stmt): s is Extract<Stmt, { k: 'assign' }> & { value: { k: 'const'; value: number } } =>
54
+ s.k === 'assign' && s.value.k === 'const';
55
+
56
+ /** Peel value-preserving `(s32)`/`(u32)` casts. Width 32 only: a narrower target truncates. A
57
+ * `volatile` cast is never peeled — the qualifier is the access's meaning, and the differ cannot
58
+ * referee its loss. */
59
+ const stripWideIntCast = (e: Expr): Expr =>
60
+ e.k === 'cast' && e.to.kind === 'int' && e.to.width === 32 && e.volatile !== true ? stripWideIntCast(e.e) : e;
61
+
62
+ /** Every deref rooted at a var through casts only. The var-root rule is a TWO-WORLD argument, not a
63
+ * volatility proof: a deref through a plain-declared pointer local may still be MMIO, but the
64
+ * /volatile axis enumerates the qualified sibling — where this lever refuses — so both worlds reach
65
+ * the differ and collapsing reads here is the plain world's own premise. A raw `*(u16 *)CONST` deref
66
+ * has NO local for /volatile to qualify, so no sibling carries the volatile world and the collapse
67
+ * would silently discard it. */
68
+ const varRooted = (e: Expr): boolean => (e.k === 'var' ? true : e.k === 'cast' ? varRooted(e.e) : false);
69
+
70
+ /** A guard re-spell's X: call/marker-free, every named leaf a non-volatile param/local of THIS
71
+ * function (a global or `&gSym` could be project-declared volatile), and every deref `varRooted`. */
72
+ const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: ReadonlySet<string>): boolean => {
73
+ if (e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
74
+ return false;
75
+ }
76
+ if (e.k === 'var' && (!ownNames.has(e.name) || volatileLocals.has(e.name))) {
77
+ return false;
78
+ }
79
+ if ((e.k === 'index' || e.k === 'field') && !varRooted(e.base)) {
80
+ return false;
81
+ }
82
+ return exprChildren(e).every((c) => hoistableRead(c, ownNames, volatileLocals));
83
+ };
84
+
85
+ /** A compare operand and the init's value denote the same 32-bit value under different SPELLINGS
86
+ * when a width-32 cast is all that separates them: `/uns-cmp` wraps one side in `(u32)` to make
87
+ * the branch unsigned, and on a zero-trip guard that side is the very const the init assigns.
88
+ * The swap is still exact — `v = X` stores X's 32 bits and `v` is 32-bit-declared
89
+ * (meaningPreserved refuses otherwise), so `v` and `(u32)X` carry the same bit pattern and only
90
+ * the compare's rendered signedness can differ, which meaningPreserved checks separately. A
91
+ * NARROWING cast changes the value and never matches, and a `volatile` one is not peeled at all. */
92
+ const sameValue = (a: Expr, b: Expr): boolean => exprEquals(stripWideIntCast(a), stripWideIntCast(b));
93
+
94
+ /** The compare-meaning gate (see SCOPE): substituting `v` for X may change the compare's
95
+ * rendered signedness through v's declared type. Sufficiency: v's declared width must be 32
96
+ * (the assignment `v = X` then represents any 32-bit-or-narrower X exactly, so v's runtime
97
+ * value EQUALS X's — a narrow-declared v would truncate and no signedness reasoning survives
98
+ * that), and then (a) both original sides provably in [0, 2^31) ⇒ signed and unsigned
99
+ * compares agree on the actual values whatever the swap does to rendered signedness; (b)
100
+ * otherwise a defined, UNCHANGED rendered signedness over equal values gives the identical
101
+ * result. Anything indeterminate refuses. */
102
+ const meaningPreserved = (
103
+ l: Expr,
104
+ r: Expr,
105
+ side: 'l' | 'r',
106
+ v: string,
107
+ env: ReturnType<typeof declaredTypes>,
108
+ ): boolean => {
109
+ const vt = env(v);
110
+ if (vt?.kind !== 'int' || vt.width !== 32) {
111
+ return false;
112
+ }
113
+ if (provablyNonNegative(l, env) && provablyNonNegative(r, env)) {
114
+ return true;
115
+ }
116
+ const before = arithConversionSignedness(l, r, env);
117
+ const vv: Expr = { k: 'var', name: v };
118
+ const after = side === 'l' ? arithConversionSignedness(vv, r, env) : arithConversionSignedness(l, vv, env);
119
+ return before !== undefined && before === after;
120
+ };
121
+
122
+ /** TOTAL (reads and pure writes) — see the note on stmtTouches. Runs against the pre-rewrite
123
+ * tree (`skip` is an original-tree statement, found by identity); the rewrites never change a
124
+ * name's presence in a subtree, so the verdict carries over to the rewritten one. */
125
+ const touchesOutside = (list: Stmt[], skip: Stmt, name: string): boolean =>
126
+ list.some(
127
+ (st) =>
128
+ st !== skip &&
129
+ (stmtExprs(st).some((e) => readsVar(e, name)) ||
130
+ (st.k === 'assign' && st.name === name) ||
131
+ touchesOutside(stmtChildren(st), skip, name)),
132
+ );
133
+
134
+ /** Every name whose ADDRESS this function takes anywhere — a local read through the captured
135
+ * pointer with no name in sight. */
136
+ function addressTakenNames(sfn: SFn): Set<string> {
137
+ const taken = new Set<string>();
138
+ for (const e of walkExprs(sfn.body)) {
139
+ if (e.k === 'addr') {
140
+ taken.add(e.name);
141
+ }
142
+ }
143
+ return taken;
144
+ }
145
+
146
+ export function initFirstGuards(sfn: SFn): SFn | null {
147
+ let changed = false;
148
+ // the names both rewrites may move a write of: this function's params and its non-volatile
149
+ // locals, minus everything whose address escapes
150
+ const addressTaken = addressTakenNames(sfn);
151
+ const fnLocal = new Set(
152
+ [...sfn.params.map((d) => d.name), ...sfn.locals.filter((l) => l.volatile !== true).map((l) => l.name)].filter(
153
+ (n) => !addressTaken.has(n),
154
+ ),
155
+ );
156
+ const volatileLocals = new Set(
157
+ sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
158
+ );
159
+ const ownNames = new Set([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
160
+ const env = declaredTypes(sfn);
161
+
162
+ // `tails`: for each ancestor list, the statements after the ancestor on the path here.
163
+ // `strong`: a loop or switch ancestor exists, so tails stop bounding what runs after.
164
+ interface Ctx {
165
+ tails: Stmt[][];
166
+ strong: boolean;
167
+ }
168
+ // Assigns this pass itself hoisted to an arm head's parent list. An ancestor `if` whose arm now
169
+ // BEGINS with one would otherwise re-spell it again — rewriting its own condition's accidental
170
+ // matching const into the variable and stealing the arrangement the inner guard needed.
171
+ const moved = new Set<Stmt>();
172
+ const rewriteList = (list: Stmt[], ctx: Ctx): Stmt[] => {
173
+ const out: Stmt[] = [];
174
+ for (let i = 0; i < list.length; i++) {
175
+ const s0 = list[i];
176
+ const s = recurse(s0, { tails: [...ctx.tails, list.slice(i + 1)], strong: ctx.strong });
177
+ if (s.k !== 'if') {
178
+ out.push(s);
179
+ continue;
180
+ }
181
+ let { cond, then, else: els } = s;
182
+ // common-arm hoist
183
+ const hoisted: { name: string; value: number }[] = [];
184
+ for (;;) {
185
+ const t0 = then[0];
186
+ const e0 = els[0];
187
+ if (
188
+ t0 === undefined ||
189
+ e0 === undefined ||
190
+ moved.has(t0) ||
191
+ moved.has(e0) ||
192
+ !isConstAssign(t0) ||
193
+ !isConstAssign(e0) ||
194
+ !fnLocal.has(t0.name) ||
195
+ t0.name !== e0.name ||
196
+ t0.value.value !== e0.value.value ||
197
+ readsVar(cond, t0.name)
198
+ ) {
199
+ break;
200
+ }
201
+ out.push(t0);
202
+ moved.add(t0);
203
+ hoisted.push({ name: t0.name, value: t0.value.value });
204
+ changed = true;
205
+ then = then.slice(1);
206
+ els = els.slice(1);
207
+ }
208
+ if (then.length === 0 && els.length > 0) {
209
+ const neg = cond.k === 'bin' ? NEGATE_REL[cond.op] : undefined;
210
+ if (neg !== undefined && cond.k === 'bin') {
211
+ cond = { ...cond, op: neg };
212
+ [then, els] = [els, then];
213
+ changed = true;
214
+ }
215
+ }
216
+ // a COMMON-hoisted variable holds its const on both paths, so the condition's matching
217
+ // const operand reads through it unconditionally — no tail gate needed.
218
+ // A BARE const only, where the guard re-spelling below uses the cast-tolerant `sameValue`:
219
+ // this site does not run `meaningPreserved`, so it may not swap in a name whose declared
220
+ // type could re-render the compare's signedness — which is exactly what peeling a `(u32)`
221
+ // would put on the table. A row that needs `/uns-cmp`'s spelling hoisted here brings the
222
+ // gate with it.
223
+ for (const hv of hoisted) {
224
+ if (cond.k === 'bin' && NEGATE_REL[cond.op]) {
225
+ if (cond.l.k === 'const' && cond.l.value === hv.value) {
226
+ cond = { ...cond, l: { k: 'var', name: hv.name } };
227
+ } else if (cond.r.k === 'const' && cond.r.value === hv.value) {
228
+ cond = { ...cond, r: { k: 'var', name: hv.name } };
229
+ }
230
+ }
231
+ }
232
+ // guard re-spelling — X a const or a hoistable read, matched as a whole comparison side
233
+ if (
234
+ els.length === 0 &&
235
+ then.length > 0 &&
236
+ then[0].k === 'assign' &&
237
+ !moved.has(then[0]) &&
238
+ fnLocal.has(then[0].name) &&
239
+ cond.k === 'bin' &&
240
+ NEGATE_REL[cond.op]
241
+ ) {
242
+ const init = then[0];
243
+ const rest = list.slice(i + 1);
244
+ const side =
245
+ isConstAssign(init) || hoistableRead(init.value, ownNames, volatileLocals)
246
+ ? sameValue(cond.l, init.value)
247
+ ? ('l' as const)
248
+ : sameValue(cond.r, init.value)
249
+ ? ('r' as const)
250
+ : null
251
+ : null;
252
+ const deadAfter = ctx.strong
253
+ ? !touchesOutside(sfn.body, s0, init.name)
254
+ : !rest.some((t) => stmtTouches(t, init.name)) &&
255
+ ctx.tails.every((tail) => !tail.some((t) => stmtTouches(t, init.name)));
256
+ if (
257
+ side !== null &&
258
+ !readsVar(cond, init.name) &&
259
+ deadAfter &&
260
+ // A READ X's hoist moves its evaluation ABOVE the whole condition, so the condition must
261
+ // carry no effect it could cross (a call there could write the cell X reads); a CONST
262
+ // init crosses nothing and keeps the wider admission.
263
+ (isConstAssign(init) || !exprHasEffect(cond)) &&
264
+ meaningPreserved(cond.l, cond.r, side, init.name, env)
265
+ ) {
266
+ out.push(init);
267
+ moved.add(init);
268
+ cond = { ...cond, [side]: { k: 'var', name: init.name } };
269
+ then = then.slice(1);
270
+ changed = true;
271
+ }
272
+ }
273
+ out.push({ ...s, cond, then, else: els });
274
+ }
275
+ return out;
276
+ };
277
+
278
+ const recurse = (s: Stmt, ctx: Ctx): Stmt => {
279
+ const strong = { ...ctx, strong: true };
280
+ switch (s.k) {
281
+ case 'if':
282
+ return { ...s, then: rewriteList(s.then, ctx), else: rewriteList(s.else, ctx) };
283
+ case 'while':
284
+ case 'dowhile':
285
+ return { ...s, body: rewriteList(s.body, strong) };
286
+ case 'for':
287
+ return { ...s, body: rewriteList(s.body, strong) };
288
+ case 'switch':
289
+ return {
290
+ ...s,
291
+ cases: s.cases.map((c) => ({ ...c, body: rewriteList(c.body, strong) })),
292
+ ...(s.default ? { default: rewriteList(s.default, strong) } : {}),
293
+ };
294
+ default:
295
+ return s;
296
+ }
297
+ };
298
+
299
+ const body = rewriteList(sfn.body, { tails: [], strong: false });
300
+ return changed ? { ...sfn, body } : null;
301
+ }
@@ -0,0 +1,193 @@
1
+ // L3 re-spelling lever: DELETE a pointer local holding a CONSTANT address and spell each access
2
+ // through it as the cast constant (`*(u16 *)0x4000208` rather than `p = (u16 *)0x4000208; *p`).
3
+ //
4
+ // The local is structure/analysis.ts's value-home spelling for a `const` with 2+ consumers that
5
+ // is LIVE ACROSS A CALL: a value the compiler needs after a call survives in a callee-saved
6
+ // register, and a named local reproduces that register. The machine fact is real — agbcc does
7
+ // park the address in `r4` across the calls — but it does NOT imply the source named anything:
8
+ // a constant re-spelled at each use is CSEd into the same one register. So the asm
9
+ // underdetermines the spelling, and this is the differ-refereed other side of it.
10
+ //
11
+ // It is codegen-visible, which is why both sides have to be enumerated rather than one picked:
12
+ // the extra `p = …;` statement is scheduled ahead of the rest of the entry block, so the pool
13
+ // load moves in front of the frame-address materialization the target emits first
14
+ // (pokeemerald:EReader_Reset, agbcc 2.9-arm-000512 — 1 insert + 1 delete, the whole residual).
15
+ //
16
+ // SEMANTICS ARE PRESERVED BY CONSTRUCTION: this is constant propagation of a local that is
17
+ // assigned once, from a compile-time constant, before anything mentions it, and whose address is
18
+ // never taken — so every use reads that constant on every path, and the substituted expression
19
+ // carries the local's own declared type AND its pointee volatility.
20
+ //
21
+ // THE QUALIFIER TRAVELS WITH THE ADDRESS. The deleted local is the only place a `volatile`
22
+ // pointee could be written, and a raw address is precisely the case with no declaration
23
+ // anywhere else to carry it — so dropping it here spells an MMIO access non-volatile in the one
24
+ // place the differ sometimes cannot referee. On pokeemerald:EReader_Reset the two spellings
25
+ // separate at 11 against 12 on their own, and are BYTE-IDENTICAL once the slot qualifier is
26
+ // there too — the shape that matches (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit
27
+ // -fhex-asm -fprologue-bugfix`; `.s` diff empty, `.o` identical under cmp). So rank.ts emits
28
+ // the qualified spelling as a second OUTPUT of this lever, `/volatile` narrowed to the locals
29
+ // it deletes. Each cast this mints carries the qualifier; a use whose width does not stride the
30
+ // declared pointee renders through the C-family printer's reinterpret cast instead, which
31
+ // carries it too (backend/cfamily.ts) — in C the access takes the OUTER type, so a plain cast
32
+ // there would spell exactly the silent drop this paragraph exists to prevent.
33
+ //
34
+ // GATE (INLINEBASE_GATES) — the local must be all of: pointer-typed; initialized by a bare
35
+ // NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own lever
36
+ // family owns that question; `0` is NULL, never an address, which is also the sibling qualifier
37
+ // lever's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
38
+ // statement's mention precedes; never address-taken; used only as the base of an `index`, at 2+
39
+ // sites (one use is not the reused address this exists for); not object-`volatile` (a
40
+ // `T *volatile p` has no inhabitant, and cfamily.ts prints that flag in the pointee's position);
41
+ // and not `frame` (a slot is an asm fact — see the SFn.locals doc). Anything else, and nothing
42
+ // qualifying at all, DECLINES (null) rather than approximating.
43
+ //
44
+ // A RE-SPELLING RATHER THAN A STRUCTURING AXIS, which is a cost choice and not the
45
+ // underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked population").
46
+ // The question — does a `const` with 2+ consumers live across a call in a named local — is the one
47
+ // `/reread-globals`, `/addr-home`, `/expr-home` and `/derived-home` each answer as a
48
+ // STRUCTURING_AXES entry. An axis here would double the enumeration on every function it admits;
49
+ // substituting on the already-homed tree costs 766 candidates over 47058 (+1.6%) across the 33 of
50
+ // 69 klonoa functions that lift with no symbol map.
51
+ //
52
+ // KNOWN GAP, and it is the price of that choice rather than an oversight: only `index` bases are
53
+ // re-spelled, so the same L2 home passed to a callee or standing as a `field` base is out of
54
+ // reach — `otherUses` refuses it. Reaching those needs the un-homed tree, which is
55
+ // structure/analysis.ts's decision; the day a row demands one, this becomes the axis.
56
+ //
57
+ // The idiom it recovers is every GBA project's register macro: `*(vu16 *)0x4000208` is what
58
+ // `REG_IME` expands to, so the deleted local is not merely an undone home.
59
+ import { type Expr, type SFn, mapExprChildren, mapStmtExprs } from './ast';
60
+ import { type Gate, firstRejection } from './gates';
61
+ import { type Mentions, localMentions } from './mentions';
62
+
63
+ /** One local as the gates read it. */
64
+ interface BaseCtx {
65
+ isPointer: boolean;
66
+ objectVolatile: boolean;
67
+ hasFrame: boolean;
68
+ m: Mentions;
69
+ }
70
+
71
+ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
72
+ {
73
+ id: 'non-pointer',
74
+ why: 'the lever re-spells an address; a scalar value home is a different question',
75
+ sound: false,
76
+ rejects: (c) => !c.isPointer,
77
+ },
78
+ {
79
+ id: 'object-volatile',
80
+ why: 'the substitution carries the POINTEE flag, so an object-volatile pointer would lose its own',
81
+ sound: true,
82
+ guardedBy: 'inlinebase.test.ts: an object-volatile or frame local declines',
83
+ rejects: (c) => c.objectVolatile,
84
+ },
85
+ {
86
+ id: 'frame',
87
+ why: 'a slot the asm materialized is an asm fact, not a spelling to undo',
88
+ sound: false,
89
+ rejects: (c) => c.hasFrame,
90
+ },
91
+ {
92
+ id: 'multi-assign',
93
+ why: 'a name assigned more than once is not one constant',
94
+ sound: true,
95
+ guardedBy: 'inlinebase.test.ts: a second assignment means the name is not one constant',
96
+ rejects: (c) => c.m.assigns !== 1,
97
+ },
98
+ {
99
+ id: 'const-init',
100
+ why: 'only a bare `const` at the body’s top level is an address available on every path',
101
+ sound: true,
102
+ guardedBy: 'inlinebase.test.ts: an assignment below the top level may not run on every path',
103
+ rejects: (c) => c.m.topAssignAt === null || c.m.constValue === null,
104
+ },
105
+ {
106
+ id: 'null-base',
107
+ why: '`0` is NULL, never an address — the sibling qualifier lever (volatileptr.ts) refuses it too',
108
+ sound: false,
109
+ rejects: (c) => c.m.constValue === 0,
110
+ },
111
+ {
112
+ id: 'use-before-assign',
113
+ why: 'a mention ahead of the assignment reads something the constant does not stand for',
114
+ sound: true,
115
+ guardedBy: 'inlinebase.test.ts: a use in a loop ABOVE the assignment reads the local before it is set',
116
+ rejects: (c) => c.m.firstAt !== c.m.topAssignAt,
117
+ },
118
+ {
119
+ id: 'addr-taken',
120
+ why: 'a deleted local has no address to take',
121
+ sound: true,
122
+ guardedBy: 'inlinebase.test.ts: an address-taken local has an identity the constant cannot stand in for',
123
+ rejects: (c) => c.m.addrTaken !== 0,
124
+ },
125
+ {
126
+ id: 'other-uses',
127
+ why: 'a use the substitution cannot reach would name the deleted local',
128
+ sound: true,
129
+ guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the lever re-spells',
130
+ rejects: (c) => c.m.otherUses !== 0,
131
+ },
132
+ {
133
+ id: 'single-use',
134
+ why: 'one use is not the reused address this lever exists for',
135
+ sound: false,
136
+ rejects: (c) => c.m.baseUses < 2,
137
+ },
138
+ ];
139
+
140
+ /** The locals INLINEBASE_GATES admits, each with the cast its uses become. */
141
+ function plan(sfn: SFn): Map<string, Extract<Expr, { k: 'cast' }>> {
142
+ const t = localMentions(sfn);
143
+ const out = new Map<string, Extract<Expr, { k: 'cast' }>>();
144
+ for (const l of sfn.locals) {
145
+ const m = t.get(l.name);
146
+ if (
147
+ m === undefined ||
148
+ firstRejection(INLINEBASE_GATES, {
149
+ isPointer: l.type.kind === 'ptr',
150
+ objectVolatile: l.volatile !== undefined,
151
+ hasFrame: l.frame !== undefined,
152
+ m,
153
+ }) !== null
154
+ ) {
155
+ continue;
156
+ }
157
+ out.set(l.name, {
158
+ k: 'cast',
159
+ to: l.type,
160
+ ...(l.pointeeVolatile ? { volatile: true as const } : {}),
161
+ e: { k: 'const', value: m.constValue! },
162
+ });
163
+ }
164
+ return out;
165
+ }
166
+
167
+ /** Which locals this lever would delete — rank.ts narrows `/volatile` to exactly these before
168
+ * pairing, so the qualified output never qualifies a pointer the lever leaves standing. */
169
+ export function inlinableConstBases(sfn: SFn): string[] {
170
+ return [...plan(sfn).keys()];
171
+ }
172
+
173
+ /** The `/inlinebase` candidate, or null when no local qualifies. Read-only: returns a fresh SFn
174
+ * whose body is rebuilt, leaving the input untouched. */
175
+ export function inlineConstBases(sfn: SFn): SFn | null {
176
+ const inline = plan(sfn);
177
+ if (inline.size === 0) {
178
+ return null;
179
+ }
180
+ // A FRESH node per use — never one shared tree — because identity-keyed rules downstream
181
+ // (contracts.ts's dot-base exemption) read node identity.
182
+ const sub = (e: Expr): Expr => {
183
+ if (e.k === 'var') {
184
+ const at = inline.get(e.name);
185
+ if (at !== undefined) {
186
+ return { ...at, e: { ...at.e } };
187
+ }
188
+ }
189
+ return mapExprChildren(e, sub);
190
+ };
191
+ const body = sfn.body.filter((s) => !(s.k === 'assign' && inline.has(s.name))).map((s) => mapStmtExprs(s, sub));
192
+ return { ...sfn, locals: sfn.locals.filter((l) => !inline.has(l.name)), body };
193
+ }
@@ -0,0 +1,113 @@
1
+ // What one L3 tree does to each of its locals, counted once.
2
+ //
3
+ // Levers ask overlapping versions of the question and would disagree if each walked the tree
4
+ // its own way: l3/inlinebase.ts needs the SHAPE of every use (only an `index` base is re-spellable,
5
+ // and the single assignment must be a top-level `const` nothing mentions earlier), while
6
+ // l3/volatileval.ts needs the COUNTS, to check the tree still performs every access the machine
7
+ // did before it declares them all observable. One walk answers both, and it has to: on the counting
8
+ // consumer a miscount is a wrong `volatile` claim, not a missed candidate.
9
+ //
10
+ // Derived from the ONE traversal vocabulary (exprChildren/stmtExprs/stmtChildren) for every node
11
+ // kind, so a new one is a compile error there rather than a silent undercount here — with `index`
12
+ // as the ONE hand-rolled case, because the callback needs to know which child stands as the base
13
+ // and `exprChildren` flattens that away. That hand-rolling is a standing hazard rather than an
14
+ // oversight: a POSITION added to `index` reaches the generic vocabulary for free and this walk not
15
+ // at all. Every position is enumerated below and pinned by
16
+ // test/array-rank-guards.test.ts, beside the generic helpers it cannot speak for.
17
+ import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
18
+
19
+ export interface Mentions {
20
+ /** assignments to the name, at any nesting */
21
+ assigns: number;
22
+ /** body-top-level index of its single top-level assignment, or null */
23
+ topAssignAt: number | null;
24
+ /** the bare-`const` value that assignment stores, or null if it stores anything else */
25
+ constValue: number | null;
26
+ addrTaken: number;
27
+ /** uses as the `base` of an `index` node — the only use shape a base lever can re-spell */
28
+ baseUses: number;
29
+ /** every other read */
30
+ otherUses: number;
31
+ /** body-top-level index of the first statement mentioning the name at all */
32
+ firstAt: number | null;
33
+ }
34
+
35
+ /** reads of the name, however spelled */
36
+ export function readsOf(m: Mentions): number {
37
+ return m.baseUses + m.otherUses;
38
+ }
39
+
40
+ const blank = (): Mentions => ({
41
+ assigns: 0,
42
+ topAssignAt: null,
43
+ constValue: null,
44
+ addrTaken: 0,
45
+ baseUses: 0,
46
+ otherUses: 0,
47
+ firstAt: null,
48
+ });
49
+
50
+ /** Visit every node, telling the callback whether it stands as an `index`'s base. */
51
+ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isIndexBase = false): void {
52
+ visit(e, isIndexBase);
53
+ if (e.k === 'index') {
54
+ walkExpr(e.base, visit, true);
55
+ // `lead` — a multidimensional global's LEADING subscripts — is an ordinary value position, so
56
+ // a name mentioned there is a real read. Missing it does not cost a candidate: it lets a lever
57
+ // DELETE a local the body still names.
58
+ for (const l of e.lead ?? []) {
59
+ walkExpr(l, visit, false);
60
+ }
61
+ walkExpr(e.idx, visit, false);
62
+ return;
63
+ }
64
+ for (const c of exprChildren(e)) {
65
+ walkExpr(c, visit, false);
66
+ }
67
+ }
68
+
69
+ /** Every mention of every local, keyed by name. Locals only — a param or a global name is not in
70
+ * the map, and a lever asking about one gets `undefined` rather than a zeroed record. */
71
+ export function localMentions(sfn: SFn): Map<string, Mentions> {
72
+ const t = new Map<string, Mentions>(sfn.locals.map((l) => [l.name, blank()]));
73
+ const seen = (name: string, at: number): Mentions | undefined => {
74
+ const m = t.get(name);
75
+ if (m && m.firstAt === null) {
76
+ m.firstAt = at;
77
+ }
78
+ return m;
79
+ };
80
+ const stmt = (s: Stmt, at: number, top: boolean): void => {
81
+ if (s.k === 'assign') {
82
+ const m = seen(s.name, at);
83
+ if (m) {
84
+ m.assigns++;
85
+ if (top) {
86
+ m.topAssignAt = at;
87
+ m.constValue = s.value.k === 'const' ? s.value.value : null;
88
+ }
89
+ }
90
+ }
91
+ for (const e of stmtExprs(s)) {
92
+ walkExpr(e, (x, isIndexBase) => {
93
+ if (x.k === 'var' || x.k === 'addr') {
94
+ const m = seen(x.name, at);
95
+ if (m) {
96
+ if (x.k === 'addr') {
97
+ m.addrTaken++;
98
+ } else if (isIndexBase) {
99
+ m.baseUses++;
100
+ } else {
101
+ m.otherUses++;
102
+ }
103
+ }
104
+ }
105
+ });
106
+ }
107
+ for (const c of stmtChildren(s)) {
108
+ stmt(c, at, false);
109
+ }
110
+ };
111
+ sfn.body.forEach((s, i) => stmt(s, i, true));
112
+ return t;
113
+ }
@@ -0,0 +1,42 @@
1
+ // L3 re-spelling lever: put the PRODUCT operand first in a commutative `+`.
2
+ //
3
+ // structure.ts's def-order rule spells commutative operands in EVALUATION order, which recovers
4
+ // gcc's left-to-right source order. IDO and mwcc break the correspondence for exactly one shape:
5
+ // in `a*b + c` they SCHEDULE the independent load of `c` above the product's `mflo`/`mullw`, so
6
+ // the machine add reads (c, product) and def order re-spells the source's product-first sum as
7
+ // c-first. Which order the source used is not recoverable from positions there — so this lever
8
+ // emits the product-first sibling and the differ referees (verified byte-identical against IDO
9
+ // on the bg_area row; the def-order spelling stays in the list for sources that really were
10
+ // c-first).
11
+ //
12
+ // SCOPE (decline over approximate): a `+` is flipped only when exactly ONE side is a product
13
+ // (`bin('*')` at the root, casts looked through) — two products or none leave nothing to anchor
14
+ // the flip on. A side carrying an effect (a call, a marker) never moves — evaluation order of
15
+ // the operands is what the lever edits. Declines (null) when no `+` changes, so no duplicate
16
+ // candidate.
17
+ import type { Expr, SFn } from './ast';
18
+ import { exprHasEffect, mapExprChildren, mapStmtExprs } from './ast';
19
+
20
+ const isProduct = (e: Expr): boolean =>
21
+ e.k === 'bin' && e.op === '*' ? true : e.k === 'cast' ? isProduct(e.e) : false;
22
+
23
+ export function mulFirstSums(sfn: SFn): SFn | null {
24
+ let changed = false;
25
+ const rewrite = (e: Expr): Expr => {
26
+ const m = mapExprChildren(e, rewrite);
27
+ if (
28
+ m.k === 'bin' &&
29
+ m.op === '+' &&
30
+ isProduct(m.r) &&
31
+ !isProduct(m.l) &&
32
+ !exprHasEffect(m.l) &&
33
+ !exprHasEffect(m.r)
34
+ ) {
35
+ changed = true;
36
+ return { ...m, l: m.r, r: m.l };
37
+ }
38
+ return m;
39
+ };
40
+ const body = sfn.body.map((s) => mapStmtExprs(s, rewrite));
41
+ return changed ? { ...sfn, body } : null;
42
+ }