@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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -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 +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -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/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- 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 +176 -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 +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- 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 +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -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 +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/raise/retsink.ts
CHANGED
|
@@ -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
|
|
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 —
|
|
15
|
-
// (`c ? x : y`, and the branchless-compare idioms `clamp0`/`le0`/…) also
|
|
16
|
-
// merge,
|
|
17
|
-
// would REGRESS
|
|
18
|
-
// SHARED arm
|
|
19
|
-
// simple diamond's arms each
|
|
20
|
-
// the merge is
|
|
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
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
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
|
|
40
|
-
return t
|
|
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
|
|
63
|
-
//
|
|
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
|
-
|
|
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
|
}
|