@asmlift/core 0.3.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.
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +125 -2
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +15 -2
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +301 -26
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +118 -4
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +5 -2
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +172 -20
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +399 -31
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// L3 structural simplification: a statement that ends EVERY arm of an `if` moves below the `if`.
|
|
2
|
+
//
|
|
3
|
+
// SSA destruction puts the same merge-variable write at the end of each arm, because each arm is
|
|
4
|
+
// where that edge's copy belongs:
|
|
5
|
+
//
|
|
6
|
+
// if (c) { v4 = 1; } else { g[594] = g[659]; v4 = 1; }
|
|
7
|
+
//
|
|
8
|
+
// The source wrote it once. Both arms execute it LAST on their own path, so hoisting it below the
|
|
9
|
+
// `if` runs it exactly once, on the same paths, in the same order relative to everything else —
|
|
10
|
+
// which is why this needs no liveness or dominance analysis and holds even for a side-effecting
|
|
11
|
+
// statement. It is the merge direction that is unconditionally sound: hoisting a common HEAD above
|
|
12
|
+
// the `if` would move it across the condition's own evaluation, which is not.
|
|
13
|
+
//
|
|
14
|
+
// Runs BEFORE `eliminateDeadStores`, whose empty-then peephole then flips the arm this empties:
|
|
15
|
+
//
|
|
16
|
+
// if (c) { } else { g[594] = g[659]; } v4 = 1; → if (!c) { g[594] = g[659]; } v4 = 1;
|
|
17
|
+
//
|
|
18
|
+
// Measured on kleod:UpdateHUDCounterDisplay: 60 → 33 (visible in the committed results.json). The
|
|
19
|
+
// placement is not a matter of taste — peeling the same statements to ABOVE the `if` instead scores
|
|
20
|
+
// 48 — but note that above-the-`if` is a THIRD option, unsound for its own reason (it crosses the
|
|
21
|
+
// condition as well as both arms); the soundness argument here covers only below-vs-in-arms.
|
|
22
|
+
//
|
|
23
|
+
// KNOWN INTERACTIONS, both byte-level rather than soundness. This pass is unconditional like
|
|
24
|
+
// `dce.ts` and `basecse.ts` rather than a differ-refereed lever, and the argument those files each
|
|
25
|
+
// state for themselves applies here too and was missing: a wrong merge changes recompiled bytes and
|
|
26
|
+
// surfaces as a LOST match under the zero-lost gate, never as wrong C.
|
|
27
|
+
//
|
|
28
|
+
// - it DEFEATS basecse's scalar-fixed-offset gate. That gate counts repeated constant offsets
|
|
29
|
+
// function-wide and refuses to hoist them; it was bought by losing the ProcessHBlankWait match.
|
|
30
|
+
// Deleting an arm's duplicate drops the count 2→1, so a base that gate would have refused is now
|
|
31
|
+
// hoisted. Systematic, not incidental.
|
|
32
|
+
// - it does NOT reach a fixpoint with `eliminateDeadStores`. A DIFFERING DEAD statement at the end
|
|
33
|
+
// of the arms hides the common tail, and DCE only removes it afterwards, so the shape this pass
|
|
34
|
+
// exists for is missed. The fix is a fixpoint of the pair, not one extra call — a lone second
|
|
35
|
+
// pass leaves an empty `if` behind.
|
|
36
|
+
//
|
|
37
|
+
// SCOPE. Only `assign`/`store`/`exprstmt` merge, compared structurally through `exprEquals`.
|
|
38
|
+
// Control flow (`break`/`continue`/`return`) is excluded: moving one out of an arm changes which
|
|
39
|
+
// statements the arm can still reach. Nested `if`/loop/`switch` statements are excluded because
|
|
40
|
+
// comparing them needs a full `Stmt` congruence, and there is no second inhabitant for one — the
|
|
41
|
+
// `Expr`-level comparison is the part that already exists, is tested, and is all this needs.
|
|
42
|
+
//
|
|
43
|
+
// An `ASMLIFT_ERROR` marker ending both arms merges like anything else. The gap stays loud (the
|
|
44
|
+
// artifact still refuses to compile) but `collectMarkers` then reports it once rather than twice,
|
|
45
|
+
// which is accurate — it is one gap that ran on both paths.
|
|
46
|
+
import type { SFn, Stmt } from './ast';
|
|
47
|
+
import { exprEquals } from './ast';
|
|
48
|
+
|
|
49
|
+
/** Statements this pass may move. Deliberately narrow — see SCOPE. */
|
|
50
|
+
type Mergeable = Extract<Stmt, { k: 'assign' } | { k: 'store' } | { k: 'exprstmt' }>;
|
|
51
|
+
const isMergeable = (s: Stmt): s is Mergeable => s.k === 'assign' || s.k === 'store' || s.k === 'exprstmt';
|
|
52
|
+
|
|
53
|
+
/** Do these two statements write the same thing from the same expression? */
|
|
54
|
+
function sameStmt(a: Stmt, b: Stmt): boolean {
|
|
55
|
+
if (!isMergeable(a) || !isMergeable(b) || a.k !== b.k) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (a.k === 'assign' && b.k === 'assign') {
|
|
59
|
+
return a.name === b.name && exprEquals(a.value, b.value);
|
|
60
|
+
}
|
|
61
|
+
if (a.k === 'store' && b.k === 'store') {
|
|
62
|
+
return exprEquals(a.lval, b.lval) && exprEquals(a.value, b.value);
|
|
63
|
+
}
|
|
64
|
+
const av = (a as Extract<Stmt, { k: 'exprstmt' }>).value;
|
|
65
|
+
const bv = (b as Extract<Stmt, { k: 'exprstmt' }>).value;
|
|
66
|
+
return exprEquals(av, bv);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Rewrite one statement, then the list it lives in. */
|
|
70
|
+
function rewrite(s: Stmt): Stmt[] {
|
|
71
|
+
const list = (xs: Stmt[]): Stmt[] => xs.flatMap(rewrite);
|
|
72
|
+
switch (s.k) {
|
|
73
|
+
case 'if': {
|
|
74
|
+
const then = list(s.then);
|
|
75
|
+
const els = list(s.else);
|
|
76
|
+
// Peel from the END of both arms while they agree. The length test is a precondition of the
|
|
77
|
+
// NEXT peel, not a floor: `if (c) { a } else { a }` peels until BOTH arms are empty, which is
|
|
78
|
+
// fine — `eliminateDeadStores` then drops the `if` and keeps its condition as an `exprstmt`
|
|
79
|
+
// only when the condition itself has a side effect. Note what that means for a condition that
|
|
80
|
+
// is a memory LOAD: a compare-and-branch present in the asm disappears from the emitted C,
|
|
81
|
+
// and the surviving bare `*(u16 *)&gReg;` is something the compiler may elide — so a read the
|
|
82
|
+
// original performed unconditionally becomes one it may not. Byte-level, and the differ sees
|
|
83
|
+
// it, but it is the one shape where a zero-arm merge is qualitatively unlike a partial one.
|
|
84
|
+
const tail: Stmt[] = [];
|
|
85
|
+
while (then.length > 0 && els.length > 0 && sameStmt(then[then.length - 1], els[els.length - 1])) {
|
|
86
|
+
tail.unshift(then[then.length - 1]);
|
|
87
|
+
then.pop();
|
|
88
|
+
els.pop();
|
|
89
|
+
}
|
|
90
|
+
return [{ ...s, then, else: els }, ...tail];
|
|
91
|
+
}
|
|
92
|
+
case 'while':
|
|
93
|
+
case 'dowhile':
|
|
94
|
+
return [{ ...s, body: list(s.body) }];
|
|
95
|
+
case 'for':
|
|
96
|
+
return [{ ...s, body: list(s.body) }];
|
|
97
|
+
case 'switch':
|
|
98
|
+
// Case bodies are NOT merged: a case that falls through to the next has no "end" of its own,
|
|
99
|
+
// so peeling its last statement would move code across a fall-through boundary.
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
...s,
|
|
103
|
+
cases: s.cases.map((c) => ({ ...c, body: list(c.body) })),
|
|
104
|
+
...(s.default ? { default: list(s.default) } : {}),
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
case 'assign':
|
|
108
|
+
case 'store':
|
|
109
|
+
case 'exprstmt':
|
|
110
|
+
case 'return':
|
|
111
|
+
case 'break':
|
|
112
|
+
case 'continue':
|
|
113
|
+
return [s];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Move every statement that ends all arms of an `if` below that `if`. */
|
|
118
|
+
export function mergeCommonTails(sfn: SFn): SFn {
|
|
119
|
+
return { ...sfn, body: sfn.body.flatMap(rewrite) };
|
|
120
|
+
}
|
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
|
@@ -12,6 +12,7 @@ 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
17
|
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
17
18
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
@@ -198,10 +199,12 @@ export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]):
|
|
|
198
199
|
// removes statements/flips branches over an already-validated tree.
|
|
199
200
|
assertResolved(raw);
|
|
200
201
|
assertDerefsTyped(raw);
|
|
201
|
-
// Then the readability/quality rewrites:
|
|
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
|
|
202
205
|
// base into a typed local pointer. The hoist moves the deref cast from each `index` node onto the
|
|
203
206
|
// local's initializer, so re-validate deref typing on the rewritten tree.
|
|
204
|
-
const sfn = hoistReusedGlobalBases(eliminateDeadStores(raw));
|
|
207
|
+
const sfn = hoistReusedGlobalBases(eliminateDeadStores(mergeCommonTails(raw)));
|
|
205
208
|
assertDerefsTyped(sfn);
|
|
206
209
|
return sfn;
|
|
207
210
|
}
|