@asmlift/core 0.5.0 → 0.7.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 +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -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 +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -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/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- 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 +176 -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 +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- 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 +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -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 +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/l3/coalesce.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
|
+
// THE OTHER COALESCER is `structure/namecoalesce.ts`, and the dividing line is worth stating: it
|
|
2
|
+
// merges the SOURCE AND DESTINATION OF A COPY, which is copy coalescing; this one merges two
|
|
3
|
+
// UNRELATED locals whose spans are disjoint, which is register reuse. Neither subsumes the other —
|
|
4
|
+
// a copy pair overlaps by construction, so `overlap` below rejects every candidate that one takes.
|
|
5
|
+
//
|
|
6
|
+
// TWO admission paths live here, each with its own gate table and its own reading of loops. The
|
|
7
|
+
// SPAN path (COALESCE_GATES) proves disjoint liveness from preorder position, so it asks which
|
|
8
|
+
// loops RE-RUN a mention of each local and refuses a pair only when one loop holds both. The
|
|
9
|
+
// ARM-DISJOINT path (ARM_DISJOINT_GATES) proves the two never coexist because one `if` picks
|
|
10
|
+
// between them, so it asks only whether ANY loop encloses that `if` — a second entry breaks the
|
|
11
|
+
// argument however the arms' own loops relate. `coalesceCandidates` offers both.
|
|
1
12
|
import { typeToString } from '../ir/types';
|
|
2
13
|
import type { Expr, SFn, Stmt } from './ast';
|
|
3
14
|
import { exprChildren, mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
4
15
|
import { type Gate, firstRejection } from './gates';
|
|
5
16
|
|
|
17
|
+
/** THE loop-kind test, shared by both admission paths in this file — the span model's enclosure
|
|
18
|
+
* walk and the arm path's `visit`. */
|
|
19
|
+
const isLoop = (s: Stmt): boolean => s.k === 'while' || s.k === 'dowhile' || s.k === 'for';
|
|
20
|
+
|
|
6
21
|
function namesIn(e: Expr, out: Set<string>): void {
|
|
7
|
-
// `addr` names a GLOBAL
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
22
|
+
// `addr` names a GLOBAL (`&gSym`) or a LOCAL — the structurer renders an `laddr` frame object
|
|
23
|
+
// as `&sp0`, an addr node over a name that IS in `sfn.locals`. Both are collected, because a
|
|
24
|
+
// name mentioned only through `&` still has a live range: a span that ignored its `addr`
|
|
25
|
+
// mentions would be SHORT, and a short span is a clobber where a long one is only a missed
|
|
26
|
+
// merge. Collecting a global name costs nothing — it is not in `sfn.locals`, so no pair is
|
|
27
|
+
// ever built for it.
|
|
28
|
+
//
|
|
29
|
+
// `rename` DISAGREES WITH THIS, and knowingly: it rewrites only `var` leaves, so a local
|
|
30
|
+
// absorbed while mentioned through `&` leaves that mention standing against a declaration the
|
|
31
|
+
// merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
|
|
32
|
+
// cleanup — so today's behaviour is pinned exactly in coalesce.test.ts ('rename') rather than
|
|
33
|
+
// repaired here.
|
|
11
34
|
if (e.k === 'var' || e.k === 'addr') out.add(e.name);
|
|
12
35
|
for (const c of exprChildren(e)) namesIn(c, out);
|
|
13
36
|
}
|
|
@@ -21,39 +44,116 @@ function mentions(e: Expr, n: string): boolean {
|
|
|
21
44
|
export interface Span {
|
|
22
45
|
first: number;
|
|
23
46
|
last: number;
|
|
24
|
-
|
|
47
|
+
/** every loop that RE-RUNS a mention of the local — the whole ancestor chain, not just the
|
|
48
|
+
* innermost, because an outer loop re-runs an inner one's statements too. A `for`'s init is
|
|
49
|
+
* not re-run by its own loop, so it contributes only the enclosing ones. */
|
|
50
|
+
loops: Set<Stmt>;
|
|
25
51
|
constFed: boolean;
|
|
26
52
|
/** the local's FIRST mention is a write, not a read */
|
|
27
53
|
firstIsWrite: boolean;
|
|
28
54
|
}
|
|
55
|
+
/** The INDUCTION VARIABLE a `for` drives: the local its init writes and whose step computes the
|
|
56
|
+
* next value from the CURRENT one, arithmetically. Those two writes are the variable's own
|
|
57
|
+
* definition and its own history — not a feed from somewhere the compiler had a reason to
|
|
58
|
+
* respect, which is what `const-fed` reads every non-const assign as.
|
|
59
|
+
*
|
|
60
|
+
* Both halves of the step test are load-bearing, and neither is what the pipeline's `for`
|
|
61
|
+
* producers check — `recognizeForLoops` (structure/structure.ts) admits ANY self-referencing step,
|
|
62
|
+
* and l3/reindex.ts's two walk rewrites mint `iv = iv + 1`, so this predicate is implied by all
|
|
63
|
+
* three rather than trusting any of them; a `for` reaching here may be a walk, not a count. `a = *p` (no self-read) and `p = p->next` / `a = tab[a]` (a self-read
|
|
64
|
+
* that is still a MEMORY read every iteration) are both locals `const-fed` exists to refuse; only
|
|
65
|
+
* arithmetic over the variable is its own history. The read must be a `var`: `mentions` counts an
|
|
66
|
+
* `addr` too, and `&a` is not a read of `a`. */
|
|
67
|
+
const readsVarArithmetically = (e: Expr, n: string): boolean => {
|
|
68
|
+
if (e.k === 'index' || e.k === 'field' || e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return (e.k === 'var' && e.name === n) || exprChildren(e).some((c) => readsVarArithmetically(c, n));
|
|
72
|
+
};
|
|
73
|
+
const stepIsArithmetic = (e: Expr, n: string): boolean => readsVarArithmetically(e, n) && !hasMemoryRead(e);
|
|
74
|
+
const hasMemoryRead = (e: Expr): boolean =>
|
|
75
|
+
e.k === 'index' || e.k === 'field' || e.k === 'call' || e.k === 'marker' || exprChildren(e).some(hasMemoryRead);
|
|
76
|
+
const forInductionVar = (s: Extract<Stmt, { k: 'for' }>): string | null =>
|
|
77
|
+
s.init.k === 'assign' &&
|
|
78
|
+
s.inc.k === 'assign' &&
|
|
79
|
+
s.init.name === s.inc.name &&
|
|
80
|
+
stepIsArithmetic(s.inc.value, s.inc.name)
|
|
81
|
+
? s.init.name
|
|
82
|
+
: null;
|
|
83
|
+
|
|
84
|
+
/** Is this local's declaration carrying a `volatile` qualifier of either kind — the object itself,
|
|
85
|
+
* or its pointee? THE one spelling of the question both gate tables' `volatile` rule asks, kept
|
|
86
|
+
* shared while the two rules stay separate objects: `typeToString` spells neither qualifier, so a
|
|
87
|
+
* path that asked only about one would let a qualified local absorb into a plain one. */
|
|
88
|
+
const isVolatileLocal = (l: SFn['locals'][number]): boolean => l.volatile === true || l.pointeeVolatile === true;
|
|
89
|
+
|
|
29
90
|
function spans(body: Stmt[]): Map<string, Span> {
|
|
30
91
|
const out = new Map<string, Span>();
|
|
31
92
|
let at = 0;
|
|
32
|
-
|
|
93
|
+
/** the statement's OWN mentions — an assign target, a condition, a scrutinee — at its own
|
|
94
|
+
* position, inside the loops it runs under */
|
|
95
|
+
const record = (s: Stmt, loops: readonly Stmt[], inductionVar: string | null = null): void => {
|
|
96
|
+
at++;
|
|
97
|
+
const here = new Set<string>();
|
|
98
|
+
if (s.k === 'assign') here.add(s.name);
|
|
99
|
+
for (const e of stmtExprs(s)) namesIn(e, here);
|
|
100
|
+
for (const n of here) {
|
|
101
|
+
const sp = out.get(n) ?? {
|
|
102
|
+
first: at,
|
|
103
|
+
last: at,
|
|
104
|
+
loops: new Set<Stmt>(),
|
|
105
|
+
constFed: true,
|
|
106
|
+
// an assign that ALSO READS the name (`b = g(b)`) is not a pure write; treating it as one
|
|
107
|
+
// let `g` receive the absorbed value
|
|
108
|
+
firstIsWrite: s.k === 'assign' && s.name === n && !stmtExprs(s).some((e) => mentions(e, n)),
|
|
109
|
+
};
|
|
110
|
+
sp.last = at;
|
|
111
|
+
for (const l of loops) {
|
|
112
|
+
sp.loops.add(l);
|
|
113
|
+
}
|
|
114
|
+
if (s.k === 'assign' && s.name === n && s.value.k !== 'const' && n !== inductionVar) sp.constFed = false;
|
|
115
|
+
out.set(n, sp);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/** the loop set a while/do-while's own condition and children run under — its own. A `for` is
|
|
119
|
+
* routed separately: `stmtChildren` hands back its init too, and the init is the one child its
|
|
120
|
+
* loop does not re-run. */
|
|
121
|
+
const under = (s: Stmt, loops: readonly Stmt[]): readonly Stmt[] => (isLoop(s) ? [...loops, s] : loops);
|
|
122
|
+
const walk = (list: Stmt[], loops: readonly Stmt[]): void => {
|
|
33
123
|
for (const s of list) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
124
|
+
// A `for`'s INIT runs ONCE, ahead of the condition and outside the loop; its cond, inc and
|
|
125
|
+
// body run per iteration. Walking the init first is what makes `for (i = *p; …)` read as a
|
|
126
|
+
// local whose first mention is a WRITE, which is what it is.
|
|
127
|
+
if (s.k === 'for') {
|
|
128
|
+
const inductionVar = forInductionVar(s);
|
|
129
|
+
// The INIT runs once, so the `for` does not re-run it — but only an ASSIGN there is a
|
|
130
|
+
// shape this case places directly. Anything else (a loop, an `if`) goes through the generic
|
|
131
|
+
// walk, which places it and its children by its own kind; `forInductionVar` requires an
|
|
132
|
+
// assign init, so `counter` is null on that path anyway. The INC mirrors it, where the
|
|
133
|
+
// routing is uniformity rather than coverage: an inc's statements sit inside the `for`,
|
|
134
|
+
// which is already in every span the pair could share.
|
|
135
|
+
if (s.init.k === 'assign') {
|
|
136
|
+
record(s.init, loops, inductionVar);
|
|
137
|
+
} else {
|
|
138
|
+
walk([s.init], loops);
|
|
139
|
+
}
|
|
140
|
+
const inner = [...loops, s];
|
|
141
|
+
record(s, inner);
|
|
142
|
+
walk(s.body, inner);
|
|
143
|
+
if (s.inc.k === 'assign') {
|
|
144
|
+
record(s.inc, inner, inductionVar);
|
|
145
|
+
} else {
|
|
146
|
+
walk([s.inc], inner);
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
52
149
|
}
|
|
53
|
-
|
|
150
|
+
// a while/do-while CONDITION re-runs with the body, so it counts as inside its own loop
|
|
151
|
+
const inner = under(s, loops);
|
|
152
|
+
record(s, inner);
|
|
153
|
+
walk(stmtChildren(s), inner);
|
|
54
154
|
}
|
|
55
155
|
};
|
|
56
|
-
walk(body,
|
|
156
|
+
walk(body, []);
|
|
57
157
|
return out;
|
|
58
158
|
}
|
|
59
159
|
function rename(body: Stmt[], from: string, to: string): Stmt[] {
|
|
@@ -89,20 +189,36 @@ export interface MergePair {
|
|
|
89
189
|
y: Span;
|
|
90
190
|
sameType: boolean;
|
|
91
191
|
eitherIsParam: boolean;
|
|
192
|
+
/** either local is object-volatile or carries a pointee-volatile qualifier — typeToString
|
|
193
|
+
* spells neither, so `sameType` alone lets a qualified local absorb into a plain one */
|
|
194
|
+
eitherIsVolatile: boolean;
|
|
195
|
+
/** some loop holds a mention of BOTH locals */
|
|
196
|
+
sharesLoop: boolean;
|
|
92
197
|
}
|
|
93
198
|
|
|
94
199
|
/** The admission rules, in evaluation order. Two arguments the `why` fields have no room for:
|
|
95
200
|
*
|
|
96
|
-
* WHAT `loop` BUYS is the right to read preorder statement order as liveness. Preorder is a
|
|
97
|
-
* topological order of the CFG except where a later-indexed statement
|
|
98
|
-
* one
|
|
99
|
-
*
|
|
100
|
-
* the
|
|
201
|
+
* WHAT `shared-loop` BUYS is the right to read preorder statement order as liveness. Preorder is a
|
|
202
|
+
* topological order of the CFG except where a back edge runs a later-indexed statement before an
|
|
203
|
+
* earlier one — and a back edge returns only to the head of its OWN loop, whose body is one
|
|
204
|
+
* contiguous preorder range. So the reordering can reach a PAIR only when some loop holds a
|
|
205
|
+
* mention of both locals: there the survivor's write can be followed, on the next iteration, by
|
|
206
|
+
* the absorbed local's read. Where no loop holds both — two sibling loops, or one local living
|
|
207
|
+
* before the loop the other lives in — no back edge connects the two ranges and preorder IS
|
|
208
|
+
* execution order. The ancestor chain is what the span records, not the innermost loop: an outer
|
|
209
|
+
* loop re-runs an inner one's statements, so it can reorder a pair that no inner loop shares.
|
|
101
210
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* candidate
|
|
211
|
+
* `first-is-write` AND `const-fed` ARE INDEPENDENT, and neither masks the other: a survivor whose
|
|
212
|
+
* every feed is a constant can still be first MENTIONED by a read, and ablating `first-is-write`
|
|
213
|
+
* alone then offers the merge (coalesce.test.ts pins exactly that program). What `const-fed`
|
|
214
|
+
* bounds is candidate GROWTH: merges go as `L(L-1)/2` in the local count, each a distinct compile.
|
|
215
|
+
* On a loop-heavy function it is what bounds them, because `shared-loop` refuses only pairs a back
|
|
216
|
+
* edge can reorder: klonoa's LoadBGTilemapData declares 43 locals — 1806 ordered pairs — and
|
|
217
|
+
* `const-fed` is what keeps three of them (ablate it and the span path offers 273). A rule
|
|
218
|
+
* refusing every in-loop local would make that bound redundant; this one does not, so any further
|
|
219
|
+
* relaxation of `const-fed` is a multiplier, and two call sites pay it (`/coalesce` and
|
|
220
|
+
* `/scopebase/coalesce`; the `/livebase` pairings enumerate the ARM path and pay
|
|
221
|
+
* ARM_DISJOINT_GATES' `arm-init` instead). */
|
|
106
222
|
export const COALESCE_GATES: readonly Gate<MergePair>[] = [
|
|
107
223
|
{
|
|
108
224
|
id: 'param',
|
|
@@ -117,15 +233,26 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
|
|
|
117
233
|
rejects: (c) => !c.sameType,
|
|
118
234
|
},
|
|
119
235
|
{
|
|
120
|
-
id: '
|
|
121
|
-
why: 'a
|
|
236
|
+
id: 'volatile',
|
|
237
|
+
why: 'a `volatile` qualifier, on the variable or on what it points to, is observable, and merging would drop or add it',
|
|
122
238
|
sound: true,
|
|
123
|
-
guardedBy: 'coalesce
|
|
124
|
-
rejects: (c) => c.
|
|
239
|
+
guardedBy: 'coalesce.test.ts: a volatile pair never merges',
|
|
240
|
+
rejects: (c) => c.eitherIsVolatile,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'shared-loop',
|
|
244
|
+
why: 'a back edge of a loop holding both locals re-runs the absorbed read after the survivor is written',
|
|
245
|
+
sound: true,
|
|
246
|
+
// The ablation sweep proves this gate is load-bearing WHILE it is in the table; it is blind to
|
|
247
|
+
// the two ways the rule can go wrong from here — a relaxation, and an outright deletion, which
|
|
248
|
+
// would simply drop the gate from the table the sweep iterates. Both land on the arm named
|
|
249
|
+
// below, which checks what the pass still emits.
|
|
250
|
+
guardedBy: 'coalesce-fuzz.test.ts: no candidate the pass emits changes a DEFINED read',
|
|
251
|
+
rejects: (c) => c.sharesLoop,
|
|
125
252
|
},
|
|
126
253
|
{
|
|
127
254
|
id: 'const-fed',
|
|
128
|
-
why: 'a load
|
|
255
|
+
why: 'a local set from a memory load, other than a `for` loop counter, is one the compiler had a reason to keep where it was',
|
|
129
256
|
sound: false,
|
|
130
257
|
rejects: (c) => !c.x.constFed || !c.y.constFed,
|
|
131
258
|
},
|
|
@@ -138,29 +265,285 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
|
|
|
138
265
|
},
|
|
139
266
|
{
|
|
140
267
|
id: 'first-is-write',
|
|
141
|
-
why: 'a survivor first
|
|
268
|
+
why: 'a survivor whose first mention is a read would see the absorbed value there',
|
|
142
269
|
sound: false,
|
|
143
270
|
rejects: (c) => !c.y.firstIsWrite,
|
|
144
271
|
},
|
|
145
272
|
];
|
|
146
273
|
|
|
147
|
-
/** Every legal single merge, each as its own tree — NOT one committed
|
|
274
|
+
/** Every legal single merge, each as its own tree — NOT one committed decision.
|
|
148
275
|
*
|
|
149
276
|
* Which pair a register allocator coalesced is not derivable from the L3 tree, and first-fit gets
|
|
150
277
|
* it wrong. Run kleod:UpdateHUDCounterDisplay's published repro script (results.json carries it)
|
|
151
278
|
* and read the candidate table: of its two legal merges, one scores WORSE than not merging at all
|
|
152
279
|
* and declaration order is the one that picks it. Emitting no merges at all costs that row its
|
|
153
280
|
* match, which is what guards this file. `rank.ts` already has the idiom for exactly this —
|
|
154
|
-
* `/regcopy`'s "the tail
|
|
281
|
+
* `/regcopy`'s "the tail decision is allocator-ambiguous, so both are ranked" — so every candidate is
|
|
155
282
|
* emitted and the differ referees.
|
|
156
283
|
*
|
|
157
284
|
* ACCEPTED, NOT FIXED: a survivor assigned only on SOME paths still absorbs the other's value on
|
|
158
|
-
* the paths that skip it. The original read an uninitialized local there, so both spellings are
|
|
285
|
+
* the paths that skip it — a loop that runs zero iterations is one such path. The original read an uninitialized local there, so both spellings are
|
|
159
286
|
* ill-defined rather than one being wrong — but this is a real difference and the differ, not any
|
|
160
287
|
* gate, is what keeps it from faking a match. The fuzz asserts it stays reachable, so the carve-out
|
|
161
288
|
* that excuses it cannot quietly become dead. */
|
|
162
289
|
export function coalesceCandidates(sfn: SFn): { merged: string; sfn: SFn }[] {
|
|
163
|
-
|
|
290
|
+
const { candidates } = coalesceUnder(COALESCE_GATES, sfn);
|
|
291
|
+
const seen = new Set(candidates.map((c) => c.merged));
|
|
292
|
+
for (const c of armDisjointCandidates(sfn)) {
|
|
293
|
+
if (!seen.has(c.merged)) {
|
|
294
|
+
candidates.push(c);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return candidates;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The survivor's declaration list after `gone` is absorbed into `kept`.
|
|
301
|
+
*
|
|
302
|
+
* The merged local's declaration is dropped, so any attribute on it would be lost — and one of
|
|
303
|
+
* them is load-bearing: `slots`, the `[sp,#k]`s the machine homed it at (ir/core.ts `SlotHomes`).
|
|
304
|
+
* The survivor takes the UNION and chooses nothing: a merged pair can reproduce at most one slot,
|
|
305
|
+
* but WHICH of the two is the earlier declaration rank depends on the frame's direction, and this
|
|
306
|
+
* function is handed a locals list with no target in it. `l3/slotorder.ts` reduces, once, where
|
|
307
|
+
* the direction is in hand. Neither homed ⇒ no slots: the merge invents nothing. */
|
|
308
|
+
function localsAfterMerge(locals: SFn['locals'], gone: string, kept: string): SFn['locals'] {
|
|
309
|
+
const goneSlots = locals.find((l) => l.name === gone)?.slots;
|
|
310
|
+
return locals
|
|
311
|
+
.filter((l) => l.name !== gone)
|
|
312
|
+
.map((l) => {
|
|
313
|
+
if (l.name !== kept || goneSlots === undefined) {
|
|
314
|
+
return l;
|
|
315
|
+
}
|
|
316
|
+
return { ...l, slots: [...new Set([...(l.slots ?? []), ...goneSlots])].sort((x, y) => x - y) };
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** One candidate ARM-DISJOINT merge: every mention of `a` inside one arm of a single `if`, every
|
|
321
|
+
* mention of `b` inside the other. There is no param gate to mirror from the span table because
|
|
322
|
+
* the enumeration refuses params structurally: pair members come from `sfn.locals` only, and a
|
|
323
|
+
* local that SHADOWS a param name is excluded too (see `confined`) — rename() must never touch
|
|
324
|
+
* a param's mentions. */
|
|
325
|
+
export interface ArmPair {
|
|
326
|
+
a: string;
|
|
327
|
+
b: string;
|
|
328
|
+
/** the confining `if` has a loop ancestor, so it can run more than once */
|
|
329
|
+
ifInLoop: boolean;
|
|
330
|
+
sameType: boolean;
|
|
331
|
+
/** either local is object-volatile or carries a pointee-volatile qualifier (see MergePair) */
|
|
332
|
+
eitherIsVolatile: boolean;
|
|
333
|
+
/** each local's FIRST preorder mention inside its arm is a pure const write */
|
|
334
|
+
bothArmConstInit: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The arm-disjoint admission — the SECOND way a pair can merge, and what it uniquely buys is the
|
|
338
|
+
* DIRECTION: the span path's survivor is always the later RANGE, because `overlap` orders the pair
|
|
339
|
+
* by position, while this path's is the earlier DECLARATION — the two disagree exactly when
|
|
340
|
+
* declaration order disagrees with range order. It also admits a pair the span gates refuse for a
|
|
341
|
+
* different reason: `arm-init` is a FIRST-MENTION rule where `const-fed` is an every-assign one,
|
|
342
|
+
* so arms that open with a const write and then compute (`x = 0; x = x + 1;`) merge here and not
|
|
343
|
+
* there. Two locals confined to
|
|
344
|
+
* OPPOSITE arms of one `if` never coexist at runtime: the `if` picks one arm, so no read of either
|
|
345
|
+
* can observe the other's write — no liveness reasoning needed. That argument is exactly what the
|
|
346
|
+
* `loop` gate here protects: a loop ancestor re-enters the `if`, later entries can take the other
|
|
347
|
+
* arm, and a value written on one visit becomes readable on the next. Note this gate wants ANY
|
|
348
|
+
* enclosing loop, not the span model's shared-loop rule: never-coexisting is a claim about one
|
|
349
|
+
* entry, so a second entry breaks it however the two arms' loops relate. */
|
|
350
|
+
export const ARM_DISJOINT_GATES: readonly Gate<ArmPair>[] = [
|
|
351
|
+
{
|
|
352
|
+
id: 'type',
|
|
353
|
+
why: 'the survivor keeps its own declared type, so the two must agree',
|
|
354
|
+
sound: false,
|
|
355
|
+
rejects: (c) => !c.sameType,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: 'volatile',
|
|
359
|
+
why: 'a `volatile` qualifier, on the variable or on what it points to, is observable, and merging would drop or add it',
|
|
360
|
+
sound: true,
|
|
361
|
+
guardedBy: 'coalesce.test.ts: a volatile pair never merges',
|
|
362
|
+
rejects: (c) => c.eitherIsVolatile,
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: 'loop',
|
|
366
|
+
why: 'a loop ancestor re-enters the if, so opposite arms both run and a value could cross',
|
|
367
|
+
sound: true,
|
|
368
|
+
guardedBy: 'coalesce.test.ts: an in-loop if never admits its arm pair',
|
|
369
|
+
rejects: (c) => c.ifInLoop,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: 'arm-init',
|
|
373
|
+
why: 'a local its arm does not first set to a constant is one the compiler had a reason to keep apart',
|
|
374
|
+
sound: false,
|
|
375
|
+
rejects: (c) => !c.bothArmConstInit,
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
/** The arm-disjoint merges alone — the class the livebase pairings enumerate (rank.ts): the
|
|
380
|
+
* demanding row's shared counter is arm-disjoint, and the span-model merges already ride the
|
|
381
|
+
* plain /coalesce variation, so pairing them too would multiply candidates with no row behind it. */
|
|
382
|
+
export function armDisjointCandidates(sfn: SFn): { merged: string; sfn: SFn }[] {
|
|
383
|
+
return armDisjointUnder(ARM_DISJOINT_GATES, sfn).candidates;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Every name each statement of `list` MENTIONS, and how many statements mention it — an assign
|
|
387
|
+
* target counts, and so does every name in the statement's own expressions. Nested statements are
|
|
388
|
+
* counted too, so this is the whole subtree's census. */
|
|
389
|
+
function countMentions(list: Stmt[]): Map<string, number> {
|
|
390
|
+
const out = new Map<string, number>();
|
|
391
|
+
const walk = (stmts: Stmt[]): void => {
|
|
392
|
+
for (const st of stmts) {
|
|
393
|
+
const here = new Set<string>();
|
|
394
|
+
if (st.k === 'assign') here.add(st.name);
|
|
395
|
+
for (const e of stmtExprs(st)) namesIn(e, here);
|
|
396
|
+
for (const n of here) out.set(n, (out.get(n) ?? 0) + 1);
|
|
397
|
+
walk(stmtChildren(st));
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
walk(list);
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The three mention queries the arm-disjoint path asks, sharing ONE set of memos.
|
|
405
|
+
*
|
|
406
|
+
* MEMOISED ON NODE IDENTITY, so the index belongs to one tree and one call: build it per
|
|
407
|
+
* `armDisjointUnder` invocation and never hold it across a rewrite. Sound because nothing on this
|
|
408
|
+
* path mutates the tree — the only rewrite is `rename`, which rebuilds every statement it touches
|
|
409
|
+
* and leaves the input alone (structure-purity.test.ts pins the same promise one level up). The
|
|
410
|
+
* memos earn their keep because `firstMention` walks a statement's whole subtree once per
|
|
411
|
+
* statement it scans, and the a×b loop's two calls each depend on only ONE of a and b.
|
|
412
|
+
*
|
|
413
|
+
* `firstMentionIn` and `firstMention` are mutually recursive and stay inside for that reason —
|
|
414
|
+
* the recursion runs back through the memo, not around it. */
|
|
415
|
+
function mentionIndex(): {
|
|
416
|
+
mentionsOf: (list: Stmt[]) => Map<string, number>;
|
|
417
|
+
mentionsUnder: (st: Stmt) => Map<string, number>;
|
|
418
|
+
firstMention: (list: Stmt[], n: string) => 'const-write' | 'other' | null;
|
|
419
|
+
} {
|
|
420
|
+
const listMentions = new Map<Stmt[], Map<string, number>>();
|
|
421
|
+
const mentionsOf = (list: Stmt[]): Map<string, number> => {
|
|
422
|
+
let m = listMentions.get(list);
|
|
423
|
+
if (m === undefined) {
|
|
424
|
+
m = countMentions(list);
|
|
425
|
+
listMentions.set(list, m);
|
|
426
|
+
}
|
|
427
|
+
return m;
|
|
428
|
+
};
|
|
429
|
+
// `stmtChildren` builds a FRESH array every call, so a statement's subtree counts are keyed on
|
|
430
|
+
// the statement rather than on the list `mentionsOf` would see.
|
|
431
|
+
const childMentions = new Map<Stmt, Map<string, number>>();
|
|
432
|
+
const mentionsUnder = (st: Stmt): Map<string, number> => {
|
|
433
|
+
let m = childMentions.get(st);
|
|
434
|
+
if (m === undefined) {
|
|
435
|
+
m = countMentions(stmtChildren(st));
|
|
436
|
+
childMentions.set(st, m);
|
|
437
|
+
}
|
|
438
|
+
return m;
|
|
439
|
+
};
|
|
440
|
+
// The first PREORDER mention of `n` in an arm, looked for through if statements whose own
|
|
441
|
+
// condition does not read it (an if's cond evaluates before either arm). 'const-write' is a
|
|
442
|
+
// pure `n = K`; anything else mentioning n first — a read, a computed assign, a loop — refuses.
|
|
443
|
+
const firstMentionIn = (list: Stmt[], n: string): 'const-write' | 'other' | null => {
|
|
444
|
+
for (const st of list) {
|
|
445
|
+
const here = new Set<string>();
|
|
446
|
+
if (st.k === 'assign') here.add(st.name);
|
|
447
|
+
for (const e of stmtExprs(st)) namesIn(e, here);
|
|
448
|
+
const inChildren = mentionsUnder(st).has(n);
|
|
449
|
+
if (!here.has(n) && !inChildren) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (st.k === 'assign' && st.name === n && st.value.k === 'const' && !mentions(st.value, n)) {
|
|
453
|
+
return 'const-write';
|
|
454
|
+
}
|
|
455
|
+
if (st.k === 'if' && !here.has(n)) {
|
|
456
|
+
const arm = mentionsOf(st.then).has(n) ? st.then : st.else;
|
|
457
|
+
return firstMention(arm, n);
|
|
458
|
+
}
|
|
459
|
+
return 'other';
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
};
|
|
463
|
+
// Per (arm, NAME): the answer depends on both, and the a×b loop asks for each `a` once per `b`
|
|
464
|
+
// and each `b` once per `a`.
|
|
465
|
+
const firstMentions = new Map<Stmt[], Map<string, 'const-write' | 'other' | null>>();
|
|
466
|
+
const firstMention = (list: Stmt[], n: string): 'const-write' | 'other' | null => {
|
|
467
|
+
let per = firstMentions.get(list);
|
|
468
|
+
if (per === undefined) {
|
|
469
|
+
per = new Map();
|
|
470
|
+
firstMentions.set(list, per);
|
|
471
|
+
}
|
|
472
|
+
if (!per.has(n)) {
|
|
473
|
+
per.set(n, firstMentionIn(list, n));
|
|
474
|
+
}
|
|
475
|
+
return per.get(n)!;
|
|
476
|
+
};
|
|
477
|
+
return { mentionsOf, mentionsUnder, firstMention };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** `armDisjointCandidates` with the gate table supplied plus which gate refused each pair — the
|
|
481
|
+
* same ablation-as-a-value seam `coalesceUnder` provides for the span table. */
|
|
482
|
+
export function armDisjointUnder(
|
|
483
|
+
gates: readonly Gate<ArmPair>[],
|
|
484
|
+
sfn: SFn,
|
|
485
|
+
): { candidates: { merged: string; sfn: SFn }[]; refusals: Map<string, number> } {
|
|
486
|
+
const refusals = new Map<string, number>();
|
|
487
|
+
if (sfn.locals.length < 2) {
|
|
488
|
+
return { candidates: [], refusals };
|
|
489
|
+
}
|
|
490
|
+
const { mentionsOf, firstMention } = mentionIndex();
|
|
491
|
+
const total = mentionsOf(sfn.body);
|
|
492
|
+
const params = new Set(sfn.params.map((p) => p.name));
|
|
493
|
+
const locals = new Map(sfn.locals.map((l) => [l.name, l]));
|
|
494
|
+
const typeOf = new Map(sfn.locals.map((l) => [l.name, typeToString(l.type)]));
|
|
495
|
+
const out: { merged: string; sfn: SFn }[] = [];
|
|
496
|
+
const declIdx = new Map(sfn.locals.map((l, i) => [l.name, i]));
|
|
497
|
+
const isVolatile = (n: string): boolean => {
|
|
498
|
+
const l = locals.get(n);
|
|
499
|
+
return l !== undefined && isVolatileLocal(l);
|
|
500
|
+
};
|
|
501
|
+
const visit = (stmts: Stmt[], inLoop: boolean): void => {
|
|
502
|
+
for (const st of stmts) {
|
|
503
|
+
if (st.k === 'if' && st.then.length && st.else.length) {
|
|
504
|
+
const thenM = mentionsOf(st.then);
|
|
505
|
+
const elseM = mentionsOf(st.else);
|
|
506
|
+
// locals only, and never a name that is ALSO a param — the span path holds the same
|
|
507
|
+
// belief as a gate, and a local shadowing a param would let rename() rewrite the param's
|
|
508
|
+
// own mentions
|
|
509
|
+
const confined = (m: Map<string, number>): string[] =>
|
|
510
|
+
[...m.entries()].filter(([n, k]) => locals.has(n) && !params.has(n) && total.get(n) === k).map(([n]) => n);
|
|
511
|
+
for (const a of confined(thenM)) {
|
|
512
|
+
for (const b of confined(elseM)) {
|
|
513
|
+
// the survivor is the earlier declaration, matching how a shared source local reads.
|
|
514
|
+
//
|
|
515
|
+
// THIS READS THE STRUCTURER'S ORDER, AND MUST. The declaration list is put into the
|
|
516
|
+
// target's frame order at EMIT time (l3/slotorder.ts), after this pass, so `declIdx`
|
|
517
|
+
// is the naming walk's order and the choice means "the earlier declaration in the
|
|
518
|
+
// source asmlift recovered". Ordering the list any earlier would silently change which
|
|
519
|
+
// local survives every arm-disjoint merge on a function whose frame order disagrees
|
|
520
|
+
// with its declaration order — exactly the population the ordering exists for.
|
|
521
|
+
const [gone, kept] = (declIdx.get(a) ?? 0) <= (declIdx.get(b) ?? 0) ? [b, a] : [a, b];
|
|
522
|
+
const refused = firstRejection(gates, {
|
|
523
|
+
a: gone,
|
|
524
|
+
b: kept,
|
|
525
|
+
ifInLoop: inLoop,
|
|
526
|
+
sameType: typeOf.get(a) === typeOf.get(b),
|
|
527
|
+
eitherIsVolatile: isVolatile(a) || isVolatile(b),
|
|
528
|
+
bothArmConstInit:
|
|
529
|
+
firstMention(st.then, a) === 'const-write' && firstMention(st.else, b) === 'const-write',
|
|
530
|
+
});
|
|
531
|
+
if (refused !== null) {
|
|
532
|
+
refusals.set(refused, (refusals.get(refused) ?? 0) + 1);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
out.push({
|
|
536
|
+
merged: `${gone}-${kept}`,
|
|
537
|
+
sfn: { ...sfn, body: rename(sfn.body, gone, kept), locals: localsAfterMerge(sfn.locals, gone, kept) },
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
visit(stmtChildren(st), inLoop || isLoop(st));
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
visit(sfn.body, false);
|
|
546
|
+
return { candidates: out, refusals };
|
|
164
547
|
}
|
|
165
548
|
|
|
166
549
|
/** `coalesceCandidates` with the gate table supplied, plus which gate refused each pair.
|
|
@@ -179,6 +562,7 @@ export function coalesceUnder(
|
|
|
179
562
|
}
|
|
180
563
|
const params = new Set(sfn.params.map((p) => p.name));
|
|
181
564
|
const typeOf = new Map(sfn.locals.map((l) => [l.name, typeToString(l.type)]));
|
|
565
|
+
const volatiles = new Set(sfn.locals.filter(isVolatileLocal).map((l) => l.name));
|
|
182
566
|
const sp = spans(sfn.body);
|
|
183
567
|
const candidates: { merged: string; sfn: SFn }[] = [];
|
|
184
568
|
for (const a of sfn.locals.map((l) => l.name)) {
|
|
@@ -197,6 +581,8 @@ export function coalesceUnder(
|
|
|
197
581
|
y,
|
|
198
582
|
sameType: typeOf.get(a) === typeOf.get(b),
|
|
199
583
|
eitherIsParam: params.has(a) || params.has(b),
|
|
584
|
+
eitherIsVolatile: volatiles.has(a) || volatiles.has(b),
|
|
585
|
+
sharesLoop: [...x.loops].some((l) => y.loops.has(l)),
|
|
200
586
|
});
|
|
201
587
|
if (refused !== null) {
|
|
202
588
|
refusals.set(refused, (refusals.get(refused) ?? 0) + 1);
|
|
@@ -207,7 +593,7 @@ export function coalesceUnder(
|
|
|
207
593
|
// that is wrong but plausible.
|
|
208
594
|
candidates.push({
|
|
209
595
|
merged: `${a}-${b}`,
|
|
210
|
-
sfn: { ...sfn, body: rename(sfn.body, a, b), locals: sfn.locals
|
|
596
|
+
sfn: { ...sfn, body: rename(sfn.body, a, b), locals: localsAfterMerge(sfn.locals, a, b) },
|
|
211
597
|
});
|
|
212
598
|
}
|
|
213
599
|
}
|
package/src/l3/dce.ts
CHANGED
|
@@ -50,12 +50,12 @@ function reads(e: Expr): Set<string> {
|
|
|
50
50
|
* - `marker` — the annotate-mode ASMLIFT_ERROR gap signal, which must survive so the gap stays loud;
|
|
51
51
|
* - the strict-mode `?` unresolved sentinel (`{k:'var', name:'?'}`) — dropping it would let a
|
|
52
52
|
* value asmlift could NOT lift slip past `assertResolved`, silently downgrading a loud gap;
|
|
53
|
-
* - a memory load (`index`/`field`) —
|
|
53
|
+
* - a memory load (`index`/`field`) — a deref's volatility is unknowable here, so a possibly-effectful read
|
|
54
54
|
* is never deleted (this pass never removes a memory access).
|
|
55
55
|
* - a read of a VOLATILE local (the frame object whose address escaped) — a volatile read is an
|
|
56
56
|
* observable access the machine performed; deleting the dead assignment would delete the read.
|
|
57
57
|
* A dead assignment whose value contains any of these is kept. */
|
|
58
|
-
function mustKeep(e: Expr, volatiles: ReadonlySet<string>
|
|
58
|
+
function mustKeep(e: Expr, volatiles: ReadonlySet<string>): boolean {
|
|
59
59
|
if (e.k === 'call' || e.k === 'marker' || e.k === 'index' || e.k === 'field') {
|
|
60
60
|
return true;
|
|
61
61
|
}
|
|
@@ -76,13 +76,32 @@ function allReadsInto(stmts: Stmt[], out: Set<string>): void {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Every name whose ADDRESS is taken anywhere within these statements. Globals land here too and
|
|
80
|
+
* are harmless — they were never store-eligible. */
|
|
81
|
+
function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
|
|
82
|
+
const walk = (e: Expr) => {
|
|
83
|
+
if (e.k === 'addr') {
|
|
84
|
+
out.add(e.name);
|
|
85
|
+
}
|
|
86
|
+
for (const c of exprChildren(e)) {
|
|
87
|
+
walk(c);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
for (const s of stmts) {
|
|
91
|
+
for (const e of stmtExprs(s)) {
|
|
92
|
+
walk(e);
|
|
93
|
+
}
|
|
94
|
+
allAddrNamesInto(stmtChildren(s), out);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
80
99
|
* returns the rewritten block and the set live on entry. */
|
|
81
100
|
function dceBlock(
|
|
82
101
|
stmts: Stmt[],
|
|
83
102
|
liveOut: ReadonlySet<string>,
|
|
84
103
|
locals: ReadonlySet<string>,
|
|
85
|
-
volatiles: ReadonlySet<string
|
|
104
|
+
volatiles: ReadonlySet<string>,
|
|
86
105
|
): { out: Stmt[]; liveIn: Set<string> } {
|
|
87
106
|
const live = new Set(liveOut);
|
|
88
107
|
const rev: Stmt[] = [];
|
|
@@ -234,13 +253,16 @@ function referencedNames(stmts: Stmt[], out: Set<string>): void {
|
|
|
234
253
|
/** Remove dead local stores and simplify the branches they empty out, then drop any local
|
|
235
254
|
* declaration left unreferenced. Returns a new SFn; the input is not mutated. */
|
|
236
255
|
export function eliminateDeadStores(sfn: SFn): SFn {
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
256
|
+
// AN ADDRESS-TAKEN local is never eligible, whatever its qualifiers: every store to it is
|
|
257
|
+
// observable through the escaped pointer wherever it sits, and this walk is BACKWARD, so the
|
|
258
|
+
// `addr`-as-read pin above only ever protected the stores UPSTREAM of an `&sp0` occurrence.
|
|
259
|
+
// Publish-the-address-then-fill (`g(&sp0); sp0 = v;`) puts one downstream, and this very pass
|
|
260
|
+
// deleted it. `volatile` stays in the test beside it because the qualifier is a separate reason
|
|
261
|
+
// (an MMIO cell the frontend never rendered an `&` for), not a spelling of this one.
|
|
262
|
+
const addressTaken = new Set<string>();
|
|
263
|
+
allAddrNamesInto(sfn.body, addressTaken);
|
|
242
264
|
const volatiles = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
|
|
243
|
-
const locals = new Set(sfn.locals.filter((l) => !l.volatile).map((l) => l.name));
|
|
265
|
+
const locals = new Set(sfn.locals.filter((l) => !l.volatile && !addressTaken.has(l.name)).map((l) => l.name));
|
|
244
266
|
const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles).out;
|
|
245
267
|
const used = new Set<string>();
|
|
246
268
|
referencedNames(body, used);
|