@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
package/src/raise/retsink.ts
CHANGED
|
@@ -15,29 +15,205 @@
|
|
|
15
15
|
// (`c ? x : y`, and the branchless-compare idioms `clamp0`/`le0`/…) also converges two arms on a return
|
|
16
16
|
// merge, but there the compiler emits the MERGE-VARIABLE form, which is what byte-matches — sinking it
|
|
17
17
|
// would REGRESS those. The distinguishing signal is structural: a short-circuit chain converges on a
|
|
18
|
-
// SHARED arm
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// SHARED arm — the common early-exit reached from ≥2 CONDITIONS — whereas a simple diamond's arms are
|
|
19
|
+
// each reached from one. So sink only when some branch-predecessor of the merge is ARRIVED at from
|
|
20
|
+
// two places; every simple select stays a merge var.
|
|
21
|
+
//
|
|
22
|
+
// THE QUANTITY IS ARRIVALS, NOT PREDECESSORS. A FALL-THROUGH switch arm is the difference:
|
|
23
|
+
// `case 2: r++; case 1: r++;` gives case 1's body two predecessors — the dispatch's `beq`, and
|
|
24
|
+
// case 2's body running on — for a reason that has nothing to do with a chain of conditions.
|
|
25
|
+
// Sinking there tail-duplicates the switch's SHARED RETURN into all five of its paths, which agbcc
|
|
26
|
+
// then constant-folds per arm (`synthetic:sw_fall:agbcc`, 5 of its 11 objdiff points).
|
|
27
|
+
//
|
|
28
|
+
// So one arrival is SUBTRACTED, and only one kind: the previous arm of the same dispatch RUNNING
|
|
29
|
+
// ON into this one (`fellInto`). It is subtracted for what it IS, not for what it computed —
|
|
30
|
+
// "this pred computed something and ran on" is a proxy for the same intuition, and it refuses the
|
|
31
|
+
// shape this pass exists for, where the two arms of `if (a) { … return 0; } if (b) { … return 0; }`
|
|
32
|
+
// both compute and both jump to the shared exit (`retsink.test.ts`'s `TWO_ARMS`; five real-tier
|
|
33
|
+
// sites have it, `kleod:EntityItemDrop:agbcc` among them).
|
|
34
|
+
//
|
|
35
|
+
// "THE PREVIOUS ARM OF THE SAME DISPATCH" IS A CLAIM ABOUT A DISPATCH, so this file models one
|
|
36
|
+
// (`scrutOf`/`armsOf` below): two arms of two DIFFERENT tests on the SAME scrutinee. A proxy that
|
|
37
|
+
// does not name a dispatch is wrong in both directions, and both readings are pinned as fixtures —
|
|
38
|
+
// "each is the target of SOME conditional branch" reads the join of an `if` with no `else` as a
|
|
39
|
+
// fall-in (`IF_NO_ELSE`), and adding "…and not siblings of the same `cond_br`" still says nothing
|
|
40
|
+
// about WHICH dispatch, so `if (a) … if (b) …` on two different values loses its sinking. A
|
|
41
|
+
// function with no comparison-tree dispatch has no fall-in to subtract, which is the truth about it.
|
|
42
|
+
//
|
|
43
|
+
// The two other clauses (`FALL_IN_GATES`): `q` must arrive by an UNCONDITIONAL branch, and it must
|
|
44
|
+
// have a BODY. `isBodyless` (ir/core.ts) is the shared spelling of the second — a bodyless arm is
|
|
45
|
+
// the record gcc leaves of a decision that RAN OUT (`emit_case_nodes` mints a `b .Ldefault` per
|
|
46
|
+
// exhausted subtree), and it arrives rather than falls in; dropping it costs
|
|
47
|
+
// `synthetic:llshr:gcc2.7.2kmc` its sinking. Its parameter half is what keeps an EMPTY case arm
|
|
48
|
+
// (one op, but it binds the accumulator) on the fall-in side.
|
|
49
|
+
//
|
|
50
|
+
// AND THE MERGE MUST BELONG TO THAT DISPATCH (`ownedBy`). A fall-through switch can SHARE its
|
|
51
|
+
// return with control flow outside itself — a guard's `goto` onto the same `return` — and there
|
|
52
|
+
// refusing to sink is exactly wrong: the merge is left standing, Regime-A switch recovery declines
|
|
53
|
+
// on it, and if-recovery duplicates the tails anyway. The pred shape alone cannot tell the two
|
|
54
|
+
// apart (both present one fell-into arm with two preds); the SCRUTINEE can, because the guard tests
|
|
55
|
+
// a different value. `synthetic:sw_fallguard` is the row: MATCH, and diff:6 with the clause dropped.
|
|
56
|
+
//
|
|
57
|
+
// REGIME SCOPE — the model is `cond_br`-seeded, so it is INERT ON A JUMP TABLE. A `switch_br`
|
|
58
|
+
// dispatch's arms are invisible to `armsOf` and `fellInto` never fires there, so on
|
|
59
|
+
// `synthetic:sw_jtfall`/`sw_jtfalldesc` the pass behaves as it does where there is no dispatch at
|
|
60
|
+
// all. Deliberate: seeding `switch_br` too would move matching rows with no row asking for it. It
|
|
61
|
+
// is the second definition of "this arm falls into that one" in the tree — `switch-recover.ts`'s
|
|
62
|
+
// `analyzeArmExit` covers both regimes and is not reachable from `raise/` — so whoever builds the
|
|
63
|
+
// Regime-B hoist should route both through one recognizer rather than widen the seed here.
|
|
21
64
|
//
|
|
22
65
|
// This does NOT recover the boolean-VALUE form `return a && b` — that is shortcircuit.ts's job
|
|
23
66
|
// (the `logic_and`/`logic_or` connective plus agbcc's `(-b|b)>>31` = `b!=0` normalisation).
|
|
24
|
-
import { Block, Fn, defOpMap, mkOp, predecessors } from '../ir/core';
|
|
67
|
+
import { Block, Fn, Op, Value, defOpMap, isBodyless, mkOp, predecessors, terminator } from '../ir/core';
|
|
68
|
+
import { NEGATED_ICMP } from '../ir/opcodes';
|
|
69
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
70
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
25
71
|
|
|
26
72
|
/** The fused short-circuit connectives (raise/shortcircuit.ts). A `cond_br` on one of these is the
|
|
27
73
|
* post-fusion record of the ≥2 conditions that used to reach a shared arm. */
|
|
28
74
|
const CONNECTIVES = new Set(['logic_and', 'logic_or']);
|
|
29
75
|
|
|
76
|
+
/** "`q` is the previous arm of the same dispatch, RUNNING ON into `target`" — the one arrival
|
|
77
|
+
* `arrivals` subtracts. `dispatches` is already the answer to the hard half (`siblingArms` and
|
|
78
|
+
* `ownedBy` in `sinkReturns`); the table is a value so each clause can be dropped and the pass
|
|
79
|
+
* re-run on real input. Every clause is `sound: false` — they trade BYTES, never correctness:
|
|
80
|
+
* admitting one wrongly spells a correct function the compiler does not re-emit, and refusing one
|
|
81
|
+
* wrongly does the same in the other direction. */
|
|
82
|
+
export interface FallInCandidate {
|
|
83
|
+
/** the predecessor under test */
|
|
84
|
+
readonly q: Block;
|
|
85
|
+
/** the block it would have fallen into */
|
|
86
|
+
readonly target: Block;
|
|
87
|
+
/** the scrutinees whose dispatch has `q` and `target` as arms of two DIFFERENT tests AND owns
|
|
88
|
+
* the return merge — empty when there is no such dispatch */
|
|
89
|
+
readonly dispatches: readonly Value[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const FALL_IN_GATES: readonly Gate<FallInCandidate>[] = [
|
|
93
|
+
{
|
|
94
|
+
// A DEFINITION rather than a tuning knob, and the one entry here nothing has been shown to
|
|
95
|
+
// move: a `cond_br` pred did not run on into this block, it chose it, so calling that a
|
|
96
|
+
// fall-in would be wrong about the CFG whatever it did to the bytes.
|
|
97
|
+
id: 'arrives-by-decision',
|
|
98
|
+
why: 'a `cond_br` pred CHOSE this block; that is a decision arriving, never a fall-in',
|
|
99
|
+
sound: false,
|
|
100
|
+
rejects: (c) => {
|
|
101
|
+
const t = terminator(c.q);
|
|
102
|
+
return t?.opcode !== 'br' || t.successors.length !== 1 || t.successors[0].block !== c.target;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// Paid for by a CORPUS row, not by a unit test: dropping it costs `synthetic:llshr:gcc2.7.2kmc`
|
|
107
|
+
// its sinking, and moves none of this file's fixtures.
|
|
108
|
+
id: 'bodyless-arm',
|
|
109
|
+
why: "gcc's `b .Ldefault` for an exhausted subtree is a decision that RAN OUT, not an arm",
|
|
110
|
+
sound: false,
|
|
111
|
+
rejects: (c) => isBodyless(c.q),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'one-dispatch-owning-the-merge',
|
|
115
|
+
why: 'both arms of ONE dispatch on one scrutinee, and that dispatch owns the return merge',
|
|
116
|
+
sound: false,
|
|
117
|
+
guardedBy: 'ablating the dispatch gate reads an `if` join, and a guarded switch, as fall-ins',
|
|
118
|
+
rejects: (c) => c.dispatches.length === 0,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** The two questions the fall-in clauses ask of the function's comparison-tree dispatches. */
|
|
123
|
+
interface DispatchModel {
|
|
124
|
+
/** Is this block part of the dispatch on `s` — either one of its tests, or an arm of one? */
|
|
125
|
+
inDispatch(b: Block, s: Value): boolean;
|
|
126
|
+
/** The scrutinees for which `q` and `target` are arms of two DIFFERENT tests: the dispatches in
|
|
127
|
+
* which one could be the previous arm of the other. Two successors of ONE `cond_br` — the body
|
|
128
|
+
* and the join of an `if` with no `else` — share no such scrutinee, which is the whole point. */
|
|
129
|
+
siblingArms(q: Block, target: Block): Value[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** THE DISPATCH MODEL. A TEST BLOCK ends in a `cond_br` on an integer comparison of exactly one
|
|
133
|
+
* non-constant value against constants — the SCRUTINEE. Two test blocks belong to the same
|
|
134
|
+
* dispatch when they test the same scrutinee: `recognizeSwitch`'s own PRE1 ("every test is on the
|
|
135
|
+
* SAME Value") read at the raise level, without its dominance, purity or interval preconditions —
|
|
136
|
+
* those decide whether a `switch` can be SPELLED, and this pass only needs to know a decision tree
|
|
137
|
+
* is there. `NEGATED_ICMP` (ir/opcodes.ts) is the shared spelling of the icmp family, so an
|
|
138
|
+
* eleventh comparison joins this model for free.
|
|
139
|
+
*
|
|
140
|
+
* Constant folding is deliberately NOT reproduced (`switch-recover.ts evalConst` folds agbcc's
|
|
141
|
+
* synthesized immediates): a test whose constant side this cannot see contributes two
|
|
142
|
+
* non-constant operands and is skipped, which loses a subtraction rather than inventing one.
|
|
143
|
+
*
|
|
144
|
+
* Built ONCE, before `sinkReturns`' merge loop, and read-only thereafter — nothing in the loop
|
|
145
|
+
* writes either table, so the merge that is rewritten first sees the same dispatches as the last. */
|
|
146
|
+
function dispatchModel(fn: Fn, defs: Map<Value, Op>): DispatchModel {
|
|
147
|
+
const scrutOf = new Map<Block, Value>();
|
|
148
|
+
/** Arms, indexed by the block reached and the scrutinee whose test sent it there — the test
|
|
149
|
+
* blocks are the value, because a fall-in requires the two arms to come from DIFFERENT tests. */
|
|
150
|
+
const armsOf = new Map<Block, Map<Value, Set<Block>>>();
|
|
151
|
+
for (const b of fn.blocks) {
|
|
152
|
+
const t = terminator(b);
|
|
153
|
+
if (t?.opcode !== 'cond_br') {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const cmp = defs.get(t.operands[0]);
|
|
157
|
+
if (!cmp || !(cmp.opcode in NEGATED_ICMP)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const vars = cmp.operands.filter((o) => defs.get(o)?.opcode !== 'const');
|
|
161
|
+
if (vars.length !== 1) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const scrut = vars[0];
|
|
165
|
+
scrutOf.set(b, scrut);
|
|
166
|
+
for (const e of t.successors) {
|
|
167
|
+
let byScrut = armsOf.get(e.block);
|
|
168
|
+
if (!byScrut) {
|
|
169
|
+
byScrut = new Map();
|
|
170
|
+
armsOf.set(e.block, byScrut);
|
|
171
|
+
}
|
|
172
|
+
const tests = byScrut.get(scrut) ?? new Set<Block>();
|
|
173
|
+
tests.add(b);
|
|
174
|
+
byScrut.set(scrut, tests);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
inDispatch: (b, s) => scrutOf.get(b) === s || !!armsOf.get(b)?.has(s),
|
|
179
|
+
siblingArms: (q, target) => {
|
|
180
|
+
const aq = armsOf.get(q);
|
|
181
|
+
const at = armsOf.get(target);
|
|
182
|
+
if (!aq || !at) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const out: Value[] = [];
|
|
186
|
+
for (const [s, testsQ] of aq) {
|
|
187
|
+
const testsT = at.get(s);
|
|
188
|
+
if (testsT && [...testsQ].some((c) => [...testsT].some((d) => c !== d))) {
|
|
189
|
+
out.push(s);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
30
197
|
/** Tail-duplicate a return-only merge block into its unconditional-branch predecessors, but ONLY in the
|
|
31
198
|
* short-circuit shape (some branch-pred is shared, or the arms are selected by a fused connective).
|
|
32
199
|
* Returns whether anything changed. A "return-only" block is exactly one `ret` whose operands are all
|
|
33
200
|
* its own block-params, so each predecessor already carries the returned value as a successor arg. */
|
|
34
|
-
export function sinkReturns(fn: Fn): boolean {
|
|
201
|
+
export function sinkReturns(fn: Fn, gates: readonly Gate<FallInCandidate>[] = FALL_IN_GATES): boolean {
|
|
35
202
|
let changed = false;
|
|
36
203
|
const preds = predecessors(fn);
|
|
37
204
|
const defs = defOpMap(fn);
|
|
205
|
+
const { inDispatch, siblingArms } = dispatchModel(fn, defs);
|
|
206
|
+
// WHICH READS NEED `terminator`'s UNDEFINED CASE, which is not "every read of a terminator". The
|
|
207
|
+
// scan over `fn.blocks` can meet a block with no ops at all, and that is the one read the guard
|
|
208
|
+
// is for: `ir/verify.ts` rejects an empty block and `pipeline.ts` verifies before calling this,
|
|
209
|
+
// but `sinkReturns` is exported and its tests build blocks by hand, where a refusal is a better
|
|
210
|
+
// answer than a TypeError. A read over a PREDECESSOR needs none and does not have one —
|
|
211
|
+
// `predecessors` is built from `successorsOf`, which is empty for a block with no terminator, so
|
|
212
|
+
// a bodyless block never appears in anyone's predecessor list. Once a block is known to end in a
|
|
213
|
+
// `br`, the rewrite below indexes its terminator directly.
|
|
38
214
|
const isBrTo = (p: Block, m: Block) => {
|
|
39
|
-
const t = p
|
|
40
|
-
return t
|
|
215
|
+
const t = terminator(p);
|
|
216
|
+
return t?.opcode === 'br' && t.successors.length === 1 && t.successors[0].block === m;
|
|
41
217
|
};
|
|
42
218
|
for (const m of [...fn.blocks]) {
|
|
43
219
|
if (m.ops.length !== 1) {
|
|
@@ -59,8 +235,9 @@ export function sinkReturns(fn: Fn): boolean {
|
|
|
59
235
|
}
|
|
60
236
|
// SHORT-CIRCUIT GATE, in two shapes — the chain must be visible in the CFG or in the value domain.
|
|
61
237
|
//
|
|
62
|
-
// (a) UNFUSED: at least one branch-pred is
|
|
63
|
-
//
|
|
238
|
+
// (a) UNFUSED: at least one branch-pred is ARRIVED AT from ≥2 places — the common early-exit
|
|
239
|
+
// reached from every condition of the chain. Everything that reaches it counts EXCEPT the
|
|
240
|
+
// previous arm running on (`fellInto` below).
|
|
64
241
|
// (b) FUSED: `branch-shortcircuit` (raise/shortcircuit.ts) rewrites the head's condition into a
|
|
65
242
|
// `logic_and`/`logic_or` and collapses the second condition block into it. That leaves both
|
|
66
243
|
// arms single-pred, so (a) cannot see the chain any more — but the CONNECTIVE is now the
|
|
@@ -84,7 +261,21 @@ export function sinkReturns(fn: Fn): boolean {
|
|
|
84
261
|
return t.opcode === 'cond_br' && CONNECTIVES.has(defs.get(t.operands[0])?.opcode ?? '');
|
|
85
262
|
});
|
|
86
263
|
const fusedDiamond = brPreds.length >= 2 && brPreds.some(selectedByConnective);
|
|
87
|
-
|
|
264
|
+
// Does the dispatch on `s` OWN this merge? Every predecessor of `m` must be part of it — one of
|
|
265
|
+
// its tests, or an arm of one. A `goto` from outside the switch onto the same `return` fails
|
|
266
|
+
// this on the scrutinee it tests, so nothing is subtracted and the merge is sunk. `ps`, not
|
|
267
|
+
// `brPreds`: that outside arrival is a `cond_br` (the guard's `bgt`), which never appears in
|
|
268
|
+
// `brPreds`.
|
|
269
|
+
const ownedBy = (s: Value) => ps.every((p) => inDispatch(p, s));
|
|
270
|
+
// `q` FELL INTO `p`: it is the previous arm of the same dispatch, running on. The clauses are
|
|
271
|
+
// `FALL_IN_GATES` above, argued in this file's header; `arrivals` counts every OTHER
|
|
272
|
+
// predecessor. Layout adjacency — `q` sitting immediately above `p`, which is what "fell
|
|
273
|
+
// through" means in the assembly — is NOT a clause: it moves no corpus row, and it would be an
|
|
274
|
+
// unpaid premise about `fn.blocks` still being address order.
|
|
275
|
+
const fellInto = (q: Block, target: Block) =>
|
|
276
|
+
firstRejection(gates, { q, target, dispatches: siblingArms(q, target).filter(ownedBy) }) === null;
|
|
277
|
+
const arrivals = (p: Block) => (preds.get(p) ?? []).filter((q) => !fellInto(q, p)).length;
|
|
278
|
+
if (!brPreds.some((p) => arrivals(p) >= 2) && !fusedDiamond) {
|
|
88
279
|
continue;
|
|
89
280
|
}
|
|
90
281
|
for (const p of brPreds) {
|
|
@@ -98,5 +289,14 @@ export function sinkReturns(fn: Fn): boolean {
|
|
|
98
289
|
fn.blocks = fn.blocks.filter((b) => b !== m);
|
|
99
290
|
}
|
|
100
291
|
}
|
|
292
|
+
// Sinking RETIRES in-edges. A merge also reached by a `cond_br` keeps that one — a conditional
|
|
293
|
+
// branch cannot carry a `ret` — and so survives with a SINGLE predecessor, where its parameter is
|
|
294
|
+
// no longer a join but an alias of that edge's argument. Left standing, the structurer destroys
|
|
295
|
+
// the alias into a local of its own (`v0 = 0; return v0;`) and Regime-A switch recovery reads the
|
|
296
|
+
// block as a second, distinct default candidate. The cleanup is `ir/simplify.ts`'s own; it simply
|
|
297
|
+
// has no other caller downstream of here.
|
|
298
|
+
if (changed) {
|
|
299
|
+
simplifyTrivialPhis(fn);
|
|
300
|
+
}
|
|
101
301
|
return changed;
|
|
102
302
|
}
|