@asmlift/core 0.3.0 → 0.5.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 (43) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +130 -4
  4. package/src/backend/cpp.ts +3 -1
  5. package/src/backend/pascal.ts +11 -0
  6. package/src/contracts.ts +181 -4
  7. package/src/declare.ts +35 -9
  8. package/src/frontend/mips.ts +37 -29
  9. package/src/frontend/opaque.ts +70 -20
  10. package/src/frontend/ppc.ts +18 -7
  11. package/src/frontend/ssa.ts +279 -56
  12. package/src/frontend/thumb.ts +1372 -87
  13. package/src/ir/alias.ts +75 -0
  14. package/src/ir/opcodes.ts +57 -3
  15. package/src/ir/simplify.ts +72 -0
  16. package/src/l3/argbase.ts +221 -0
  17. package/src/l3/ast.ts +127 -5
  18. package/src/l3/basecse.ts +58 -62
  19. package/src/l3/coalesce.ts +215 -0
  20. package/src/l3/dce.ts +33 -41
  21. package/src/l3/gates.ts +67 -0
  22. package/src/l3/hoist.ts +65 -0
  23. package/src/l3/reindex.ts +7 -0
  24. package/src/l3/scopebase.ts +440 -0
  25. package/src/l3/tailmerge.ts +124 -0
  26. package/src/macros.ts +222 -13
  27. package/src/pattern/engine.ts +99 -6
  28. package/src/pipeline.ts +65 -6
  29. package/src/raise/divpow2.ts +227 -0
  30. package/src/raise/gvn.ts +151 -0
  31. package/src/raise/pre-recovery.ts +39 -3
  32. package/src/raise/recover.ts +24 -7
  33. package/src/raise/retsink.ts +37 -7
  34. package/src/raise/shortcircuit.ts +262 -22
  35. package/src/raise/struct-arrays.ts +2 -1
  36. package/src/raise/structs.ts +41 -3
  37. package/src/rank.ts +196 -20
  38. package/src/structure/analysis.ts +175 -89
  39. package/src/structure/structure.ts +588 -55
  40. package/src/structure/switch-recover.ts +117 -30
  41. package/src/symbols.ts +128 -13
  42. package/src/target.ts +4 -2
  43. package/src/trace.ts +9 -0
@@ -3,12 +3,22 @@
3
3
  // CFG (predecessors per block) and, per block, emits ops through `readVar`/`writeVar`; this
4
4
  // module materialises block-argument phis at joins and back-edges.
5
5
  //
6
+ // `preds` is an EDGE list, not a block list: it carries one entry per CFG edge, so a `switch_br`
7
+ // with several case values reaching one block appears there several times. Both readings are
8
+ // needed and they are not interchangeable — phi wiring wants the distinct predecessor BLOCKS (one
9
+ // value each), while the args it appends belong to the EDGES (every one of them). `distinctPreds`
10
+ // names the first; `appendSuccessorArg` walks the second. (ir/core.ts `predecessors` and
11
+ // structure.ts `predecessorBlocks` have the same duality, and structure.ts already dedups ad hoc
12
+ // at its two join sites.)
13
+ //
6
14
  // Protocol: create the builder, then fill blocks in index order. For each block, emit its
7
15
  // computation via read/writeVar, push its terminator op last (successors referencing
8
16
  // `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
9
17
  // blocks are filled, call `finish()` to remove trivial phis.
10
- import { Block, Fn, Successor, Value, mkValue, replaceAllUsesWith } from '../ir/core';
18
+ import { Block, Fn, Op, Value, mkValue } from '../ir/core';
19
+ import { simplifyTrivialPhis } from '../ir/simplify';
11
20
  import { T } from '../ir/types';
21
+ import { FrontendUnsupportedError } from './errors';
12
22
 
13
23
  export interface SsaBuilder {
14
24
  fn: Fn;
@@ -19,14 +29,44 @@ export interface SsaBuilder {
19
29
  writeVar(reg: string, b: number, v: Value): void;
20
30
  /** Mark block `b` fully emitted (terminator pushed); seals any now-ready successors. */
21
31
  markFilled(b: number): void;
22
- /** Live-in parameter value → the ABI register it arrived on (for calling-convention order). */
32
+ /** Live-in parameter value → the key it arrived on (for calling-convention order). Usually an
33
+ * ABI register name, but a frontend's virtual key (see the module header) ranks here too. */
23
34
  paramReg: Map<Value, string>;
35
+ /** Assert that block `b` takes a parameter for `key`, whether or not anything reads it.
36
+ *
37
+ * `readVar` cannot express this. It asks "what value does `key` hold here?", so a key the block
38
+ * DEFINES before any read answers with that local definition and no parameter is created — to it
39
+ * "never read" and "written before first read" are the same thing. When a calling convention
40
+ * proves an argument exists, that is an obligation on the SIGNATURE, independent of whether the
41
+ * body happens to use it, so it needs its own verb.
42
+ *
43
+ * Never touches the block's definitions: the parameter is added and left unused, so any local
44
+ * value already flowing keeps flowing. Only meaningful on a block with no predecessors —
45
+ * elsewhere a parameter is a phi whose position is aligned with its predecessors' terminator
46
+ * args, and appending an unpaired one would corrupt that. */
47
+ ensureParam(key: string, b: number): void;
24
48
  /** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic). */
25
49
  hasReachingDef(reg: string, b: number, seen?: Set<number>): boolean;
26
- /** Remove trivial phis; call once every block is filled. */
50
+ /** Record that block `b` makes a call HERE: the ABI's caller-saved registers stop being ones the
51
+ * caller set up. Call it AFTER `recordGuessedCall` for the same instruction, and before writing
52
+ * the call's own result. */
53
+ noteCall(b: number): void;
54
+ /** Register a `call` op whose arity was GUESSED (no prototype), so `finish` can cut it back to the
55
+ * argument registers that were actually set up on every path (see {@link trimClobberedCallArgs}).
56
+ * `argRegs` is the target's argument-register order. */
57
+ recordGuessedCall(op: Op, b: number, argRegs: string[]): void;
58
+ /** Remove trivial phis and enforce the frontend's postconditions; call once every block is
59
+ * filled. Throws FrontendUnsupportedError if a stack slot escaped as an entry parameter. */
27
60
  finish(): void;
28
61
  }
29
62
 
63
+ /** `preds` is per-EDGE (see the module header): one entry per CFG edge into each block. */
64
+ // VARIABLE NAMES ARE NOT ALWAYS MACHINE REGISTERS. `readVar`/`writeVar` key on an arbitrary string,
65
+ // and frontends mint VIRTUAL keys for storage the ISA has no register for — MIPS `sp@<off>` for a
66
+ // stack slot (frontend/mips.ts), Thumb `@sarg<k>` for an incoming stack argument (frontend/thumb.ts).
67
+ // A virtual key must be outside its ISA's register grammar so it cannot collide with a real one, and
68
+ // a key read with no reaching def becomes a function PARAMETER by the live-in path below — which is
69
+ // how both of those capabilities get their parameters without a new opcode or pass.
30
70
  export function makeSsaBuilder(name: string, blockCount: number, preds: number[][]): SsaBuilder {
31
71
  const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
32
72
  const fn: Fn = { name, blocks: irBlocks };
@@ -36,15 +76,43 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
36
76
  const filled: boolean[] = irBlocks.map(() => false);
37
77
  const incompletePhis: Array<Map<string, Value>> = irBlocks.map(() => new Map());
38
78
  const phiBlock = new Map<Value, number>();
79
+ // The key each phi stands for. `paramReg` covers live-ins only, so without this a slot that
80
+ // arrives as a PHI — which is what happens when the entry block is itself a loop header — is
81
+ // invisible to the escape check below. Braun's construction gives no other way to tell.
82
+ const phiKey = new Map<Value, string>();
39
83
  const paramReg = new Map<Value, string>();
84
+ // Parameters created by ensureParam that nothing has read yet. They are deliberately NOT in
85
+ // `defs`: a parameter asserted because a calling convention proves it exists is not evidence that
86
+ // a VALUE reaches anything, and writing one into `defs` would say it does. That distinction is
87
+ // load-bearing — `hasReachingDef` feeds `fallbackArgc`, so a def here silently raises the guessed
88
+ // arity of every prototype-less call in the function, making it pass registers the calling block
89
+ // never set up (`unknown(1)` became `unknown(1, a1, a2, a3)`). The first read adopts the value
90
+ // from here instead of minting a second parameter for the same key.
91
+ const obligedParams: Array<Map<string, Value>> = irBlocks.map(() => new Map());
92
+
93
+ // `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
94
+ const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
40
95
 
41
- const writeVar = (reg: string, b: number, v: Value) => defs[b].set(reg, v);
96
+ // CALLER-SAVED CLOBBER, for guessed call arities (see trimClobberedCallArgs). Tracked HERE
97
+ // because every register write in every frontend already goes through `writeVar`: a frontend
98
+ // that gathered this itself would be sound only while it remembered to route each write past a
99
+ // wrapper, and a MISSED write under-counts an arity — which drops a real argument silently.
100
+ const writtenSinceCall: Array<Set<string>> = irBlocks.map(() => new Set());
101
+ const callsIn = new Set<number>();
102
+ const guessedCalls: GuessedCallSite[] = [];
103
+ let argRegsSeen: string[] = [];
104
+
105
+ const writeVar = (reg: string, b: number, v: Value) => {
106
+ writtenSinceCall[b].add(reg);
107
+ defs[b].set(reg, v);
108
+ };
42
109
  const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
43
110
 
44
111
  const newPhi = (reg: string, b: number): Value => {
45
112
  const phi = mkValue(T.unk(32));
46
113
  irBlocks[b].params.push(phi);
47
114
  phiBlock.set(phi, b);
115
+ phiKey.set(phi, reg);
48
116
  defs[b].set(reg, phi); // set before wiring operands to break cycles
49
117
  return phi;
50
118
  };
@@ -55,9 +123,20 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
55
123
  incompletePhis[b].set(reg, phi);
56
124
  return phi;
57
125
  }
58
- const ps = preds[b];
126
+ // DISTINCT predecessor blocks: a switch_br reaching this block on several case values is one
127
+ // predecessor with several edges, and it supplies ONE value — counting the edges instead would
128
+ // manufacture a join (and a phi) where there is none.
129
+ const ps = distinctPreds(b);
59
130
  if (ps.length === 0) {
60
131
  // live-in with no predecessor: an incoming argument register → function parameter.
132
+ // If one was already asserted for this key (ensureParam), adopt it — minting a second
133
+ // parameter for the same key would put the key in the signature twice.
134
+ const obliged = obligedParams[b].get(reg);
135
+ if (obliged !== undefined) {
136
+ obligedParams[b].delete(reg);
137
+ defs[b].set(reg, obliged);
138
+ return obliged;
139
+ }
61
140
  const p = mkValue(T.unk(32));
62
141
  irBlocks[b].params.push(p);
63
142
  defs[b].set(reg, p);
@@ -75,16 +154,24 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
75
154
  return phi;
76
155
  };
77
156
  const addPhiOperands = (reg: string, b: number) => {
78
- for (const p of preds[b]) {
157
+ for (const p of distinctPreds(b)) {
79
158
  appendSuccessorArg(p, b, readVar(reg, p));
80
159
  }
81
160
  };
82
- // Append `arg` to predecessor p's terminator successor that targets block b.
161
+ // Append `arg` to EVERY successor edge of predecessor p that targets block b.
162
+ //
163
+ // A predecessor normally has one edge to a given successor, but a `switch_br` has as many as it
164
+ // has case values, and two cases sharing a body (`case 1: case 2:`) is ordinary C. Block args
165
+ // belong to the EDGE, so each of those edges needs its own copy: appending to just the first (a
166
+ // `find`) left the others short, while `preds` listing the block once per edge made the loop run
167
+ // k times and pile k copies onto that same first edge. Both halves of that — every edge, once per
168
+ // predecessor BLOCK — have to hold together, which is why they are fixed in one place.
83
169
  const appendSuccessorArg = (p: number, b: number, arg: Value) => {
84
170
  const term = irBlocks[p].ops[irBlocks[p].ops.length - 1];
85
- const s = term.successors.find((su) => su.block === irBlocks[b]);
86
- if (s) {
87
- s.args.push(arg);
171
+ for (const s of term.successors) {
172
+ if (s.block === irBlocks[b]) {
173
+ s.args.push(arg);
174
+ }
88
175
  }
89
176
  };
90
177
  const sealBlock = (b: number) => {
@@ -106,6 +193,25 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
106
193
  };
107
194
  sealReadyBlocks(); // seals the entry (no predecessors) up front
108
195
 
196
+ // See the interface docs. Two cases, and the split is the whole point: when nothing defines the
197
+ // key, the ordinary live-in path already does exactly the right thing; when something does, a
198
+ // parameter still has to exist for the signature, and it must be added WITHOUT redirecting the
199
+ // dataflow to it.
200
+ const ensureParam = (key: string, b: number): void => {
201
+ if (preds[b].length > 0) {
202
+ return; // a parameter here is a phi; see the precondition on the interface
203
+ }
204
+ for (const p of irBlocks[b].params) {
205
+ if (paramReg.get(p) === key) {
206
+ return; // already a parameter, however it got there
207
+ }
208
+ }
209
+ const p = mkValue(T.unk(32));
210
+ irBlocks[b].params.push(p);
211
+ paramReg.set(p, key); // ranked by the ABI sort like any other parameter
212
+ obligedParams[b].set(key, p);
213
+ };
214
+
109
215
  const hasReachingDef = (reg: string, b: number, seen = new Set<number>()): boolean => {
110
216
  if (defs[b].has(reg)) {
111
217
  return true;
@@ -123,12 +229,71 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
123
229
  readVar,
124
230
  writeVar,
125
231
  paramReg,
232
+ ensureParam,
126
233
  hasReachingDef,
234
+ noteCall: (b: number) => {
235
+ callsIn.add(b);
236
+ writtenSinceCall[b] = new Set(); // the callee clobbers the caller-saved registers
237
+ },
238
+ recordGuessedCall: (op: Op, b: number, argRegs: string[]) => {
239
+ argRegsSeen = argRegs;
240
+ guessedCalls.push({
241
+ block: b,
242
+ op,
243
+ freshBefore: new Set(writtenSinceCall[b]),
244
+ afterCallInBlock: callsIn.has(b), // `noteCall` runs after this, so this means an EARLIER call
245
+ });
246
+ },
127
247
  markFilled: (b: number) => {
128
248
  filled[b] = true;
129
249
  sealReadyBlocks();
130
250
  },
131
- finish: () => simplifyTrivialPhis(fn, phiBlock),
251
+ finish: () => {
252
+ // Guessed arities counted argument registers by reaching definition alone; now that every
253
+ // block's calls are known, drop the ones an intervening call had already clobbered.
254
+ if (guessedCalls.length) {
255
+ trimClobberedCallArgs({
256
+ argRegs: argRegsSeen,
257
+ preds,
258
+ freshAtEnd: writtenSinceCall,
259
+ callsIn,
260
+ sites: guessedCalls,
261
+ });
262
+ }
263
+ simplifyTrivialPhis(fn, (p) => {
264
+ phiBlock.delete(p);
265
+ phiKey.delete(p);
266
+ });
267
+ // A STACK SLOT MAY NEVER LEAVE AS AN ENTRY PARAMETER. A slot is memory the function itself
268
+ // allocated, so its value can only come from a store the function made; arriving as a live-in
269
+ // instead means it was read on a path that never stored it, and the signature has grown an
270
+ // argument the function does not take, standing in for uninitialised stack.
271
+ //
272
+ // Checked here, of the FINISHED function, rather than as a precondition at each read. The
273
+ // per-read test available during construction (`hasReachingDef`) asks whether a store reaches
274
+ // on SOME path, which a diamond defeats; strengthening it to "every path" is not answerable
275
+ // mid-fill, because a loop's back-edge predecessor is not filled yet and the query would
276
+ // report "unassigned" for a slot initialised before the loop — the commonest real shape.
277
+ // Asking about the symptom instead costs one pass and cannot be defeated by fill order.
278
+ //
279
+ // It is total because in Braun's construction a value undefined on some path can surface only
280
+ // as a live-in of a block with no predecessors — and BOTH spellings of that are checked:
281
+ // `paramReg` for the live-in path, `phiKey` for the case where the entry block is itself a
282
+ // loop header and the fabricated value arrives as a phi instead. Missing the second is what
283
+ // let this survive on MIPS.
284
+ //
285
+ // In `finish()` and not a helper each frontend remembers to call: this is the frontend's only
286
+ // semantic postcondition, and a postcondition enforced by convention is not enforced.
287
+ for (const p of irBlocks[0].params) {
288
+ const key = paramReg.get(p) ?? phiKey.get(p);
289
+ if (key?.startsWith(SLOT_PREFIX)) {
290
+ throw new FrontendUnsupportedError(
291
+ `cannot lift '${name}': stack slot ${key} is read on a path that never stores it ` +
292
+ `(partially-initialised local) — not modelled`,
293
+ );
294
+ }
295
+ }
296
+ },
132
297
  };
133
298
  }
134
299
 
@@ -150,6 +315,109 @@ export function fallbackArgc(
150
315
  return n;
151
316
  }
152
317
 
318
+ /** One call site whose arity was GUESSED by {@link fallbackArgc}, with what the lifting scan saw
319
+ * of its own block up to that instruction. */
320
+ export interface GuessedCallSite {
321
+ block: number;
322
+ /** the `call` op — its operands are the guessed arguments, in argument-register order */
323
+ op: Op;
324
+ /** argument registers written between the last call in this block (or the block's start) and here */
325
+ freshBefore: Set<string>;
326
+ /** did this block already make a call before this one? */
327
+ afterCallInBlock: boolean;
328
+ }
329
+
330
+ export interface CallArgTrim {
331
+ argRegs: string[];
332
+ /** one entry per CFG edge, as passed to {@link makeSsaBuilder} */
333
+ preds: number[][];
334
+ /** per block: the keys written since its LAST call (since its start if it makes none). Indexed by
335
+ * block, and it holds every key the builder saw, not only argument registers. */
336
+ freshAtEnd: Array<Set<string>>;
337
+ /** blocks that make at least one call */
338
+ callsIn: Set<number>;
339
+ sites: GuessedCallSite[];
340
+ }
341
+
342
+ /** Cut a GUESSED call arity down by the ABI's caller-saved clobber.
343
+ *
344
+ * `fallbackArgc` counts argument registers that merely have a reaching definition. A call clobbers
345
+ * r0..r3, so a definition the call sits between cannot be an argument the caller set up — correct
346
+ * compiled code would have re-materialized it. Counting it anyway INVENTS arguments
347
+ * (`m4aSongNumStart(0x89, 30, x, &g)` for a one-argument callee) — a hard compile error where the
348
+ * project's own header is in scope, and silently wrong code where C89's implicit declaration
349
+ * covers for it.
350
+ *
351
+ * SCOPE: this closes the arguments an intervening CALL disproves, which is the common case in real
352
+ * code. It does not close the rest — a dead value the compiler happened to leave in the next
353
+ * argument register with no call in between still reads as an argument, and nothing about the
354
+ * register file can say otherwise. Only a declared prototype closes those.
355
+ *
356
+ * A must-analysis: a register is FRESH at a point iff on EVERY path reaching it, it was written
357
+ * after the last call. The entry block starts all-fresh (those are the caller's own arguments).
358
+ * The result only ever SHRINKS an arity — a register the analysis cannot prove clobbered stays an
359
+ * argument — so no real argument can be dropped by it.
360
+ *
361
+ * Frontend-agnostic: the caller supplies what its own lifting scan observed, so nothing here
362
+ * re-derives which instruction writes which register. */
363
+ export function trimClobberedCallArgs(inp: CallArgTrim): void {
364
+ const { argRegs, preds, freshAtEnd, callsIn, sites } = inp;
365
+ const blockCount = freshAtEnd.length;
366
+ const all = () => new Set(argRegs);
367
+ const localEnd = (b: number) => freshAtEnd[b] ?? new Set<string>();
368
+ // freshOut[b]: registers fresh where b ends. A block that calls forgets everything before its
369
+ // last call; one that does not passes its input through, plus what it wrote.
370
+ const freshOut: Set<string>[] = Array.from({ length: blockCount }, () => all());
371
+ const freshIn: Set<string>[] = Array.from({ length: blockCount }, () => all());
372
+ const inOf = (b: number): Set<string> => {
373
+ // A block with NO predecessors is the function entry (or unreachable): its argument registers
374
+ // are the ones the caller set up. An entry that DOES have predecessors — an entry that is also
375
+ // a loop header — gets the ordinary intersection instead, because on the back edge the caller's
376
+ // setup is long gone and an intervening call may have clobbered it.
377
+ const ps = [...new Set(preds[b] ?? [])];
378
+ if (ps.length === 0) {
379
+ return all();
380
+ }
381
+ const acc = new Set(freshOut[ps[0]]);
382
+ for (const p of ps.slice(1)) {
383
+ for (const r of [...acc]) {
384
+ if (!freshOut[p].has(r)) {
385
+ acc.delete(r);
386
+ }
387
+ }
388
+ }
389
+ return acc;
390
+ };
391
+ for (let changed = true; changed;) {
392
+ changed = false;
393
+ for (let b = 0; b < blockCount; b++) {
394
+ const fin = inOf(b);
395
+ const fout = callsIn.has(b) ? localEnd(b) : new Set([...fin, ...localEnd(b)]);
396
+ if (fout.size !== freshOut[b].size || [...fout].some((r) => !freshOut[b].has(r))) {
397
+ changed = true;
398
+ }
399
+ freshIn[b] = fin;
400
+ freshOut[b] = fout;
401
+ }
402
+ }
403
+ for (const s of sites) {
404
+ const fresh = s.afterCallInBlock ? s.freshBefore : new Set([...freshIn[s.block], ...s.freshBefore]);
405
+ let n = 0;
406
+ while (n < argRegs.length && fresh.has(argRegs[n])) {
407
+ n++;
408
+ }
409
+ if (n < s.op.operands.length) {
410
+ s.op.operands.length = n;
411
+ }
412
+ }
413
+ }
414
+
415
+ /** The stack-slot key both the MIPS and Thumb frontends use for a word-sized local in the
416
+ * function's own frame. Shared so the two spell it identically and `assertNoSlotEscaped` can
417
+ * recognise either frontend's slots. See the virtual-key note in the module header. */
418
+ const SLOT_PREFIX = 'sp@';
419
+ export const stackSlotKey = (off: number): string => `${SLOT_PREFIX}${off}`;
420
+
153
421
  /** Order the TRUE entry block's parameters by ABI argument register, so downstream naming
154
422
  * (`a0`, `a1`, …) matches the calling convention, not first-read order (a callee-saved copy can
155
423
  * read a later argument register first). No-op when the entry has predecessors — a loop
@@ -167,48 +435,3 @@ export function abiSortEntryParams(
167
435
  }
168
436
  entry.params.sort((x, y) => rank(x) - rank(y));
169
437
  }
170
-
171
- // Remove block-parameters that are really trivial phis: those whose incoming operands (across
172
- // every predecessor edge, ignoring self-references from a back-edge) are all the same single
173
- // value. Such a parameter carries no join information — a loop-invariant register or a value
174
- // defined before the join — so it is replaced by that value and the corresponding argument
175
- // dropped from each predecessor's terminator. Iterated to fixpoint because removing one phi
176
- // can make another trivial.
177
- function simplifyTrivialPhis(fn: Fn, phiBlock: Map<Value, number>): void {
178
- const edgesTo = (b: Block): Successor[] => {
179
- const out: Successor[] = [];
180
- for (const pb of fn.blocks) {
181
- for (const op of pb.ops) {
182
- for (const s of op.successors) {
183
- if (s.block === b) {
184
- out.push(s);
185
- }
186
- }
187
- }
188
- }
189
- return out;
190
- };
191
- let changed = true;
192
- while (changed) {
193
- changed = false;
194
- for (const b of fn.blocks) {
195
- const incoming = edgesTo(b);
196
- for (let i = b.params.length - 1; i >= 0; i--) {
197
- const param = b.params[i];
198
- const operands = incoming.map((s) => s.args[i]);
199
- const distinct = [...new Set(operands.filter((v) => v !== param))];
200
- if (distinct.length !== 1) {
201
- continue;
202
- } // a genuine join (or unreachable) — keep it
203
- const v = distinct[0];
204
- replaceAllUsesWith(fn, param, v);
205
- b.params.splice(i, 1);
206
- for (const s of incoming) {
207
- s.args.splice(i, 1);
208
- }
209
- phiBlock.delete(param);
210
- changed = true;
211
- }
212
- }
213
- }
214
- }