@asmlift/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +130 -4
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +181 -4
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +37 -29
- package/src/frontend/opaque.ts +70 -20
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +279 -56
- package/src/frontend/thumb.ts +1372 -87
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +57 -3
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +221 -0
- package/src/l3/ast.ts +127 -5
- package/src/l3/basecse.ts +58 -62
- package/src/l3/coalesce.ts +215 -0
- package/src/l3/dce.ts +33 -41
- package/src/l3/gates.ts +67 -0
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +440 -0
- package/src/l3/tailmerge.ts +124 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +65 -6
- package/src/raise/divpow2.ts +227 -0
- package/src/raise/gvn.ts +151 -0
- package/src/raise/pre-recovery.ts +39 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +37 -7
- package/src/raise/shortcircuit.ts +262 -22
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +41 -3
- package/src/rank.ts +196 -20
- package/src/structure/analysis.ts +175 -89
- package/src/structure/structure.ts +588 -55
- package/src/structure/switch-recover.ts +117 -30
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// asmlift — signed division by a power of two, in its BRANCHING form.
|
|
2
|
+
//
|
|
3
|
+
// `x / 2^k` must round toward zero, but an arithmetic right shift rounds toward minus infinity, so a
|
|
4
|
+
// compiler biases the dividend by `2^k - 1` when it is negative. Two lowerings exist, and asmlift
|
|
5
|
+
// already folded one of them: the branchless `(x + (x >>u 31)) >> 1` that agbcc and GCC emit for
|
|
6
|
+
// `/2` is the SDIV_POW2_2 idiom pattern (pattern/engine.ts). The other is a BRANCH — what IDO emits,
|
|
7
|
+
// and what GCC emits for larger k:
|
|
8
|
+
//
|
|
9
|
+
// bgez a0, .L2 ; skip the bias when x >= 0
|
|
10
|
+
// addiu at, a0, 1 ; bias = 2^k - 1
|
|
11
|
+
// .L2: sra v0, at, 1 ; >> k
|
|
12
|
+
//
|
|
13
|
+
// That is a CFG diamond, not an instruction window, so the patterns-as-data idiom layer cannot state
|
|
14
|
+
// it — its match DAG is over the def-graph inside a block. It earns a pre-recovery pass for the same
|
|
15
|
+
// reason raise/shortcircuit.ts is one: the thing being recognised is a shape in the CONTROL FLOW.
|
|
16
|
+
// (m2c reached the same conclusion from the other side — its `49b5d87` adds a third and fourth
|
|
17
|
+
// instruction-window pattern for the GCC spellings, and the commit notes GCC reorders the final
|
|
18
|
+
// `sra` and defeats the window. Matching the value graph instead is immune to that.)
|
|
19
|
+
//
|
|
20
|
+
// Both spellings of the merge appear in the corpus and both are recognised here:
|
|
21
|
+
//
|
|
22
|
+
// SUNK the shift is after the merge — phi(x, x + 2^k-1) feeding `shr_s(phi, k)`
|
|
23
|
+
// SPLIT the shift is duplicated into both arms — phi(shr_s(x, k), shr_s(x + 2^k-1, k)),
|
|
24
|
+
// which is what a delay-slot fill produces (the `sra` runs on both paths)
|
|
25
|
+
//
|
|
26
|
+
// The identity is arithmetic — for signed x, `(x < 0 ? x + 2^k-1 : x) >> k` is exactly `x / 2^k`
|
|
27
|
+
// under C's truncating division — but by this project's taxonomy that is NOT on its own a licence
|
|
28
|
+
// to run ungated: the compiler-pinned idiom patterns are gated precisely because they trade one
|
|
29
|
+
// spelling for another and are byte-safe only where measured, and this pass does trade a spelling.
|
|
30
|
+
// What carries it is raise/magicdiv.ts's argument, which applies here unchanged: the round-trip is
|
|
31
|
+
// SELF-VERIFYING. asmlift emits a plain `x / 2^k` and the target compiler regenerates ITS own
|
|
32
|
+
// lowering; a wrong divisor recompiles to different bytes and shows up as a nonmatch, never as a
|
|
33
|
+
// false match. The residual exposure is a lost match, not a miscompile — on a compiler that lowers
|
|
34
|
+
// `/2^k` branchlessly, a diamond of this shape came from hand-written biasing, and respelling it
|
|
35
|
+
// costs a match that used to land. Measured positive on ido7.1 (two flips), agbcc and gcc2.7.2kmc
|
|
36
|
+
// (modpow2 stays byte-exact through the respelling); mwcc_242_81 and gcc2.7.2 have no inhabitant, so
|
|
37
|
+
// they are unmeasured rather than clean.
|
|
38
|
+
//
|
|
39
|
+
// It is deliberately IDENTITY-OR-DECLINE about the shape (the bias constant must be exactly
|
|
40
|
+
// `2^k - 1` for the SAME k the shift uses, the guard must test the SAME value the arms divide, and
|
|
41
|
+
// the biased arm must be the negative one — the consistency check m2c's `49b5d87` also adds),
|
|
42
|
+
// because every one of those is a way for a superficially similar diamond to mean something else.
|
|
43
|
+
import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
|
|
44
|
+
import { HOIST_UNSAFE_OPS } from '../ir/opcodes';
|
|
45
|
+
import { T } from '../ir/types';
|
|
46
|
+
|
|
47
|
+
/** `shr_s v {imm=k}` → k, else null. */
|
|
48
|
+
function shiftAmount(defs: Map<Value, Op>, v: Value): { k: number; src: Value; op: Op } | null {
|
|
49
|
+
const d = defs.get(v);
|
|
50
|
+
if (!d || d.opcode !== 'shr_s' || d.operands.length !== 1 || typeof d.attrs.imm !== 'number') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return { k: d.attrs.imm, src: d.operands[0], op: d };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `add v, const(2^k - 1)` → the addend's base, else null. The add is commutative. */
|
|
57
|
+
function biasedBy(defs: Map<Value, Op>, v: Value, k: number): Value | null {
|
|
58
|
+
const d = defs.get(v);
|
|
59
|
+
if (!d || d.opcode !== 'add' || d.operands.length !== 2) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
for (const [a, b] of [
|
|
63
|
+
[0, 1],
|
|
64
|
+
[1, 0],
|
|
65
|
+
] as const) {
|
|
66
|
+
const c = defs.get(d.operands[b]);
|
|
67
|
+
if (c && c.opcode === 'const' && c.attrs.value === 2 ** k - 1) {
|
|
68
|
+
return d.operands[a];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fold the branching signed-division-by-2^k diamond into `sdiv x {imm=2^k}`. Returns true if the IR
|
|
76
|
+
* changed. Leaves the now-dead bias/shift ops behind for the driver's DCE.
|
|
77
|
+
*/
|
|
78
|
+
export function recognizeDivPow2(fn: Fn): boolean {
|
|
79
|
+
let changed = false;
|
|
80
|
+
let progress = true;
|
|
81
|
+
while (progress) {
|
|
82
|
+
progress = false;
|
|
83
|
+
const defs = defOpMap(fn);
|
|
84
|
+
const preds = predecessors(fn);
|
|
85
|
+
const term = (b: Block) => b.ops[b.ops.length - 1];
|
|
86
|
+
|
|
87
|
+
outer: for (const m of fn.blocks) {
|
|
88
|
+
// The merge carries exactly the quotient (or the value about to be shifted into it), and is
|
|
89
|
+
// reached only by the two arms of this diamond — a third predecessor means the phi is a join
|
|
90
|
+
// of something larger and retiring it would drop that edge's value.
|
|
91
|
+
//
|
|
92
|
+
// NEITHER end of the diamond may be the ENTRY block. `predecessors()` walks successor edges
|
|
93
|
+
// only, so an entry that is also a loop header shows two predecessors while really being a
|
|
94
|
+
// three-way join — the implicit entry edge is invisible, and the head does not dominate it.
|
|
95
|
+
// The bias-arm case blows up loudly in the verifier; the MERGE case is silent, because its
|
|
96
|
+
// params are the FUNCTION'S OWN PARAMETERS and `m.params = []` below would quietly delete one,
|
|
97
|
+
// handing back a signature with an argument missing. raise/shortcircuit.ts documents the same
|
|
98
|
+
// trap for its feeder; only half of that guard was copied here at first.
|
|
99
|
+
if (m.params.length !== 1 || (preds.get(m) ?? []).length !== 2 || m === fn.blocks[0]) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const p = m.params[0];
|
|
103
|
+
for (const bias of preds.get(m)!) {
|
|
104
|
+
// The BIAS arm: sole predecessor is the head, and it does nothing but bias (and possibly
|
|
105
|
+
// shift). The whole block is DELETED, not hoisted, so anything else in it would be silently
|
|
106
|
+
// dropped — a store, a call or an opaque there would simply stop happening (an opaque
|
|
107
|
+
// whether or not its result is read: liveness says nothing about what the instruction did).
|
|
108
|
+
const bt = term(bias);
|
|
109
|
+
if (bt.opcode !== 'br' || bt.successors[0]?.block !== m || bias === fn.blocks[0]) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const bp = preds.get(bias) ?? [];
|
|
113
|
+
if (bp.length !== 1 || bias.ops.some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const h = bp[0];
|
|
117
|
+
const ht = term(h);
|
|
118
|
+
if (ht.opcode !== 'cond_br') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const [taken, fall] = ht.successors;
|
|
122
|
+
const direct = taken.block === m ? taken : fall.block === m ? fall : null;
|
|
123
|
+
if (!direct || (taken.block !== bias && fall.block !== bias)) {
|
|
124
|
+
continue; // the head's two successors must be exactly {merge, bias arm}
|
|
125
|
+
}
|
|
126
|
+
const biasedIsTaken = taken.block === bias;
|
|
127
|
+
|
|
128
|
+
// Read the two incoming values and split into the SUNK and SPLIT spellings. `k` and the
|
|
129
|
+
// dividend come from whichever arm shape matches; both arms must agree on both.
|
|
130
|
+
const vDirect = direct.args[0];
|
|
131
|
+
const vBias = bt.successors[0].args[0];
|
|
132
|
+
if (vDirect === undefined || vBias === undefined) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// SPLIT is TRIED, not assumed, and a failed attempt falls through to SUNK. The direct arm
|
|
136
|
+
// being a `shr_s` does not make this the split form — the dividend may simply BE a shifted
|
|
137
|
+
// value (`(a >> 2) / 4`), and treating that as split-with-a-broken-bias-arm used to abandon
|
|
138
|
+
// a perfectly good sunk diamond.
|
|
139
|
+
let split: { k: number; x: Value } | null = null;
|
|
140
|
+
const splitDirect = shiftAmount(defs, vDirect);
|
|
141
|
+
if (splitDirect) {
|
|
142
|
+
const splitBias = shiftAmount(defs, vBias);
|
|
143
|
+
if (
|
|
144
|
+
splitBias &&
|
|
145
|
+
splitBias.k === splitDirect.k &&
|
|
146
|
+
biasedBy(defs, splitBias.src, splitDirect.k) === splitDirect.src
|
|
147
|
+
) {
|
|
148
|
+
split = { k: splitDirect.k, x: splitDirect.src };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let k: number, x: Value, sunkShift: Op | null;
|
|
152
|
+
if (split) {
|
|
153
|
+
// SPLIT: both arms shift. The direct arm shifts the dividend, the bias arm the biased one.
|
|
154
|
+
k = split.k;
|
|
155
|
+
x = split.x;
|
|
156
|
+
sunkShift = null;
|
|
157
|
+
} else {
|
|
158
|
+
// SUNK: the merge value is the (maybe biased) dividend, shifted once after the join. The
|
|
159
|
+
// phi must feed exactly that shift and NOTHING else — the fold stops computing the
|
|
160
|
+
// unshifted biased value, so any second consumer would silently read the quotient in its
|
|
161
|
+
// place. A use is an operand OR a successor argument (block args are uses too, and live in
|
|
162
|
+
// `successors[].args`, which is why `replaceAllUsesWith` rewrites both) — counting only
|
|
163
|
+
// operands let a phi that was also passed along an edge through this guard, and the arm's
|
|
164
|
+
// value then came out as the quotient: `(y >> 2) + y` folded to `(x / 4) + (x / 4)`.
|
|
165
|
+
const uses = fn.blocks
|
|
166
|
+
.flatMap((b) => b.ops)
|
|
167
|
+
.filter((op) => op.operands.includes(p) || op.successors.some((su) => su.args.includes(p)));
|
|
168
|
+
if (uses.length !== 1 || !uses[0].operands.includes(p)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const sunk = shiftAmount(defs, uses[0].results[0]);
|
|
172
|
+
// The shift must be IN the merge block: the cleanup below removes it from there, so a
|
|
173
|
+
// shift found elsewhere would be searched for and not cleaned, leaving the two out of step.
|
|
174
|
+
if (!sunk || sunk.src !== p || !m.ops.includes(sunk.op)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
k = sunk.k;
|
|
178
|
+
x = vDirect;
|
|
179
|
+
if (biasedBy(defs, vBias, k) !== x) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
sunkShift = sunk.op;
|
|
183
|
+
}
|
|
184
|
+
if (k < 1 || k > 30) {
|
|
185
|
+
continue; // 2^k must be a representable positive divisor
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The guard must test THE DIVIDEND against zero, and route the biased value to the NEGATIVE
|
|
189
|
+
// side. `x >= 0` takes the direct arm; `x < 0` takes the bias arm. Anything else — a compare
|
|
190
|
+
// against another value, a different operand, the arms the other way round — is a diamond
|
|
191
|
+
// that merely looks like this one.
|
|
192
|
+
const cond = defs.get(ht.operands[0]);
|
|
193
|
+
if (!cond || cond.operands.length !== 2 || cond.operands[0] !== x) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const zero = defs.get(cond.operands[1]);
|
|
197
|
+
if (!zero || zero.opcode !== 'const' || zero.attrs.value !== 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const takenIsNegative = cond.opcode === 'icmp_slt';
|
|
201
|
+
if (!takenIsNegative && cond.opcode !== 'icmp_sge') {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (takenIsNegative !== biasedIsTaken) {
|
|
205
|
+
continue; // the bias is on the wrong side — this is not a round-toward-zero correction
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Rewrite: the head computes the quotient outright and falls straight to the merge; the bias
|
|
209
|
+
// arm and the phi go away. The dead bias/shift ops are left for the driver's DCE.
|
|
210
|
+
const q = mkValue(T.s());
|
|
211
|
+
h.ops.splice(h.ops.length - 1, 0, mkOp('sdiv', { operands: [x], results: [q], attrs: { imm: 2 ** k } }));
|
|
212
|
+
h.ops[h.ops.length - 1] = mkOp('br', { successors: [{ block: m, args: [] }] });
|
|
213
|
+
if (sunkShift) {
|
|
214
|
+
replaceAllUsesWith(fn, sunkShift.results[0], q);
|
|
215
|
+
m.ops = m.ops.filter((op) => op !== sunkShift);
|
|
216
|
+
}
|
|
217
|
+
replaceAllUsesWith(fn, p, q);
|
|
218
|
+
m.params = [];
|
|
219
|
+
fn.blocks = fn.blocks.filter((b) => b !== bias);
|
|
220
|
+
changed = true;
|
|
221
|
+
progress = true;
|
|
222
|
+
break outer; // defs/preds are stale after the mutation
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return changed;
|
|
227
|
+
}
|
package/src/raise/gvn.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// asmlift — value numbering for OPERAND-FREE PURE definitions (today: `gaddr`).
|
|
2
|
+
//
|
|
3
|
+
// A compiler materializes a global's address wherever it needs one. Two arms of an `if` that both
|
|
4
|
+
// touch `gTable` each get their own pool load, so the frontend lifts two DISTINCT SSA values that
|
|
5
|
+
// denote the same address, and a merge of those arms gets a block param over them:
|
|
6
|
+
//
|
|
7
|
+
// ^bb6: %29 = gaddr {sym="gBgTilemapBufs"} br ^bb9(%29)
|
|
8
|
+
// ^bb8: %43 = gaddr {sym="gBgTilemapBufs"} br ^bb9(%43)
|
|
9
|
+
// ^bb9(%45: u16*): … %45[594] …
|
|
10
|
+
//
|
|
11
|
+
// Nothing downstream can see that `%29` and `%43` are equal, so `%45` is a real phi: the structurer
|
|
12
|
+
// destroys it into a local and every later access reads it. The source it came from had no such
|
|
13
|
+
// variable — it just named the global at each use, and let the compiler decide where to put the
|
|
14
|
+
// address. In emitted C, that is the whole difference:
|
|
15
|
+
//
|
|
16
|
+
// before: v5 = (u16 *)&gBgTilemapBufs; … v5[594] = v5[659];
|
|
17
|
+
// after: gBgTilemapBufs[0][594] = gBgTilemapBufs[0][659];
|
|
18
|
+
//
|
|
19
|
+
// RUNS FIRST in PRE_RECOVERY_PASSES: collapsing addresses removes block params every later
|
|
20
|
+
// recognizer would otherwise have to reason around, and it can only shrink the value graph.
|
|
21
|
+
//
|
|
22
|
+
// SOUNDNESS. `gaddr` takes no operands and reads no memory: its result is a function of its `attrs`
|
|
23
|
+
// alone, so two with equal attrs are equal in every execution, on every path, always. Replacing all
|
|
24
|
+
// of them with ONE definition is exact.
|
|
25
|
+
//
|
|
26
|
+
// THE ADMISSION RULE IS `gaddr`, NOT "operand-free and pure" — and the difference is the whole
|
|
27
|
+
// safety argument, so do not relax it to the general-sounding version. `const` is ALSO operand-free
|
|
28
|
+
// and pure, and numbering consts function-wide would be actively harmful: structure/analysis.ts
|
|
29
|
+
// materializes a multi-use `const` that is live across a call into a named local, and its own
|
|
30
|
+
// comment records that this exact widening ("the small-constant regression") already cost matches
|
|
31
|
+
// once. The gate is a MATCHING policy, not a property of the opcode — which is why it is not a flag
|
|
32
|
+
// on the opcode table, where `const` would satisfy it.
|
|
33
|
+
//
|
|
34
|
+
// PLACEMENT. One fresh definition per class is created in the ENTRY block, which dominates every
|
|
35
|
+
// REACHABLE block — so no reachable use can precede it (unreachable blocks are excluded from the
|
|
36
|
+
// scan for exactly that reason; see `reachable` below) (the originals are deleted rather than moved — a fresh Value
|
|
37
|
+
// keeps the rewrite uniform, including for a duplicate that was already in the entry block). That is safe here precisely because the op is free: `gaddr` lowers to
|
|
38
|
+
// nothing on its own — the structurer inlines a pure non-`const` value at each use site (see
|
|
39
|
+
// analysis.ts, whose materialize-into-a-local rule covers `const`, `call` and the memory reads, NOT
|
|
40
|
+
// address ops), so the address is re-spelled at each access exactly as the original source did.
|
|
41
|
+
// Hoisting therefore does not create the long live range that hoisting a LOADED value would.
|
|
42
|
+
// That is a promise ANOTHER module keeps, so `test/addr-placement.test.ts` holds it to it: let
|
|
43
|
+
// analysis.ts materialize an address op and the entry hoist becomes a function-top local — the one
|
|
44
|
+
// this pass exists to delete, reintroduced one level up.
|
|
45
|
+
//
|
|
46
|
+
// SCOPE, deliberately narrow: `code: true` symbols (a promoted function pointer, spelled `(u32)Name`
|
|
47
|
+
// rather than `&Name`) are numbered separately from data ones, because the attr is part of what the
|
|
48
|
+
// value renders as.
|
|
49
|
+
//
|
|
50
|
+
// THE WIN IS CONTINGENT ON THE SYMBOL MAP, which is worth knowing before relying on it. With a map
|
|
51
|
+
// supplying an array's rank the accesses render as `gSym[0][i]`, a `var` base that
|
|
52
|
+
// `l3/basecse.ts`'s `isHoistableBase` cannot see, so nothing re-creates the local this pass
|
|
53
|
+
// deleted. WITHOUT one the same accesses spell as `addr`, basecse sees the reuse, and it hoists a
|
|
54
|
+
// function-top `p0 = (u16 *)&gBgTilemapBufs` — the same local, one level up. Both arms are pinned
|
|
55
|
+
// in `test/addr-placement.test.ts`; before that they rested on a run nobody could repeat.
|
|
56
|
+
//
|
|
57
|
+
// FOUR modules now answer "is this address a local?" with independent policies — here: never;
|
|
58
|
+
// basecse: at the function top, when reused 2+ times; l3/scopebase.ts: at the innermost scope
|
|
59
|
+
// holding the uses; l3/argbase.ts: immediately before a call whose arguments share it. Reconciling
|
|
60
|
+
// them is recorded debt, and the same test pins the two places they actively disagree, because a
|
|
61
|
+
// consolidation has to PICK rather than discover them: a `for`'s init (basecse reads it at loop
|
|
62
|
+
// cadence and refuses, scopebase at the enclosing one and hoists) and a global name shadowed by a
|
|
63
|
+
// local (scopebase must refuse — it re-spells the base as `&g` — while argbase may fire, because it
|
|
64
|
+
// keeps the base expression verbatim).
|
|
65
|
+
import { Block, Fn, Op, Value, mkOp, replaceAllUsesWith } from '../ir/core';
|
|
66
|
+
|
|
67
|
+
/** Ops whose result depends on `attrs` alone — no operands, no memory, no control flow. */
|
|
68
|
+
const NUMBERABLE = new Set(['gaddr', 'laddr']); // laddr: same argument — operand-free, pure, attr-keyed
|
|
69
|
+
|
|
70
|
+
/** The value-number key: the opcode plus every attribute, in a stable order. */
|
|
71
|
+
function keyOf(op: Op): string {
|
|
72
|
+
const attrs = Object.keys(op.attrs)
|
|
73
|
+
.sort()
|
|
74
|
+
.map((k) => `${k}=${String(op.attrs[k])}`)
|
|
75
|
+
.join(',');
|
|
76
|
+
return `${op.opcode}(${attrs})`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Collapse operand-free pure definitions that share a key down to one apiece, defined in the entry
|
|
81
|
+
* block. Returns the number of definitions removed (0 when nothing changed).
|
|
82
|
+
*/
|
|
83
|
+
export function numberPureValues(fn: Fn): number {
|
|
84
|
+
// REACHABLE blocks only. The entry dominates everything REACHABLE — it does not dominate an
|
|
85
|
+
// unreachable block, and verify()'s dominator fixpoint models that faithfully (a block with no
|
|
86
|
+
// predecessors converges to dom = {itself}). Numbering a group with a member in such a block
|
|
87
|
+
// deletes its local definition and leaves the use reading one the entry holds, which fails
|
|
88
|
+
// `def does not dominate use` — turning a fully-decompiled function into an ASMLIFT_ERROR stub.
|
|
89
|
+
// Loud, not silent, but a real loss: the thumb frontend deliberately KEEPS unreachable blocks
|
|
90
|
+
// ("Other unreachable blocks are LEFT ALONE"), so this shape is supported, not malformed.
|
|
91
|
+
const reachable = new Set<Block>();
|
|
92
|
+
const walk = (b: Block): void => {
|
|
93
|
+
if (reachable.has(b)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
reachable.add(b);
|
|
97
|
+
for (const op of b.ops) {
|
|
98
|
+
for (const s of op.successors) {
|
|
99
|
+
walk(s.block);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
if (fn.blocks[0]) {
|
|
104
|
+
walk(fn.blocks[0]);
|
|
105
|
+
}
|
|
106
|
+
const groups = new Map<string, { op: Op; block: number }[]>();
|
|
107
|
+
fn.blocks.forEach((b, bi) => {
|
|
108
|
+
if (!reachable.has(b)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const op of b.ops) {
|
|
112
|
+
if (NUMBERABLE.has(op.opcode) && op.results.length === 1) {
|
|
113
|
+
const k = keyOf(op);
|
|
114
|
+
groups.set(k, [...(groups.get(k) ?? []), { op, block: bi }]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const entry = fn.blocks[0];
|
|
120
|
+
let removed = 0;
|
|
121
|
+
const hoisted: Op[] = [];
|
|
122
|
+
for (const dups of groups.values()) {
|
|
123
|
+
if (dups.length < 2) {
|
|
124
|
+
continue; // nothing to number — a single definition already dominates its own uses
|
|
125
|
+
}
|
|
126
|
+
// One fresh definition for the class. A fresh Value (rather than reusing the first duplicate's)
|
|
127
|
+
// keeps the rewrite uniform: every original result is replaced, including the one in the entry
|
|
128
|
+
// block, so no path is left reading a definition this pass has moved.
|
|
129
|
+
const survivor = dups[0].op;
|
|
130
|
+
const value: Value = { type: survivor.results[0].type };
|
|
131
|
+
hoisted.push(
|
|
132
|
+
mkOp(survivor.opcode as Parameters<typeof mkOp>[0], { results: [value], attrs: { ...survivor.attrs } }),
|
|
133
|
+
);
|
|
134
|
+
for (const d of dups) {
|
|
135
|
+
replaceAllUsesWith(fn, d.op.results[0], value);
|
|
136
|
+
removed++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (hoisted.length === 0) {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
// Drop every numbered definition, then seed the survivors at the entry block's head. Done as a
|
|
143
|
+
// filter over each block's op list rather than by index, because the replacement above may have
|
|
144
|
+
// rewritten successor args and left the op lists otherwise untouched.
|
|
145
|
+
const dead = new Set<Op>([...groups.values()].filter((d) => d.length >= 2).flatMap((d) => d.map((x) => x.op)));
|
|
146
|
+
for (const b of fn.blocks) {
|
|
147
|
+
b.ops = b.ops.filter((op) => !dead.has(op));
|
|
148
|
+
}
|
|
149
|
+
entry.ops.unshift(...hoisted);
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
// pass that changed the IR is INTRINSIC to the pass (it declares whether it leaves dead ops) and lives
|
|
12
12
|
// in the driver.
|
|
13
13
|
import { Fn } from '../ir/core';
|
|
14
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
14
15
|
import { dce } from '../pattern/engine';
|
|
15
16
|
import type { TargetDescription } from '../target';
|
|
16
17
|
import { recognizeArrays } from './arrays';
|
|
17
18
|
import { recognizeConsts } from './const';
|
|
19
|
+
import { recognizeDivPow2 } from './divpow2';
|
|
20
|
+
import { numberPureValues } from './gvn';
|
|
18
21
|
import { recognizeMagicDivision } from './magicdiv';
|
|
19
|
-
import { recognizeShortCircuit } from './shortcircuit';
|
|
22
|
+
import { recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
|
|
20
23
|
import { recognizeSoftDiv } from './softdiv';
|
|
21
24
|
import { recognizeStructArrays } from './struct-arrays';
|
|
22
25
|
import { recognizeStructs } from './structs';
|
|
@@ -33,11 +36,35 @@ export interface PreRecoveryPass {
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
|
|
36
|
-
* const-materialize → magic-division →
|
|
37
|
-
* struct-pointer → short-circuit. See each recognizer's file
|
|
39
|
+
* address-numbering → const-materialize → magic-division → pow2-division → soft-division → array-legalize →
|
|
40
|
+
* struct-array → struct-pointer → short-circuit → branch-short-circuit. See each recognizer's file
|
|
41
|
+
* for the rationale. */
|
|
38
42
|
export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
43
|
+
// FIRST: collapsing duplicate address definitions removes block params every later recognizer
|
|
44
|
+
// would otherwise have to reason around, and it can only shrink the value graph.
|
|
45
|
+
{
|
|
46
|
+
id: 'addrnum',
|
|
47
|
+
// Numbering alone is not enough and not safe to ship alone: collapsing the duplicates leaves a
|
|
48
|
+
// block param whose edges now all carry one value, and the structurer still destroys THAT into
|
|
49
|
+
// a local (it only reuses a name a carrier already has, and an inlined `gaddr` has none).
|
|
50
|
+
// Numbering alone costs kleod:UpdateHUDCounterDisplay its match, so the pair is the atomic
|
|
51
|
+
// unit, expressed as a body rather than a sum of two unrelated counts. It is NOT monotone,
|
|
52
|
+
// which is worth knowing before tuning either half: dropping the cleanup IMPROVES
|
|
53
|
+
// kleod:ConfigureEntityBehavior and kleod:CountCollectedGems, neither of them near matching.
|
|
54
|
+
run: (fn) => {
|
|
55
|
+
const n = numberPureValues(fn);
|
|
56
|
+
return n + simplifyTrivialPhis(fn);
|
|
57
|
+
},
|
|
58
|
+
dce: false,
|
|
59
|
+
},
|
|
39
60
|
{ id: 'const', run: recognizeConsts, dce: true },
|
|
40
61
|
{ id: 'magicdiv', run: recognizeMagicDivision, dce: true },
|
|
62
|
+
// Position is NOT load-bearing, unlike its neighbours above and below, and saying so is the point:
|
|
63
|
+
// no other pass can see this shape. magicdiv matches a `mulh` DAG; both short-circuit folds require
|
|
64
|
+
// a boolean const arm or a `cond_br`-terminated second block, and this diamond has an `add` arm
|
|
65
|
+
// ending in `br`. Verified by running it before and after each of them — same result. It sits
|
|
66
|
+
// beside magicdiv so that a reader looking for division recovery finds both together.
|
|
67
|
+
{ id: 'divpow2', run: recognizeDivPow2, dce: true },
|
|
41
68
|
{ id: 'softdiv', run: (fn) => recognizeSoftDiv(fn), dce: false, gate: (t) => !t.capabilities.hwDivide },
|
|
42
69
|
{ id: 'arrays', run: recognizeArrays, dce: true },
|
|
43
70
|
// struct-arrays AFTER arrays (scalar stride==width shapes are claimed first — see the
|
|
@@ -46,6 +73,15 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
46
73
|
{ id: 'struct-arrays', run: recognizeStructArrays, dce: true },
|
|
47
74
|
{ id: 'structs', run: recognizeStructs, dce: false },
|
|
48
75
|
{ id: 'shortcircuit', run: recognizeShortCircuit, dce: true },
|
|
76
|
+
// The control-flow sibling, and this order IS load-bearing — value form FIRST.
|
|
77
|
+
//
|
|
78
|
+
// Their input SHAPES are disjoint (the value form's second block ends in `br` carrying a phi
|
|
79
|
+
// argument, this one's ends in `cond_br`), so neither can eat the other's literal pattern. But
|
|
80
|
+
// this pass REWRITES its head's condition into a `logic_or`/`logic_and`, and the value form
|
|
81
|
+
// refuses any head whose condition is not a negatable icmp — so running this one first can
|
|
82
|
+
// permanently disqualify a value fold that was available. The reverse cannot happen: the value
|
|
83
|
+
// form replaces its head's `cond_br` with a `br`, which this pass never matches.
|
|
84
|
+
{ id: 'branch-shortcircuit', run: recognizeBranchShortCircuit, dce: true },
|
|
49
85
|
];
|
|
50
86
|
|
|
51
87
|
/** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
|
package/src/raise/recover.ts
CHANGED
|
@@ -40,8 +40,11 @@ export function recoverTypes(fn: Fn): void {
|
|
|
40
40
|
op.operands.forEach((o) => setInt(o, false));
|
|
41
41
|
op.results.forEach((r) => setInt(r, false));
|
|
42
42
|
}
|
|
43
|
-
// A rotate is a bitwise permutation
|
|
44
|
-
//
|
|
43
|
+
// A rotate is a bitwise permutation, so its value operand and result are unsigned. This is
|
|
44
|
+
// now a TYPE fact only: the C idiom's logical shift is stated on the node (structure.ts
|
|
45
|
+
// spells the idiom with `>>>`, and the C backend supplies whatever cast that needs), so the
|
|
46
|
+
// spelling no longer depends on this seeding to round-trip. Kept because it is true, and
|
|
47
|
+
// because it keeps the rotated value from rendering as a signed operand elsewhere. The
|
|
45
48
|
// rotate AMOUNT (operand 1, register form) keeps its own signedness.
|
|
46
49
|
if (op.opcode === 'rotr' || op.opcode === 'rotl') {
|
|
47
50
|
setInt(op.operands[0], false);
|
|
@@ -199,14 +202,28 @@ function propagatePointers(fn: Fn): void {
|
|
|
199
202
|
}
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
/** The recovered return type
|
|
205
|
+
/** The recovered return type: `void` when EVERY `ret` is operand-less, otherwise the type of the
|
|
206
|
+
* first value any `ret` carries. */
|
|
203
207
|
export function returnType(fn: Fn): IrType {
|
|
208
|
+
// VOID needs EVERY exit to agree. A function can have several `ret`s, and a frontend decides
|
|
209
|
+
// per block whether the return register holds anything — so an operand-less FIRST `ret` beside a
|
|
210
|
+
// valued second one is a real shape (MIPS/PPC compute it per block). Answering `void` off the
|
|
211
|
+
// first would declare void over a body the structurer still emits `return expr;` in, which is
|
|
212
|
+
// ill-formed C and a signature that contradicts its own body.
|
|
213
|
+
const rets = fn.blocks.map((b) => b.ops[b.ops.length - 1]).filter((t) => t?.opcode === 'ret');
|
|
214
|
+
if (rets.length > 0 && rets.every((t) => t!.operands.length === 0)) {
|
|
215
|
+
// NO operand is not "unknown type", it is NO VALUE. Every frontend already says it the same
|
|
216
|
+
// way: MIPS/PPC emit an operand-less `ret` when the return register has no reaching
|
|
217
|
+
// definition, and Thumb when the epilogue branches THROUGH that register (`bx r0`, where r0
|
|
218
|
+
// is the return address and so cannot also be a value). Defaulting to `s32` produced a
|
|
219
|
+
// non-void signature over a body with no `return` value — C's implicit-int function that
|
|
220
|
+
// falls off its end — contradicting the project's own prototype and keeping otherwise-dead
|
|
221
|
+
// computation alive to feed a return that never happens.
|
|
222
|
+
return T.void();
|
|
223
|
+
}
|
|
204
224
|
for (const b of fn.blocks) {
|
|
205
225
|
const term = b.ops[b.ops.length - 1];
|
|
206
|
-
if (term?.opcode === 'ret') {
|
|
207
|
-
if (term.operands.length === 0) {
|
|
208
|
-
return T.s(32);
|
|
209
|
-
}
|
|
226
|
+
if (term?.opcode === 'ret' && term.operands.length > 0) {
|
|
210
227
|
const v = term.operands[0];
|
|
211
228
|
return v.type.kind === 'unknown' ? T.s(32) : v.type;
|
|
212
229
|
}
|
package/src/raise/retsink.ts
CHANGED
|
@@ -21,15 +21,20 @@
|
|
|
21
21
|
//
|
|
22
22
|
// This does NOT recover the boolean-VALUE form `return a && b` — that is shortcircuit.ts's job
|
|
23
23
|
// (the `logic_and`/`logic_or` connective plus agbcc's `(-b|b)>>31` = `b!=0` normalisation).
|
|
24
|
-
import { Block, Fn, mkOp, predecessors } from '../ir/core';
|
|
24
|
+
import { Block, Fn, defOpMap, mkOp, predecessors } from '../ir/core';
|
|
25
|
+
|
|
26
|
+
/** The fused short-circuit connectives (raise/shortcircuit.ts). A `cond_br` on one of these is the
|
|
27
|
+
* post-fusion record of the ≥2 conditions that used to reach a shared arm. */
|
|
28
|
+
const CONNECTIVES = new Set(['logic_and', 'logic_or']);
|
|
25
29
|
|
|
26
30
|
/** Tail-duplicate a return-only merge block into its unconditional-branch predecessors, but ONLY in the
|
|
27
|
-
* short-circuit shape (some branch-pred is shared
|
|
28
|
-
* block is exactly one `ret` whose operands are all
|
|
29
|
-
* carries the returned value as a successor arg. */
|
|
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. */
|
|
30
34
|
export function sinkReturns(fn: Fn): boolean {
|
|
31
35
|
let changed = false;
|
|
32
36
|
const preds = predecessors(fn);
|
|
37
|
+
const defs = defOpMap(fn);
|
|
33
38
|
const isBrTo = (p: Block, m: Block) => {
|
|
34
39
|
const t = p.ops[p.ops.length - 1];
|
|
35
40
|
return t.opcode === 'br' && t.successors.length === 1 && t.successors[0].block === m;
|
|
@@ -52,9 +57,34 @@ export function sinkReturns(fn: Fn): boolean {
|
|
|
52
57
|
if (brPreds.length === 0) {
|
|
53
58
|
continue;
|
|
54
59
|
}
|
|
55
|
-
// SHORT-CIRCUIT GATE
|
|
56
|
-
//
|
|
57
|
-
|
|
60
|
+
// SHORT-CIRCUIT GATE, in two shapes — the chain must be visible in the CFG or in the value domain.
|
|
61
|
+
//
|
|
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.
|
|
64
|
+
// (b) FUSED: `branch-shortcircuit` (raise/shortcircuit.ts) rewrites the head's condition into a
|
|
65
|
+
// `logic_and`/`logic_or` and collapses the second condition block into it. That leaves both
|
|
66
|
+
// arms single-pred, so (a) cannot see the chain any more — but the CONNECTIVE is now the
|
|
67
|
+
// record of the ≥2 conditions the shared arm used to be. That pass runs in pre-recovery,
|
|
68
|
+
// i.e. BEFORE this one, so on `ifand`/`and3` shape (b) is the only one that ever fires.
|
|
69
|
+
//
|
|
70
|
+
// The connective ALONE is not enough, and the extra requirement is BOTH arms being real
|
|
71
|
+
// blocks (≥2 `br` preds). `return a || b` (synthetic:lor:agbcc) also ends up as a
|
|
72
|
+
// `cond_br` on a `logic_or`, but it is a value-merge: one edge runs from the head STRAIGHT
|
|
73
|
+
// into the merge, so the merge has a single `br` pred. Sinking it replaces the merge
|
|
74
|
+
// variable that byte-matches: dropping the `brPreds.length >= 2` half of this gate costs
|
|
75
|
+
// that row its match. A two-armed diamond is what
|
|
76
|
+
// distinguishes `if (a && b) return X; return Y;` from every value-merge.
|
|
77
|
+
//
|
|
78
|
+
// A simple single-condition select is excluded by both arms of the gate for the same reason:
|
|
79
|
+
// `clamp0`/`sel` reach their merge on the `cond_br` edge itself, so they too have exactly one
|
|
80
|
+
// `br` pred, and their condition is a bare icmp rather than a connective.
|
|
81
|
+
const selectedByConnective = (p: Block) =>
|
|
82
|
+
(preds.get(p) ?? []).some((q) => {
|
|
83
|
+
const t = q.ops[q.ops.length - 1];
|
|
84
|
+
return t.opcode === 'cond_br' && CONNECTIVES.has(defs.get(t.operands[0])?.opcode ?? '');
|
|
85
|
+
});
|
|
86
|
+
const fusedDiamond = brPreds.length >= 2 && brPreds.some(selectedByConnective);
|
|
87
|
+
if (!brPreds.some((p) => (preds.get(p)?.length ?? 0) >= 2) && !fusedDiamond) {
|
|
58
88
|
continue;
|
|
59
89
|
}
|
|
60
90
|
for (const p of brPreds) {
|