@asmlift/core 0.2.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +154 -5
  4. package/src/backend/cpp.ts +3 -1
  5. package/src/backend/pascal.ts +11 -0
  6. package/src/contracts.ts +37 -5
  7. package/src/declare.ts +251 -0
  8. package/src/frontend/frontend.ts +12 -2
  9. package/src/frontend/mips.ts +24 -23
  10. package/src/frontend/opaque.ts +39 -2
  11. package/src/frontend/ssa.ts +32 -53
  12. package/src/frontend/thumb.ts +420 -32
  13. package/src/ir/opcodes.ts +44 -0
  14. package/src/ir/simplify.ts +72 -0
  15. package/src/l3/argbase.ts +216 -0
  16. package/src/l3/ast.ts +126 -6
  17. package/src/l3/basecse.ts +3 -40
  18. package/src/l3/coalesce.ts +146 -0
  19. package/src/l3/dce.ts +2 -23
  20. package/src/l3/hoist.ts +65 -0
  21. package/src/l3/reindex.ts +7 -0
  22. package/src/l3/scopebase.ts +436 -0
  23. package/src/l3/symbol-refs.ts +61 -0
  24. package/src/l3/tailmerge.ts +120 -0
  25. package/src/l3/typing.ts +4 -0
  26. package/src/macros.ts +335 -0
  27. package/src/pattern/engine.ts +99 -6
  28. package/src/pipeline.ts +20 -6
  29. package/src/proto.ts +55 -0
  30. package/src/raise/divpow2.ts +226 -0
  31. package/src/raise/gvn.ts +141 -0
  32. package/src/raise/pre-recovery.ts +37 -3
  33. package/src/raise/recover.ts +24 -7
  34. package/src/raise/retsink.ts +36 -7
  35. package/src/raise/shortcircuit.ts +264 -22
  36. package/src/raise/structs.ts +12 -2
  37. package/src/rank.ts +370 -79
  38. package/src/structure/analysis.ts +42 -1
  39. package/src/structure/structure.ts +852 -67
  40. package/src/structure/switch-recover.ts +21 -3
  41. package/src/symbols.ts +541 -0
  42. package/src/target.ts +4 -2
  43. package/src/trace.ts +17 -2
package/src/macros.ts ADDED
@@ -0,0 +1,335 @@
1
+ // asmlift — address-cast macros: the OTHER way a project names a fixed RAM cell.
2
+ //
3
+ // Some decomp projects declare `extern u16 gCounter;` and let the linker place it; others write
4
+ // `#define gCounter (*(u16 *)0x03001234)`. Both read the same cell, but they are NOT
5
+ // interchangeable in the bytes an old compiler emits: an `extern` produces a RELOCATED literal-pool
6
+ // word (`.word gCounter`), the macro a NUMERIC one (`.word 0x3001234`). A target that shows the
7
+ // numeric word can therefore only be matched by the macro spelling — a symtab name will not do,
8
+ // and no `.symtab` carries these names in the first place (a macro is not a symbol).
9
+ //
10
+ // This module is the PURE recognizer over preprocessor output (`cpp -dD`). Everything it accepts is
11
+ // a fact it can name exactly; everything else is refused, because a wrong width or a dropped
12
+ // `volatile` is the plausible-but-wrong class — see the guards on {@link addressCastMacros}.
13
+
14
+ /** One recognized address-cast macro. */
15
+ export interface AddressMacro {
16
+ name: string;
17
+ /** the cell's address — the map key this macro names */
18
+ address: number;
19
+ /** the macro body VERBATIM, as the declaration must reproduce it */
20
+ body: string;
21
+ /** the cast's byte width */
22
+ size: number;
23
+ /** the cast type's signedness */
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;
28
+ }
29
+
30
+ /** The scalar type spellings a cast may use, and what each one means. Deliberately a CLOSED table:
31
+ * an unrecognized spelling (a project typedef, an enum, a struct) is refused rather than guessed,
32
+ * and every `volatile` alias is absent so it can never be silently dropped — the qualifier changes
33
+ * whether repeated reads may be folded, which is both a byte and a semantic difference. */
34
+ const SCALAR_TYPES: Record<string, { size: number; signed: boolean; volatile?: true }> = {
35
+ u8: { size: 1, signed: false },
36
+ s8: { size: 1, signed: true },
37
+ u16: { size: 2, signed: false },
38
+ s16: { size: 2, signed: true },
39
+ u32: { size: 4, signed: false },
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 },
51
+ };
52
+
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]+$/;
218
+
219
+ /**
220
+ * Recognize the address-cast macros in `cpp -dD` output, keyed by the address each names.
221
+ *
222
+ * REFUSALS, all of them because the alternative is a plausible-but-wrong spelling:
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;
225
+ * - two macros naming the SAME address (`REG_VCOUNT`/`REG_VCOUNT_L`/`REG_VCOUNT_H` at 0x04000006
226
+ * differ in width, and picking wrong turns an `ldrh` into an `ldrb`) — both are dropped;
227
+ * - one name defined at two addresses, which no correct spelling can disambiguate.
228
+ */
229
+ export function addressCastMacros(cppOutput: string): Map<number, AddressMacro> {
230
+ return addressCastMacrosFrom(cppOutput.split('\n'));
231
+ }
232
+
233
+ /** The same recognizer over already-split `#define NAME body` lines — what a DWARF
234
+ * `.debug_macinfo` reader produces once each definition is re-spelled as a directive. */
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
+ }
251
+ const byAddress = new Map<number, AddressMacro>();
252
+ const collided = new Set<number>();
253
+ const seenNames = new Map<string, number>();
254
+ for (const line of defineLines) {
255
+ const m = ADDRESS_CAST.exec(line);
256
+ if (!m) {
257
+ continue;
258
+ }
259
+ const [, name, rawBody, typeName, addrText] = m;
260
+ const type = SCALAR_TYPES[typeName];
261
+ if (!type) {
262
+ continue; // a spelling outside the closed table — refuse
263
+ }
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
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()})`;
281
+ const priorAddr = seenNames.get(name);
282
+ if (priorAddr !== undefined && priorAddr !== address) {
283
+ collided.add(priorAddr);
284
+ collided.add(address);
285
+ continue;
286
+ }
287
+ seenNames.set(name, address);
288
+ const prior = byAddress.get(address);
289
+ if (prior && prior.name !== name) {
290
+ collided.add(address);
291
+ continue;
292
+ }
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
+ });
301
+ }
302
+ for (const addr of collided) {
303
+ byAddress.delete(addr);
304
+ }
305
+ return byAddress;
306
+ }
307
+
308
+ /**
309
+ * The `#define` lines for every address-cast macro in `symbols` that `source` actually names.
310
+ *
311
+ * A published source that spells `gCollisionMapPtr` only compiles where that macro is defined —
312
+ * and a REPRODUCTION of it must therefore carry the definition, or the script the benchmark
313
+ * publishes cannot build the very source it publishes. Selected by the names the source uses
314
+ * rather than by the whole map, so a reproduction context stays the size of what it needs.
315
+ *
316
+ * Name-sorted and deduplicated: the materialized context must be byte-stable across machines.
317
+ */
318
+ export function macroDefinesUsedBy(
319
+ symbols: Map<number, { name: string; macroBody?: string }[]>,
320
+ source: string,
321
+ ): string {
322
+ const used = new Map<string, string>();
323
+ for (const infos of symbols.values()) {
324
+ for (const info of infos) {
325
+ if (info.macroBody === undefined || used.has(info.name)) {
326
+ continue;
327
+ }
328
+ if (new RegExp(`\\b${info.name}\\b`).test(source)) {
329
+ used.set(info.name, info.macroBody);
330
+ }
331
+ }
332
+ }
333
+ const names = [...used.keys()].sort();
334
+ return names.length ? names.map((n) => `#define ${n} ${used.get(n)}`).join('\n') + '\n' : '';
335
+ }
@@ -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; each is `{compilers}`-gated (patternApplies), so this one global list
227
- // self-selects per target — agbcc/gcc get sdiv-pow2, agbcc/ido/gcc get the mul-const folds, and a
228
- // target whose compiler matches none (mwcc) applies nothing. Ordered like the sub-bundles: the
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
- const COMMUTATIVE = new Set(['add', 'mul', 'and', 'or', 'xor']);
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
@@ -12,13 +12,15 @@ import { VerifyError, verify } from './ir/verify';
12
12
  import { Expr, LanguageBackend, SFn, Stmt, exprChildren, stmtChildren, stmtExprs } from './l3/ast';
13
13
  import { hoistReusedGlobalBases } from './l3/basecse';
14
14
  import { eliminateDeadStores } from './l3/dce';
15
+ import { mergeCommonTails } from './l3/tailmerge';
15
16
  import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
16
- import type { Prototypes } from './proto';
17
+ import { type Prototypes, prototypesFromSymbols } from './proto';
17
18
  import { RaiseUnsupportedError } from './raise/errors';
18
19
  import { type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
19
20
  import { recoverTypes } from './raise/recover';
20
21
  import { sinkReturns } from './raise/retsink';
21
22
  import { StructureError, structure } from './structure/structure';
23
+ import { type SymbolMap, symbolsByName } from './symbols';
22
24
  import { type TargetDescription, structureOptionsFor } from './target';
23
25
 
24
26
  /** How a gap (a construct asmlift cannot faithfully model) degrades:
@@ -53,6 +55,10 @@ export interface DecompileOptions {
53
55
  * MIPS/PPC switch declines/loud-fails; present ⇒ the frontend recovers the `switch_br`.
54
56
  * Produced by `extractAsmData(obj, target)` from the scoring object. */
55
57
  asmData?: AsmData;
58
+ /** OPTIONAL address→symbol map (symbols.ts) — the project's own names (ELF symtab) and
59
+ * declaration shapes (DWARF types-sidecar). Drives the Thumb numeric-pool promotion and the
60
+ * byte-sensitive global spellings. Absent ⇒ byte-identical to today. */
61
+ symbols?: SymbolMap;
56
62
  /** gap policy — see `OnGap`. Default "strict". */
57
63
  onGap?: OnGap;
58
64
  }
@@ -97,9 +103,11 @@ function runTower(
97
103
  onGap: OnGap,
98
104
  ): DecompileResult {
99
105
  const backend = opts.backend ?? cBackend;
100
- const prototypes = opts.prototypes ?? {};
106
+ // The project's own DWARF signatures fill in what the caller did not state — in practice the
107
+ // CALLEES (a function still in assembly has none), which is what makes this transferable.
108
+ const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
101
109
  // (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
102
- const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
110
+ const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
103
111
  verify(fn);
104
112
  const raw = print(fn);
105
113
 
@@ -115,7 +123,11 @@ function runTower(
115
123
 
116
124
  // (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
117
125
  // every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
118
- const sfn = structureChecked(fn, { ...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false), onGap });
126
+ const sfn = structureChecked(fn, {
127
+ ...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
128
+ onGap,
129
+ ...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
130
+ });
119
131
 
120
132
  // (5) lower + print: neutral AST → target language
121
133
  const source = backend.emit(sfn);
@@ -187,10 +199,12 @@ export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]):
187
199
  // removes statements/flips branches over an already-validated tree.
188
200
  assertResolved(raw);
189
201
  assertDerefsTyped(raw);
190
- // Then the readability/quality rewrites: drop dead stores, then hoist a reused aggregate-global
202
+ // Then the readability/quality rewrites: merge a statement common to every arm of an if,
203
+ // drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist a
204
+ // reused aggregate-global
191
205
  // base into a typed local pointer. The hoist moves the deref cast from each `index` node onto the
192
206
  // local's initializer, so re-validate deref typing on the rewritten tree.
193
- const sfn = hoistReusedGlobalBases(eliminateDeadStores(raw));
207
+ const sfn = hoistReusedGlobalBases(eliminateDeadStores(mergeCommonTails(raw)));
194
208
  assertDerefsTyped(sfn);
195
209
  return sfn;
196
210
  }
package/src/proto.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { SymbolMap } from './symbols';
2
+
1
3
  // asmlift — function prototypes: the single carrier for the caller-supplied facts a
2
4
  // matching-decomp project reads from its headers (arg counts, void-ness). One `Prototypes`
3
5
  // map, keyed by symbol, is threaded through every entry point and resolved at the point of
@@ -40,3 +42,56 @@ export function protoArity(p: FnProto | undefined): number | undefined {
40
42
  // fall back to the frontend's arg-register heuristic rather than misread a string's `.length`.
41
43
  return undefined;
42
44
  }
45
+
46
+ /** The C type spelling for one declared parameter/return, or null when the facts do not
47
+ * determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
48
+ * makes every stride explicit — so nothing is guessed about what it points at. */
49
+ function typeSpelling(t: { size: number | null; signed: boolean | null; pointer?: boolean }): ParamType | null {
50
+ if (t.pointer) {
51
+ return 'void *';
52
+ }
53
+ if (t.size === 1 || t.size === 2 || t.size === 4) {
54
+ // A signless 4-byte type is the C89 enum idiom (int); a signless NARROW one has no honest
55
+ // spelling, and the width alone would not fix its load, so it is refused.
56
+ if (t.signed === null) {
57
+ return t.size === 4 ? 's32' : null;
58
+ }
59
+ return `${t.signed ? 's' : 'u'}${t.size * 8}`;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Prototypes the project's own DWARF states, merged UNDER the caller's.
66
+ *
67
+ * A caller-supplied proto always wins: it comes from the user's headers or the benchmark
68
+ * manifest, and it is the thing a real user actually has for the function they are decompiling.
69
+ * The map fills the rest — in practice the CALLEES, since a function still written in assembly
70
+ * has no signature in its project's ELF (see SymbolSignature).
71
+ *
72
+ * Every parameter must spell faithfully or the whole entry is dropped: a partly-typed list would
73
+ * be read for its LENGTH and give the right arity with the wrong widths, which is worse than the
74
+ * arg-register heuristic it would replace.
75
+ */
76
+ export function prototypesFromSymbols(symbols: SymbolMap | undefined, base: Prototypes = {}): Prototypes {
77
+ if (!symbols) {
78
+ return base;
79
+ }
80
+ const out: Prototypes = { ...base };
81
+ for (const infos of symbols.values()) {
82
+ for (const info of infos) {
83
+ if (info.kind !== 'code' || !info.signature || out[info.name] !== undefined) {
84
+ continue;
85
+ }
86
+ const params = info.signature.params.map(typeSpelling);
87
+ if (params.some((p) => p === null)) {
88
+ continue;
89
+ }
90
+ out[info.name] = {
91
+ params: params as ParamType[],
92
+ ...(info.signature.returns === null ? { returnsVoid: true } : {}),
93
+ };
94
+ }
95
+ }
96
+ return out;
97
+ }