@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,410 @@
|
|
|
1
|
+
// asmlift structurer — Regime-A SWITCH RECOVERY: recognise a comparison tree over a single
|
|
2
|
+
// scrutinee rooted at a cond_br and rebuild the `switch` — or DECLINE (null) to plain
|
|
3
|
+
// if-recovery, which is behaviourally identical (a clean nonmatch, never a miscompile). The
|
|
4
|
+
// factory takes its dependencies EXPLICITLY (`SwitchRecoverDeps`); `expr`/`structureRegion` are
|
|
5
|
+
// late-bound callbacks into the emission phase, so case bodies reuse the ordinary structuring
|
|
6
|
+
// machinery (loops/ifs inside cases, the onStack guard).
|
|
7
|
+
import { Block, Fn, Op, Value, successorsOf } from '../ir/core';
|
|
8
|
+
import { Expr, Stmt, SwitchCase } from '../l3/ast';
|
|
9
|
+
|
|
10
|
+
export interface SwitchRecoverDeps {
|
|
11
|
+
fn: Fn;
|
|
12
|
+
defs: Map<Value, Op>;
|
|
13
|
+
dom: Map<Block, Set<Block>>;
|
|
14
|
+
ipdom: Map<Block, Block | null>;
|
|
15
|
+
opBlock: Map<Op, Block>;
|
|
16
|
+
/** does this value carry a variable name? (named values are not constants) */
|
|
17
|
+
isNamed: (v: Value) => boolean;
|
|
18
|
+
/** is this opcode an integer comparison? */
|
|
19
|
+
isCmpOpcode: (opcode: string) => boolean;
|
|
20
|
+
switchAllowsNeqCase: boolean;
|
|
21
|
+
expr: (v: Value) => Expr;
|
|
22
|
+
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SwitchRecovery {
|
|
26
|
+
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
27
|
+
/** shared with the Regime-B (`switch_br`) path in structure.ts, which throws where A declines */
|
|
28
|
+
caseRegionReachesSibling: (targets: Set<Block>, b: Block, merge: Block | null) => boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
32
|
+
const { fn, defs, dom, ipdom, opBlock, isNamed, isCmpOpcode, switchAllowsNeqCase, expr, structureRegion } = deps;
|
|
33
|
+
|
|
34
|
+
// --- Regime A: comparison-tree switch recovery ----------------------------------------------------
|
|
35
|
+
// Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
|
|
36
|
+
// scrutinee identity/dominance, no fall-through, concrete interval consistency, test purity.
|
|
37
|
+
|
|
38
|
+
// Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
|
|
39
|
+
// `250 << 2` for a large sparse case) to a number — else null.
|
|
40
|
+
const evalConst = (v: Value): number | null => {
|
|
41
|
+
if (isNamed(v)) {
|
|
42
|
+
return null;
|
|
43
|
+
} // a named variable is not a constant
|
|
44
|
+
const d = defs.get(v);
|
|
45
|
+
if (!d) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (d.opcode === 'const') {
|
|
49
|
+
return (d.attrs.value as number) | 0;
|
|
50
|
+
}
|
|
51
|
+
// Resolve the two operands of a binary op to constants (2-operand → both; 1-operand → operand +
|
|
52
|
+
// `imm` attr, which MUST be present, else decline — a missing imm would wrongly fold `and x` to 0).
|
|
53
|
+
const operands2 = (): [number, number] | null => {
|
|
54
|
+
const a = evalConst(d.operands[0]);
|
|
55
|
+
if (a === null) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
let c: number | null;
|
|
59
|
+
if (d.operands.length === 2) {
|
|
60
|
+
c = evalConst(d.operands[1]);
|
|
61
|
+
} else if (typeof d.attrs.imm === 'number') {
|
|
62
|
+
c = d.attrs.imm | 0;
|
|
63
|
+
} else {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return c === null ? null : [a, c];
|
|
67
|
+
};
|
|
68
|
+
const bin = (f: (a: number, c: number) => number): number | null => {
|
|
69
|
+
const p = operands2();
|
|
70
|
+
return p === null ? null : f(p[0], p[1]) | 0;
|
|
71
|
+
};
|
|
72
|
+
const shift = (f: (a: number, c: number) => number): number | null => {
|
|
73
|
+
const p = operands2();
|
|
74
|
+
if (p === null || p[1] < 0 || p[1] >= 32) {
|
|
75
|
+
return null;
|
|
76
|
+
} // out-of-range shift amount → decline
|
|
77
|
+
return f(p[0], p[1]) | 0;
|
|
78
|
+
};
|
|
79
|
+
switch (d.opcode) {
|
|
80
|
+
case 'shl':
|
|
81
|
+
return shift((a, c) => a << c);
|
|
82
|
+
case 'shr_u':
|
|
83
|
+
return shift((a, c) => a >>> c);
|
|
84
|
+
case 'shr_s':
|
|
85
|
+
return shift((a, c) => a >> c);
|
|
86
|
+
case 'or':
|
|
87
|
+
return bin((a, c) => a | c);
|
|
88
|
+
case 'and':
|
|
89
|
+
return bin((a, c) => a & c);
|
|
90
|
+
case 'xor':
|
|
91
|
+
return bin((a, c) => a ^ c);
|
|
92
|
+
case 'add':
|
|
93
|
+
return bin((a, c) => a + c);
|
|
94
|
+
case 'sub':
|
|
95
|
+
return bin((a, c) => a - c);
|
|
96
|
+
case 'neg': {
|
|
97
|
+
const a = evalConst(d.operands[0]);
|
|
98
|
+
return a === null ? null : -a | 0;
|
|
99
|
+
}
|
|
100
|
+
case 'not': {
|
|
101
|
+
const a = evalConst(d.operands[0]);
|
|
102
|
+
return a === null ? null : ~a | 0;
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// A "pure test block": its only computation is constants + one integer comparison feeding its
|
|
110
|
+
// cond_br terminator (no store/call/load/opaque — its body is DISCARDED when the tree collapses to a
|
|
111
|
+
// switch, so a side effect there would be lost). PRE4 (purity). The root block is exempt from the
|
|
112
|
+
// "only const/icmp" rule because its non-terminator ops are already emitted as sideEffects(b) before
|
|
113
|
+
// the switch; a non-root test block must be strictly pure.
|
|
114
|
+
const SIDE_EFFECTFUL = new Set(['store', 'astore', 'call', 'load', 'aload', 'opaque']);
|
|
115
|
+
interface TestInfo {
|
|
116
|
+
x: Value;
|
|
117
|
+
k: number;
|
|
118
|
+
cls: 'eq' | 'ne' | 'rel';
|
|
119
|
+
opcode: string;
|
|
120
|
+
xOnLeft: boolean;
|
|
121
|
+
}
|
|
122
|
+
const testInfo = (blk: Block, isRoot: boolean): TestInfo | null => {
|
|
123
|
+
const term = blk.ops[blk.ops.length - 1];
|
|
124
|
+
if (term.opcode !== 'cond_br') {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const cmp = defs.get(term.operands[0]);
|
|
128
|
+
if (!cmp || !isCmpOpcode(cmp.opcode)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (!isRoot && blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode))) {
|
|
132
|
+
return null;
|
|
133
|
+
} // PRE4
|
|
134
|
+
// Which operand is the scrutinee, which is the constant?
|
|
135
|
+
const [lo, ro] = cmp.operands;
|
|
136
|
+
const lc = evalConst(lo),
|
|
137
|
+
rc = evalConst(ro);
|
|
138
|
+
let x: Value, k: number, xOnLeft: boolean;
|
|
139
|
+
if (lc === null && rc !== null) {
|
|
140
|
+
x = lo;
|
|
141
|
+
k = rc;
|
|
142
|
+
xOnLeft = true;
|
|
143
|
+
} else if (rc === null && lc !== null) {
|
|
144
|
+
x = ro;
|
|
145
|
+
k = lc;
|
|
146
|
+
xOnLeft = false;
|
|
147
|
+
} else {
|
|
148
|
+
return null;
|
|
149
|
+
} // both/neither const
|
|
150
|
+
const cls = cmp.opcode === 'icmp_eq' ? 'eq' : cmp.opcode === 'icmp_ne' ? 'ne' : 'rel';
|
|
151
|
+
return { x, k, cls, opcode: cmp.opcode, xOnLeft };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Evaluate a test predicate for a CONCRETE scrutinee value — used to SIMULATE the decision tree and
|
|
155
|
+
// verify recovered case values (below). Returns true iff the `taken` (successors[0]) edge is followed.
|
|
156
|
+
// Signed/unsigned per the icmp opcode (PRE3, done concretely rather than via interval lattices).
|
|
157
|
+
const evalCmp = (opcode: string, xOnLeft: boolean, xv: number, k: number): boolean => {
|
|
158
|
+
const uns = opcode.startsWith('icmp_u');
|
|
159
|
+
const [xn, kn] = uns ? [xv >>> 0, k >>> 0] : [xv | 0, k | 0];
|
|
160
|
+
const [l, r] = xOnLeft ? [xn, kn] : [kn, xn]; // put the scrutinee where it textually appears
|
|
161
|
+
switch (opcode) {
|
|
162
|
+
case 'icmp_eq':
|
|
163
|
+
return l === r;
|
|
164
|
+
case 'icmp_ne':
|
|
165
|
+
return l !== r;
|
|
166
|
+
case 'icmp_slt':
|
|
167
|
+
case 'icmp_ult':
|
|
168
|
+
return l < r;
|
|
169
|
+
case 'icmp_sle':
|
|
170
|
+
case 'icmp_ule':
|
|
171
|
+
return l <= r;
|
|
172
|
+
case 'icmp_sgt':
|
|
173
|
+
case 'icmp_ugt':
|
|
174
|
+
return l > r;
|
|
175
|
+
case 'icmp_sge':
|
|
176
|
+
case 'icmp_uge':
|
|
177
|
+
return l >= r;
|
|
178
|
+
default:
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Can any case/default entry's region reach a SIBLING entry (switch fall-through)? Region =
|
|
184
|
+
// blocks strictly dominated by `b`, short of `merge`. Shared by Regime A (declines to
|
|
185
|
+
// if-recovery) and Regime B (throws — a jump-table has no fallback).
|
|
186
|
+
const caseRegionReachesSibling = (targets: Set<Block>, b: Block, merge: Block | null): boolean => {
|
|
187
|
+
const inRegion = (blk: Block) => blk !== merge && dom.get(blk)!.has(b);
|
|
188
|
+
for (const entry of targets) {
|
|
189
|
+
const rseen = new Set<Block>([entry]);
|
|
190
|
+
const q = [entry];
|
|
191
|
+
while (q.length) {
|
|
192
|
+
const cur = q.pop()!;
|
|
193
|
+
for (const s of successorsOf(cur)) {
|
|
194
|
+
if (s === entry) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (targets.has(s)) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (inRegion(s) && !rseen.has(s)) {
|
|
201
|
+
rseen.add(s);
|
|
202
|
+
q.push(s);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
|
|
211
|
+
const root = testInfo(b, true);
|
|
212
|
+
if (!root) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const scrut = root.x;
|
|
216
|
+
// PRE1 (scrutinee identity + dominance): the scrutinee is a single raw SSA Value that must DOMINATE
|
|
217
|
+
// the whole region. A block param (phi) is rejected — it is not one definition across the region.
|
|
218
|
+
// Params are seeded into names (isNamed); a value defined by an op has a defining block that must dominate
|
|
219
|
+
// b. Function params (entry params) dominate everything.
|
|
220
|
+
const scrutDef = defs.get(scrut);
|
|
221
|
+
const entryBlk = fn.blocks[0];
|
|
222
|
+
if (scrutDef) {
|
|
223
|
+
const defBlk = opBlock.get(scrutDef)!;
|
|
224
|
+
if (!dom.get(b)!.has(defBlk)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
} else if (!entryBlk.params.includes(scrut)) {
|
|
228
|
+
return null; // a non-entry block param → decline
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Walk the test tree. `cases`: value → case-entry block. `defaultCands`: leaves reached without an
|
|
232
|
+
// equality pin. A test-block DAG cycle, or a `!=` case when the compiler disallows it, declines.
|
|
233
|
+
const cases = new Map<number, Block>();
|
|
234
|
+
const defaultCands = new Set<Block>();
|
|
235
|
+
// Skip pure forwarding blocks — a block whose only op is an unconditional `br` (no side effects, no
|
|
236
|
+
// params). agbcc's binary-search layout branches to the shared default through such empty `b .Ldef`
|
|
237
|
+
// blocks; without skipping them each becomes a DISTINCT default candidate and the whole tree declines.
|
|
238
|
+
const skipForward = (blk: Block): Block => {
|
|
239
|
+
let cur = blk;
|
|
240
|
+
const guard = new Set<Block>();
|
|
241
|
+
// Only skip a truly empty forwarding block: a lone `br` with no params AND no successor ARGS —
|
|
242
|
+
// an edge that carries a phi arg is NOT transparent (skipping it would drop that assignment).
|
|
243
|
+
while (
|
|
244
|
+
cur.ops.length === 1 &&
|
|
245
|
+
cur.ops[0].opcode === 'br' &&
|
|
246
|
+
cur.params.length === 0 &&
|
|
247
|
+
cur.ops[0].successors[0].args.length === 0 &&
|
|
248
|
+
!guard.has(cur)
|
|
249
|
+
) {
|
|
250
|
+
guard.add(cur);
|
|
251
|
+
cur = cur.ops[0].successors[0].block;
|
|
252
|
+
}
|
|
253
|
+
return cur;
|
|
254
|
+
};
|
|
255
|
+
// Concretely SIMULATE the decision tree for a scrutinee value `xv`, returning the leaf block it
|
|
256
|
+
// reaches (or null on an unexpected cycle). This is PRE3 done concretely: it lets us verify each
|
|
257
|
+
// recovered case value actually routes to its recorded body in the ORIGINAL tree.
|
|
258
|
+
const simulateTree = (xv: number): Block | null => {
|
|
259
|
+
let cur = b;
|
|
260
|
+
const guard = new Set<Block>();
|
|
261
|
+
for (;;) {
|
|
262
|
+
const ti = testInfo(cur, cur === b);
|
|
263
|
+
if (!ti || ti.x !== scrut) {
|
|
264
|
+
return cur;
|
|
265
|
+
} // reached a leaf (case body / default)
|
|
266
|
+
if (guard.has(cur)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
guard.add(cur);
|
|
270
|
+
const term = cur.ops[cur.ops.length - 1];
|
|
271
|
+
const taken = evalCmp(ti.opcode, ti.xOnLeft, xv, ti.k);
|
|
272
|
+
cur = skipForward(term.successors[taken ? 0 : 1].block);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const seen = new Set<Block>();
|
|
276
|
+
const work: Block[] = [b];
|
|
277
|
+
while (work.length) {
|
|
278
|
+
const blk = work.pop()!;
|
|
279
|
+
if (seen.has(blk)) {
|
|
280
|
+
return null;
|
|
281
|
+
} // a test-block DAG cycle → decline
|
|
282
|
+
seen.add(blk);
|
|
283
|
+
const ti = testInfo(blk, blk === b);
|
|
284
|
+
if (!ti || ti.x !== scrut) {
|
|
285
|
+
return null;
|
|
286
|
+
} // PRE1: every test is on the SAME Value
|
|
287
|
+
const term = blk.ops[blk.ops.length - 1];
|
|
288
|
+
const taken = skipForward(term.successors[0].block),
|
|
289
|
+
fall = skipForward(term.successors[1].block);
|
|
290
|
+
const asLeafOrTest = (child: Block, role: 'case' | 'nav', k?: number) => {
|
|
291
|
+
const isTest = !!testInfo(child, false) && testInfo(child, false)!.x === scrut;
|
|
292
|
+
if (role === 'case') {
|
|
293
|
+
if (isTest) {
|
|
294
|
+
return false;
|
|
295
|
+
} // a case target that's a test → decline
|
|
296
|
+
if (child.params.length) {
|
|
297
|
+
return false;
|
|
298
|
+
} // case entry with a phi → decline
|
|
299
|
+
if (cases.has(k!)) {
|
|
300
|
+
return false;
|
|
301
|
+
} // duplicate case value → decline
|
|
302
|
+
cases.set(k!, child);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
// navigation edge
|
|
306
|
+
if (isTest) {
|
|
307
|
+
work.push(child);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
defaultCands.add(child); // a non-test leaf reached by nav = default
|
|
311
|
+
return true;
|
|
312
|
+
};
|
|
313
|
+
if (ti.cls === 'eq') {
|
|
314
|
+
if (!asLeafOrTest(taken, 'case', ti.k)) {
|
|
315
|
+
return null;
|
|
316
|
+
} // x==k → taken is case k
|
|
317
|
+
if (!asLeafOrTest(fall, 'nav')) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
} else if (ti.cls === 'ne') {
|
|
321
|
+
if (!switchAllowsNeqCase) {
|
|
322
|
+
return null;
|
|
323
|
+
} // per-compiler gate
|
|
324
|
+
if (!asLeafOrTest(fall, 'case', ti.k)) {
|
|
325
|
+
return null;
|
|
326
|
+
} // x!=k → the EQUAL side (fall) is case k
|
|
327
|
+
if (!asLeafOrTest(taken, 'nav')) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
// relational → pure navigation
|
|
332
|
+
if (!asLeafOrTest(taken, 'nav')) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
if (!asLeafOrTest(fall, 'nav')) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (cases.size < 2) {
|
|
342
|
+
return null;
|
|
343
|
+
} // not worth a switch (m2c: ≥2 cases)
|
|
344
|
+
// The default is the single non-test leaf that is NOT a case body. 0 → no default; ≥2 distinct → decline.
|
|
345
|
+
const caseBlocks = new Set(cases.values());
|
|
346
|
+
const defaults = [...defaultCands].filter((d) => !caseBlocks.has(d));
|
|
347
|
+
if (defaults.length > 1) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const defaultBlk = defaults[0] ?? null;
|
|
351
|
+
if (defaultBlk && defaultBlk.params.length) {
|
|
352
|
+
return null;
|
|
353
|
+
} // default entry with a phi → decline
|
|
354
|
+
// A default candidate that is ALSO a case body means a relational edge hit a case leaf → ambiguous.
|
|
355
|
+
if ([...defaultCands].some((d) => caseBlocks.has(d))) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// PRE1 dominance of the whole region: b must dominate every case body + the default (single-entry).
|
|
360
|
+
for (const cb of caseBlocks) {
|
|
361
|
+
if (!dom.get(cb)!.has(b)) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (defaultBlk && !dom.get(defaultBlk)!.has(b)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// PRE2 (fall-through): only NON-fall-through switches are handled — decline if any case body
|
|
370
|
+
// can reach ANOTHER case body (or the default) while staying inside the region. (The SAME
|
|
371
|
+
// predicate serves the Regime-B path, which throws instead.)
|
|
372
|
+
const merge = ipdom.get(b) ?? stop;
|
|
373
|
+
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
374
|
+
if (caseRegionReachesSibling(targets, b, merge)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// PRE3 (concrete interval consistency): a `case k` is only sound if the ORIGINAL tree
|
|
379
|
+
// actually routes x==k to its recorded body. A relational guard can make an `x==k` test DEAD (e.g.
|
|
380
|
+
// `if(x<5){ if(x==20) … }` — x==20 is unreachable under x<5); a naive switch would resurrect `case 20`
|
|
381
|
+
// and misroute x==20. Simulating the tree per case value catches exactly this — decline on any mismatch.
|
|
382
|
+
for (const [k, blk] of cases) {
|
|
383
|
+
if (simulateTree(k) !== blk) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Build the switch. Cases sorted ascending (safe: no fall-through — PRE2). Bodies delegate to the
|
|
389
|
+
// existing structureRegion (loops/ifs inside cases, the onStack guard — all reused).
|
|
390
|
+
const scrutExpr = expr(scrut);
|
|
391
|
+
const sortedCases = [...cases.entries()].sort((a, c) => a[0] - c[0]);
|
|
392
|
+
const outCases: SwitchCase[] = sortedCases.map(([k, blk]) => ({
|
|
393
|
+
values: [k],
|
|
394
|
+
body: structureRegion(blk, merge),
|
|
395
|
+
fallsThrough: false,
|
|
396
|
+
}));
|
|
397
|
+
const sw: Stmt = {
|
|
398
|
+
k: 'switch',
|
|
399
|
+
scrutinee: scrutExpr,
|
|
400
|
+
cases: outCases,
|
|
401
|
+
...(defaultBlk ? { default: structureRegion(defaultBlk, merge) } : {}),
|
|
402
|
+
};
|
|
403
|
+
const out: Stmt[] = [sw];
|
|
404
|
+
if (merge && merge !== stop) {
|
|
405
|
+
out.push(...structureRegion(merge, stop));
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
};
|
|
409
|
+
return { recognizeSwitch, caseRegionReachesSibling };
|
|
410
|
+
}
|
package/src/target.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// asmlift — the Target: (isa, compiler) as first-class axes. ABI + capabilities are DATA
|
|
2
|
+
// consumed generically by shared passes — never a target-name branch inside a shared pass
|
|
3
|
+
// (m2c's `arch.arch ==` leakage).
|
|
4
|
+
//
|
|
5
|
+
// What each datum drives:
|
|
6
|
+
// • id → frontend dispatch (registry.ts)
|
|
7
|
+
// • compiler → idiom gating (patternApplies) + the report
|
|
8
|
+
// • argRegs / returnReg → entry-param ordering and return-value read in the frontends
|
|
9
|
+
// • capabilities.hwDivide → gates the MIPS hardware-divide decode (mips.ts), the soft-division
|
|
10
|
+
// pre-recovery pass, and idiom gating; a `div` on a target declaring no divider degrades to
|
|
11
|
+
// a loud opaque (exercised by packages/cli/test/matching/divmul.test.ts). `hwFloat` → idiom
|
|
12
|
+
// gating only (no float pass yet).
|
|
13
|
+
// • capabilities.endianness / flags → RESERVED hardware facts, not yet read by any pass
|
|
14
|
+
// (byte-addressing will consume endianness; PPC condition regs → flags).
|
|
15
|
+
// • compilerBehaviors.* → all consumed by the structurer (threaded via StructureOptions).
|
|
16
|
+
//
|
|
17
|
+
// `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization choices) are
|
|
18
|
+
// deliberately separate bags: a new compiler must set its behaviors EXPLICITLY instead of
|
|
19
|
+
// silently inheriting a universal that is really per-compiler. `coalesceLoopInit` already
|
|
20
|
+
// differs across targets (IDO true, agbcc/GCC false).
|
|
21
|
+
// This module is browser-pure by contract (no Node APIs, enforced by
|
|
22
|
+
// test/browser-safe.test.ts): the toolchain paths that COMPILE for these targets
|
|
23
|
+
// live in @asmlift/toolchains.
|
|
24
|
+
import type { StructureOptions } from './structure/structure';
|
|
25
|
+
|
|
26
|
+
export interface TargetDescription {
|
|
27
|
+
id: string; // the ISA — 'armv4t' / 'mips' / 'ppc'. Selects the frontend (registry.ts).
|
|
28
|
+
// The COMPILER is a first-class axis distinct from the ISA (matching = deoptimize to a specific
|
|
29
|
+
// compiler): two targets can share an ISA (⇒ one frontend) yet differ here — e.g. MIPS_IDO vs
|
|
30
|
+
// MIPS_GCC. Consumed by pattern gating (patternApplies) and the report. (version/flags/language
|
|
31
|
+
// are future axes, added when earned.)
|
|
32
|
+
compiler: string; // 'agbcc' / 'ido' / 'gcc' / 'mwcc'
|
|
33
|
+
argRegs: string[];
|
|
34
|
+
returnReg: string;
|
|
35
|
+
// HARDWARE / ISA facts — independent of the compiler.
|
|
36
|
+
capabilities: {
|
|
37
|
+
endianness: 'little' | 'big'; // RESERVED — no pass reads it yet (byte-addressing will)
|
|
38
|
+
hwDivide: boolean; // consumed by patternApplies (idiom gating)
|
|
39
|
+
hwFloat: boolean; // consumed by patternApplies (idiom gating)
|
|
40
|
+
flags: boolean; // RESERVED — no pass reads it yet (PPC condition regs will)
|
|
41
|
+
};
|
|
42
|
+
// COMPILER BEHAVIORS — the specific compiler's canonicalization choices, distinct from
|
|
43
|
+
// hardware `capabilities`. All consumed by the structurer (threaded through StructureOptions).
|
|
44
|
+
compilerBehaviors: {
|
|
45
|
+
// When a loop induction variable's initial value comes from an argument register, some
|
|
46
|
+
// compilers keep mutating that register across the loop (coalesce → no init copy); others
|
|
47
|
+
// copy to a fresh local. IDO -O2 reuses the arg register (true); agbcc/KMC-GCC allocate
|
|
48
|
+
// fresh (false).
|
|
49
|
+
coalesceLoopInit?: boolean;
|
|
50
|
+
// Divergent-if (both arms terminate, no join): reproduce the source branch DIRECTION by
|
|
51
|
+
// emitting the forward-branch-on-negated-condition (taken arm as `else`). IDO/MIPS preserves
|
|
52
|
+
// source direction so this must be on to be byte-exact; agbcc/GCC canonicalize either way so
|
|
53
|
+
// true is a safe default there. A compiler that inverts branch canonicalization sets it
|
|
54
|
+
// false. Absent ⇒ true; a compiler opts OUT.
|
|
55
|
+
preserveDivergentBranchSense?: boolean;
|
|
56
|
+
// Order the parallel-copy assignments at a CFG edge by the order their values are COMPUTED
|
|
57
|
+
// in the predecessor (vs. source/param order), matching a compiler that lays defining ops
|
|
58
|
+
// (and the copies reading them) out in computation order. Uniform (true) across all current
|
|
59
|
+
// compilers. Absent ⇒ true; a compiler opts OUT.
|
|
60
|
+
orderArgCopiesByComputation?: boolean;
|
|
61
|
+
// Regime-A switch recovery: accept an `x != K` test as a case (the EQUAL side is the case
|
|
62
|
+
// body). GCC freely emits `!=`; IDO prefers `==`/`<`. Absent ⇒ true (permissive); the
|
|
63
|
+
// decline path keeps recovery sound either way.
|
|
64
|
+
switchAllowsNeqCase?: boolean;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const ARMV4T_AGBCC: TargetDescription = {
|
|
69
|
+
id: 'armv4t',
|
|
70
|
+
compiler: 'agbcc',
|
|
71
|
+
argRegs: ['r0', 'r1', 'r2', 'r3'],
|
|
72
|
+
returnReg: 'r0',
|
|
73
|
+
capabilities: { endianness: 'little', hwDivide: false, hwFloat: false, flags: true },
|
|
74
|
+
compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** MIPS-II / IDO 7.1 target. IDO is the IRIX C compiler,
|
|
78
|
+
* statically recompiled to run natively (ido-static-recomp). Unlike agbcc
|
|
79
|
+
* it emits no textual asm, so asmlift's input is the DISASSEMBLED object (`mips-linux-gnu-
|
|
80
|
+
* objdump -d`); the arch-agnostic objdiff scorer scores the MIPS object directly. Big-endian,
|
|
81
|
+
* hardware divide + FPU (N64). */
|
|
82
|
+
export const MIPS_IDO: TargetDescription = {
|
|
83
|
+
id: 'mips',
|
|
84
|
+
compiler: 'ido',
|
|
85
|
+
argRegs: ['a0', 'a1', 'a2', 'a3'],
|
|
86
|
+
returnReg: 'v0',
|
|
87
|
+
capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
|
|
88
|
+
// `switchAllowsNeqCase: false` — IDO's switch dispatch uses `==`/`<`, never `!=` cases;
|
|
89
|
+
// leaving it permissive mis-recognises `!=`-rooted if-else chains as switches.
|
|
90
|
+
compilerBehaviors: {
|
|
91
|
+
coalesceLoopInit: true,
|
|
92
|
+
preserveDivergentBranchSense: true,
|
|
93
|
+
orderArgCopiesByComputation: true,
|
|
94
|
+
switchAllowsNeqCase: false,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** MIPS + KMC GCC — the SAME ISA as MIPS_IDO, a DIFFERENT compiler: `id:"mips"` reuses the
|
|
99
|
+
* `mips` frontend verbatim, only `compiler` varies. Same N64 hardware ⇒ identical hardware
|
|
100
|
+
* capabilities to IDO. */
|
|
101
|
+
export const MIPS_GCC: TargetDescription = {
|
|
102
|
+
id: 'mips',
|
|
103
|
+
compiler: 'gcc',
|
|
104
|
+
argRegs: ['a0', 'a1', 'a2', 'a3'],
|
|
105
|
+
returnReg: 'v0',
|
|
106
|
+
// KMC GCC allocates a fresh local for the loop init (coalesceLoopInit false — where it differs
|
|
107
|
+
// from IDO); the structuring levers take the universal default until a KMC fixture says otherwise.
|
|
108
|
+
capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
|
|
109
|
+
compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** PowerPC (GameCube/Wii) + Metrowerks CodeWarrior. The real GC/Wii matching target is
|
|
113
|
+
* CodeWarrior `mwcceppc` (not GCC): active decomp projects and decomp.me standardize on it.
|
|
114
|
+
* `-proc gekko` = the GC Gekko CPU. Big-endian, hardware divide + FPU. `flags: true`: PPC has
|
|
115
|
+
* condition registers (cr0–cr7), but compare→branch still fuses into a single `cond_br`
|
|
116
|
+
* (test/ppc-seam.test.ts), so `flags` stays a documented hardware fact, not yet an IR concern —
|
|
117
|
+
* real flags-as-data is deferred until a fixture reuses/combines a cr field. */
|
|
118
|
+
export const PPC_MWCC: TargetDescription = {
|
|
119
|
+
id: 'ppc',
|
|
120
|
+
compiler: 'mwcc',
|
|
121
|
+
// PPC EABI: r3–r10 pass integer/pointer arguments; r3 also returns.
|
|
122
|
+
argRegs: ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'],
|
|
123
|
+
returnReg: 'r3',
|
|
124
|
+
capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: true },
|
|
125
|
+
// CodeWarrior's structuring levers are UNKNOWN until fixtures reveal them — safe universal
|
|
126
|
+
// defaults; coalesceLoopInit false until a CW loop fixture says otherwise.
|
|
127
|
+
compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Build the structurer's options for a target: the function's own `returnsVoid` plus every
|
|
131
|
+
* `compilerBehaviors` lever (they map 1:1 onto StructureOptions field names). The ONE place a
|
|
132
|
+
* target's compiler behaviors flow into the target-agnostic structurer — a new behavior lever
|
|
133
|
+
* is a field in `compilerBehaviors`, consumed automatically. */
|
|
134
|
+
export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
|
|
135
|
+
return { returnsVoid, ...t.compilerBehaviors };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const C_TYPEDEFS =
|
|
139
|
+
'typedef unsigned char u8;typedef unsigned short u16;typedef unsigned int u32;' +
|
|
140
|
+
'typedef signed char s8;typedef short s16;typedef int s32;\n';
|