@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,435 @@
|
|
|
1
|
+
// asmlift structurer — copy coalescing over the interference graph, run once the naming pipeline
|
|
2
|
+
// has given every merge value a name and before anything reads those names. Off by default;
|
|
3
|
+
// rank.ts enumerates it as the `/merge-names` candidate and the differ referees (StructureOptions
|
|
4
|
+
// `coalesceMergeNames`).
|
|
5
|
+
//
|
|
6
|
+
// WHAT IS LEFT TO COALESCE. Destroying SSA turns each block parameter into a variable and each
|
|
7
|
+
// edge argument into a copy into it, so a name shared between a parameter and its argument is a
|
|
8
|
+
// copy that disappears. `structure.ts` already shares one where it can: a parameter adopts the
|
|
9
|
+
// name of an incoming argument, checked against the same liveness this file uses. But that walk
|
|
10
|
+
// runs ONCE, over the blocks in address order, and only ever looks BACKWARD along an edge — so a
|
|
11
|
+
// parameter whose arguments are still unnamed takes a fresh name and never revisits it, and a
|
|
12
|
+
// parameter with several named arguments can adopt only ONE of them. Both leave the same residue.
|
|
13
|
+
// Three switch arms that each compute the same three quantities and feed one join reach it as
|
|
14
|
+
// three parameters apiece; the arm the join adopted pays nothing and every other arm pays a copy
|
|
15
|
+
// per value.
|
|
16
|
+
//
|
|
17
|
+
// So this pass asks the question the naming walk cannot: given the FINAL names, which two of them
|
|
18
|
+
// would a copy join, and may they be one variable? That is copy coalescing, and the answer is the
|
|
19
|
+
// interference graph — not the order the blocks happen to sit in. `l3/coalesce.ts` is a different
|
|
20
|
+
// question: two UNRELATED locals whose spans are disjoint, which is register reuse.
|
|
21
|
+
//
|
|
22
|
+
// WHY THIS IS A CANDIDATE AND NOT A FIX. Removing a copy is worth far less than it looks: the
|
|
23
|
+
// compiler coalesces most of them itself, so a function's two spellings compile to nearly the same
|
|
24
|
+
// code — the klonoa function this was built for drops 56 of its 74 copies and 6 of its 906
|
|
25
|
+
// instructions. What actually moves the score is which values end up sharing a register, and that
|
|
26
|
+
// splits per function: there the merged spelling wins by 13 points, on `mergeif` and `mergeloop`
|
|
27
|
+
// the un-merged one does. Which of the two a compiler's own coalescer landed on is not derivable
|
|
28
|
+
// from the naming, so both are emitted and the differ referees.
|
|
29
|
+
//
|
|
30
|
+
// WHEN TWO NAMES MAY BE ONE. A name denotes a set of SSA values. Merging names X and Y makes every
|
|
31
|
+
// value under either read and write one variable, so it is legal exactly when no value of X is
|
|
32
|
+
// live where a value of Y is written, and vice versa. `interferes` is that sentence.
|
|
33
|
+
//
|
|
34
|
+
// WHERE A VALUE IS WRITTEN, and OVER WHAT RANGE the other one has to be checked, are both places a
|
|
35
|
+
// block-granular answer is wrong. A block parameter is written by the edge copies into its block,
|
|
36
|
+
// which run at the end of each predecessor: `liveIn` of the block is exact for that, since the
|
|
37
|
+
// other arguments of the same edge are what `sequentialize` orders. A materialized definition
|
|
38
|
+
// writes MID-block, and there `liveIn` is not exact in either direction — it omits every value
|
|
39
|
+
// DEFINED in that block, however long it lives afterwards, which is precisely the range a mid-block
|
|
40
|
+
// write lands inside. `liveAt` answers that one per-op. This is the difference between this pass
|
|
41
|
+
// and `canTakeName`, which may use `liveIn` because it is only ever asked about a block PARAMETER,
|
|
42
|
+
// whose range starts at a block boundary; a name class holds arbitrary members.
|
|
43
|
+
//
|
|
44
|
+
// The exception to the edge-copy story is a loop emitter MOVING a copy — it rotates the update to
|
|
45
|
+
// the bottom of the body and sinks an exit copy in ahead of it — and where that can happen the
|
|
46
|
+
// write site widens to every block those predecessors reach in one step, the superset
|
|
47
|
+
// `canTakeName` takes everywhere. Function parameters are never written, so they have no write
|
|
48
|
+
// sites at all; a merge onto `a0` is still checked in the other direction.
|
|
49
|
+
//
|
|
50
|
+
// A LOOP VARIABLE'S NAME MEANS DIFFERENT THINGS IN DIFFERENT PLACES, and that is the one thing
|
|
51
|
+
// liveness cannot answer. The update sits at the bottom of the body, so on an exiting edge the name
|
|
52
|
+
// still holds the value the iteration started with — `structure.ts` refuses to let a merge outside
|
|
53
|
+
// the loop adopt it for exactly that reason (`carriesPreUpdate`), and refuses to let an inner
|
|
54
|
+
// loop's variable adopt an enclosing one's, which the inner loop would then mutate every iteration.
|
|
55
|
+
// `loop-escape` restates both over classes: a class holding a loop header's parameter may absorb
|
|
56
|
+
// only values the loop's body contains. Two loops' variables always fail it — a nested pair because
|
|
57
|
+
// the outer variable's home is outside the inner body, a disjoint pair in both directions — so the
|
|
58
|
+
// enclosing-loop rule needs no gate of its own.
|
|
59
|
+
//
|
|
60
|
+
// It is NOT marked sound, because it has not been shown to be, and it is BLUNTER than the rule it
|
|
61
|
+
// restates: `carriesPreUpdate` branches on which emitter owns the latch and names four shapes that
|
|
62
|
+
// are not the hazard, none of which this has. Dropping it over 773 benchmark rows improves 12 and
|
|
63
|
+
// regresses none, measured at #55 — `nestedloop` is `int s = 0; … s += i*j`, one accumulator the
|
|
64
|
+
// pass currently emits as two. Lifting `carriesPreUpdate` itself to name classes is the work that would take those
|
|
65
|
+
// 12 rows; it needs the class-level closure, since a merge can reach a loop variable's name through
|
|
66
|
+
// an edge that carried no loop variable at all.
|
|
67
|
+
//
|
|
68
|
+
// TWO KNOWN GAPS, both on the READ side of a relocated write:
|
|
69
|
+
//
|
|
70
|
+
// • `sitesOf` widens where a loop emitter may MOVE a copy, but the read side does not follow. For
|
|
71
|
+
// a block parameter `clobbers` falls to `liveIn` of the widened site, and a parameter's SSA def
|
|
72
|
+
// point is its own block's entry, not the position the emitter actually writes it at — so a
|
|
73
|
+
// header definition is not seen to clobber a value the sink copies to the top of the body. The
|
|
74
|
+
// slot that is actually sunk is unreachable (its exit arg is a header parameter, which
|
|
75
|
+
// `loop-escape` rejects); reaching it needs a second, non-loop predecessor of the exit block
|
|
76
|
+
// passing the header's definition into the same slot, and no input has been built that does.
|
|
77
|
+
// • `canTakeName` applies its own widening UNCONDITIONALLY, while `relocatable` applies it only
|
|
78
|
+
// where a loop can move the copy. One of the two is wrong: either that widening is unnecessary
|
|
79
|
+
// on the committed path, where removing it would drop copies from EVERY spelling rather than
|
|
80
|
+
// from an opt-in candidate, or `relocatable` is too narrow. Nothing has measured which.
|
|
81
|
+
//
|
|
82
|
+
// AND ONE ASYMMETRY THAT IS NOT A GAP. `type` is sound here, and `canTakeName` has no full
|
|
83
|
+
// equivalent: it declares a name from its FIRST taker and never re-checks a later adopter, a
|
|
84
|
+
// disagreement it reaches 136 times over klonoa's 69 liftable functions. That sounds like the same
|
|
85
|
+
// defect on the committed path, and it was measured: adding the check there moves one row better
|
|
86
|
+
// and two worse, no match flips. What it tolerates is the 32-BIT scalar case — `s32` against
|
|
87
|
+
// `u32`, same width, same bytes at a read — and that is a property of the RULE, not of the corpus:
|
|
88
|
+
// `canTakeName` compares signedness below 32 bits, because a narrow declaration IS the extension
|
|
89
|
+
// it replaced (the rule carries its own comment in structure.ts). What makes the rule SOUND here
|
|
90
|
+
// is the pointer case, where the survivor's declared type decides how its arithmetic scales.
|
|
91
|
+
//
|
|
92
|
+
// PURE: it reads the analysis maps and returns the renaming. Applying it is the caller's job.
|
|
93
|
+
import { type Block, type Op, type Value, successorsOf } from '../ir/core';
|
|
94
|
+
import { type IrType, typeEquals } from '../ir/types';
|
|
95
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
96
|
+
import type { UseSite } from './analysis';
|
|
97
|
+
|
|
98
|
+
export interface NameCoalesceDeps {
|
|
99
|
+
/** the function's blocks, in address order — the iteration order that makes merges deterministic */
|
|
100
|
+
blocks: readonly Block[];
|
|
101
|
+
/** `blocks[0]`; its parameters are the function's own, and are never written */
|
|
102
|
+
entry: Block;
|
|
103
|
+
preds: ReadonlyMap<Block, Block[]>;
|
|
104
|
+
/** values read at-or-after each block's entry (analysis.ts) */
|
|
105
|
+
liveIn: ReadonlyMap<Block, Set<Value>>;
|
|
106
|
+
/** op → the block holding it (analysis.ts) */
|
|
107
|
+
opBlock: ReadonlyMap<Op, Block>;
|
|
108
|
+
/** op → its position within its block (analysis.ts) */
|
|
109
|
+
opIndex: ReadonlyMap<Op, number>;
|
|
110
|
+
/** every positioned use of a value (analysis.ts) */
|
|
111
|
+
useSitesOf: ReadonlyMap<Value, UseSite[]>;
|
|
112
|
+
/** value → defining op (defOpMap) */
|
|
113
|
+
defs: ReadonlyMap<Value, Op>;
|
|
114
|
+
/** the defs the structurer bound to a local instead of re-rendering at each use */
|
|
115
|
+
materialize: ReadonlySet<Op>;
|
|
116
|
+
/** value → the name the naming pipeline settled on */
|
|
117
|
+
varName: ReadonlyMap<Value, string>;
|
|
118
|
+
/** name → the type it is declared with */
|
|
119
|
+
varType: ReadonlyMap<string, IrType>;
|
|
120
|
+
/** every natural loop, as its header and the blocks its body contains (structure/loops.ts) */
|
|
121
|
+
loops: readonly { header: Block; body: ReadonlySet<Block> }[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One candidate merge: the two name classes a would-be copy joins. */
|
|
125
|
+
export interface NameMerge {
|
|
126
|
+
/** the name absorbed */
|
|
127
|
+
from: string;
|
|
128
|
+
/** the surviving name */
|
|
129
|
+
into: string;
|
|
130
|
+
/** a class holds a loop variable, and the other holds a value from outside that loop's body */
|
|
131
|
+
loopEscapes: boolean;
|
|
132
|
+
/** either class holds a parameter of the function itself */
|
|
133
|
+
functionParam: boolean;
|
|
134
|
+
/** two values of the merged class are parameters of ONE block */
|
|
135
|
+
siblingParams: boolean;
|
|
136
|
+
/** the two names are declared with the same type */
|
|
137
|
+
sameType: boolean;
|
|
138
|
+
/** some value of one class is live where some value of the other is written */
|
|
139
|
+
interferes: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** SOUND RULES FIRST. `mayMerge` computes every field eagerly, so the order costs nothing to
|
|
143
|
+
* evaluate — what it decides is BLAME. With a heuristic first, every pair that both escapes a loop
|
|
144
|
+
* and interferes is attributed to the heuristic, which is exactly backwards for reading what each
|
|
145
|
+
* rule actually rejects on its own. */
|
|
146
|
+
export const NAME_COALESCE_GATES: readonly Gate<NameMerge>[] = [
|
|
147
|
+
{
|
|
148
|
+
id: 'interference',
|
|
149
|
+
why: 'a value live where the other is written would be clobbered by the merged variable',
|
|
150
|
+
sound: true,
|
|
151
|
+
guardedBy: 'namecoalesce.test.ts: ablating interference clobbers a value live across the copy',
|
|
152
|
+
rejects: (c) => c.interferes,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'sibling-params',
|
|
156
|
+
why: 'two parameters of one block share every in-edge, so one edge would write the name twice',
|
|
157
|
+
sound: true,
|
|
158
|
+
guardedBy: 'namecoalesce.test.ts: ablating sibling-params puts two parameters of one block under one name',
|
|
159
|
+
rejects: (c) => c.siblingParams,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'type',
|
|
163
|
+
why: 'the survivor keeps its own declared type, so the two must agree',
|
|
164
|
+
sound: true,
|
|
165
|
+
guardedBy: 'namecoalesce.test.ts: ablating type merges two names the declarations disagree about',
|
|
166
|
+
rejects: (c) => !c.sameType,
|
|
167
|
+
},
|
|
168
|
+
// MIRROR: `structure.ts`'s `FRESH_MERGE_GATES` asks a parameter question too, and the two are not
|
|
169
|
+
// one predicate. This gate refuses merging two already-settled NAME CLASSES and is on by default
|
|
170
|
+
// within `/merge-names`; that one refuses one carrier at one merge SLOT and is off by default.
|
|
171
|
+
// Only their `entry.params` membership test coincides.
|
|
172
|
+
{
|
|
173
|
+
id: 'param',
|
|
174
|
+
why: 'a function parameter is the signature the source wrote, not a recovered local',
|
|
175
|
+
sound: false,
|
|
176
|
+
rejects: (c) => c.functionParam,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'loop-escape',
|
|
180
|
+
why: 'outside the loop a loop variable’s name holds the value from BEFORE the update',
|
|
181
|
+
sound: false,
|
|
182
|
+
rejects: (c) => c.loopEscapes,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/** The renaming to apply to the naming maps: absorbed name → survivor. Empty when nothing merges.
|
|
187
|
+
* `refusals` counts which gate stopped each rejected pair, so a gate nothing ever reaches shows up
|
|
188
|
+
* as a rule no test can be failing on purpose. */
|
|
189
|
+
export function coalesceNames(
|
|
190
|
+
deps: NameCoalesceDeps,
|
|
191
|
+
gates: readonly Gate<NameMerge>[] = NAME_COALESCE_GATES,
|
|
192
|
+
): { renames: Map<string, string>; refusals: Map<string, number> } {
|
|
193
|
+
const { blocks, entry, preds, liveIn, opBlock, opIndex, useSitesOf, defs, materialize, varName, varType, loops } =
|
|
194
|
+
deps;
|
|
195
|
+
const refusals = new Map<string, number>();
|
|
196
|
+
|
|
197
|
+
const valuesOf = new Map<string, Value[]>();
|
|
198
|
+
const order = new Map<string, number>();
|
|
199
|
+
for (const [v, n] of varName) {
|
|
200
|
+
if (!order.has(n)) {
|
|
201
|
+
order.set(n, order.size);
|
|
202
|
+
}
|
|
203
|
+
const vs = valuesOf.get(n) ?? [];
|
|
204
|
+
vs.push(v);
|
|
205
|
+
valuesOf.set(n, vs);
|
|
206
|
+
}
|
|
207
|
+
if (valuesOf.size < 2) {
|
|
208
|
+
return { renames: new Map(), refusals };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const paramBlock = new Map<Value, Block>();
|
|
212
|
+
for (const b of blocks) {
|
|
213
|
+
for (const p of b.params) {
|
|
214
|
+
paramBlock.set(p, b);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** A block whose in-edge copies a loop emitter may MOVE off the edge that carries them. */
|
|
218
|
+
const loopBlocks = new Set<Block>(loops.flatMap((l) => [...l.body]));
|
|
219
|
+
const relocatable = (blk: Block): boolean =>
|
|
220
|
+
loops.some((l) => l.header === blk) || (preds.get(blk) ?? []).some((pr) => loopBlocks.has(pr));
|
|
221
|
+
// Where a write to this value's variable can land — see the file header.
|
|
222
|
+
const writeSites = new Map<Value, Block[]>();
|
|
223
|
+
const sitesOf = (v: Value): Block[] => {
|
|
224
|
+
let s = writeSites.get(v);
|
|
225
|
+
if (s) {
|
|
226
|
+
return s;
|
|
227
|
+
}
|
|
228
|
+
const blk = paramBlock.get(v);
|
|
229
|
+
if (blk && blk !== entry) {
|
|
230
|
+
s = relocatable(blk) ? [blk, ...(preds.get(blk) ?? []).flatMap((pr) => successorsOf(pr))] : [blk];
|
|
231
|
+
} else if (!blk) {
|
|
232
|
+
const d = defs.get(v);
|
|
233
|
+
s = d && materialize.has(d) ? [opBlock.get(d)!] : [];
|
|
234
|
+
} else {
|
|
235
|
+
s = [];
|
|
236
|
+
}
|
|
237
|
+
writeSites.set(v, s);
|
|
238
|
+
return s;
|
|
239
|
+
};
|
|
240
|
+
// WHAT READING A VALUE ACTUALLY SPELLS. A def the structurer did not bind to a local is
|
|
241
|
+
// RE-RENDERED at each use, so the names its operand tree reads are read wherever that expression
|
|
242
|
+
// lands — arbitrarily far past the point SSA liveness says those operands died. Liveness of SSA
|
|
243
|
+
// VALUES is therefore not liveness of the emitted program, and the gap is a silent clobber: the
|
|
244
|
+
// fuzz found `v0 = v0 - a1;` emitted ahead of an `f1(a0 + (v0 - a1))` that still wanted the old
|
|
245
|
+
// `v0`. Every live set below is expanded through this.
|
|
246
|
+
const renderCache = new Map<Value, Set<Value>>();
|
|
247
|
+
const renders = (v: Value): ReadonlySet<Value> => {
|
|
248
|
+
const hit = renderCache.get(v);
|
|
249
|
+
if (hit) {
|
|
250
|
+
return hit;
|
|
251
|
+
}
|
|
252
|
+
const out = new Set<Value>([v]);
|
|
253
|
+
renderCache.set(v, out); // before recursing: a def tree is acyclic, but this costs nothing
|
|
254
|
+
if (!varName.has(v)) {
|
|
255
|
+
for (const o of defs.get(v)?.operands ?? []) {
|
|
256
|
+
for (const x of renders(o)) {
|
|
257
|
+
out.add(x);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
};
|
|
263
|
+
const expand = (vs: Iterable<Value>): Set<Value> => {
|
|
264
|
+
const out = new Set<Value>();
|
|
265
|
+
for (const v of vs) {
|
|
266
|
+
for (const x of renders(v)) {
|
|
267
|
+
out.add(x);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
};
|
|
272
|
+
// Which values' RENDERING reads `v` — so a use of one of them is a read of `v`, wherever it sits.
|
|
273
|
+
const renderedBy = new Map<Value, Set<Value>>();
|
|
274
|
+
for (const b of blocks) {
|
|
275
|
+
for (const op of b.ops) {
|
|
276
|
+
for (const r of op.results) {
|
|
277
|
+
for (const x of renders(r)) {
|
|
278
|
+
const set = renderedBy.get(x) ?? new Set<Value>();
|
|
279
|
+
set.add(r);
|
|
280
|
+
renderedBy.set(x, set);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const liveInR = new Map<Block, Set<Value>>(blocks.map((b) => [b, expand(liveIn.get(b) ?? [])]));
|
|
286
|
+
// Values live OUT of a block: what its successors read, plus what its terminator hands them.
|
|
287
|
+
const liveOut = new Map<Block, Set<Value>>();
|
|
288
|
+
for (const b of blocks) {
|
|
289
|
+
const out = new Set<Value>();
|
|
290
|
+
for (const sc of b.ops[b.ops.length - 1]?.successors ?? []) {
|
|
291
|
+
for (const v of liveInR.get(sc.block) ?? []) {
|
|
292
|
+
out.add(v);
|
|
293
|
+
}
|
|
294
|
+
for (const a of expand(sc.args)) {
|
|
295
|
+
out.add(a);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
liveOut.set(b, out);
|
|
299
|
+
}
|
|
300
|
+
// Is `v` live ACROSS position `at` in `b`? `liveIn` alone cannot say: it is block-granular, so a
|
|
301
|
+
// value DEFINED in `b` is absent from it however long it lives afterwards. That is exactly the
|
|
302
|
+
// range a mid-block write lands in the middle of — and the reason `canTakeName` can use `liveIn`
|
|
303
|
+
// and this cannot. `canTakeName` is only ever asked about a block PARAMETER, whose range starts
|
|
304
|
+
// at a block boundary; a name class holds arbitrary members, materialized defs included.
|
|
305
|
+
const liveAt = (v: Value, b: Block, at: number): boolean => {
|
|
306
|
+
const d = defs.get(v);
|
|
307
|
+
const dIdx = d !== undefined && opBlock.get(d) === b ? opIndex.get(d) : undefined;
|
|
308
|
+
const started = liveInR.get(b)!.has(v) || b.params.includes(v) || (dIdx !== undefined && dIdx < at);
|
|
309
|
+
if (!started) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if (liveOut.get(b)!.has(v)) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
// a use of anything whose RENDERING reads `v` is a read of `v` at that position
|
|
316
|
+
for (const w of [v, ...(renderedBy.get(v) ?? [])]) {
|
|
317
|
+
if ((useSitesOf.get(w) ?? []).some((u) => u.blk === b && u.idx >= at)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
// A block parameter's copies run before its block starts, so `liveIn` is exact for them. A
|
|
324
|
+
// materialized definition writes mid-block and needs the range above.
|
|
325
|
+
const clobbers = (writer: Value, other: Value): boolean => {
|
|
326
|
+
const d = paramBlock.get(writer) === undefined ? defs.get(writer) : undefined;
|
|
327
|
+
const at = d !== undefined ? opIndex.get(d) : undefined;
|
|
328
|
+
// `liveAt` folds in the block's own parameters; an edge copy's site does not, because it runs
|
|
329
|
+
// before the block starts. Two parameters of ONE block are written by the same edge copies and
|
|
330
|
+
// so are invisible here by construction — that is `sibling-params`, not this rule.
|
|
331
|
+
return sitesOf(writer).some((b) =>
|
|
332
|
+
at !== undefined && opBlock.get(d!) === b ? liveAt(other, b, at) : liveInR.get(b)!.has(other),
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Union-find over names. The survivor is the name introduced FIRST, which makes the result
|
|
337
|
+
// independent of the order the pairs happen to be visited in.
|
|
338
|
+
const parent = new Map<string, string>();
|
|
339
|
+
const find = (n: string): string => {
|
|
340
|
+
let r = n;
|
|
341
|
+
while (parent.get(r) !== undefined) {
|
|
342
|
+
r = parent.get(r)!;
|
|
343
|
+
}
|
|
344
|
+
return r;
|
|
345
|
+
};
|
|
346
|
+
const members = new Map<string, Value[]>([...valuesOf].map(([n, vs]) => [n, [...vs]]));
|
|
347
|
+
|
|
348
|
+
const mayMerge = (x: string, y: string): NameMerge => {
|
|
349
|
+
const vx = members.get(x)!;
|
|
350
|
+
const vy = members.get(y)!;
|
|
351
|
+
// A loop variable is the parameter of a header; the loops it belongs to decide what its name
|
|
352
|
+
// may absorb. `homeOf` is the block a value's variable is written in, which for these two rules
|
|
353
|
+
// is all "inside the loop" needs to mean.
|
|
354
|
+
const homeOf = (v: Value): Block | undefined => paramBlock.get(v) ?? opBlock.get(defs.get(v)!);
|
|
355
|
+
const loopsOf = (vs: readonly Value[]): typeof loops =>
|
|
356
|
+
loops.filter((l) => vs.some((v) => l.header.params.includes(v)));
|
|
357
|
+
const lx = loopsOf(vx);
|
|
358
|
+
const ly = loopsOf(vy);
|
|
359
|
+
// Two loops' variables always escape each other: a nested pair because the outer variable's
|
|
360
|
+
// home is not in the inner body, a disjoint pair in both directions. So the enclosing-loop rule
|
|
361
|
+
// needs no gate of its own.
|
|
362
|
+
const escapes = (ls: typeof loops, other: readonly Value[]): boolean =>
|
|
363
|
+
ls.some((l) => other.some((v) => !l.body.has(homeOf(v)!)));
|
|
364
|
+
const loopEscapes = escapes(lx, vy) || escapes(ly, vx);
|
|
365
|
+
const functionParam = [...vx, ...vy].some((v) => entry.params.includes(v));
|
|
366
|
+
let siblingParams = false;
|
|
367
|
+
let interferes = false;
|
|
368
|
+
for (const u of vx) {
|
|
369
|
+
for (const w of vy) {
|
|
370
|
+
if (paramBlock.get(u) !== undefined && paramBlock.get(u) === paramBlock.get(w)) {
|
|
371
|
+
siblingParams = true;
|
|
372
|
+
}
|
|
373
|
+
if (clobbers(u, w) || clobbers(w, u)) {
|
|
374
|
+
interferes = true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const tx = varType.get(x);
|
|
379
|
+
const ty = varType.get(y);
|
|
380
|
+
return {
|
|
381
|
+
from: y,
|
|
382
|
+
into: x,
|
|
383
|
+
loopEscapes,
|
|
384
|
+
functionParam,
|
|
385
|
+
siblingParams,
|
|
386
|
+
sameType: tx !== undefined && ty !== undefined && typeEquals(tx, ty),
|
|
387
|
+
interferes,
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Every would-be copy, in block/slot/edge order: a parameter and the argument one edge hands it.
|
|
392
|
+
for (const b of blocks) {
|
|
393
|
+
if (b === entry) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
for (const pr of new Set(preds.get(b) ?? [])) {
|
|
397
|
+
for (const s of pr.ops[pr.ops.length - 1].successors) {
|
|
398
|
+
if (s.block !== b) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
b.params.forEach((p, i) => {
|
|
402
|
+
const np = varName.get(p);
|
|
403
|
+
const na = varName.get(s.args[i]);
|
|
404
|
+
if (np === undefined || na === undefined) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const rp = find(np);
|
|
408
|
+
const ra = find(na);
|
|
409
|
+
if (rp === ra) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// survivor first
|
|
413
|
+
const [x, y] = order.get(rp)! < order.get(ra)! ? [rp, ra] : [ra, rp];
|
|
414
|
+
const refused = firstRejection(gates, mayMerge(x, y));
|
|
415
|
+
if (refused !== null) {
|
|
416
|
+
refusals.set(refused, (refusals.get(refused) ?? 0) + 1);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
parent.set(y, x);
|
|
420
|
+
members.get(x)!.push(...members.get(y)!);
|
|
421
|
+
members.set(y, []);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const renames = new Map<string, string>();
|
|
428
|
+
for (const n of valuesOf.keys()) {
|
|
429
|
+
const r = find(n);
|
|
430
|
+
if (r !== n) {
|
|
431
|
+
renames.set(n, r);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { renames, refusals };
|
|
435
|
+
}
|