@asmlift/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) 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 +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -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 +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -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/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -1,6 +1,6 @@
1
1
  // asmlift — return-sinking (F-CFG-class structural pass; successor-aware, ISA-neutral).
2
2
  //
3
- // A short-circuit `if (a && b) return X; return Y;` (and the `||` / value-returning variants) compiles to
3
+ // A short-circuit `if (a && b) return X; return Y;` (and the `||` / value-returning forms) compiles to
4
4
  // a diamond whose arms converge on a single RETURN block: `br ^merge(X)` / `br ^merge(Y)` into
5
5
  // `^merge(v): ret v`. The structurer lowers that merge as a shared VARIABLE — `v0 = X … v0 = Y … return v0`
6
6
  // — which is byte-exact-CORRECT but recompiles DIFFERENTLY from the source: agbcc/gcc, given the natural
@@ -11,35 +11,532 @@
11
11
  // merge. The structurer then emits early returns in each arm (it already duplicates a shared arm block),
12
12
  // which recompiles to the compiler's shared-return form. Purely structural: no new IR/AST vocabulary.
13
13
  //
14
- // GATE — only the SHORT-CIRCUIT shape, never a simple value-select. A single-condition select
15
- // (`c ? x : y`, and the branchless-compare idioms `clamp0`/`le0`/…) also converges two arms on a return
16
- // merge, but there the compiler emits the MERGE-VARIABLE form, which is what byte-matches — sinking it
17
- // would REGRESS those. The distinguishing signal is structural: a short-circuit chain converges on a
18
- // SHARED arm (the common early-exit reached from ≥2 conditions, so it has ≥2 predecessors), whereas a
19
- // simple diamond's arms each have exactly one predecessor. So sink only when some branch-predecessor of
20
- // the merge is itself shared (≥2 preds); every simple select stays a merge var.
14
+ // GATE — the SHORT-CIRCUIT shape, plus the one single-condition shape a merge variable cannot spell.
15
+ // A single-condition select (`c ? x : y`, and the branchless-compare idioms `clamp0`/`le0`/…) also
16
+ // converges two arms on a return merge, and for most of them the compiler emits the MERGE-VARIABLE
17
+ // form, which is what byte-matches — sinking those would REGRESS them. The distinguishing signal is
18
+ // structural: a short-circuit chain converges on a SHARED arm the common early-exit reached from
19
+ // ≥2 CONDITIONS — whereas a simple diamond's arms are each reached from one. So sink when some
20
+ // branch-predecessor of the merge is ARRIVED at from two places.
21
+ //
22
+ // A ONE-SET-ARM DIAMOND IS THE EXCEPTION (`SELECT_GATES`), and it is a compiler fact rather than a
23
+ // preference — THE SAME compiler fact `raise/narrowlocal.ts` already owns, read in the same
24
+ // backwards direction. gcc 2.x's `jump_optimize` (`gcc/jump.c:443-445`, guard at `:471-502`)
25
+ // collapses `if (c) v = a; else v = b;` into `v = b; if (c) v = a;` when both arms are ONE
26
+ // speculatable SET, so agbcc never emits that diamond back: one arm is HOISTED above the compare
27
+ // and the other becomes a conditional skip — `movs r0,#5; cmp r1,#0; bne .L; movs r0,#3; .L: bx lr`,
28
+ // four blocks collapsed to two, with no unconditional branch to the merge at all. For the {0,1} pair
29
+ // it goes further and folds branchlessly (`negs r0,r0; lsrs r0,r0,#31`), erasing the comparison too.
30
+ // So where the TARGET holds that diamond, a merge variable is the spelling of some other function,
31
+ // and sinking is the only candidate that can match (measured on `kleod:IsSelectButtonPressed:agbcc`,
32
+ // retired 2026-09-13).
33
+ //
34
+ // ONE MODEL, NOT TWO. The predicate is `narrowlocal.ts`'s exported `armIsOneSet` — no op that
35
+ // `REEVAL_UNSAFE_OPS` calls unsafe, and EXACTLY one result-producing op — read here by
36
+ // `arms-are-one-set`. It is cited there line by line to `jump.c:474/:480/:482/:483` and
37
+ // `rtlanal.c:1770-1784`, with the three things an op count alone gets wrong, and a reach census over
38
+ // 13733 blocks. It is SHARED rather than re-derived here because it is one optimizer guard and a
39
+ // second model of it would drift; where the shared predicate costs this pass something narrowlocal
40
+ // does not pay is named below.
41
+ //
42
+ // THE EVIDENCE IS COMPILED AND COMMITTED, not described — seven functions written each way, agbcc
43
+ // -O2 -mthumb (`test/corpus/agbcc-select-{merge,early}.s`, regenerated by
44
+ // `scripts/regen-select-spelling-probes.ts`, asserted by `select-spelling.test.ts`). Read as a
45
+ // truth-table for `armIsOneSet` in THIS direction:
46
+ //
47
+ // selbtn v = 1 / v = 0 one SET merge LOSES the diamond, early keeps it ADMIT
48
+ // selk53 v = 5 / v = 3 one SET merge LOSES the diamond, early keeps it ADMIT
49
+ // selpool two 32-bit pool consts one SET merge LOSES the diamond, early keeps it ADMIT
50
+ // selcomp v = a + b / v = a - b one SET merge LOSES the diamond, early keeps it ADMIT
51
+ // selbody *p = 1; v = 5 / … bodied BOTH keep it, opposite ARM ORDER refuse
52
+ // selcomp3 v = a + b + 7 / … 3 ops BOTH keep it, opposite ARM ORDER refuse
53
+ // selload v = *p / v = *q a READ BOTH keep it, opposite ARM ORDER refuse
54
+ //
55
+ // The refusals are the load-bearing half. Where the arms are not one SET the hoist does not
56
+ // happen, so BOTH spellings emit a diamond and they differ only
57
+ // in ARM ORDER — sinking there is not wrong, it is the wrong SENSE, and the unranked primary
58
+ // (`packages/cli/test/matching`, the CLI one-shot, apps/web's preset) has no `/flip-branch` to
59
+ // recover it. Five shapes were measured losing a byte-exact match to the missing clause — a bodied
60
+ // arm, one bodied arm, a bodied `x == 3` head, bodied pool constants, and a two-way `switch` with a
61
+ // `default` — and they are pinned in `packages/cli/test/matching/fixtures.ts` (`selbody`, `sw2`).
62
+ //
63
+ // WHERE THE SHARED PREDICATE IS CONSERVATIVE, and why that is accepted. `armIsOneSet` COUNTS
64
+ // constants, because the lifted IR does not say which immediate a target can fold. `selk3`
65
+ // (`v = a + 3` / `v = a - 3`) is two ops in the IR and one `adds` on this target, so `jump.c`'s own
66
+ // guard counts one SET where this predicate counts two and the arm is hoisted while the clause
67
+ // refuses it — an over-refusal that costs a CANDIDATE here, where narrowlocal's same over-refusal
68
+ // costs nothing (its fallback is the spelling it emits anyway). That divergence is ARGUED from the
69
+ // optimizer, not compiled: `selk3` is deliberately absent from the committed pair below, which is
70
+ // the truth-table for the predicate rather than for the compiler. Accepted rather than forked: no
71
+ // corpus row inhabits the difference, and a cost model neither pass has is a worse thing to own than
72
+ // a shared conservative predicate. Two more shapes it refuses on the same counting grounds, both
73
+ // unreached on the corpus: an arm left EMPTY by an earlier asmlift pass hoisting its constant into
74
+ // the head (`:480` runs `single_set` on the arm's own insn, and an arm whose only insn is its jump
75
+ // has none), and an arm carrying TWO constants into a multi-operand `ret`.
76
+ //
77
+ // THAT IS A FACT ABOUT ONE COMPILER, so it has a per-compiler home rather than an `arch ==` branch,
78
+ // and it is the field narrowlocal already threads: `compilerBehaviors.hoistsSingleSetArm`
79
+ // (target.ts), threaded in as `RetSinkOptions` from `decompile`'s own target, read by the
80
+ // `compiler-hoists-single-set-arm` clause. ONE field, because it is one guard: a second boolean for
81
+ // it would let a round that measures mwcc's `jump_optimize` set one reader's and leave the other's
82
+ // false. Set on agbcc
83
+ // and nowhere else — this pass is ISA-neutral and runs for IDO, gcc2.7.2kmc and mwcc too, and
84
+ // nothing has compiled the pair on any of them. The short-circuit admissions above take no such
85
+ // clause: their argument is about a SHARED ARM in the CFG, not about a compiler's hoist.
86
+ //
87
+ // THE COMPILER CLAUSE IS LAST IN THE TABLE, not first. A census of a gate table counts the clause
88
+ // that FIRST refuses each site, and `target.ts` states that this admission reaches no non-agbcc row.
89
+ // Placed first, this COMPILER fact collects 28 of the 74 sites — 26 of them shapes that are not
90
+ // diamonds at all — and a census then reads the opposite of that sentence. Last, it decides 0 sites
91
+ // on the entire corpus, which is what the sentence claims. Costs nothing either way (measured: 0 of
92
+ // 1039 rows move on the reorder).
93
+ //
94
+ // WHAT EACH CLAUSE ACTUALLY DECIDES, measured by ablating it and re-lifting all 1039 corpus rows
95
+ // offline (`targetAsm` out of the artifact, source-hash diff), ON THE TABLE AS IT STANDS — a number
96
+ // here is only true of the clause ORDER it was measured under, so `retsink.test.ts` pins that order
97
+ // and the next reorder fails a test:
98
+ //
99
+ // two-arms-one-head 61 sites / 0 rows arms-are-one-set 8 sites / 6 rows
100
+ // pre-diamond 2 sites / 0 rows a-value-is-returned 2 sites / 0 rows
101
+ // compiler-hoists-single-set-arm 0 sites / 0 rows ADMIT 1 site
102
+ // no-arrival-but-the-arms 0 sites / 0 rows
103
+ //
104
+ // The table is reached at 74 sites over the 1039 rows and admits ONE, and the SIX rows
105
+ // `arms-are-one-set` decides alone are `pokeemerald:GiveBerryPowder:agbcc`,
106
+ // `pokeemerald:MathUtil_Div16:agbcc`, `pokeemerald:MathUtil_Div16Shift:agbcc`,
107
+ // `sa3:sub_8001FD4:agbcc`, `synthetic:armkeep:agbcc`, `synthetic:bgfixed:agbcc` — five of them MATCH
108
+ // today. Three clauses decide 0 rows, and they are not the same kind of zero:
109
+ //
110
+ // • `compiler-hoists-single-set-arm` decides 0 SITES as well, which is the point of putting it
111
+ // last, and is what `target.ts` claims for it. Moved back to first it collects 28 sites and
112
+ // still moves 0 rows.
113
+ // • `no-arrival-but-the-arms` and `pre-diamond` decide the SAME two sites
114
+ // (`pokeemerald:GetGenderFromSpeciesAndPersonality:agbcc`,
115
+ // `pokeemerald:TrySetCantSelectMoveBattleScript:agbcc`) and the order between them decides which
116
+ // one the census bills. Both are merges with a third in-edge: `mergeArms` counts ALL
117
+ // predecessors where `two-arms-one-head` counts only the `br` ones, so the pre-recovery map
118
+ // already called them no diamond. NO MANUFACTURED DIAMOND REACHES THIS TABLE TODAY — the reason
119
+ // is still `fusedDiamond` being tested first in the same disjunction, and `pre-diamond` is the
120
+ // clause that stops that being an accident. Argued, not corpus-paid, and marked as such.
121
+ // • `a-value-is-returned` first-refuses two agbcc sites (below) and is subsumed by
122
+ // `arms-are-one-set` a step later.
123
+ //
124
+ // THE THREADING IS NOT FRAGILE, which was the open question when `pre-diamond` was proposed: of the
125
+ // 74 sites, ZERO find their merge block ABSENT from the pre-recovery map. Block identity survives
126
+ // all twelve pre-recovery passes, their eight `dce` runs and `recoverTypes`, so every refusal here
127
+ // is a real `diamond: false` and never a lost key.
128
+ //
129
+ // The named single-condition controls — `maxi`/`mini`/`absdiff`/`clamp0`/`bittest` — are NOT
130
+ // protected by any of these clauses: agbcc
131
+ // emits those as a branch-over-one-instruction (or branchlessly), so their targets hold no two-armed
132
+ // diamond and `two-arms-one-head` has already refused them. A control that stays MATCH because it
133
+ // never reaches the table is not evidence the table is right — the shapes that DO reach it are the
134
+ // bodied ones above, and they are what `packages/cli/test/matching` pins.
135
+ //
136
+ // REACH: ONE corpus row. Re-lifting all 1039 rows against `origin/main` changes the emitted source
137
+ // of `kleod:IsSelectButtonPressed:agbcc` and of nothing else. That is one inhabitant because the
138
+ // corpus has one, not because the rule is shaped to it: the three synthetic rows minted for this
139
+ // admission (`selconst`, `selhead` — a body in the HEAD, arms still one SET each — and `selloop`, a
140
+ // loop ahead of the diamond; see `apps/benchmark/dataset/synthetic.ts`) are MATCH on agbcc as well,
141
+ // the last of them composing with another variation. FAILURE DIRECTION: a wrong admission costs
142
+ // a SPELLING and never an answer — the transform is a tail duplication, every arm keeps the value it
143
+ // carried — which is why every clause here is `sound: false`.
144
+ //
145
+ // SINKING IS NECESSARY, NOT SUFFICIENT: `/flip-branch` was necessary on every inhabitant measured —
146
+ // the 4 shapes the admission was built on (`if (x & 0x40) return 1; return 0;` and its inverse,
147
+ // `if (x > 3) return 5; return 3;`, `if (x == 0) return 1; return 0;`), and the three synthetic rows
148
+ // above (the loop one's winner's variations are `signed/flip-branch/indexed`). Unranked, all four of the first
149
+ // score 3 and none matches; the target row's own winner's variations moved `unsigned` → `unsigned/flip-branch`. The mechanism is structural rather than a
150
+ // property of the sample: a sunk diamond has NO JOIN left, so the shipped joined-if default (the
151
+ // layout reading) does not cover it, and on every diamond measured here agbcc puts the source's
152
+ // taken arm in the FAR block, which makes the layout reading systematically inverted. "Both arms are
153
+ // one SET" is a per-SITE fact this pass holds at the moment it fires, so emitting the target's
154
+ // sense HERE would move the unranked path too — the CLI one-shot, apps/web's preset, and
155
+ // `packages/cli/test/matching`, none of which the ranked fan rescues. That is a separate round with
156
+ // its own regression surface (`regression.test.ts`'s fixtures, the byte-pinned playground preset)
157
+ // and is booked rather than bundled.
158
+ //
159
+ // WHY A GATE AND NOT A VARIATION, since `l3/unmerge.ts` is this tree's other "duplicate a join back
160
+ // into the arms" pass and IS one. `unmerge.ts` is a variation on the stated grounds that the mapping
161
+ // from its tree back to a source is not a function and not uniformly many-to-one — which way it
162
+ // goes is a property of the SHAPE, and no gate there can read that off the tree. This admission
163
+ // asserts the opposite for its own shape, and the assertion is what `arms-are-one-set` IS: on the
164
+ // question that clause reads, the mapping is a function, the differ never has to referee it, and the fan
165
+ // does not grow (4 candidates before, 4 after). A variation where a default belongs doubles every
166
+ // enumeration to referee a question with one answer.
167
+ //
168
+ // WHAT MOVES THE THING THIS DEFAULT PLACES. `docs/level-tower.md`: a compiler behavior read
169
+ // backwards "owes an explicit refusal for every pass that moves the thing it is placing".
170
+ // The thing placed is a TWO-ARMED DIAMOND, and asmlift manufactures one. `raise/shortcircuit.ts`'s
171
+ // `branch-shortcircuit` builds diamonds out of condition trees the ROM never merged — measured in
172
+ // `narrowlocal.ts`'s `mergeShapes` header, where 20 sa3 blocks gain `diamond` between lift and
173
+ // pre-recovery's end and that pass accounts for all 10 that gain `hoistable`. `sinkReturns` runs
174
+ // LATER STILL (`pipeline.ts`: after all twelve pre-recovery passes, eight of them followed by `dce`,
175
+ // and after `recoverTypes`), so it sees every one of them.
176
+ //
177
+ // THE REFUSAL IS `pre-diamond`, and it is the same answer narrowlocal reached: the shape is read
178
+ // from `PreRecoveryFacts.mergeShapes`, computed ONCE on the CFG as it ENTERS pre-recovery and
179
+ // threaded down. A merge block absent from that map — created by a later pass — reads as no diamond
180
+ // and is refused. A manufactured diamond therefore cannot be mistaken for one the ROM held, which is
181
+ // the whole content of the backwards read.
182
+ //
183
+ // IT IS AN ARGUED GUARD AND NOT A CORPUS-PAID ONE, and the honest version of the claim is this: no
184
+ // manufactured diamond reaches this table today, because `fusedDiamond` is tested before
185
+ // `constantSelect()` in the same disjunction, so a `logic_and`-fused manufacture takes the
186
+ // short-circuit path instead. That is a coincidence of evaluation order on a corpus with exactly ONE
187
+ // admit site — nothing here could have caught it changing — and `pre-diamond` is what makes the
188
+ // refusal stated rather than accidental. A pre-recovery fusion CAN destroy the CFG signal these
189
+ // clauses read without any hosted gate noticing, so the divergence is worth pinning even unreached:
190
+ // the fixture (`retsink.test.ts`) builds it by hand — a live diamond the pre-recovery map does not
191
+ // have — because the corpus supplies none.
192
+ // What the clause does NOT claim to cover is the second half of the tower's sentence — an IR
193
+ // boundary the FRONTEND invents. `mergeShapes`' own header names that residue (the frontend cuts
194
+ // blocks at labels, `applyIdiomPatterns` folds shift pairs, both before pre-recovery runs, and both
195
+ // only ever make an arm SHORTER), and it is inherited here unchanged rather than re-argued.
196
+ //
197
+ // THE QUANTITY IS ARRIVALS, NOT PREDECESSORS. A FALL-THROUGH switch arm is the difference:
198
+ // `case 2: r++; case 1: r++;` gives case 1's body two predecessors — the dispatch's `beq`, and
199
+ // case 2's body running on — for a reason that has nothing to do with a chain of conditions.
200
+ // Sinking there tail-duplicates the switch's SHARED RETURN into all five of its paths, which agbcc
201
+ // then constant-folds per arm (`synthetic:sw_fall:agbcc`, 5 of its 11 objdiff points).
202
+ //
203
+ // So one arrival is SUBTRACTED, and only one kind: the previous arm of the same dispatch RUNNING
204
+ // ON into this one (`fellInto`). It is subtracted for what it IS, not for what it computed —
205
+ // "this pred computed something and ran on" is a proxy for the same intuition, and it refuses the
206
+ // shape this pass exists for, where the two arms of `if (a) { … return 0; } if (b) { … return 0; }`
207
+ // both compute and both jump to the shared exit (`retsink.test.ts`'s `TWO_ARMS`; five real-tier
208
+ // sites have it, `kleod:EntityItemDrop:agbcc` among them).
209
+ //
210
+ // "THE PREVIOUS ARM OF THE SAME DISPATCH" IS A CLAIM ABOUT A DISPATCH, so this file models one
211
+ // (`scrutOf`/`armsOf` below): two arms of two DIFFERENT tests on the SAME scrutinee. A proxy that
212
+ // does not name a dispatch is wrong in both directions, and both readings are pinned as fixtures —
213
+ // "each is the target of SOME conditional branch" reads the join of an `if` with no `else` as a
214
+ // fall-in (`IF_NO_ELSE`), and adding "…and not siblings of the same `cond_br`" still says nothing
215
+ // about WHICH dispatch, so `if (a) … if (b) …` on two different values loses its sinking. A
216
+ // function with no comparison-tree dispatch has no fall-in to subtract, which is the truth about it.
217
+ //
218
+ // The two other clauses (`FALL_IN_GATES`): `q` must arrive by an UNCONDITIONAL branch, and it must
219
+ // have a BODY. `isBodyless` (ir/core.ts) is the shared spelling of the second — a bodyless arm is
220
+ // the record gcc leaves of a decision that RAN OUT (`emit_case_nodes` mints a `b .Ldefault` per
221
+ // exhausted subtree), and it arrives rather than falls in; dropping it costs
222
+ // `synthetic:llshr:gcc2.7.2kmc` its sinking. Its parameter half is what keeps an EMPTY case arm
223
+ // (one op, but it binds the accumulator) on the fall-in side.
224
+ //
225
+ // AND THE MERGE MUST BELONG TO THAT DISPATCH (`ownedBy`). A fall-through switch can SHARE its
226
+ // return with control flow outside itself — a guard's `goto` onto the same `return` — and there
227
+ // refusing to sink is exactly wrong: the merge is left standing, Regime-A switch recovery declines
228
+ // on it, and if-recovery duplicates the tails anyway. The pred shape alone cannot tell the two
229
+ // apart (both present one fell-into arm with two preds); the SCRUTINEE can, because the guard tests
230
+ // a different value. `synthetic:sw_fallguard` is the row: MATCH, and diff:6 with the clause dropped.
231
+ //
232
+ // REGIME SCOPE — the model is `cond_br`-seeded, so it is INERT ON A JUMP TABLE. A `switch_br`
233
+ // dispatch's arms are invisible to `armsOf` and `fellInto` never fires there, so on
234
+ // `synthetic:sw_jtfall`/`sw_jtfalldesc` the pass behaves as it does where there is no dispatch at
235
+ // all. Deliberate: seeding `switch_br` too would move matching rows with no row asking for it. It
236
+ // is the second definition of "this arm falls into that one" in the tree — `switch-recover.ts`'s
237
+ // `analyzeArmExit` covers both regimes and is not reachable from `raise/` — so whoever builds the
238
+ // Regime-B hoist should route both through one recognizer rather than widen the seed here.
21
239
  //
22
240
  // This does NOT recover the boolean-VALUE form `return a && b` — that is shortcircuit.ts's job
23
241
  // (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';
242
+ import { Block, Fn, Op, Value, defOpMap, isBodyless, mkOp, predecessors, terminator } from '../ir/core';
243
+ import { NEGATED_ICMP } from '../ir/opcodes';
244
+ import { simplifyTrivialPhis } from '../ir/simplify';
245
+ import { type Gate, firstRejection } from '../l3/gates';
246
+ import { type MergeShape, armIsOneSet } from './narrowlocal';
25
247
 
26
248
  /** The fused short-circuit connectives (raise/shortcircuit.ts). A `cond_br` on one of these is the
27
249
  * post-fusion record of the ≥2 conditions that used to reach a shared arm. */
28
250
  const CONNECTIVES = new Set(['logic_and', 'logic_or']);
29
251
 
30
- /** Tail-duplicate a return-only merge block into its unconditional-branch predecessors, but ONLY in the
31
- * short-circuit shape (some branch-pred is shared, or the arms are selected by a fused connective).
32
- * Returns whether anything changed. A "return-only" block is exactly one `ret` whose operands are all
33
- * its own block-params, so each predecessor already carries the returned value as a successor arg. */
34
- export function sinkReturns(fn: Fn): boolean {
252
+ /** "`q` is the previous arm of the same dispatch, RUNNING ON into `target`" — the one arrival
253
+ * `arrivals` subtracts. `dispatches` is already the answer to the hard half (`siblingArms` and
254
+ * `ownedBy` in `sinkReturns`); the table is a value so each clause can be dropped and the pass
255
+ * re-run on real input. Every clause is `sound: false` they trade BYTES, never correctness:
256
+ * admitting one wrongly spells a correct function the compiler does not re-emit, and refusing one
257
+ * wrongly does the same in the other direction. */
258
+ export interface FallInCandidate {
259
+ /** the predecessor under test */
260
+ readonly q: Block;
261
+ /** the block it would have fallen into */
262
+ readonly target: Block;
263
+ /** the scrutinees whose dispatch has `q` and `target` as arms of two DIFFERENT tests AND owns
264
+ * the return merge — empty when there is no such dispatch */
265
+ readonly dispatches: readonly Value[];
266
+ }
267
+
268
+ export const FALL_IN_GATES: readonly Gate<FallInCandidate>[] = [
269
+ {
270
+ // A DEFINITION rather than a tuning knob, and the one entry here nothing has been shown to
271
+ // move: a `cond_br` pred did not run on into this block, it chose it, so calling that a
272
+ // fall-in would be wrong about the CFG whatever it did to the bytes.
273
+ id: 'arrives-by-decision',
274
+ why: 'a `cond_br` pred CHOSE this block; that is a decision arriving, never a fall-in',
275
+ sound: false,
276
+ rejects: (c) => {
277
+ const t = terminator(c.q);
278
+ return t?.opcode !== 'br' || t.successors.length !== 1 || t.successors[0].block !== c.target;
279
+ },
280
+ },
281
+ {
282
+ // Paid for by a CORPUS row, not by a unit test: dropping it costs `synthetic:llshr:gcc2.7.2kmc`
283
+ // its sinking, and moves none of this file's fixtures.
284
+ id: 'bodyless-arm',
285
+ why: "gcc's `b .Ldefault` for an exhausted subtree is a decision that RAN OUT, not an arm",
286
+ sound: false,
287
+ rejects: (c) => isBodyless(c.q),
288
+ },
289
+ {
290
+ id: 'one-dispatch-owning-the-merge',
291
+ why: 'both arms of ONE dispatch on one scrutinee, and that dispatch owns the return merge',
292
+ sound: false,
293
+ guardedBy: 'retsink.test.ts: ablating the dispatch gate reads an `if` join, and a guarded switch, as fall-ins',
294
+ rejects: (c) => c.dispatches.length === 0,
295
+ },
296
+ ];
297
+
298
+ /** A return merge offered to the ONE-SET-ARM admission of the header. Like `FallInCandidate` the
299
+ * table is a value, so each clause can be dropped and the pass re-run on real input. */
300
+ export interface SelectCandidate {
301
+ /** the unconditional-branch predecessors of the merge */
302
+ readonly brPreds: readonly Block[];
303
+ /** every predecessor of the merge, `brPreds` included */
304
+ readonly preds: readonly Block[];
305
+ /** the block both arms are reached from, when exactly one block reaches both by a `cond_br`
306
+ * whose two successors ARE the arms — null when the shape is anything else */
307
+ readonly head: Block | null;
308
+ /** whether the merge was ALREADY a two-armed diamond on the CFG as it entered pre-recovery
309
+ * (`PreRecoveryFacts.mergeShapes`) — false for a block a later pass created or reshaped, and
310
+ * false whenever the caller threaded no map */
311
+ readonly preDiamond: boolean;
312
+ /** the ops defining the values the arms carry in, one per arm per returned operand; `undefined`
313
+ * where the value has no defining op (a block parameter, or a live-in) */
314
+ readonly carried: readonly (Op | undefined)[];
315
+ /** whether EVERY arm is one speculatable SET — `raise/narrowlocal.ts`'s exported `armIsOneSet`,
316
+ * this tree's one model of `gcc/jump.c:471-502`'s guard */
317
+ readonly armsOneSet: boolean;
318
+ /** whether THIS target's compiler is one the hoist was measured on
319
+ * (`compilerBehaviors.hoistsSingleSetArm`) */
320
+ readonly targetHoists: boolean;
321
+ }
322
+
323
+ export const SELECT_GATES: readonly Gate<SelectCandidate>[] = [
324
+ {
325
+ // The diamond itself: two distinct arms, each reached only from one head, and that head's
326
+ // `cond_br` choosing between exactly the two of them. A FALL-THROUGH switch has neither — its
327
+ // arms run on into one another and its tests reach the shared return directly — so this
328
+ // admission never overlaps the fall-in machinery above. What it DOES overlap is a two-way
329
+ // `switch` with a `default`: that is a diamond, it
330
+ // reaches this table, and it is judged on the shape it has rather than the keyword that spelled
331
+ // it (`sw2`, packages/cli/test/matching — a bodied one, so `arms-are-one-set` refuses it).
332
+ id: 'two-arms-one-head',
333
+ why: 'both arms chosen by ONE `cond_br` and reached from nowhere else — the diamond itself',
334
+ sound: false,
335
+ rejects: (c) => c.head === null,
336
+ },
337
+ {
338
+ // THE TOWER'S OBLIGATION FOR A BACKWARDS DEFAULT, discharged (header). The clause above reads
339
+ // the CFG as it stands at this pass's turn; this one asks whether the ROM had the same shape,
340
+ // by reading `mergeShapes` off the CFG as it ENTERED pre-recovery. `raise/shortcircuit.ts`
341
+ // MANUFACTURES two-armed diamonds out of condition trees the ROM never merged, and it runs
342
+ // before this pass; a manufactured one is not evidence about how agbcc spelled anything.
343
+ id: 'pre-diamond',
344
+ why: "the diamond must be the ROM's, not one a pre-recovery pass manufactured out of a condition tree",
345
+ sound: false,
346
+ guardedBy: 'retsink.test.ts: a diamond absent from the pre-recovery map is refused',
347
+ rejects: (c) => !c.preDiamond,
348
+ },
349
+ {
350
+ // `carried` is read off the two arms, so an arrival that is not an arm carries a value nothing
351
+ // here has judged. A guard branching onto the same `return` hands the merge whatever it was
352
+ // holding — and the hoist argument is about ALL of a merge variable's assignments, not two of
353
+ // the three.
354
+ id: 'no-arrival-but-the-arms',
355
+ why: 'a third in-edge carries a value the arm test never saw',
356
+ sound: false,
357
+ // Dropping it also changes the lift of `pokeemerald:GetGenderFromSpeciesAndPersonality:agbcc`.
358
+ guardedBy: 'retsink.test.ts: every one-set-arm clause refuses a shape the two-armed evidence does not cover',
359
+ rejects: (c) => c.preds.length !== c.brPreds.length,
360
+ },
361
+ {
362
+ // The claim is about a merge VARIABLE, and a `ret` with no operands has none: there is no value
363
+ // for agbcc to hoist above the compare, so nothing says it would not re-emit this shape.
364
+ //
365
+ // SUBSUMED ON THIS CORPUS, and the row ids are the point. Instrumenting `firstRejection` over
366
+ // all 1039 rows shows the clause first-refusing two agbcc sites — `kleod:Decompress` (kl-eod-decomp's source, before 2026-09-13) and
367
+ // `kleod:ReadKeyInput` — so the ARM Thumb frontend really does hand this table operand-less
368
+ // `ret` merges. Ablating it still moves 0 rows,
369
+ // because `arms-are-one-set` refuses both a step later. It is kept for the same reason
370
+ // `no-arrival-but-the-arms` is: the two make independent claims, and this one is the only thing
371
+ // between an operand-less `ret` and admission on a shape whose arms ARE one SET each.
372
+ id: 'a-value-is-returned',
373
+ why: 'a `ret` with no operands carries no merge variable, so the hoist the admission rests on cannot apply',
374
+ sound: false,
375
+ rejects: (c) => c.carried.length === 0,
376
+ },
377
+ {
378
+ // THE HOIST'S OWN GUARD, borrowed rather than re-derived: `raise/narrowlocal.ts`'s
379
+ // `armIsOneSet`, cited there line by line to `gcc/jump.c:474/:480/:482/:483`. An arm that is not
380
+ // one speculatable SET is not hoisted, so the merge-variable spelling keeps its diamond too and
381
+ // sinking trades a byte-exact match for the same shape in the other ARM ORDER — which only the
382
+ // ranked `/flip-branch` can pay back, and the unranked primary cannot. Paid for by five measured
383
+ // byte-exact matches (header).
384
+ //
385
+ // The three places this shared predicate is narrower than the optimizer it models — a
386
+ // foldable-immediate arm, an emptied arm, a two-constant arm — are named in the header; no
387
+ // corpus row inhabits any of them, and all three are in the refusing direction.
388
+ id: 'arms-are-one-set',
389
+ why: 'an arm that is not ONE speculatable SET is never hoisted, so the merge variable keeps the diamond too',
390
+ sound: false,
391
+ // The clause the compiled evidence is committed FOR: `selbody`/`selcomp3`/`selload` keep their
392
+ // diamond in both spellings while `selbtn`/`selk53`/`selpool`/`selcomp` lose it in the merge one
393
+ // (select-spelling.test.ts), and five shapes lose a byte-exact match without it
394
+ // (packages/cli/test/matching). On the corpus, dropping it changes `pokeemerald:GiveBerryPowder`.
395
+ guardedBy: 'select-spelling.test.ts: an arm that is NOT one SET CAN be spelled with a merge variable',
396
+ rejects: (c) => !c.armsOneSet,
397
+ },
398
+ {
399
+ // THE COMPILER THE ARGUMENT IS ABOUT. Every measurement behind this table is agbcc -O2
400
+ // -mthumb; nothing has compiled the pair on IDO, KMC GCC or mwcc. The rest of this file is
401
+ // ISA-neutral and runs for all four, so without this clause an agbcc cost model would decide a
402
+ // PowerPC function with nothing in the code saying so. It claims nothing instead
403
+ // (`target.ts hoistsSingleSetArm`, absent ⇒ false), which is free: re-lifting all 1039 corpus
404
+ // rows shows the admission reaches no non-agbcc row either way.
405
+ //
406
+ // LAST in the table on purpose. A census attributes a site to the clause that FIRST refuses it,
407
+ // so first position would charge this COMPILER fact with 28 refusals, 26 of them shapes that are
408
+ // not diamonds at all. Last, it decides 0 sites on the whole corpus, which is exactly what
409
+ // `target.ts` claims for it.
410
+ id: 'compiler-hoists-single-set-arm',
411
+ why: 'the hoist this admission reads backwards was measured on agbcc and declared nowhere else',
412
+ sound: false,
413
+ rejects: (c) => !c.targetHoists,
414
+ },
415
+ ];
416
+
417
+ /** The two questions the fall-in clauses ask of the function's comparison-tree dispatches. */
418
+ interface DispatchModel {
419
+ /** Is this block part of the dispatch on `s` — either one of its tests, or an arm of one? */
420
+ inDispatch(b: Block, s: Value): boolean;
421
+ /** The scrutinees for which `q` and `target` are arms of two DIFFERENT tests: the dispatches in
422
+ * which one could be the previous arm of the other. Two successors of ONE `cond_br` — the body
423
+ * and the join of an `if` with no `else` — share no such scrutinee, which is the whole point. */
424
+ siblingArms(q: Block, target: Block): Value[];
425
+ }
426
+
427
+ /** THE DISPATCH MODEL. A TEST BLOCK ends in a `cond_br` on an integer comparison of exactly one
428
+ * non-constant value against constants — the SCRUTINEE. Two test blocks belong to the same
429
+ * dispatch when they test the same scrutinee: `recognizeSwitch`'s own PRE1 ("every test is on the
430
+ * SAME Value") read at the raise level, without its dominance, purity or interval preconditions —
431
+ * those decide whether a `switch` can be SPELLED, and this pass only needs to know a decision tree
432
+ * is there. `NEGATED_ICMP` (ir/opcodes.ts) is the shared spelling of the icmp family, so an
433
+ * eleventh comparison joins this model for free.
434
+ *
435
+ * Constant folding is deliberately NOT reproduced (`switch-recover.ts evalConst` folds agbcc's
436
+ * synthesized immediates): a test whose constant side this cannot see contributes two
437
+ * non-constant operands and is skipped, which loses a subtraction rather than inventing one.
438
+ *
439
+ * Built ONCE, before `sinkReturns`' merge loop, and read-only thereafter — nothing in the loop
440
+ * writes either table, so the merge that is rewritten first sees the same dispatches as the last. */
441
+ function dispatchModel(fn: Fn, defs: Map<Value, Op>): DispatchModel {
442
+ const scrutOf = new Map<Block, Value>();
443
+ /** Arms, indexed by the block reached and the scrutinee whose test sent it there — the test
444
+ * blocks are the value, because a fall-in requires the two arms to come from DIFFERENT tests. */
445
+ const armsOf = new Map<Block, Map<Value, Set<Block>>>();
446
+ for (const b of fn.blocks) {
447
+ const t = terminator(b);
448
+ if (t?.opcode !== 'cond_br') {
449
+ continue;
450
+ }
451
+ const cmp = defs.get(t.operands[0]);
452
+ if (!cmp || !(cmp.opcode in NEGATED_ICMP)) {
453
+ continue;
454
+ }
455
+ const vars = cmp.operands.filter((o) => defs.get(o)?.opcode !== 'const');
456
+ if (vars.length !== 1) {
457
+ continue;
458
+ }
459
+ const scrut = vars[0];
460
+ scrutOf.set(b, scrut);
461
+ for (const e of t.successors) {
462
+ let byScrut = armsOf.get(e.block);
463
+ if (!byScrut) {
464
+ byScrut = new Map();
465
+ armsOf.set(e.block, byScrut);
466
+ }
467
+ const tests = byScrut.get(scrut) ?? new Set<Block>();
468
+ tests.add(b);
469
+ byScrut.set(scrut, tests);
470
+ }
471
+ }
472
+ return {
473
+ inDispatch: (b, s) => scrutOf.get(b) === s || !!armsOf.get(b)?.has(s),
474
+ siblingArms: (q, target) => {
475
+ const aq = armsOf.get(q);
476
+ const at = armsOf.get(target);
477
+ if (!aq || !at) {
478
+ return [];
479
+ }
480
+ const out: Value[] = [];
481
+ for (const [s, testsQ] of aq) {
482
+ const testsT = at.get(s);
483
+ if (testsT && [...testsQ].some((c) => [...testsT].some((d) => c !== d))) {
484
+ out.push(s);
485
+ }
486
+ }
487
+ return out;
488
+ },
489
+ };
490
+ }
491
+
492
+ /** What the one-set-arm admission needs from OUTSIDE the IR it is handed.
493
+ *
494
+ * `hoistsSingleSetArm` is the COMPILER fact, threaded from `decompile`'s own target — the SAME
495
+ * `compilerBehaviors` field `raise/narrowlocal.ts` reads, because it is the same `gcc/jump.c` guard.
496
+ * Absent ⇒ the one-set-arm admission never fires; the short-circuit admissions are
497
+ * compiler-independent and unaffected.
498
+ *
499
+ * `mergeShapes` is the CFG AS IT ENTERED PRE-RECOVERY (`PreRecoveryFacts`), and it is threaded for
500
+ * the reason narrowlocal threads it: `raise/shortcircuit.ts` manufactures two-armed diamonds before
501
+ * this pass runs, and a manufactured one says nothing about how agbcc spelled the function. Absent
502
+ * ⇒ `pre-diamond` refuses every site, so a caller that does not thread it gets no one-set-arm
503
+ * admission at all — the refusing direction, and what `sinkReturns`' hand-built unit callers get
504
+ * unless they opt in. */
505
+ export interface RetSinkOptions {
506
+ readonly hoistsSingleSetArm?: boolean;
507
+ readonly mergeShapes?: ReadonlyMap<Block, MergeShape>;
508
+ }
509
+
510
+ /** Tail-duplicate a return-only merge block into its unconditional-branch predecessors, in the three
511
+ * shapes the header argues for: a short-circuit chain visible in the CFG, one fused into a
512
+ * connective, and a two-armed diamond whose arms are ONE speculatable SET each. Returns whether
513
+ * anything changed. A "return-only" block is exactly one `ret` whose operands are all its own
514
+ * block-params, so each predecessor already carries the returned value as a successor arg. */
515
+ export function sinkReturns(
516
+ fn: Fn,
517
+ opts: RetSinkOptions = {},
518
+ gates: readonly Gate<FallInCandidate>[] = FALL_IN_GATES,
519
+ selectGates: readonly Gate<SelectCandidate>[] = SELECT_GATES,
520
+ ): boolean {
35
521
  let changed = false;
36
522
  const preds = predecessors(fn);
37
523
  const defs = defOpMap(fn);
524
+ const { inDispatch, siblingArms } = dispatchModel(fn, defs);
525
+ // WHICH READS NEED `terminator`'s UNDEFINED CASE, which is not "every read of a terminator". The
526
+ // scan over `fn.blocks` can meet a block with no ops at all, and that is the one read the guard
527
+ // is for: `ir/verify.ts` rejects an empty block and `pipeline.ts` verifies before calling this,
528
+ // but `sinkReturns` is exported and its tests build blocks by hand, where a refusal is a better
529
+ // answer than a TypeError. A read over a PREDECESSOR needs none and does not have one —
530
+ // `predecessors` is built from `successorsOf`, which is empty for a block with no terminator, so
531
+ // a bodyless block never appears in anyone's predecessor list. Once a block is known to end in a
532
+ // `br`, the rewrite below indexes its terminator directly.
38
533
  const isBrTo = (p: Block, m: Block) => {
39
- const t = p.ops[p.ops.length - 1];
40
- return t.opcode === 'br' && t.successors.length === 1 && t.successors[0].block === m;
534
+ const t = terminator(p);
535
+ return t?.opcode === 'br' && t.successors.length === 1 && t.successors[0].block === m;
41
536
  };
42
537
  for (const m of [...fn.blocks]) {
538
+ // RETURN-ONLY merges. A merged `store…; ret` is `raise/tailsink.ts`'s, and only as rank.ts's
539
+ // `/shared-tail` twin: its IR comes from both spellings.
43
540
  if (m.ops.length !== 1) {
44
541
  continue;
45
542
  }
@@ -59,8 +556,9 @@ export function sinkReturns(fn: Fn): boolean {
59
556
  }
60
557
  // SHORT-CIRCUIT GATE, in two shapes — the chain must be visible in the CFG or in the value domain.
61
558
  //
62
- // (a) UNFUSED: at least one branch-pred is a shared block (≥2 preds of its own) — the common
63
- // early-exit reached from every condition of the chain.
559
+ // (a) UNFUSED: at least one branch-pred is ARRIVED AT from ≥2 places — the common early-exit
560
+ // reached from every condition of the chain. Everything that reaches it counts EXCEPT the
561
+ // previous arm running on (`fellInto` below).
64
562
  // (b) FUSED: `branch-shortcircuit` (raise/shortcircuit.ts) rewrites the head's condition into a
65
563
  // `logic_and`/`logic_or` and collapses the second condition block into it. That leaves both
66
564
  // arms single-pred, so (a) cannot see the chain any more — but the CONNECTIVE is now the
@@ -84,7 +582,66 @@ export function sinkReturns(fn: Fn): boolean {
84
582
  return t.opcode === 'cond_br' && CONNECTIVES.has(defs.get(t.operands[0])?.opcode ?? '');
85
583
  });
86
584
  const fusedDiamond = brPreds.length >= 2 && brPreds.some(selectedByConnective);
87
- if (!brPreds.some((p) => (preds.get(p)?.length ?? 0) >= 2) && !fusedDiamond) {
585
+ // Does the dispatch on `s` OWN this merge? Every predecessor of `m` must be part of it — one of
586
+ // its tests, or an arm of one. A `goto` from outside the switch onto the same `return` fails
587
+ // this on the scrutinee it tests, so nothing is subtracted and the merge is sunk. `ps`, not
588
+ // `brPreds`: that outside arrival is a `cond_br` (the guard's `bgt`), which never appears in
589
+ // `brPreds`.
590
+ const ownedBy = (s: Value) => ps.every((p) => inDispatch(p, s));
591
+ // `q` FELL INTO `p`: it is the previous arm of the same dispatch, running on. The clauses are
592
+ // `FALL_IN_GATES` above, argued in this file's header; `arrivals` counts every OTHER
593
+ // predecessor. Layout adjacency — `q` sitting immediately above `p`, which is what "fell
594
+ // through" means in the assembly — is NOT a clause: it moves no corpus row, and it would be an
595
+ // unpaid premise about `fn.blocks` still being address order.
596
+ const fellInto = (q: Block, target: Block) =>
597
+ firstRejection(gates, { q, target, dispatches: siblingArms(q, target).filter(ownedBy) }) === null;
598
+ const arrivals = (p: Block) => (preds.get(p) ?? []).filter((q) => !fellInto(q, p)).length;
599
+ // (c) ONE-SET-ARM DIAMOND — the header's compiler fact. `head` is the diamond read backwards:
600
+ // each arm's only predecessor is the same block, and that block's `cond_br` chooses between the
601
+ // two of them. `carried` is what each arm hands the merge, one entry per arm per returned
602
+ // operand, so a pair whose values come in by different edges is judged together.
603
+ //
604
+ // It is deliberately NOT `narrowlocal.ts`'s `mergeArms`, even though the two agree conjunct for
605
+ // conjunct once `no-arrival-but-the-arms` has run. That one answers "was this a diamond in the
606
+ // ROM" and is read off the PRE-RECOVERY CFG; `pre-diamond` carries that answer here. This one
607
+ // answers "is it a diamond NOW", at the moment a tail duplication is about to rewrite these
608
+ // terminators, and only the live CFG can say so.
609
+ const armsMeetAt = (): Block | null => {
610
+ const [x, y] = brPreds;
611
+ if (brPreds.length !== 2 || x === y) {
612
+ return null;
613
+ }
614
+ const [px, py] = [preds.get(x) ?? [], preds.get(y) ?? []];
615
+ if (px.length !== 1 || py.length !== 1 || px[0] !== py[0]) {
616
+ return null;
617
+ }
618
+ const t = terminator(px[0]);
619
+ const succs = t?.opcode === 'cond_br' ? t.successors.map((e) => e.block) : [];
620
+ return succs.length === 2 && succs.includes(x) && succs.includes(y) ? px[0] : null;
621
+ };
622
+ /** What an arm carries in: one entry per returned operand, resolved through the function-wide
623
+ * `defs` because the value may be defined in the head rather than in the arm. */
624
+ const carriedBy = (p: Block) => {
625
+ const args = p.ops[p.ops.length - 1].successors[0].args;
626
+ return ret.operands.map((o) => defs.get(args[m.params.indexOf(o)]));
627
+ };
628
+ // LAZY, and not for speed: `gate-census.ts` counts a table's refusals as sites it DECIDED, and
629
+ // the short-circuit admissions above settle most sites before this table would have a say.
630
+ // Evaluating it there would report refusals at sites whose answer was never in question.
631
+ const constantSelect = () => {
632
+ return (
633
+ firstRejection(selectGates, {
634
+ brPreds,
635
+ preds: ps,
636
+ head: armsMeetAt(),
637
+ preDiamond: opts.mergeShapes?.get(m)?.diamond === true,
638
+ carried: brPreds.flatMap(carriedBy),
639
+ armsOneSet: brPreds.every(armIsOneSet),
640
+ targetHoists: opts.hoistsSingleSetArm === true,
641
+ }) === null
642
+ );
643
+ };
644
+ if (!brPreds.some((p) => arrivals(p) >= 2) && !fusedDiamond && !constantSelect()) {
88
645
  continue;
89
646
  }
90
647
  for (const p of brPreds) {
@@ -98,5 +655,14 @@ export function sinkReturns(fn: Fn): boolean {
98
655
  fn.blocks = fn.blocks.filter((b) => b !== m);
99
656
  }
100
657
  }
658
+ // Sinking RETIRES in-edges. A merge also reached by a `cond_br` keeps that one — a conditional
659
+ // branch cannot carry a `ret` — and so survives with a SINGLE predecessor, where its parameter is
660
+ // no longer a join but an alias of that edge's argument. Left standing, the structurer destroys
661
+ // the alias into a local of its own (`v0 = 0; return v0;`) and Regime-A switch recovery reads the
662
+ // block as a second, distinct default candidate. The cleanup is `ir/simplify.ts`'s own; it simply
663
+ // has no other caller downstream of here.
664
+ if (changed) {
665
+ simplifyTrivialPhis(fn);
666
+ }
101
667
  return changed;
102
668
  }