@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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- 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
|
+
}
|