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