@asmlift/core 0.1.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/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// L3 pass: hoist a REUSED pointer base (a global address or a numeric pointer constant) into a
|
|
2
|
+
// typed local pointer.
|
|
3
|
+
//
|
|
4
|
+
// A base indexed at 2+ sites — `((u8 *)&gTable)[i+5]` and `[i+6]`, or the MMIO/RAM constant
|
|
5
|
+
// `((s32 *)0x40000d4)[0]`, `[1]`, `[2]` — re-materialized the address (a fresh pool load) at each
|
|
6
|
+
// access, whereas agbcc loads it ONCE into a register and reuses it (the reference spells this as a
|
|
7
|
+
// local: `u8 *t = gTable; t[i+5]; t[i+6]`). This pass reproduces that register: it hoists the shared
|
|
8
|
+
// base into a local pointer `T *p = (T *)base` and points each access at `p`, so the recompiled code
|
|
9
|
+
// keeps the address in one register instead of reloading it.
|
|
10
|
+
//
|
|
11
|
+
// SCOPE / SOUNDNESS. Only an `index` node whose base is a bare `addr` (a global address) or a bare
|
|
12
|
+
// `const` (a numeric pointer address) is eligible, and only when 2+ such nodes share the SAME
|
|
13
|
+
// (base, width, signedness) — an AGGREGATE base (F9 spells a SCALAR global as a bare `var`, which is
|
|
14
|
+
// never an `index`-of-leaf, so scalar recovery is untouched). Non-leaf bases (a local, a
|
|
15
|
+
// struct-element `p[a0]`, arithmetic) are excluded: agbcc may re-derive those, so hoisting them can
|
|
16
|
+
// MISMATCH (empirically confirmed). The hoisted local carries the access's pointer type, so the
|
|
17
|
+
// deref cast the C backend applied inline at each `index` now lands ONCE on the local's initializer
|
|
18
|
+
// and the accesses stride correctly with no per-use cast. A wrong hoist (a base agbcc would actually
|
|
19
|
+
// re-materialize) only changes recompiled bytes -> a LOST match under the zero-lost gate, never a
|
|
20
|
+
// miscompile: the address value is identical, just held in a different place.
|
|
21
|
+
import { type IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
22
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
23
|
+
import { mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
24
|
+
|
|
25
|
+
// A HOISTABLE base is a bare `addr` (a global address) or a bare `const` (a numeric pointer
|
|
26
|
+
// address). Both are relocation-invariant leaves whose value the compiler keeps in one register
|
|
27
|
+
// when it indexes them at 2+ sites. Anything else (a local var, a struct-element `p[a0]`, arbitrary
|
|
28
|
+
// arithmetic) is NOT — agbcc may re-derive it.
|
|
29
|
+
type HoistableBase = Extract<Expr, { k: 'addr' } | { k: 'const' }>;
|
|
30
|
+
const isHoistableBase = (e: Expr): e is HoistableBase => e.k === 'addr' || e.k === 'const';
|
|
31
|
+
const baseId = (b: HoistableBase): string => (b.k === 'addr' ? `a:${b.name}` : `c:${b.value}`);
|
|
32
|
+
|
|
33
|
+
/** The (base, access-shape) key an `index`-of-hoistable-base shares with its reuse siblings. */
|
|
34
|
+
const keyOf = (base: HoistableBase, width: number, signed: boolean): string => `${baseId(base)} ${width} ${signed}`;
|
|
35
|
+
|
|
36
|
+
interface Collected {
|
|
37
|
+
count: Map<string, number>;
|
|
38
|
+
order: string[];
|
|
39
|
+
meta: Map<string, { base: HoistableBase; width: number; signed: boolean }>;
|
|
40
|
+
/** keys with ANY use inside a loop — disqualified (see the loop note in `hoistReusedGlobalBases`). */
|
|
41
|
+
inLoop: Set<string>;
|
|
42
|
+
/** per key, how many times each CONSTANT offset was accessed. A constant offset touched 2+ times
|
|
43
|
+
* is a SCALAR access at one fixed location (a `*(T*)C |= x` MMIO read-modify-write, or repeated
|
|
44
|
+
* `*p`), which the compiler re-materializes rather than register-holds — hoisting it MISMATCHES
|
|
45
|
+
* (it broke the ProcessHBlankWait match). A key with ANY repeated constant offset is therefore
|
|
46
|
+
* disqualified, even if it ALSO has distinct-offset uses (a mixed scalar+array base). A genuine
|
|
47
|
+
* reused array base touches each constant offset once, or uses a variable index (not tallied). */
|
|
48
|
+
constOffCount: Map<string, Map<number, number>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Every `index` node whose base is a hoistable leaf, tallied by key (for the 2+-reuse test) and in
|
|
52
|
+
* first-appearance order (so the hoisted assignments emit in the order the bases are first used,
|
|
53
|
+
* matching the compiler's pool-load order). `loop` marks uses nested in a while/do-while/for. */
|
|
54
|
+
function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
55
|
+
const visitExpr = (e: Expr, inLoop: boolean): void => {
|
|
56
|
+
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
57
|
+
const k = keyOf(e.base, e.width, e.signed);
|
|
58
|
+
if (!c.count.has(k)) {
|
|
59
|
+
c.order.push(k);
|
|
60
|
+
c.meta.set(k, { base: e.base, width: e.width, signed: e.signed });
|
|
61
|
+
}
|
|
62
|
+
c.count.set(k, (c.count.get(k) ?? 0) + 1);
|
|
63
|
+
if (inLoop) {
|
|
64
|
+
c.inLoop.add(k);
|
|
65
|
+
}
|
|
66
|
+
if (e.idx.k === 'const') {
|
|
67
|
+
const m = c.constOffCount.get(k) ?? c.constOffCount.set(k, new Map()).get(k)!;
|
|
68
|
+
m.set(e.idx.value, (m.get(e.idx.value) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const ch of exprChildrenOf(e)) {
|
|
72
|
+
visitExpr(ch, inLoop);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
for (const s of stmts) {
|
|
76
|
+
// A loop's OWN condition (`stmtExprs` of a while/do-while/for) runs every iteration, so a base
|
|
77
|
+
// there is loop-invariant just like a body use — visit it with `nested`, not the outer flag.
|
|
78
|
+
const nested = loop || s.k === 'while' || s.k === 'dowhile' || s.k === 'for';
|
|
79
|
+
for (const e of stmtExprs(s)) {
|
|
80
|
+
visitExpr(e, nested);
|
|
81
|
+
}
|
|
82
|
+
collect(stmtChildren(s), c, nested);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// local re-export to avoid importing exprChildren twice (mapExprChildren covers rewrite).
|
|
87
|
+
function exprChildrenOf(e: Expr): Expr[] {
|
|
88
|
+
const out: Expr[] = [];
|
|
89
|
+
mapExprChildren(e, (c) => {
|
|
90
|
+
out.push(c);
|
|
91
|
+
return c;
|
|
92
|
+
});
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local. */
|
|
97
|
+
function rewrite(e: Expr, localFor: Map<string, string>): Expr {
|
|
98
|
+
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
99
|
+
const nm = localFor.get(keyOf(e.base, e.width, e.signed));
|
|
100
|
+
if (nm) {
|
|
101
|
+
return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor) };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return mapExprChildren(e, (c) => rewrite(c, localFor));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function rewriteStmt(s: Stmt, localFor: Map<string, string>): Stmt {
|
|
108
|
+
const mapS = (x: Stmt): Stmt => rewriteStmt(x, localFor);
|
|
109
|
+
switch (s.k) {
|
|
110
|
+
case 'assign':
|
|
111
|
+
return { ...s, value: rewrite(s.value, localFor) };
|
|
112
|
+
case 'store':
|
|
113
|
+
return { ...s, lval: rewrite(s.lval, localFor), value: rewrite(s.value, localFor) };
|
|
114
|
+
case 'exprstmt':
|
|
115
|
+
return { ...s, value: rewrite(s.value, localFor) };
|
|
116
|
+
case 'return':
|
|
117
|
+
return s.value ? { ...s, value: rewrite(s.value, localFor) } : s;
|
|
118
|
+
case 'if':
|
|
119
|
+
return { ...s, cond: rewrite(s.cond, localFor), then: s.then.map(mapS), else: s.else.map(mapS) };
|
|
120
|
+
case 'while':
|
|
121
|
+
case 'dowhile':
|
|
122
|
+
return { ...s, cond: rewrite(s.cond, localFor), body: s.body.map(mapS) };
|
|
123
|
+
case 'for':
|
|
124
|
+
return { ...s, init: mapS(s.init), cond: rewrite(s.cond, localFor), inc: mapS(s.inc), body: s.body.map(mapS) };
|
|
125
|
+
case 'switch':
|
|
126
|
+
return {
|
|
127
|
+
...s,
|
|
128
|
+
scrutinee: rewrite(s.scrutinee, localFor),
|
|
129
|
+
cases: s.cases.map((c) => ({ ...c, body: c.body.map(mapS) })),
|
|
130
|
+
default: s.default?.map(mapS),
|
|
131
|
+
};
|
|
132
|
+
case 'break':
|
|
133
|
+
case 'continue':
|
|
134
|
+
return s;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A name not already used by a param/local/global in `sfn`, of the form `p<n>`. */
|
|
139
|
+
function freshName(taken: Set<string>): string {
|
|
140
|
+
let n = 0;
|
|
141
|
+
while (taken.has(`p${n}`)) {
|
|
142
|
+
n++;
|
|
143
|
+
}
|
|
144
|
+
const nm = `p${n}`;
|
|
145
|
+
taken.add(nm);
|
|
146
|
+
return nm;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function hoistReusedGlobalBases(sfn: SFn): SFn {
|
|
150
|
+
const c: Collected = { count: new Map(), order: [], meta: new Map(), inLoop: new Set(), constOffCount: new Map() };
|
|
151
|
+
collect(sfn.body, c, false);
|
|
152
|
+
// A repeated CONSTANT offset means a scalar re-access at a fixed location (MMIO RMW / repeated
|
|
153
|
+
// `*p`) the compiler re-materializes — disqualify the whole base, even mixed with array uses.
|
|
154
|
+
const hasRepeatedConstOffset = (k: string): boolean => {
|
|
155
|
+
for (const n of c.constOffCount.get(k)?.values() ?? []) {
|
|
156
|
+
if (n >= 2) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Reuse 2+ and NOT used inside a loop. A loop-body base is loop-invariant, so the compiler ALSO
|
|
164
|
+
// keeps it in a register across the loop — but hoisting it to the function top forces a
|
|
165
|
+
// callee-saved register that can add prologue push/pop the original avoided, worsening the match
|
|
166
|
+
// (register-pressure matching, not a correctness issue). Straight-line / branch reuse is the safe
|
|
167
|
+
// win; a loop-body base is left inline for a future scope-aware hoist.
|
|
168
|
+
const { count, order, meta } = c;
|
|
169
|
+
const hoisted = order.filter((k) => (count.get(k) ?? 0) >= 2 && !c.inLoop.has(k) && !hasRepeatedConstOffset(k));
|
|
170
|
+
if (hoisted.length === 0) {
|
|
171
|
+
return sfn;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const taken = new Set<string>([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
|
|
175
|
+
// globals are referenced by name; a hoist name must not shadow one that appears in the body.
|
|
176
|
+
collectNames(sfn.body, taken);
|
|
177
|
+
|
|
178
|
+
const localFor = new Map<string, string>();
|
|
179
|
+
const newLocals: { name: string; type: IrType }[] = [];
|
|
180
|
+
const hoistStmts: Stmt[] = [];
|
|
181
|
+
for (const k of hoisted) {
|
|
182
|
+
const m = meta.get(k)!;
|
|
183
|
+
const ptrType = T.ptr(scalarTypeForAccess(m.width, m.signed));
|
|
184
|
+
const nm = freshName(taken);
|
|
185
|
+
localFor.set(k, nm);
|
|
186
|
+
newLocals.push({ name: nm, type: ptrType });
|
|
187
|
+
// `p = (T *)base` — the cast makes the local the access's pointer type so each `p[i]` strides it.
|
|
188
|
+
hoistStmts.push({ k: 'assign', name: nm, value: { k: 'cast', to: ptrType, e: m.base } });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const body = [...hoistStmts, ...sfn.body.map((s) => rewriteStmt(s, localFor))];
|
|
192
|
+
return { ...sfn, body, locals: [...sfn.locals, ...newLocals] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Every `var`/`addr`/called-function name mentioned anywhere in `stmts` (so a hoist name collides
|
|
196
|
+
* with none — a global via `addr`, a local via `var`, OR a callee via `call.fn`). */
|
|
197
|
+
function collectNames(stmts: Stmt[], out: Set<string>): void {
|
|
198
|
+
const walk = (e: Expr): void => {
|
|
199
|
+
if (e.k === 'var' || e.k === 'addr') {
|
|
200
|
+
out.add(e.name);
|
|
201
|
+
}
|
|
202
|
+
if (e.k === 'call') {
|
|
203
|
+
out.add(e.fn); // a hoist local must not shadow a called function symbol
|
|
204
|
+
}
|
|
205
|
+
for (const c of exprChildrenOf(e)) {
|
|
206
|
+
walk(c);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
for (const s of stmts) {
|
|
210
|
+
if (s.k === 'assign') {
|
|
211
|
+
out.add(s.name);
|
|
212
|
+
}
|
|
213
|
+
for (const e of stmtExprs(s)) {
|
|
214
|
+
walk(e);
|
|
215
|
+
}
|
|
216
|
+
collectNames(stmtChildren(s), out);
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/l3/dce.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// L3 readability pass: dead-LOCAL-store elimination + empty-branch simplification.
|
|
2
|
+
//
|
|
3
|
+
// asmlift's SSA-destruction (structure.ts) can emit an assignment to a synthesized local whose
|
|
4
|
+
// value is never read afterwards — a merge/phi copy the structurer materialized for a value that
|
|
5
|
+
// turns out dead (classically, both arms of an `if` write a merge local that nothing downstream
|
|
6
|
+
// reads). The compiler DCEs these, so they never affect the MATCH, but they clutter the source:
|
|
7
|
+
// if ((v0 - 1) << 16 != 0) { v0 = (v0 - 1) << 16; } else { flag = 1; v0 = 1; }
|
|
8
|
+
// with the two dead `v0 = …` gone the inner `if` has an empty then-arm, which flips to the clean
|
|
9
|
+
// if ((v0 - 1) << 16 == 0) { flag = 1; }
|
|
10
|
+
//
|
|
11
|
+
// SOUNDNESS. This only ever REMOVES a statement (or flips a branch whose semantics a compiler
|
|
12
|
+
// normalizes identically), so it can never invent a false match: a wrongly-removed live store
|
|
13
|
+
// changes the recompiled bytes and shows up as a LOST match under the benchmark's zero-lost gate.
|
|
14
|
+
// The liveness below is a CONSERVATIVE over-approximation (loops/switches treat every in-scope
|
|
15
|
+
// read as live throughout), so a removal only happens when the local is provably dead. Only
|
|
16
|
+
// names in `locals` are eligible — globals (side effects, referenced by name from headers) and
|
|
17
|
+
// params are never touched — and a value carrying a side effect / gap signal / memory load is
|
|
18
|
+
// never dropped (see `mustKeep`). Locals are never address-taken in L3 (`addr` names a global),
|
|
19
|
+
// so `var` reads are a COMPLETE account of a local's uses.
|
|
20
|
+
//
|
|
21
|
+
// Ordering: structureChecked runs `assertResolved` BEFORE this pass, so in strict mode an
|
|
22
|
+
// unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
|
|
23
|
+
// keep is defense-in-depth for any future caller that skips that check.
|
|
24
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
25
|
+
import { exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
26
|
+
|
|
27
|
+
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
28
|
+
* `addr` node names a global, not a local, so it is not a local read. */
|
|
29
|
+
function readsInto(e: Expr, out: Set<string>): void {
|
|
30
|
+
if (e.k === 'var') {
|
|
31
|
+
out.add(e.name);
|
|
32
|
+
}
|
|
33
|
+
for (const c of exprChildren(e)) {
|
|
34
|
+
readsInto(c, out);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function reads(e: Expr): Set<string> {
|
|
39
|
+
const s = new Set<string>();
|
|
40
|
+
readsInto(e, s);
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True if `e` carries any reason NOT to speculatively delete its assignment/condition:
|
|
45
|
+
* - `call` — may write memory/globals (a side effect);
|
|
46
|
+
* - `marker` — the annotate-mode ASMLIFT_ERROR gap signal, which must survive so the gap stays loud;
|
|
47
|
+
* - the strict-mode `?` unresolved sentinel (`{k:'var', name:'?'}`) — dropping it would let a
|
|
48
|
+
* value asmlift could NOT lift slip past `assertResolved`, silently downgrading a loud gap;
|
|
49
|
+
* - a memory load (`index`/`field`) — asmlift models no `volatile`, so a possibly-effectful read
|
|
50
|
+
* is never deleted (this pass never removes a memory access).
|
|
51
|
+
* A dead assignment whose value contains any of these is kept. */
|
|
52
|
+
function mustKeep(e: Expr): boolean {
|
|
53
|
+
if (e.k === 'call' || e.k === 'marker' || e.k === 'index' || e.k === 'field') {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (e.k === 'var' && e.name === '?') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return exprChildren(e).some(mustKeep);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Every local read anywhere within these statements (exprs + nested statements). Used to give
|
|
63
|
+
* loops/switches a conservative live-out so a store read on another iteration/case is never cut. */
|
|
64
|
+
function allReadsInto(stmts: Stmt[], out: Set<string>): void {
|
|
65
|
+
for (const s of stmts) {
|
|
66
|
+
for (const e of stmtExprs(s)) {
|
|
67
|
+
readsInto(e, out);
|
|
68
|
+
}
|
|
69
|
+
allReadsInto(stmtChildren(s), out);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Negate a condition, flipping a relational operator directly (`!= → ==`, `< → >=`, …) so an
|
|
74
|
+
* empty-then flip reads cleanly; anything else wraps in `!( … )`. Both forms are semantically
|
|
75
|
+
* exact over C's total integer order. */
|
|
76
|
+
function negate(cond: Expr): Expr {
|
|
77
|
+
if (cond.k === 'bin') {
|
|
78
|
+
const table: Record<string, '==' | '!=' | '<' | '<=' | '>' | '>='> = {
|
|
79
|
+
'==': '!=',
|
|
80
|
+
'!=': '==',
|
|
81
|
+
'<': '>=',
|
|
82
|
+
'>=': '<',
|
|
83
|
+
'>': '<=',
|
|
84
|
+
'<=': '>',
|
|
85
|
+
};
|
|
86
|
+
const f = table[cond.op];
|
|
87
|
+
if (f) {
|
|
88
|
+
return { k: 'bin', op: f, l: cond.l, r: cond.r };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { k: 'un', op: '!', e: cond };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
95
|
+
* returns the rewritten block and the set live on entry. */
|
|
96
|
+
function dceBlock(
|
|
97
|
+
stmts: Stmt[],
|
|
98
|
+
liveOut: ReadonlySet<string>,
|
|
99
|
+
locals: ReadonlySet<string>,
|
|
100
|
+
): { out: Stmt[]; liveIn: Set<string> } {
|
|
101
|
+
const live = new Set(liveOut);
|
|
102
|
+
const rev: Stmt[] = [];
|
|
103
|
+
const setLive = (next: Set<string>) => {
|
|
104
|
+
live.clear();
|
|
105
|
+
for (const n of next) {
|
|
106
|
+
live.add(n);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
for (let i = stmts.length - 1; i >= 0; i--) {
|
|
110
|
+
const s = stmts[i];
|
|
111
|
+
switch (s.k) {
|
|
112
|
+
case 'assign': {
|
|
113
|
+
if (locals.has(s.name) && !live.has(s.name) && !mustKeep(s.value)) {
|
|
114
|
+
continue; // dead local store — drop it; liveness is unchanged (it was a no-op)
|
|
115
|
+
}
|
|
116
|
+
live.delete(s.name); // the write kills the name for statements before it …
|
|
117
|
+
for (const r of reads(s.value)) {
|
|
118
|
+
live.add(r); // … then its own reads (incl. a self-reference like v = v - 1) are live
|
|
119
|
+
}
|
|
120
|
+
rev.push(s);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case 'store': {
|
|
124
|
+
for (const r of reads(s.lval)) {
|
|
125
|
+
live.add(r);
|
|
126
|
+
}
|
|
127
|
+
for (const r of reads(s.value)) {
|
|
128
|
+
live.add(r);
|
|
129
|
+
}
|
|
130
|
+
rev.push(s);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'exprstmt': {
|
|
134
|
+
for (const r of reads(s.value)) {
|
|
135
|
+
live.add(r);
|
|
136
|
+
}
|
|
137
|
+
rev.push(s);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'return': {
|
|
141
|
+
if (s.value) {
|
|
142
|
+
for (const r of reads(s.value)) {
|
|
143
|
+
live.add(r);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
rev.push(s);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'break':
|
|
150
|
+
case 'continue': {
|
|
151
|
+
rev.push(s);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'if': {
|
|
155
|
+
const t = dceBlock(s.then, live, locals);
|
|
156
|
+
const e = dceBlock(s.else, live, locals);
|
|
157
|
+
const nlive = new Set<string>();
|
|
158
|
+
for (const r of reads(s.cond)) {
|
|
159
|
+
nlive.add(r);
|
|
160
|
+
}
|
|
161
|
+
for (const r of t.liveIn) {
|
|
162
|
+
nlive.add(r);
|
|
163
|
+
}
|
|
164
|
+
for (const r of e.liveIn) {
|
|
165
|
+
nlive.add(r);
|
|
166
|
+
}
|
|
167
|
+
setLive(nlive);
|
|
168
|
+
if (t.out.length === 0 && e.out.length === 0) {
|
|
169
|
+
// both arms empty: keep only if the condition itself has a side effect
|
|
170
|
+
if (mustKeep(s.cond)) {
|
|
171
|
+
rev.push({ k: 'exprstmt', value: s.cond });
|
|
172
|
+
}
|
|
173
|
+
} else if (t.out.length === 0) {
|
|
174
|
+
rev.push({ k: 'if', cond: negate(s.cond), then: e.out, else: [] });
|
|
175
|
+
} else {
|
|
176
|
+
rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case 'while':
|
|
181
|
+
case 'dowhile': {
|
|
182
|
+
// Conservative: any local read anywhere in the loop is live throughout it, so a
|
|
183
|
+
// loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
|
|
184
|
+
const loopLive = new Set(live);
|
|
185
|
+
allReadsInto([s], loopLive);
|
|
186
|
+
const b = dceBlock(s.body, loopLive, locals);
|
|
187
|
+
const nlive = new Set(loopLive);
|
|
188
|
+
for (const r of b.liveIn) {
|
|
189
|
+
nlive.add(r);
|
|
190
|
+
}
|
|
191
|
+
setLive(nlive);
|
|
192
|
+
rev.push({ ...s, body: b.out });
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'for': {
|
|
196
|
+
const loopLive = new Set(live);
|
|
197
|
+
allReadsInto([s], loopLive);
|
|
198
|
+
const b = dceBlock(s.body, loopLive, locals);
|
|
199
|
+
const nlive = new Set(loopLive);
|
|
200
|
+
for (const r of b.liveIn) {
|
|
201
|
+
nlive.add(r);
|
|
202
|
+
}
|
|
203
|
+
setLive(nlive);
|
|
204
|
+
rev.push({ ...s, body: b.out }); // init/inc left intact (never DCE'd)
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 'switch': {
|
|
208
|
+
// Conservative: fall-through makes a case's live-out include later cases, so treat every
|
|
209
|
+
// read anywhere in the switch as live throughout — no case-body store is ever cut.
|
|
210
|
+
const swLive = new Set(live);
|
|
211
|
+
allReadsInto([s], swLive);
|
|
212
|
+
const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals).out }));
|
|
213
|
+
const def = s.default ? dceBlock(s.default, swLive, locals).out : s.default;
|
|
214
|
+
const nlive = new Set(swLive);
|
|
215
|
+
for (const r of reads(s.scrutinee)) {
|
|
216
|
+
nlive.add(r);
|
|
217
|
+
}
|
|
218
|
+
setLive(nlive);
|
|
219
|
+
rev.push({ ...s, cases, default: def });
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
default: {
|
|
223
|
+
// Exhaustiveness guard: a new Stmt kind must be handled here explicitly, never silently
|
|
224
|
+
// dropped (matches the l3/ast.ts "exhaustive under noImplicitReturns" walker discipline).
|
|
225
|
+
const _never: never = s;
|
|
226
|
+
throw new Error(`dce: unhandled statement kind ${(_never as Stmt).k}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
rev.reverse();
|
|
231
|
+
return { out: rev, liveIn: live };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Every local name still referenced (read or assigned) in the final body — used to prune local
|
|
235
|
+
* declarations that became unused after DCE. */
|
|
236
|
+
function referencedNames(stmts: Stmt[], out: Set<string>): void {
|
|
237
|
+
for (const s of stmts) {
|
|
238
|
+
if (s.k === 'assign') {
|
|
239
|
+
out.add(s.name);
|
|
240
|
+
}
|
|
241
|
+
for (const e of stmtExprs(s)) {
|
|
242
|
+
readsInto(e, out);
|
|
243
|
+
}
|
|
244
|
+
referencedNames(stmtChildren(s), out);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Remove dead local stores and simplify the branches they empty out, then drop any local
|
|
249
|
+
* declaration left unreferenced. Returns a new SFn; the input is not mutated. */
|
|
250
|
+
export function eliminateDeadStores(sfn: SFn): SFn {
|
|
251
|
+
const locals = new Set(sfn.locals.map((l) => l.name));
|
|
252
|
+
const body = dceBlock(sfn.body, new Set<string>(), locals).out;
|
|
253
|
+
const used = new Set<string>();
|
|
254
|
+
referencedNames(body, used);
|
|
255
|
+
return { ...sfn, body, locals: sfn.locals.filter((l) => used.has(l.name)) };
|
|
256
|
+
}
|