@asmlift/core 0.6.0 → 0.8.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 +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -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,13 +11,188 @@
|
|
|
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 — the common early-exit reached from
|
|
19
|
-
// each reached from one. So sink
|
|
20
|
-
//
|
|
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.
|
|
21
196
|
//
|
|
22
197
|
// THE QUANTITY IS ARRIVALS, NOT PREDECESSORS. A FALL-THROUGH switch arm is the difference:
|
|
23
198
|
// `case 2: r++; case 1: r++;` gives case 1's body two predecessors — the dispatch's `beq`, and
|
|
@@ -64,10 +239,11 @@
|
|
|
64
239
|
//
|
|
65
240
|
// This does NOT recover the boolean-VALUE form `return a && b` — that is shortcircuit.ts's job
|
|
66
241
|
// (the `logic_and`/`logic_or` connective plus agbcc's `(-b|b)>>31` = `b!=0` normalisation).
|
|
67
|
-
import { Block, Fn, Op, Value, defOpMap, isBodyless, mkOp, predecessors, terminator } from '../ir/core';
|
|
242
|
+
import { Block, Fn, Op, Value, defOpMap, fallThroughOf, isBodyless, mkOp, predecessors, terminator } from '../ir/core';
|
|
68
243
|
import { NEGATED_ICMP } from '../ir/opcodes';
|
|
69
244
|
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
70
245
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
246
|
+
import { type MergeShape, armIsOneSet } from './narrowlocal';
|
|
71
247
|
|
|
72
248
|
/** The fused short-circuit connectives (raise/shortcircuit.ts). A `cond_br` on one of these is the
|
|
73
249
|
* post-fusion record of the ≥2 conditions that used to reach a shared arm. */
|
|
@@ -114,11 +290,130 @@ export const FALL_IN_GATES: readonly Gate<FallInCandidate>[] = [
|
|
|
114
290
|
id: 'one-dispatch-owning-the-merge',
|
|
115
291
|
why: 'both arms of ONE dispatch on one scrutinee, and that dispatch owns the return merge',
|
|
116
292
|
sound: false,
|
|
117
|
-
guardedBy: 'ablating the dispatch gate reads an `if` join, and a guarded switch, as fall-ins',
|
|
293
|
+
guardedBy: 'retsink.test.ts: ablating the dispatch gate reads an `if` join, and a guarded switch, as fall-ins',
|
|
118
294
|
rejects: (c) => c.dispatches.length === 0,
|
|
119
295
|
},
|
|
120
296
|
];
|
|
121
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
|
+
|
|
122
417
|
/** The two questions the fall-in clauses ask of the function's comparison-tree dispatches. */
|
|
123
418
|
interface DispatchModel {
|
|
124
419
|
/** Is this block part of the dispatch on `s` — either one of its tests, or an arm of one? */
|
|
@@ -194,11 +489,35 @@ function dispatchModel(fn: Fn, defs: Map<Value, Op>): DispatchModel {
|
|
|
194
489
|
};
|
|
195
490
|
}
|
|
196
491
|
|
|
197
|
-
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
|
|
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 {
|
|
202
521
|
let changed = false;
|
|
203
522
|
const preds = predecessors(fn);
|
|
204
523
|
const defs = defOpMap(fn);
|
|
@@ -216,6 +535,8 @@ export function sinkReturns(fn: Fn, gates: readonly Gate<FallInCandidate>[] = FA
|
|
|
216
535
|
return t?.opcode === 'br' && t.successors.length === 1 && t.successors[0].block === m;
|
|
217
536
|
};
|
|
218
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.
|
|
219
540
|
if (m.ops.length !== 1) {
|
|
220
541
|
continue;
|
|
221
542
|
}
|
|
@@ -275,13 +596,62 @@ export function sinkReturns(fn: Fn, gates: readonly Gate<FallInCandidate>[] = FA
|
|
|
275
596
|
const fellInto = (q: Block, target: Block) =>
|
|
276
597
|
firstRejection(gates, { q, target, dispatches: siblingArms(q, target).filter(ownedBy) }) === null;
|
|
277
598
|
const arrivals = (p: Block) => (preds.get(p) ?? []).filter((q) => !fellInto(q, p)).length;
|
|
278
|
-
|
|
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()) {
|
|
279
645
|
continue;
|
|
280
646
|
}
|
|
281
647
|
for (const p of brPreds) {
|
|
282
|
-
const
|
|
283
|
-
const sunk = ret.operands.map((o) => args[m.params.indexOf(o)]);
|
|
284
|
-
|
|
648
|
+
const t = p.ops[p.ops.length - 1];
|
|
649
|
+
const sunk = ret.operands.map((o) => t.successors[0].args[m.params.indexOf(o)]);
|
|
650
|
+
// The `br` being replaced carries whether the machine BRANCHED to this epilogue or fell into
|
|
651
|
+
// it, which is what `structure/retspell.ts` reads. Sinking puts the return ON that edge, so
|
|
652
|
+
// the edge's fact travels with it; ask the new return block's in-edges instead and they answer
|
|
653
|
+
// how control reached the statements above the return, not how it reached the epilogue.
|
|
654
|
+
p.ops[p.ops.length - 1] = mkOp('ret', { operands: sunk, attrs: fallThroughOf(t) });
|
|
285
655
|
changed = true;
|
|
286
656
|
}
|
|
287
657
|
// If no predecessor still branches to m (all were unconditional), it is unreachable — drop it.
|