@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/package.json +14 -0
  4. package/src/backend/c.ts +20 -0
  5. package/src/backend/cfamily.ts +352 -0
  6. package/src/backend/cpp.ts +145 -0
  7. package/src/backend/pascal.ts +279 -0
  8. package/src/contracts.ts +131 -0
  9. package/src/detect.ts +12 -0
  10. package/src/frontend/asmdata.ts +170 -0
  11. package/src/frontend/disasm.ts +102 -0
  12. package/src/frontend/emit.ts +57 -0
  13. package/src/frontend/errors.ts +14 -0
  14. package/src/frontend/format.ts +47 -0
  15. package/src/frontend/frontend.ts +22 -0
  16. package/src/frontend/mips.ts +875 -0
  17. package/src/frontend/opaque.ts +82 -0
  18. package/src/frontend/ppc.ts +990 -0
  19. package/src/frontend/registry.ts +34 -0
  20. package/src/frontend/ssa.ts +214 -0
  21. package/src/frontend/thumb.ts +1419 -0
  22. package/src/ir/core.ts +104 -0
  23. package/src/ir/opcodes.ts +143 -0
  24. package/src/ir/parse.ts +221 -0
  25. package/src/ir/print.ts +77 -0
  26. package/src/ir/types.ts +106 -0
  27. package/src/ir/verify.ts +221 -0
  28. package/src/l3/ast.ts +301 -0
  29. package/src/l3/basecse.ts +218 -0
  30. package/src/l3/dce.ts +256 -0
  31. package/src/l3/regspell.ts +331 -0
  32. package/src/l3/reindex.ts +447 -0
  33. package/src/l3/typing.ts +145 -0
  34. package/src/mangle.ts +135 -0
  35. package/src/pattern/engine.ts +392 -0
  36. package/src/pipeline.ts +272 -0
  37. package/src/proto.ts +42 -0
  38. package/src/raise/arrays.ts +84 -0
  39. package/src/raise/const.ts +52 -0
  40. package/src/raise/errors.ts +10 -0
  41. package/src/raise/magicdiv.ts +386 -0
  42. package/src/raise/pre-recovery.ts +71 -0
  43. package/src/raise/recover.ts +215 -0
  44. package/src/raise/retsink.ts +72 -0
  45. package/src/raise/shortcircuit.ts +207 -0
  46. package/src/raise/softdiv.ts +62 -0
  47. package/src/raise/struct-arrays.ts +257 -0
  48. package/src/raise/structs.ts +223 -0
  49. package/src/rank.ts +208 -0
  50. package/src/structure/analysis.ts +410 -0
  51. package/src/structure/hazards.ts +142 -0
  52. package/src/structure/loops.ts +169 -0
  53. package/src/structure/structure.ts +1726 -0
  54. package/src/structure/switch-recover.ts +410 -0
  55. package/src/target.ts +140 -0
  56. 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
+ }