@asmlift/core 0.1.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/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// asmlift — magic-number constant-division recovery (L1 recognition; gcc-mips + mwcc-ppc).
|
|
2
|
+
//
|
|
3
|
+
// A compiler replaces `x / C` for a non-power-of-2 constant `C` with a HIGH-WORD MULTIPLY by a
|
|
4
|
+
// precomputed "magic" reciprocal `M`, a shift `s`, and a sign correction. The frontend lifts the
|
|
5
|
+
// high-word multiply to a transient `mulh`/`mulhu` op (mips `mfhi` after `mult`/`multu`; ppc
|
|
6
|
+
// `mulhw`/`mulhwu`); this pass matches the DAG hanging off it, RECONSTRUCTS `C`, PROVES it with the
|
|
7
|
+
// forward Hacker's-Delight generator, and rewrites the tree to `sdiv`/`udiv(x, const C)` — the same
|
|
8
|
+
// op the hardware-divide and softdiv paths emit, which the structurer prints as `x / C` and the
|
|
9
|
+
// compiler re-lowers to the identical magic sequence.
|
|
10
|
+
//
|
|
11
|
+
// Soundness: the round-trip is SELF-VERIFYING — asmlift emits a plain `x / C` and the target
|
|
12
|
+
// compiler regenerates ITS magic; a wrong `C` recompiles to different bytes → nonmatch, never a
|
|
13
|
+
// false match. The forward-verify below is therefore a MATCH-RATE filter (don't emit a nonsense
|
|
14
|
+
// divide from a `mulh` chain that isn't a division) rather than the trust barrier. An unrecognised /
|
|
15
|
+
// unverifiable shape leaves the `mulh` in place → it loud-fails at the structurer boundary (the
|
|
16
|
+
// transient op has no C spelling). The guard is a COMPUTATION, which the patterns-as-data layer
|
|
17
|
+
// cannot state, so this lives as a bespoke always-on pass run before type recovery.
|
|
18
|
+
import { Fn, Op, Value, defOpMap, mkOp, mkValue } from '../ir/core';
|
|
19
|
+
import { T } from '../ir/types';
|
|
20
|
+
|
|
21
|
+
/** Forward signed magic generator (Hacker's Delight §10-3). Given divisor `d` (2 ≤ d), returns the
|
|
22
|
+
* 32-bit magic multiplier `M` and shift `s` a compiler would pick. All intermediates stay < 2^33, so
|
|
23
|
+
* plain Number arithmetic is exact (no 32-bit-overflow products are taken). */
|
|
24
|
+
function magicS(d: number): { M: number; s: number } {
|
|
25
|
+
const two31 = 0x80000000;
|
|
26
|
+
const ad = Math.abs(d);
|
|
27
|
+
const t = two31 + (d < 0 ? 1 : 0);
|
|
28
|
+
const anc = t - 1 - (t % ad);
|
|
29
|
+
let p = 31;
|
|
30
|
+
let q1 = Math.floor(two31 / anc),
|
|
31
|
+
r1 = two31 - q1 * anc;
|
|
32
|
+
let q2 = Math.floor(two31 / ad),
|
|
33
|
+
r2 = two31 - q2 * ad;
|
|
34
|
+
let delta: number;
|
|
35
|
+
do {
|
|
36
|
+
p++;
|
|
37
|
+
q1 = 2 * q1;
|
|
38
|
+
r1 = 2 * r1;
|
|
39
|
+
if (r1 >= anc) {
|
|
40
|
+
q1++;
|
|
41
|
+
r1 -= anc;
|
|
42
|
+
}
|
|
43
|
+
q2 = 2 * q2;
|
|
44
|
+
r2 = 2 * r2;
|
|
45
|
+
if (r2 >= ad) {
|
|
46
|
+
q2++;
|
|
47
|
+
r2 -= ad;
|
|
48
|
+
}
|
|
49
|
+
delta = ad - r2;
|
|
50
|
+
} while (q1 < delta || (q1 === delta && r1 === 0));
|
|
51
|
+
let M = q2 + 1;
|
|
52
|
+
if (d < 0) {
|
|
53
|
+
M = -M;
|
|
54
|
+
}
|
|
55
|
+
return { M: M >>> 0, s: p - 32 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Forward UNSIGNED magic generator (Hacker's Delight §10-8). Returns the multiplier `M`, shift `s`,
|
|
59
|
+
* and the `add` indicator (true ⇒ the "add-correction" variant that needs an extra `+x` term —
|
|
60
|
+
* matched by matchUnsignedAddCorrection, not the simple `mulhu>>s` shape). */
|
|
61
|
+
function magicU(d: number): { M: number; s: number; add: boolean } {
|
|
62
|
+
const u = (x: number) => x >>> 0;
|
|
63
|
+
let p = 31;
|
|
64
|
+
const nc = u(u(-1) - u(u(-d) % d));
|
|
65
|
+
let q1 = Math.floor(0x80000000 / nc),
|
|
66
|
+
r1 = u(0x80000000 - q1 * nc);
|
|
67
|
+
let q2 = Math.floor(0x7fffffff / d),
|
|
68
|
+
r2 = u(0x7fffffff - q2 * d);
|
|
69
|
+
let add = false,
|
|
70
|
+
delta: number;
|
|
71
|
+
do {
|
|
72
|
+
p++;
|
|
73
|
+
if (r1 >= nc - r1) {
|
|
74
|
+
q1 = u(2 * q1 + 1);
|
|
75
|
+
r1 = u(2 * r1 - nc);
|
|
76
|
+
} else {
|
|
77
|
+
q1 = u(2 * q1);
|
|
78
|
+
r1 = u(2 * r1);
|
|
79
|
+
}
|
|
80
|
+
if (r2 + 1 >= d - r2) {
|
|
81
|
+
if (q2 >= 0x7fffffff) {
|
|
82
|
+
add = true;
|
|
83
|
+
}
|
|
84
|
+
q2 = u(2 * q2 + 1);
|
|
85
|
+
r2 = u(2 * r2 + 1 - d);
|
|
86
|
+
} else {
|
|
87
|
+
if (q2 >= 0x80000000) {
|
|
88
|
+
add = true;
|
|
89
|
+
}
|
|
90
|
+
q2 = u(2 * q2);
|
|
91
|
+
r2 = u(2 * r2 + 1);
|
|
92
|
+
}
|
|
93
|
+
delta = d - 1 - r2;
|
|
94
|
+
} while (p < 64 && (q1 < delta || (q1 === delta && r1 === 0)));
|
|
95
|
+
return { M: u(q2 + 1), s: p - 32, add };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Realistic constant divisors are small; bound the inverse search generously (covers fixed-point
|
|
99
|
+
// scales) but finitely. The search is only reached once a `mulh`/`mulhu` anchor with a const magic is
|
|
100
|
+
// found (rare — non-division functions have no high-word multiply), so cost is negligible.
|
|
101
|
+
const DIVISOR_MAX = 0x40000;
|
|
102
|
+
|
|
103
|
+
/** Invert a magic: find the divisor `C` whose FORWARD magic is exactly the observed `(M, s)` —
|
|
104
|
+
* a proof by exact reproduction. `kind` picks the catalog: signed (magicS), simple unsigned
|
|
105
|
+
* (magicU, no add-correction), or add-correction unsigned (magicU with `add`). Returns null if
|
|
106
|
+
* no divisor in range reproduces the pair. */
|
|
107
|
+
function recoverDivisor(M: number, s: number, kind: 'signed' | 'unsigned' | 'unsigned-add'): number | null {
|
|
108
|
+
const mu = M >>> 0;
|
|
109
|
+
for (let d = 2; d <= DIVISOR_MAX; d++) {
|
|
110
|
+
if (kind === 'signed') {
|
|
111
|
+
const m = magicS(d);
|
|
112
|
+
if (m.M === mu && m.s === s) {
|
|
113
|
+
return d;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
const m = magicU(d);
|
|
117
|
+
if (m.M === mu && m.s === s && m.add === (kind === 'unsigned-add')) {
|
|
118
|
+
return d;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── DAG helpers ───────────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
interface Ctx {
|
|
128
|
+
defOf: Map<Value, Op>;
|
|
129
|
+
usesOf: Map<Value, Op[]>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildCtx(fn: Fn): Ctx {
|
|
133
|
+
const usesOf = new Map<Value, Op[]>();
|
|
134
|
+
for (const b of fn.blocks) {
|
|
135
|
+
for (const op of b.ops) {
|
|
136
|
+
for (const o of op.operands) {
|
|
137
|
+
const arr = usesOf.get(o);
|
|
138
|
+
if (arr) {
|
|
139
|
+
arr.push(op);
|
|
140
|
+
} else {
|
|
141
|
+
usesOf.set(o, [op]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { defOf: defOpMap(fn), usesOf };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const constVal = (ctx: Ctx, v: Value): number | null => {
|
|
150
|
+
const d = ctx.defOf.get(v);
|
|
151
|
+
return d && d.opcode === 'const' ? (d.attrs.value as number) : null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Bind a high-word multiply's operands: M = the const operand, x = the other. Exactly one of
|
|
155
|
+
* the two must be a constant. */
|
|
156
|
+
function bindMulOperands(ctx: Ctx, mul: Op): { x: Value; M: number } | null {
|
|
157
|
+
const [a, b] = mul.operands;
|
|
158
|
+
const ca = constVal(ctx, a),
|
|
159
|
+
cb = constVal(ctx, b);
|
|
160
|
+
if (ca !== null && cb === null) {
|
|
161
|
+
return { M: ca, x: b };
|
|
162
|
+
}
|
|
163
|
+
if (cb !== null && ca === null) {
|
|
164
|
+
return { M: cb, x: a };
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A single unique use of `v` matching `pred`, or null (ambiguous / absent both yield null). */
|
|
170
|
+
function uniqueUse(ctx: Ctx, v: Value, pred: (op: Op) => boolean): Op | null {
|
|
171
|
+
const hits = (ctx.usesOf.get(v) ?? []).filter(pred);
|
|
172
|
+
return hits.length === 1 ? hits[0] : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** An immediate-form shift `op(base){imm}` result → the shift amount, else null. */
|
|
176
|
+
function immShiftAmt(op: Op, opcode: string): number | null {
|
|
177
|
+
return op.opcode === opcode && op.operands.length === 1 && typeof op.attrs.imm === 'number'
|
|
178
|
+
? (op.attrs.imm as number)
|
|
179
|
+
: null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The block+index of `op`, or null. */
|
|
183
|
+
function locate(fn: Fn, op: Op): { block: number; idx: number } | null {
|
|
184
|
+
for (let b = 0; b < fn.blocks.length; b++) {
|
|
185
|
+
const i = fn.blocks[b].ops.indexOf(op);
|
|
186
|
+
if (i >= 0) {
|
|
187
|
+
return { block: b, idx: i };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── The recogniser ──────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/** Rewrite each recognised magic-division tree to `sdiv`/`udiv(x, const C)`, in place. Returns whether
|
|
196
|
+
* anything changed. Runs BEFORE type recovery so the new op's operands get signedness typing. */
|
|
197
|
+
export function recognizeMagicDivision(fn: Fn): boolean {
|
|
198
|
+
const ctx = buildCtx(fn);
|
|
199
|
+
let changed = false;
|
|
200
|
+
|
|
201
|
+
for (const b of fn.blocks) {
|
|
202
|
+
for (const op of [...b.ops]) {
|
|
203
|
+
let rec: Match | null = null;
|
|
204
|
+
if (op.opcode === 'mulh') {
|
|
205
|
+
rec = matchSignedMagic(ctx, op);
|
|
206
|
+
} // signed magic division
|
|
207
|
+
else if (op.opcode === 'mulhu')
|
|
208
|
+
// unsigned: simple, else add-correction
|
|
209
|
+
{
|
|
210
|
+
rec = matchUnsignedSimpleMagic(ctx, op) ?? matchUnsignedAddCorrection(ctx, op);
|
|
211
|
+
} else {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!rec) {
|
|
215
|
+
continue;
|
|
216
|
+
} // uncatalogued / unverifiable → leave the high-mul → loud-fail
|
|
217
|
+
const loc = locate(fn, rec.root);
|
|
218
|
+
if (!loc) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const block = fn.blocks[loc.block];
|
|
222
|
+
const cRes = mkValue(T.unk(32));
|
|
223
|
+
const cOp = mkOp('const', { results: [cRes], attrs: { value: rec.C } });
|
|
224
|
+
// Reuse the root's result Value so every downstream use auto-points at the divide; DCE reaps the
|
|
225
|
+
// now-dead mulh/add/shift/correction ops.
|
|
226
|
+
const divOp = mkOp(rec.signed ? 'sdiv' : 'udiv', { operands: [rec.x, cRes], results: [rec.root.results[0]] });
|
|
227
|
+
block.ops.splice(loc.idx, 1, cOp, divOp);
|
|
228
|
+
changed = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return changed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface Match {
|
|
235
|
+
x: Value;
|
|
236
|
+
C: number;
|
|
237
|
+
root: Op;
|
|
238
|
+
signed: boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Match the signed magic-division DAG rooted at a `mulh`, in either the MIPS (`- (x>>31)`) or PPC
|
|
242
|
+
* (`+ (t>>u31)`) sign-correction form, and reconstruct+verify the divisor. */
|
|
243
|
+
function matchSignedMagic(ctx: Ctx, mul: Op): Match | null {
|
|
244
|
+
// Bind M (the const operand) and x (the other).
|
|
245
|
+
const bound = bindMulOperands(ctx, mul);
|
|
246
|
+
if (!bound) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const { x, M } = bound;
|
|
250
|
+
const r = mul.results[0];
|
|
251
|
+
|
|
252
|
+
// base = mulh result, or `add(mulh, x)` when the magic needed the +x "33rd bit" correction (M high).
|
|
253
|
+
let base = r;
|
|
254
|
+
const addX = uniqueUse(
|
|
255
|
+
ctx,
|
|
256
|
+
r,
|
|
257
|
+
(o) =>
|
|
258
|
+
o.opcode === 'add' &&
|
|
259
|
+
((o.operands[0] === r && o.operands[1] === x) || (o.operands[1] === r && o.operands[0] === x)),
|
|
260
|
+
);
|
|
261
|
+
if (addX) {
|
|
262
|
+
base = addX.results[0];
|
|
263
|
+
}
|
|
264
|
+
// The +x correction is REQUIRED exactly when M's top bit is set (M reads as negative in the
|
|
265
|
+
// signed mulh, so the product under-counts by x) and FORBIDDEN otherwise. recoverDivisor
|
|
266
|
+
// verifies only (M, s), which is identical for both shapes — an untied match would rewrite a
|
|
267
|
+
// spurious-+x or missing-+x tree to a divide computing a DIFFERENT value than the matched asm.
|
|
268
|
+
if (M >>> 0 >= 0x80000000 !== (addX !== null)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// shifted = shr_s(base, s) (arithmetic right shift by the magic shift)
|
|
273
|
+
const shOp = uniqueUse(ctx, base, (o) => immShiftAmt(o, 'shr_s') !== null);
|
|
274
|
+
if (!shOp) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const s = immShiftAmt(shOp, 'shr_s')!;
|
|
278
|
+
const shifted = shOp.results[0];
|
|
279
|
+
|
|
280
|
+
// Sign correction → root. MIPS: sub(shifted, shr_s(x,31)); PPC: add(shifted, shr_u(shifted,31)).
|
|
281
|
+
let root: Op | null = null;
|
|
282
|
+
const subUse = uniqueUse(ctx, shifted, (o) => o.opcode === 'sub' && o.operands[0] === shifted);
|
|
283
|
+
if (subUse) {
|
|
284
|
+
const w = ctx.defOf.get(subUse.operands[1]);
|
|
285
|
+
if (w && immShiftAmt(w, 'shr_s') === 31 && w.operands[0] === x) {
|
|
286
|
+
root = subUse;
|
|
287
|
+
} // - (x>>31)
|
|
288
|
+
}
|
|
289
|
+
if (!root) {
|
|
290
|
+
const addUse = uniqueUse(
|
|
291
|
+
ctx,
|
|
292
|
+
shifted,
|
|
293
|
+
(o) => o.opcode === 'add' && (o.operands[0] === shifted || o.operands[1] === shifted),
|
|
294
|
+
);
|
|
295
|
+
if (addUse) {
|
|
296
|
+
const other = addUse.operands[0] === shifted ? addUse.operands[1] : addUse.operands[0];
|
|
297
|
+
const w = ctx.defOf.get(other);
|
|
298
|
+
if (w && immShiftAmt(w, 'shr_u') === 31 && w.operands[0] === shifted) {
|
|
299
|
+
root = addUse;
|
|
300
|
+
} // + (t>>u31)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (!root) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const C = recoverDivisor(M, s, 'signed');
|
|
308
|
+
if (C === null) {
|
|
309
|
+
return null;
|
|
310
|
+
} // no divisor reproduces (M,s) exactly → not a division we trust
|
|
311
|
+
return { x, C, root, signed: true };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Match the SIMPLE unsigned magic-division DAG `shr_u(mulhu(x, M), s)` (no correction) and
|
|
315
|
+
* reconstruct+verify the divisor. The add-correction variant (`t + ((x−t)>>1)`) is not matched
|
|
316
|
+
* here — its `mulhu` result feeds a `sub`, not a direct `shr_u`, so this matcher naturally declines it. */
|
|
317
|
+
function matchUnsignedSimpleMagic(ctx: Ctx, mul: Op): Match | null {
|
|
318
|
+
const bound = bindMulOperands(ctx, mul);
|
|
319
|
+
if (!bound) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const { x, M } = bound;
|
|
323
|
+
const r = mul.results[0];
|
|
324
|
+
|
|
325
|
+
// shifted = shr_u(mulhu, s) — the quotient. The simple form has NO sign/round correction: the shift
|
|
326
|
+
// result IS the returned value, so it is the root.
|
|
327
|
+
const shOp = uniqueUse(ctx, r, (o) => immShiftAmt(o, 'shr_u') !== null);
|
|
328
|
+
if (!shOp) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const s = immShiftAmt(shOp, 'shr_u')!;
|
|
332
|
+
|
|
333
|
+
const C = recoverDivisor(M, s, 'unsigned');
|
|
334
|
+
if (C === null) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
return { x, C, root: shOp, signed: false };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Match the ADD-CORRECTION unsigned magic DAG (used when the magic reciprocal didn't fit in 32 bits):
|
|
341
|
+
* t = mulhu(x, M); d1 = x − t; d2 = d1 >>u 1; d3 = t + d2; root = d3 >>u (s−1)
|
|
342
|
+
* and reconstruct+verify the divisor (`magicU(C).add === true`, `magicU(C).s === s`). */
|
|
343
|
+
function matchUnsignedAddCorrection(ctx: Ctx, mul: Op): Match | null {
|
|
344
|
+
const bound = bindMulOperands(ctx, mul);
|
|
345
|
+
if (!bound) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const { x, M } = bound;
|
|
349
|
+
const t = mul.results[0];
|
|
350
|
+
|
|
351
|
+
// d1 = sub(x, t) [x − t]
|
|
352
|
+
const d1 = uniqueUse(ctx, t, (o) => o.opcode === 'sub' && o.operands[0] === x && o.operands[1] === t);
|
|
353
|
+
if (!d1) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
// d2 = shr_u(d1, 1)
|
|
357
|
+
const d2 = uniqueUse(ctx, d1.results[0], (o) => immShiftAmt(o, 'shr_u') === 1);
|
|
358
|
+
if (!d2) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
// d3 = add(t, d2) (either operand order)
|
|
362
|
+
const d3 = uniqueUse(
|
|
363
|
+
ctx,
|
|
364
|
+
d2.results[0],
|
|
365
|
+
(o) => o.opcode === 'add' && (o.operands[0] === d2.results[0] || o.operands[1] === d2.results[0]),
|
|
366
|
+
);
|
|
367
|
+
if (!d3) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const otherAdd = d3.operands[0] === d2.results[0] ? d3.operands[1] : d3.operands[0];
|
|
371
|
+
if (otherAdd !== t) {
|
|
372
|
+
return null;
|
|
373
|
+
} // the add must combine `t` and `d2`
|
|
374
|
+
// root = shr_u(d3, s−1)
|
|
375
|
+
const rootOp = uniqueUse(ctx, d3.results[0], (o) => immShiftAmt(o, 'shr_u') !== null);
|
|
376
|
+
if (!rootOp) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
const s2 = immShiftAmt(rootOp, 'shr_u')!;
|
|
380
|
+
|
|
381
|
+
const C = recoverDivisor(M, s2 + 1, 'unsigned-add'); // the final shift is s−1, so the magic shift is s2+1
|
|
382
|
+
if (C === null) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
return { x, C, root: rootOp, signed: false };
|
|
386
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// asmlift — the pre-recovery raise-pass sequence, as ONE shared ordered list.
|
|
2
|
+
//
|
|
3
|
+
// These recognizers run AFTER idiom-pattern folding and BEFORE type recovery, each rewriting the IR
|
|
4
|
+
// into a form recovery/structuring can reason about. Their ORDER and per-pass `dce`/gating semantics
|
|
5
|
+
// are load-bearing; this module is the single source of truth — add a pass HERE and every caller
|
|
6
|
+
// (pipeline, rank, report) picks it up. A pass added to one call site alone leaves the others
|
|
7
|
+
// hitting the unlowered op → a spurious noncompile.
|
|
8
|
+
//
|
|
9
|
+
// Callers supply an `afterPass` hook for their own per-pass concern (pipeline's raiseRecovered and
|
|
10
|
+
// rank's score-probe both verify; the report's trace entries ride pipeline's hook). The `dce` after a
|
|
11
|
+
// pass that changed the IR is INTRINSIC to the pass (it declares whether it leaves dead ops) and lives
|
|
12
|
+
// in the driver.
|
|
13
|
+
import { Fn } from '../ir/core';
|
|
14
|
+
import { dce } from '../pattern/engine';
|
|
15
|
+
import type { TargetDescription } from '../target';
|
|
16
|
+
import { recognizeArrays } from './arrays';
|
|
17
|
+
import { recognizeConsts } from './const';
|
|
18
|
+
import { recognizeMagicDivision } from './magicdiv';
|
|
19
|
+
import { recognizeShortCircuit } from './shortcircuit';
|
|
20
|
+
import { recognizeSoftDiv } from './softdiv';
|
|
21
|
+
import { recognizeStructArrays } from './struct-arrays';
|
|
22
|
+
import { recognizeStructs } from './structs';
|
|
23
|
+
|
|
24
|
+
export interface PreRecoveryPass {
|
|
25
|
+
/** stable id — also the report's trace-stage key. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** run the recognizer; returns a truthy value (a change count, or `true`) iff it CHANGED the IR. */
|
|
28
|
+
run: (fn: Fn) => number | boolean;
|
|
29
|
+
/** run `dce` after this pass changes the IR (the pass declares it leaves dead ops behind). */
|
|
30
|
+
dce: boolean;
|
|
31
|
+
/** optional target gate (soft-div only fires on a no-hardware-divide target — see raise/softdiv.ts). */
|
|
32
|
+
gate?: (target: TargetDescription) => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
|
|
36
|
+
* const-materialize → magic-division → soft-division → array-legalize → struct-array →
|
|
37
|
+
* struct-pointer → short-circuit. See each recognizer's file for the rationale. */
|
|
38
|
+
export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
39
|
+
{ id: 'const', run: recognizeConsts, dce: true },
|
|
40
|
+
{ id: 'magicdiv', run: recognizeMagicDivision, dce: true },
|
|
41
|
+
{ id: 'softdiv', run: (fn) => recognizeSoftDiv(fn), dce: false, gate: (t) => !t.capabilities.hwDivide },
|
|
42
|
+
{ id: 'arrays', run: recognizeArrays, dce: true },
|
|
43
|
+
// struct-arrays AFTER arrays (scalar stride==width shapes are claimed first — see the
|
|
44
|
+
// discriminator note in raise/struct-arrays.ts) and BEFORE structs (an element's field
|
|
45
|
+
// accesses must not be re-derived as constant-offset struct-pointer accesses).
|
|
46
|
+
{ id: 'struct-arrays', run: recognizeStructArrays, dce: true },
|
|
47
|
+
{ id: 'structs', run: recognizeStructs, dce: false },
|
|
48
|
+
{ id: 'shortcircuit', run: recognizeShortCircuit, dce: true },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
|
|
52
|
+
* `dce` when the pass declares it, then invoke `afterPass(pass, result)` (the caller's verify/trace
|
|
53
|
+
* hook), if given. */
|
|
54
|
+
export function runPreRecovery(
|
|
55
|
+
fn: Fn,
|
|
56
|
+
target: TargetDescription,
|
|
57
|
+
afterPass?: (pass: PreRecoveryPass, result: number | boolean) => void,
|
|
58
|
+
): void {
|
|
59
|
+
for (const pass of PRE_RECOVERY_PASSES) {
|
|
60
|
+
if (pass.gate && !pass.gate(target)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const result = pass.run(fn);
|
|
64
|
+
if (result) {
|
|
65
|
+
if (pass.dce) {
|
|
66
|
+
dce(fn);
|
|
67
|
+
}
|
|
68
|
+
afterPass?.(pass, result);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// asmlift — L1→L2 type recovery (a constraint pass). Seeds signedness from op semantics
|
|
2
|
+
// (signed comparisons/divisions ⇒ signed operands), types memory-access bases as pointers,
|
|
3
|
+
// propagates pointer-ness across the SSA, then defaults the rest to s32. Emits recovered
|
|
4
|
+
// types onto the SSA values in place.
|
|
5
|
+
import { Fn, Value, defOpMap } from '../ir/core';
|
|
6
|
+
import { IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
|
|
7
|
+
|
|
8
|
+
const SIGNED_CMP = new Set(['icmp_slt', 'icmp_sle', 'icmp_sgt', 'icmp_sge']);
|
|
9
|
+
const UNSIGNED_CMP = new Set(['icmp_ult', 'icmp_ule', 'icmp_ugt', 'icmp_uge']);
|
|
10
|
+
// Division/remainder carry signedness in the OPCODE (a hardware `div` vs `divu`), so they seed it
|
|
11
|
+
// onto their operands AND result — the reason the backend can pick signed `/` over unsigned operands.
|
|
12
|
+
const SIGNED_DIV = new Set(['sdiv', 'smod']);
|
|
13
|
+
const UNSIGNED_DIV = new Set(['udiv', 'umod']);
|
|
14
|
+
|
|
15
|
+
export function recoverTypes(fn: Fn): void {
|
|
16
|
+
const setInt = (v: Value, signed: boolean) => {
|
|
17
|
+
if (v.type.kind === 'unknown') {
|
|
18
|
+
v.type = T.int(v.type.width, signed);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
// Seed: operands of a signed comparison are signed integers.
|
|
22
|
+
for (const b of fn.blocks) {
|
|
23
|
+
for (const op of b.ops) {
|
|
24
|
+
if (SIGNED_CMP.has(op.opcode)) {
|
|
25
|
+
op.operands.forEach((o) => setInt(o, true));
|
|
26
|
+
}
|
|
27
|
+
if (UNSIGNED_CMP.has(op.opcode)) {
|
|
28
|
+
op.operands.forEach((o) => setInt(o, false));
|
|
29
|
+
} // sltu ⇒ u32 operands
|
|
30
|
+
if (op.opcode.startsWith('icmp')) {
|
|
31
|
+
op.results.forEach((r) => (r.type = T.u(32)));
|
|
32
|
+
} // bool
|
|
33
|
+
// The register (2-operand) division form seeds operand+result signedness; the immediate form
|
|
34
|
+
// (`sdiv X {imm=…}`, 1 operand) is always the signed strength-reduced divisor.
|
|
35
|
+
if (SIGNED_DIV.has(op.opcode)) {
|
|
36
|
+
op.operands.forEach((o) => setInt(o, true));
|
|
37
|
+
op.results.forEach((r) => setInt(r, true));
|
|
38
|
+
}
|
|
39
|
+
if (UNSIGNED_DIV.has(op.opcode)) {
|
|
40
|
+
op.operands.forEach((o) => setInt(o, false));
|
|
41
|
+
op.results.forEach((r) => setInt(r, false));
|
|
42
|
+
}
|
|
43
|
+
// A rotate is a bitwise permutation: its value operand and result are unsigned (the C
|
|
44
|
+
// idiom's `>>` must be the LOGICAL shift or the spelling stops round-tripping). The
|
|
45
|
+
// rotate AMOUNT (operand 1, register form) keeps its own signedness.
|
|
46
|
+
if (op.opcode === 'rotr' || op.opcode === 'rotl') {
|
|
47
|
+
setInt(op.operands[0], false);
|
|
48
|
+
op.results.forEach((r) => setInt(r, false));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// A value used as the base of a memory access is a pointer; its pointee type comes from the
|
|
53
|
+
// access width (and, for loads, signedness). This must run before the s32 default so the
|
|
54
|
+
// base is typed `T *` rather than being flattened to a plain integer. Both the constant-offset
|
|
55
|
+
// forms (load/store, width) and the variable-index forms (aload/astore, elemSize) type their
|
|
56
|
+
// base operand[0]; only the scale attribute differs.
|
|
57
|
+
|
|
58
|
+
for (const b of fn.blocks) {
|
|
59
|
+
for (const op of b.ops) {
|
|
60
|
+
let width: number, signed: boolean;
|
|
61
|
+
switch (op.opcode) {
|
|
62
|
+
case 'load':
|
|
63
|
+
width = op.attrs.width as number;
|
|
64
|
+
signed = op.attrs.signed as boolean;
|
|
65
|
+
break;
|
|
66
|
+
case 'store':
|
|
67
|
+
width = op.attrs.width as number;
|
|
68
|
+
signed = width === 4;
|
|
69
|
+
break;
|
|
70
|
+
case 'aload':
|
|
71
|
+
width = op.attrs.elemSize as number;
|
|
72
|
+
signed = op.attrs.signed as boolean;
|
|
73
|
+
break;
|
|
74
|
+
case 'astore':
|
|
75
|
+
width = op.attrs.elemSize as number;
|
|
76
|
+
signed = width === 4;
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const base = op.operands[0];
|
|
82
|
+
if (base.type.kind === 'unknown') {
|
|
83
|
+
base.type = T.ptr(scalarTypeForAccess(width, signed));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Propagate pointer-ness across the SSA. The seed above types only the DIRECT base of a dereference;
|
|
88
|
+
// a loop-carried pointer reaches its dereference through a block-arg phi (its incoming `a0`) and a
|
|
89
|
+
// `p = p + stride` walk, so those values stay `unknown` and the s32 default below would spell them
|
|
90
|
+
// `int` — an `int→int*` assignment mwcc/agbcc REJECT (gcc warns). Flow the pointer type across the
|
|
91
|
+
// exact same-value edges (a phi is one value; `ptr ± const` is the same pointer type). Union-find over
|
|
92
|
+
// Value identity. SOUND: every edge connects values that provably hold the same pointer, and we only
|
|
93
|
+
// fill `unknown`s — a class with a conflicting int member or two distinct pointees is left untouched.
|
|
94
|
+
propagatePointers(fn);
|
|
95
|
+
// Default every still-unknown value to s32. This is a COMPILER default (agbcc/IDO/GCC all take
|
|
96
|
+
// plain `int` as the integer default), not a hardware fact — applied uniformly.
|
|
97
|
+
for (const b of fn.blocks) {
|
|
98
|
+
for (const p of b.params) {
|
|
99
|
+
if (p.type.kind === 'unknown') {
|
|
100
|
+
p.type = T.s(32);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const op of b.ops) {
|
|
104
|
+
for (const r of op.results) {
|
|
105
|
+
if (r.type.kind === 'unknown') {
|
|
106
|
+
r.type = T.s(32);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Union-find pointer propagation (see the call site). Unions (1) each successor arg with the block param
|
|
114
|
+
// it binds (the phi/copy identity) and (2) an `add`/`sub` result with its non-constant operand when the
|
|
115
|
+
// other operand is a constant (a pointer ± an integer offset stays the same pointer type). Then, for each
|
|
116
|
+
// class with EXACTLY ONE distinct pointee and NO conflicting int-typed member, every `unknown` member of
|
|
117
|
+
// the class is typed with that pointee. Conservative on conflict: a genuinely ambiguous value (used as
|
|
118
|
+
// both pointer and integer, or with two pointee widths) is left for the s32 default — sound, never a
|
|
119
|
+
// mistype. Types only; changes no op, so it cannot alter compiled bytes except to fix the `int→ptr` form.
|
|
120
|
+
//
|
|
121
|
+
// Domain assumption (edge type 1): in WELL-TYPED compiler output a block-arg phi merges values of ONE
|
|
122
|
+
// type, so a pointer's phi has only pointer incoming args — propagating the pointee across it is exact.
|
|
123
|
+
// Genuinely type-punned asm (one register aliasing a pointer and an integer across a merge) has no
|
|
124
|
+
// byte-exact C anyway; the `hasInt` conflict guard blocks the common form, and the residual is ill-typed
|
|
125
|
+
// input, not a miscompile of well-typed code.
|
|
126
|
+
function propagatePointers(fn: Fn): void {
|
|
127
|
+
const parent = new Map<Value, Value>();
|
|
128
|
+
const find = (v: Value): Value => {
|
|
129
|
+
if (!parent.has(v)) {
|
|
130
|
+
parent.set(v, v);
|
|
131
|
+
return v;
|
|
132
|
+
}
|
|
133
|
+
let r = v;
|
|
134
|
+
while (parent.get(r)! !== r) {
|
|
135
|
+
r = parent.get(r)!;
|
|
136
|
+
}
|
|
137
|
+
for (let c = v; parent.get(c)! !== r;) {
|
|
138
|
+
const n = parent.get(c)!;
|
|
139
|
+
parent.set(c, r);
|
|
140
|
+
c = n;
|
|
141
|
+
}
|
|
142
|
+
return r;
|
|
143
|
+
};
|
|
144
|
+
const union = (a: Value, b: Value) => {
|
|
145
|
+
parent.set(find(a), find(b));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const defs = defOpMap(fn);
|
|
149
|
+
const isConst = (v: Value) => defs.get(v)?.opcode === 'const';
|
|
150
|
+
for (const b of fn.blocks) {
|
|
151
|
+
for (const op of b.ops) {
|
|
152
|
+
for (const s of op.successors) {
|
|
153
|
+
const n = Math.min(s.args.length, s.block.params.length);
|
|
154
|
+
for (let i = 0; i < n; i++) {
|
|
155
|
+
union(s.args[i], s.block.params[i]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if ((op.opcode === 'add' || op.opcode === 'sub') && op.results.length === 1 && op.operands.length === 2) {
|
|
159
|
+
const [x, y] = op.operands;
|
|
160
|
+
const xc = isConst(x),
|
|
161
|
+
yc = isConst(y);
|
|
162
|
+
if (xc !== yc) {
|
|
163
|
+
union(op.results[0], xc ? y : x);
|
|
164
|
+
} // exactly one const ⇒ pointer ± offset
|
|
165
|
+
// A `base + index` add (two non-constant operands) is NOT unioned: base and index are not
|
|
166
|
+
// reliably distinguishable here. Attempting it mis-typed the INDEX as the pointer whenever
|
|
167
|
+
// the true base was itself int-seeded by a pointer comparison (`if (p < lim)` → `sltu`/`cmplw`
|
|
168
|
+
// seeds the base `int`, the index stays `unknown`) — silent-wrong pointer-scaled C. Recovering
|
|
169
|
+
// `add(base,index)` bases needs a stronger base/index discriminator; deferred rather than
|
|
170
|
+
// risk a miscompile.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Per class: the distinct pointees seen, and whether any member is a definite integer (a conflict).
|
|
176
|
+
const info = new Map<Value, { pointees: IrType[]; hasInt: boolean }>();
|
|
177
|
+
for (const v of parent.keys()) {
|
|
178
|
+
const root = find(v);
|
|
179
|
+
let e = info.get(root);
|
|
180
|
+
if (!e) {
|
|
181
|
+
e = { pointees: [], hasInt: false };
|
|
182
|
+
info.set(root, e);
|
|
183
|
+
}
|
|
184
|
+
if (v.type.kind === 'ptr' && !e.pointees.some((t) => typeEquals(t, v.type))) {
|
|
185
|
+
e.pointees.push(v.type);
|
|
186
|
+
}
|
|
187
|
+
if (v.type.kind === 'int') {
|
|
188
|
+
e.hasInt = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const v of parent.keys()) {
|
|
192
|
+
if (v.type.kind !== 'unknown') {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const e = info.get(find(v))!;
|
|
196
|
+
if (e.pointees.length === 1 && !e.hasInt) {
|
|
197
|
+
v.type = e.pointees[0];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The recovered return type = the type of the value returned by the first `ret`. */
|
|
203
|
+
export function returnType(fn: Fn): IrType {
|
|
204
|
+
for (const b of fn.blocks) {
|
|
205
|
+
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
|
+
}
|
|
210
|
+
const v = term.operands[0];
|
|
211
|
+
return v.type.kind === 'unknown' ? T.s(32) : v.type;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return T.s(32);
|
|
215
|
+
}
|