@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
package/src/macros.ts
CHANGED
|
@@ -22,32 +22,206 @@ export interface AddressMacro {
|
|
|
22
22
|
size: number;
|
|
23
23
|
/** the cast type's signedness */
|
|
24
24
|
signed: boolean;
|
|
25
|
+
/** the cast type was volatile-qualified (`vu16`) — the MMIO idiom. Load-bearing: a
|
|
26
|
+
* non-volatile spelling lets the compiler fold or reorder repeated accesses. */
|
|
27
|
+
volatile?: true;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/** The scalar type spellings a cast may use, and what each one means. Deliberately a CLOSED table:
|
|
28
31
|
* an unrecognized spelling (a project typedef, an enum, a struct) is refused rather than guessed,
|
|
29
32
|
* and every `volatile` alias is absent so it can never be silently dropped — the qualifier changes
|
|
30
33
|
* whether repeated reads may be folded, which is both a byte and a semantic difference. */
|
|
31
|
-
const SCALAR_TYPES: Record<string, { size: number; signed: boolean }> = {
|
|
34
|
+
const SCALAR_TYPES: Record<string, { size: number; signed: boolean; volatile?: true }> = {
|
|
32
35
|
u8: { size: 1, signed: false },
|
|
33
36
|
s8: { size: 1, signed: true },
|
|
34
37
|
u16: { size: 2, signed: false },
|
|
35
38
|
s16: { size: 2, signed: true },
|
|
36
39
|
u32: { size: 4, signed: false },
|
|
37
40
|
s32: { size: 4, signed: true },
|
|
41
|
+
// The `volatile` aliases. They were excluded so the qualifier could never be silently dropped;
|
|
42
|
+
// it is now CARRIED instead (`volatile: true`, reproduced by every spelling this feeds), which
|
|
43
|
+
// is the same guarantee without the cost — refusing them lost every MMIO register name a GBA
|
|
44
|
+
// project has, since those are exactly the cells one declares volatile.
|
|
45
|
+
vu8: { size: 1, signed: false, volatile: true },
|
|
46
|
+
vs8: { size: 1, signed: true, volatile: true },
|
|
47
|
+
vu16: { size: 2, signed: false, volatile: true },
|
|
48
|
+
vs16: { size: 2, signed: true, volatile: true },
|
|
49
|
+
vu32: { size: 4, signed: false, volatile: true },
|
|
50
|
+
vs32: { size: 4, signed: true, volatile: true },
|
|
38
51
|
};
|
|
39
52
|
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
53
|
+
/** A pointer cast inside an address expression (`(void *)0x4000000`). The VALUE is the integer it
|
|
54
|
+
* wraps: these headers spell a register base as a `void *` and add a byte offset to it, which is
|
|
55
|
+
* GCC's byte-arithmetic extension, so the cast contributes nothing to the address.
|
|
56
|
+
*
|
|
57
|
+
* BYTE-SIZED POINTEES ONLY, and that restriction is load-bearing rather than tidy. C pointer
|
|
58
|
+
* arithmetic SCALES by the pointee: `(vu16 *)0x4000000 + 5` is 0x400000A, not 0x4000005. Stripping
|
|
59
|
+
* a wider cast would fold the wrong address AND then republish it in a synthesized body that
|
|
60
|
+
* agrees with itself — so the candidate still byte-matches the numeric pool word it was looked up
|
|
61
|
+
* by, while naming a different register. A wrong name that survives the differ is the one failure
|
|
62
|
+
* this module cannot let through.
|
|
63
|
+
*
|
|
64
|
+
* A wider pointee is REFUSED EXPLICITLY below, not left to fall out of the token grammar further
|
|
65
|
+
* down — the enforcing line belongs next to the rule it enforces. The cost is named rather than
|
|
66
|
+
* hidden: a wider cast with NO arithmetic after it would fold correctly and is refused anyway,
|
|
67
|
+
* because the hazard is cast-THEN-add and this cannot tell which it is looking at. */
|
|
68
|
+
const PTR_CAST_ANY = /\(\s*(\w+)\s*\*\s*\)/g;
|
|
69
|
+
const BYTE_POINTEE = new Set(['void', 'u8', 's8', 'vu8', 'vs8']);
|
|
70
|
+
|
|
71
|
+
/** `src` with byte-sized pointer casts removed, or null if any cast SCALES. */
|
|
72
|
+
function stripPointerCasts(src: string): string | null {
|
|
73
|
+
let scaling = false;
|
|
74
|
+
const out = src.replace(PTR_CAST_ANY, (_m, pointee: string) => {
|
|
75
|
+
if (!BYTE_POINTEE.has(pointee)) {
|
|
76
|
+
scaling = true;
|
|
77
|
+
}
|
|
78
|
+
return ' ';
|
|
79
|
+
});
|
|
80
|
+
return scaling ? null : out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** An object-like `#define NAME body`, for the expansion table the address evaluator resolves
|
|
84
|
+
* identifiers against. Function-like macros (`NAME(x)`) are deliberately excluded: an address
|
|
85
|
+
* expression that calls one is refused, not expanded. */
|
|
86
|
+
const OBJECT_DEFINE = /^\s*#define\s+([A-Za-z_]\w*)\s+(\S.*?)\s*$/;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Evaluate a macro's ADDRESS operand to a number, or null when it is not a constant expression
|
|
90
|
+
* this module can be sure of.
|
|
91
|
+
*
|
|
92
|
+
* Real decomp headers rarely write the address as a literal. The Klonoa headers spell every
|
|
93
|
+
* register as `(*(vu16 *)REG_ADDR_BLDALPHA)` over `REG_ADDR_BLDALPHA = (REG_BASE +
|
|
94
|
+
* REG_OFFSET_BLDALPHA)`, `REG_BASE = (void *)0x4000000`, `REG_OFFSET_BLDALPHA = 0x52` — so a
|
|
95
|
+
* literal-only recognizer sees none of the 466 `REG_*` names, and reads every MMIO cell as a
|
|
96
|
+
* decimal address instead.
|
|
97
|
+
*
|
|
98
|
+
* The accepted language is deliberately tiny — integer literals, `+`, `-`, parentheses, pointer
|
|
99
|
+
* casts (see {@link PTR_CAST}), and identifiers that resolve to another object-like define. Any
|
|
100
|
+
* other token, an unknown identifier, a function-like macro, a cycle, or a negative result refuses
|
|
101
|
+
* the whole expression. Folding is done on the EXPANDED integer text, so an operand only ever
|
|
102
|
+
* evaluates to a number every step of which this module recognized.
|
|
103
|
+
*/
|
|
104
|
+
function evalAddressExpr(
|
|
105
|
+
src: string,
|
|
106
|
+
defines: ReadonlyMap<string, string>,
|
|
107
|
+
seen: ReadonlySet<string>,
|
|
108
|
+
memo: Map<string, number | null> = new Map(),
|
|
109
|
+
): number | null {
|
|
110
|
+
if (seen.size > 12) {
|
|
111
|
+
return null; // pathological nesting — refuse rather than walk further
|
|
112
|
+
}
|
|
113
|
+
const stripped = stripPointerCasts(src);
|
|
114
|
+
if (stripped === null) {
|
|
115
|
+
return null; // a scaling pointer cast — see PTR_CAST_ANY
|
|
116
|
+
}
|
|
117
|
+
const tokens = stripped.match(/[A-Za-z_]\w*|0[xX][0-9A-Fa-f]+|\d+|[()+-]/g);
|
|
118
|
+
// every character must belong to a token — anything else (`*`, `<<`, a comma) is out of language
|
|
119
|
+
if (!tokens || tokens.join('') !== stripped.replace(/\s+/g, '')) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const expanded: string[] = [];
|
|
123
|
+
for (const tok of tokens) {
|
|
124
|
+
if (/^[A-Za-z_]/.test(tok)) {
|
|
125
|
+
const body = defines.get(tok);
|
|
126
|
+
if (body === undefined || seen.has(tok)) {
|
|
127
|
+
return null; // undefined name, or a cycle
|
|
128
|
+
}
|
|
129
|
+
// Memoized per NAME. The depth cap bounds nesting but not BRANCHING — a define mentioning k
|
|
130
|
+
// others re-evaluates the whole subtree k times, so a deep, wide table costs exponentially.
|
|
131
|
+
//
|
|
132
|
+
// A name's result CAN depend on the path that reached it: both refusals below are
|
|
133
|
+
// path-sensitive (already in `seen`; depth cap hit), so a cached `null` may be pessimistic
|
|
134
|
+
// for a shorter path. Safe in ONE direction only — path-dependence can make this refuse
|
|
135
|
+
// more, never fold a wrong address, which is the direction this module may be wrong in.
|
|
136
|
+
//
|
|
137
|
+
// The memo being PER TOP-LEVEL MACRO (the default parameter, fresh at each entry) is
|
|
138
|
+
// load-bearing rather than incidental: hoisting it across macros to "go faster" would let
|
|
139
|
+
// one deep macro poison a name for every macro after it, silently dropping recognized cells.
|
|
140
|
+
let inner: number | null;
|
|
141
|
+
if (memo.has(tok)) {
|
|
142
|
+
inner = memo.get(tok)!;
|
|
143
|
+
} else {
|
|
144
|
+
inner = evalAddressExpr(body, defines, new Set([...seen, tok]), memo);
|
|
145
|
+
memo.set(tok, inner);
|
|
146
|
+
}
|
|
147
|
+
if (inner === null) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
expanded.push(`(${inner})`);
|
|
151
|
+
} else if (/^0[xX]/.test(tok)) {
|
|
152
|
+
expanded.push(String(Number.parseInt(tok, 16)));
|
|
153
|
+
} else {
|
|
154
|
+
expanded.push(tok);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const folded = foldIntegerExpr(expanded.join(' '));
|
|
158
|
+
return folded !== null && Number.isSafeInteger(folded) && folded >= 0 ? folded : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Fold a fully-expanded `+`/`-`/parenthesis integer expression. Written out rather than handed to
|
|
162
|
+
* an evaluator so nothing outside that grammar can ever be executed. */
|
|
163
|
+
function foldIntegerExpr(text: string): number | null {
|
|
164
|
+
const toks = text.match(/\d+|[()+-]/g);
|
|
165
|
+
if (!toks || toks.join('') !== text.replace(/\s+/g, '')) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
let at = 0;
|
|
169
|
+
const expr = (): number | null => {
|
|
170
|
+
let acc = term();
|
|
171
|
+
if (acc === null) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
while (toks[at] === '+' || toks[at] === '-') {
|
|
175
|
+
const op = toks[at++];
|
|
176
|
+
const rhs = term();
|
|
177
|
+
if (rhs === null) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
acc = op === '+' ? acc + rhs : acc - rhs;
|
|
181
|
+
}
|
|
182
|
+
return acc;
|
|
183
|
+
};
|
|
184
|
+
const term = (): number | null => {
|
|
185
|
+
if (toks[at] === '(') {
|
|
186
|
+
at++;
|
|
187
|
+
const inner = expr();
|
|
188
|
+
if (inner === null || toks[at] !== ')') {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
at++;
|
|
192
|
+
return inner;
|
|
193
|
+
}
|
|
194
|
+
if (toks[at] === '-') {
|
|
195
|
+
at++;
|
|
196
|
+
const v = term();
|
|
197
|
+
return v === null ? null : -v;
|
|
198
|
+
}
|
|
199
|
+
const tok = toks[at];
|
|
200
|
+
if (tok === undefined || !/^\d+$/.test(tok)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
at++;
|
|
204
|
+
return Number(tok);
|
|
205
|
+
};
|
|
206
|
+
const value = expr();
|
|
207
|
+
return value !== null && at === toks.length ? value : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `#define NAME (*(TYPE *)ADDR)` — the ONE shape recognized, where ADDR is any constant
|
|
211
|
+
* expression {@link evalAddressExpr} can be sure of (a literal, or names that resolve to one).
|
|
212
|
+
* Anything else — a two-level indirection `(*(T **)…)`, a function-like macro, a bare integer
|
|
213
|
+
* constant — does not match and is therefore refused by construction. */
|
|
214
|
+
const ADDRESS_CAST = /^\s*#define\s+([A-Za-z_]\w*)\s+(\(\s*\*\s*\(\s*(\w+)\s*\*\s*\)\s*(.+?)\s*\))\s*$/;
|
|
215
|
+
|
|
216
|
+
/** A bare hex literal — the operand form whose macro body is already self-contained. */
|
|
217
|
+
const HEX_LITERAL = /^0[xX][0-9A-Fa-f]+$/;
|
|
44
218
|
|
|
45
219
|
/**
|
|
46
220
|
* Recognize the address-cast macros in `cpp -dD` output, keyed by the address each names.
|
|
47
221
|
*
|
|
48
222
|
* REFUSALS, all of them because the alternative is a plausible-but-wrong spelling:
|
|
49
|
-
* - a cast type outside {@link SCALAR_TYPES} —
|
|
50
|
-
*
|
|
223
|
+
* - a cast type outside {@link SCALAR_TYPES} — a project typedef, an enum, a struct;
|
|
224
|
+
* - an address expression {@link evalAddressExpr} cannot fold to a definite number;
|
|
51
225
|
* - two macros naming the SAME address (`REG_VCOUNT`/`REG_VCOUNT_L`/`REG_VCOUNT_H` at 0x04000006
|
|
52
226
|
* differ in width, and picking wrong turns an `ldrh` into an `ldrb`) — both are dropped;
|
|
53
227
|
* - one name defined at two addresses, which no correct spelling can disambiguate.
|
|
@@ -59,6 +233,21 @@ export function addressCastMacros(cppOutput: string): Map<number, AddressMacro>
|
|
|
59
233
|
/** The same recognizer over already-split `#define NAME body` lines — what a DWARF
|
|
60
234
|
* `.debug_macinfo` reader produces once each definition is re-spelled as a directive. */
|
|
61
235
|
export function addressCastMacrosFrom(defineLines: readonly string[]): Map<number, AddressMacro> {
|
|
236
|
+
// Pass 1: every object-like define, so an address expression can resolve the names it mentions.
|
|
237
|
+
// A macro's address is frequently spelled in terms of others (`REG_BASE + REG_OFFSET_X`), and
|
|
238
|
+
// those helpers are not themselves address casts — they exist only to be expanded.
|
|
239
|
+
//
|
|
240
|
+
// LAST DEFINITION WINS, and `#undef` is not modelled: the record is a flat list with no scope, so
|
|
241
|
+
// a name redefined differently across translation units resolves to whichever came last. Sound
|
|
242
|
+
// for a project whose headers agree (the Klonoa ELF redefines no name with a differing body);
|
|
243
|
+
// a project where they disagree would need per-CU scoping, which the record does not carry.
|
|
244
|
+
const defines = new Map<string, string>();
|
|
245
|
+
for (const line of defineLines) {
|
|
246
|
+
const d = OBJECT_DEFINE.exec(line);
|
|
247
|
+
if (d) {
|
|
248
|
+
defines.set(d[1], d[2]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
62
251
|
const byAddress = new Map<number, AddressMacro>();
|
|
63
252
|
const collided = new Set<number>();
|
|
64
253
|
const seenNames = new Map<string, number>();
|
|
@@ -67,15 +256,28 @@ export function addressCastMacrosFrom(defineLines: readonly string[]): Map<numbe
|
|
|
67
256
|
if (!m) {
|
|
68
257
|
continue;
|
|
69
258
|
}
|
|
70
|
-
const [, name,
|
|
259
|
+
const [, name, rawBody, typeName, addrText] = m;
|
|
71
260
|
const type = SCALAR_TYPES[typeName];
|
|
72
261
|
if (!type) {
|
|
73
|
-
continue; //
|
|
262
|
+
continue; // a spelling outside the closed table — refuse
|
|
74
263
|
}
|
|
75
|
-
const address =
|
|
76
|
-
if (
|
|
77
|
-
continue;
|
|
264
|
+
const address = evalAddressExpr(addrText, defines, new Set([name]));
|
|
265
|
+
if (address === null) {
|
|
266
|
+
continue; // an address expression this module cannot be sure of — refuse
|
|
78
267
|
}
|
|
268
|
+
// The body must be SELF-CONTAINED and COMPILABLE, because it is republished verbatim as the
|
|
269
|
+
// definition a reproduction compiles against (macroDefinesUsedBy) — a body naming
|
|
270
|
+
// `REG_ADDR_VCOUNT` would need that macro, and its two helpers, carried along with it. An
|
|
271
|
+
// unqualified literal address keeps the project's own spelling; anything else is re-spelled at
|
|
272
|
+
// the address it evaluated to, which is the same cell and the same type.
|
|
273
|
+
// A VOLATILE body is re-spelled even when its address is already a literal: the alias it uses
|
|
274
|
+
// (`vu8`) is a PROJECT typedef, and the prelude a candidate compiles against declares only
|
|
275
|
+
// u8/u16/u32 + s8/s16/s32. Keeping such a body verbatim republishes a `#define` that does not
|
|
276
|
+
// compile — latent, because it only bites in the self-declared world.
|
|
277
|
+
const body =
|
|
278
|
+
HEX_LITERAL.test(addrText) && !type.volatile
|
|
279
|
+
? rawBody
|
|
280
|
+
: `(*(${type.volatile ? 'volatile ' : ''}${type.signed ? 's' : 'u'}${type.size * 8} *)0x${address.toString(16).toUpperCase()})`;
|
|
79
281
|
const priorAddr = seenNames.get(name);
|
|
80
282
|
if (priorAddr !== undefined && priorAddr !== address) {
|
|
81
283
|
collided.add(priorAddr);
|
|
@@ -88,7 +290,14 @@ export function addressCastMacrosFrom(defineLines: readonly string[]): Map<numbe
|
|
|
88
290
|
collided.add(address);
|
|
89
291
|
continue;
|
|
90
292
|
}
|
|
91
|
-
byAddress.set(address, {
|
|
293
|
+
byAddress.set(address, {
|
|
294
|
+
name,
|
|
295
|
+
address,
|
|
296
|
+
body,
|
|
297
|
+
size: type.size,
|
|
298
|
+
signed: type.signed,
|
|
299
|
+
...(type.volatile ? { volatile: true as const } : {}),
|
|
300
|
+
});
|
|
92
301
|
}
|
|
93
302
|
for (const addr of collided) {
|
|
94
303
|
byAddress.delete(addr);
|
package/src/pattern/engine.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Crucially, rewrites go through replaceAllUsesWith + DCE — never in-place opcode
|
|
7
7
|
// mutation of a live value.
|
|
8
8
|
import { Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
9
|
-
import { type Opcode, isDceSafe } from '../ir/opcodes';
|
|
9
|
+
import { NEGATED_ICMP, type Opcode, isDceSafe } from '../ir/opcodes';
|
|
10
10
|
import type { IrType } from '../ir/types';
|
|
11
11
|
import { T } from '../ir/types';
|
|
12
12
|
|
|
@@ -219,18 +219,107 @@ const sextPat = (w: number, k: number): RewritePattern => ({
|
|
|
219
219
|
|
|
220
220
|
/** Byte/half zero- and sign-extension casts. Byte = shift by 24, half = shift by 16. The
|
|
221
221
|
* zero-extend forms fix a miscompile; the sign-extend forms already byte-matched as raw shifts and
|
|
222
|
-
* fold here for readability + `(s8)`/`(s16)` parity, staying byte-exact (`(s8)x` → `lsl;asr`).
|
|
222
|
+
* fold here for readability + `(s8)`/`(s16)` parity, staying byte-exact (`(s8)x` → `lsl;asr`).
|
|
223
|
+
*
|
|
224
|
+
* SHADOWING NOTE: these run at the idiom stage, BEFORE structuring — so a symbol-map BITFIELD of
|
|
225
|
+
* width exactly 8 or 16 whose bits start at bit 0 of its load is folded to `(u8)x`/`(u16)x` here
|
|
226
|
+
* and never reaches the bitfield member recognizer (structure.ts, which matches the raw
|
|
227
|
+
* `shr(shl(load))` shape only). Honest output, not a miscompile — the field just keeps the cast
|
|
228
|
+
* spelling at those widths. Teaching the recognizer a zext/sext arm is the coverage extension if
|
|
229
|
+
* a row ever needs it. */
|
|
223
230
|
export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16), sextPat(8, 24), sextPat(16, 16)];
|
|
224
231
|
|
|
232
|
+
// ── boolean-negation idiom ───────────────────────────────────────────────────────────────────
|
|
233
|
+
// `cmp ^ 1` IS `!cmp`: an `icmp_*` result is 0 or 1 by construction (ir/opcodes.ts), so xoring the
|
|
234
|
+
// low bit flips exactly the boolean. A compiler with no set-on-greater-equal spells a MATERIALISED
|
|
235
|
+
// `a >= b` as its opposite plus that flip — MIPS `slt v0,a0,a1; xori v0,v0,1`, the shape IDO and
|
|
236
|
+
// both GCCs emit and the only one asmlift has measured (m2c `40cbae3` reports ARM `eor #1` too;
|
|
237
|
+
// no agbcc row in the corpus carries it, agbcc materializing the same boolean via branches).
|
|
238
|
+
// The naive lift prints that as the double-negative `a < b ^ 1`, and hides the comparison from
|
|
239
|
+
// every consumer that reasons about booleans: the short-circuit recognizer's `&&`/`||` fold matches
|
|
240
|
+
// an `icmp` feeder, not an `xor` of one. (It does not by itself unblock that fold — measured on
|
|
241
|
+
// this idiom's one benchmark inhabitant, the diamond still declines because raise/shortcircuit.ts
|
|
242
|
+
// additionally wants a 0/1 CONST arm and both arms here are comparisons. It removes one of the two
|
|
243
|
+
// blockers, and the spelling win stands on its own.)
|
|
244
|
+
//
|
|
245
|
+
// UNGATED, unlike the compiler-pinned folds above. Two independent reasons, and the second is the
|
|
246
|
+
// load-bearing one:
|
|
247
|
+
// • it is a semantic IDENTITY on asmlift's own IR, not a spelling trade — `xor(icmp, 1)` cannot
|
|
248
|
+
// mean anything but the negated compare on any target;
|
|
249
|
+
// • THE SHAPE IS ITS OWN GATE. The pattern can only fire where the compiler itself emitted the
|
|
250
|
+
// flip, and wherever it did, "the negated comparison" is precisely what it was spelling. A
|
|
251
|
+
// compiler with a set-on-greater-equal never produces the shape and so can never be harmed.
|
|
252
|
+
// Byte evidence is narrower than the reasoning: synthetic:inrange stays MATCH under gcc2.7.2kmc
|
|
253
|
+
// with the folded spelling, which proves the round-trip there; elsewhere it is unmeasured.
|
|
254
|
+
//
|
|
255
|
+
// The negation comes from THE shared table (ir/opcodes.ts NEGATED_ICMP), so this fold, the MIPS
|
|
256
|
+
// `slt …; beqz` branch fold and the short-circuit diamond negation cannot disagree about what the
|
|
257
|
+
// opposite of a compare is. One pattern per comparison — a data-driven fold needs a fixed
|
|
258
|
+
// replacement opcode, so the table is unrolled into ten patterns rather than expressed as a
|
|
259
|
+
// (nonexistent) computed-opcode replacement.
|
|
260
|
+
const notCmpPat = (cmp: string): RewritePattern => ({
|
|
261
|
+
id: `not-${cmp}`,
|
|
262
|
+
applies: {},
|
|
263
|
+
match: {
|
|
264
|
+
op: 'xor',
|
|
265
|
+
args: [
|
|
266
|
+
{ op: cmp, args: [{ bind: 'A' }, { bind: 'B' }] },
|
|
267
|
+
{ op: 'const', attrEquals: { value: 1 }, args: [] },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
// Pinned u32, matching CNTLZW_EQ0 — the other pattern in this file that produces a comparison.
|
|
271
|
+
// It is what raise/recover.ts stamps on every icmp result unconditionally anyway, so inheriting
|
|
272
|
+
// the `xor`'s type would reach the same place; saying it here keeps the two icmp-producing
|
|
273
|
+
// patterns on one discipline instead of leaving a reader to infer which is canonical.
|
|
274
|
+
replaceWith: { op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) },
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// The BRANCH-form siblings. Testing a boolean against zero is the same negation by another
|
|
278
|
+
// spelling, and it is what a compare-and-branch ISA actually emits: MIPS `slt v0,…; xori v0,v0,1;
|
|
279
|
+
// beqz v0,L` lifts to `icmp_eq(icmp_sge(…), 0)` once the `xori` has folded, because the branch is a
|
|
280
|
+
// genuine test of the materialised boolean (the frontend's own `slt …; beqz` fusion cannot fire —
|
|
281
|
+
// its pending compare was invalidated by the `xori` that redefined the register). Without these the
|
|
282
|
+
// `^ 1` fold just trades one double negative for another: `a0 >= a1 == 0`.
|
|
283
|
+
// icmp_eq(cmp, 0) → !cmp `(a >= b) == 0` is `a < b`
|
|
284
|
+
// icmp_ne(cmp, 0) → cmp `(a >= b) != 0` is `a >= b`
|
|
285
|
+
// Same soundness argument as the `^ 1` fold (an icmp result is 0/1, so `== 0` is exactly negation)
|
|
286
|
+
// and the same shape-is-its-own-gate reason to leave them ungated.
|
|
287
|
+
const cmpZeroPat = (cmp: string, test: 'icmp_eq' | 'icmp_ne'): RewritePattern => ({
|
|
288
|
+
id: `${test === 'icmp_eq' ? 'not' : 'is'}-zerotest-${cmp}`,
|
|
289
|
+
applies: {},
|
|
290
|
+
match: {
|
|
291
|
+
op: test,
|
|
292
|
+
args: [
|
|
293
|
+
{ op: cmp, args: [{ bind: 'A' }, { bind: 'B' }] },
|
|
294
|
+
{ op: 'const', attrEquals: { value: 0 }, args: [] },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
replaceWith: { op: test === 'icmp_eq' ? NEGATED_ICMP[cmp] : cmp, args: ['A', 'B'], resultType: T.u(32) },
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/** `cmp ^ 1` and the zero-test forms `cmp == 0` / `cmp != 0` → the (negated) comparison. Every
|
|
301
|
+
* entry is one comparison of `NEGATED_ICMP`; the bundle is what the boolean-reasoning consumers
|
|
302
|
+
* downstream (short-circuit recovery, the structurer's condition spelling) actually match on. */
|
|
303
|
+
export const NOT_CMP_PATTERNS: RewritePattern[] = [
|
|
304
|
+
...Object.keys(NEGATED_ICMP).map(notCmpPat),
|
|
305
|
+
...Object.keys(NEGATED_ICMP).flatMap((cmp) => [cmpZeroPat(cmp, 'icmp_eq'), cmpZeroPat(cmp, 'icmp_ne')]),
|
|
306
|
+
];
|
|
307
|
+
|
|
225
308
|
// The DEFAULT idiom bundle `decompile()` applies when the caller passes no `patterns`. It is
|
|
226
|
-
// EVERY idiom asmlift owns;
|
|
227
|
-
//
|
|
228
|
-
//
|
|
309
|
+
// EVERY idiom asmlift owns; the list self-selects per target through patternApplies — agbcc/gcc get
|
|
310
|
+
// sdiv-pow2, agbcc/ido/gcc get the mul-const folds, mwcc gets cntlzw-eq0 + rotl-mirror, and agbcc
|
|
311
|
+
// gets the casts. MOST patterns are `{compilers}`-gated because they trade one spelling for another
|
|
312
|
+
// and are only byte-safe where measured; the boolean-negation folds are deliberately UNGATED (see
|
|
313
|
+
// their comment — the shape is its own gate), so "gated per compiler" is the common case, not the
|
|
314
|
+
// invariant. Ordered like the sub-bundles: the
|
|
229
315
|
// division idiom, then the multiplies (base folds before the composite tail). Passing an explicit
|
|
230
316
|
// `patterns` (including `[]`) overrides this — `[]` runs the naive lift with no idiom folding.
|
|
231
317
|
export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
|
|
232
318
|
SDIV_POW2_2,
|
|
233
319
|
CNTLZW_EQ0,
|
|
320
|
+
// AFTER cntlzw-eq0, which is what turns mwcc's `clz(x) >> 5` into the `icmp_eq` this fold then
|
|
321
|
+
// negates — `!(x == 0)` composes only in that order (each pattern runs to fixpoint in turn).
|
|
322
|
+
...NOT_CMP_PATTERNS,
|
|
234
323
|
ROTL_MIRROR,
|
|
235
324
|
...MUL_CONST_PATTERNS,
|
|
236
325
|
...CAST_PATTERNS,
|
|
@@ -238,7 +327,11 @@ export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
|
|
|
238
327
|
|
|
239
328
|
// Ops whose operands a compiler may emit in either order — so an idiom's match must try both
|
|
240
329
|
// (agbcc emits `add(X, shr_u(X,31))`; KMC GCC emits `add(shr_u(X,31), X)` for the SAME `x/2`).
|
|
241
|
-
|
|
330
|
+
// `icmp_eq`/`icmp_ne` are here for the same reason, not as arithmetic: `x == 0` and `0 == x` are the
|
|
331
|
+
// same test, and which one a frontend builds is an accident of how the branch was decoded — the
|
|
332
|
+
// zero-test folds must match either. The ORDERED comparisons are deliberately absent: swapping the
|
|
333
|
+
// operands of `a < b` is `b > a`, a different opcode, which this mechanism cannot express.
|
|
334
|
+
const COMMUTATIVE = new Set(['add', 'mul', 'and', 'or', 'xor', 'icmp_eq', 'icmp_ne']);
|
|
242
335
|
|
|
243
336
|
interface Binds {
|
|
244
337
|
values: Map<string, Value>;
|
package/src/pipeline.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
// asmlift — the library entry point. `decompile(name, asm, target)` runs the raising tower and
|
|
2
2
|
// returns structured results: the source, the per-level IR dumps, and diagnostics.
|
|
3
3
|
import { cBackend } from './backend/c';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ContractError,
|
|
6
|
+
assertDerefsTyped,
|
|
7
|
+
assertEffectsPreserved,
|
|
8
|
+
assertResolved,
|
|
9
|
+
assertTypesRecovered,
|
|
10
|
+
} from './contracts';
|
|
5
11
|
import type { AsmData } from './frontend/asmdata';
|
|
6
12
|
import { FrontendUnsupportedError } from './frontend/errors';
|
|
7
13
|
import { frontendFor } from './frontend/registry';
|
|
8
|
-
import type
|
|
14
|
+
import { type Block, type Fn, successorsOf } from './ir/core';
|
|
9
15
|
import { print } from './ir/print';
|
|
10
16
|
import { T } from './ir/types';
|
|
11
17
|
import { VerifyError, verify } from './ir/verify';
|
|
12
|
-
import { Expr, LanguageBackend, SFn, Stmt, exprChildren, stmtChildren, stmtExprs } from './l3/ast';
|
|
18
|
+
import { Expr, LanguageBackend, SFn, Stmt, exprChildren, gapReasonFor, stmtChildren, stmtExprs } from './l3/ast';
|
|
13
19
|
import { hoistReusedGlobalBases } from './l3/basecse';
|
|
14
20
|
import { eliminateDeadStores } from './l3/dce';
|
|
21
|
+
import { mergeCommonTails } from './l3/tailmerge';
|
|
15
22
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
16
23
|
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
17
24
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
@@ -188,9 +195,55 @@ export function raiseRecovered(fn: Fn, target: TargetDescription, hooks: RaiseHo
|
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
|
|
198
|
+
/** Run `body`; if it declines, name the unmodelled instructions the function carries.
|
|
199
|
+
*
|
|
200
|
+
* An `opaque` degrades its own value AND makes its block impure, so a shape recognizer refuses:
|
|
201
|
+
* `headerPure` rejects a header holding one, and the loop declines with "unrecovered back-edge …".
|
|
202
|
+
* True and useless — the shape is fine, an instruction is missing — and the benchmark classifies
|
|
203
|
+
* declines by that text, so the round is filed as a loop-capability gap and the improvement loop
|
|
204
|
+
* builds the wrong thing.
|
|
205
|
+
*
|
|
206
|
+
* Only ADDS attribution: never converts a decline into a success, never fires without an
|
|
207
|
+
* unmodelled instruction, reachable blocks only (one in dead code did not cause the refusal). */
|
|
208
|
+
function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
209
|
+
try {
|
|
210
|
+
return body();
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// Attribution is a nicety, so it must not be able to throw: a crash here would replace a
|
|
213
|
+
// DESIGNED loud failure with an incidental one, which contract-invariant.test.ts rejects by name.
|
|
214
|
+
if (!(e instanceof StructureError) || !fn.blocks[0]) {
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
const seen = new Set<Block>([fn.blocks[0]]);
|
|
218
|
+
for (const stack = [fn.blocks[0]]; stack.length;) {
|
|
219
|
+
for (const s of successorsOf(stack.pop()!)) {
|
|
220
|
+
if (!seen.has(s)) {
|
|
221
|
+
seen.add(s);
|
|
222
|
+
stack.push(s);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const names = new Set<string>();
|
|
227
|
+
for (const b of seen) {
|
|
228
|
+
for (const op of b.ops) {
|
|
229
|
+
if (op.opcode === 'opaque') {
|
|
230
|
+
names.add(typeof op.attrs.mnemonic === 'string' ? op.attrs.mnemonic : '?');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!names.size || /unmodelled instruction/.test(e.message)) {
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
237
|
+
// Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written variant
|
|
238
|
+
// misses the mnemonic-anchored classes and every attributed decline lands in the generic bucket.
|
|
239
|
+
const list = [...names].sort().map(gapReasonFor).join(', ');
|
|
240
|
+
throw new StructureError(`${e.message} — and the function carries ${list}, which is the more likely cause`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
191
244
|
/** Stage 4 — structure + its boundary contracts, always as a pair. */
|
|
192
245
|
export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]): SFn {
|
|
193
|
-
const raw = structure(fn, opts);
|
|
246
|
+
const raw = attributeOpaques(fn, () => structure(fn, opts));
|
|
194
247
|
// BOTH boundary contracts run on the pre-DCE tree: the readability pass must never be able to
|
|
195
248
|
// hide a structuring defect by dropping the dead statement that carries it. assertResolved
|
|
196
249
|
// catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
|
|
@@ -198,11 +251,17 @@ export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]):
|
|
|
198
251
|
// removes statements/flips branches over an already-validated tree.
|
|
199
252
|
assertResolved(raw);
|
|
200
253
|
assertDerefsTyped(raw);
|
|
201
|
-
|
|
254
|
+
assertEffectsPreserved(fn, raw);
|
|
255
|
+
// Then the readability/quality rewrites: merge a statement common to every arm of an if,
|
|
256
|
+
// drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist a
|
|
257
|
+
// reused aggregate-global
|
|
202
258
|
// base into a typed local pointer. The hoist moves the deref cast from each `index` node onto the
|
|
203
259
|
// local's initializer, so re-validate deref typing on the rewritten tree.
|
|
204
|
-
const sfn = hoistReusedGlobalBases(eliminateDeadStores(raw));
|
|
260
|
+
const sfn = hoistReusedGlobalBases(eliminateDeadStores(mergeCommonTails(raw)));
|
|
205
261
|
assertDerefsTyped(sfn);
|
|
262
|
+
// Re-checked after the readability rewrites for the same reason deref typing is: a pass that
|
|
263
|
+
// merges arms or drops statements must not be able to lose or duplicate a call.
|
|
264
|
+
assertEffectsPreserved(fn, sfn);
|
|
206
265
|
return sfn;
|
|
207
266
|
}
|
|
208
267
|
|