@asmlift/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
package/src/mangle.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// asmlift — CodeWarrior (Metrowerks) C++ name mangling + demangling.
|
|
2
|
+
//
|
|
3
|
+
// objdiff aligns a candidate to its target by SYMBOL, and a C++ target's symbol is the MANGLED
|
|
4
|
+
// string (`Vec::dot(Vec*)` → `dot__3VecFP3Vec`). The backend GENERATES that symbol from the
|
|
5
|
+
// recovered signature (`mangle`); to name it idiomatically for a human it RECOVERS the
|
|
6
|
+
// scope/signature from the target symbol (`demangle`). Both directions are pure string
|
|
7
|
+
// transforms, fully offline-testable (test/mangle.test.ts) — no toolchain needed.
|
|
8
|
+
//
|
|
9
|
+
// Scheme (Metrowerks, NOT Itanium). A function symbol is `<name>__<qual><F><argcodes>`:
|
|
10
|
+
// • free function `f(int,int)` → `f__Fii`
|
|
11
|
+
// • member function `Vec::dot(Vec*)` → `dot__3VecFP3Vec` (`3Vec` = class, len-prefixed)
|
|
12
|
+
// • no-arg `g()` → `g__Fv`
|
|
13
|
+
// The implicit `this` of a member is NOT in the arg list. Type codes are below (CODE ↔ spelling).
|
|
14
|
+
|
|
15
|
+
/** A recovered C++ type: a base (builtin name or a class) with a pointer depth. */
|
|
16
|
+
export interface CppType {
|
|
17
|
+
base: string;
|
|
18
|
+
ptr: number;
|
|
19
|
+
} // e.g. {base:"Vec", ptr:1} = `Vec *`
|
|
20
|
+
export interface CppSig {
|
|
21
|
+
name: string;
|
|
22
|
+
cls?: string;
|
|
23
|
+
params: CppType[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Builtin type ↔ Metrowerks code. Unsigned builtins take a `U` prefix; a class type is length-
|
|
27
|
+
// prefixed (`3Vec`), so builtins and classes are disambiguated by a leading digit.
|
|
28
|
+
const BUILTIN_CODE: Record<string, string> = {
|
|
29
|
+
void: 'v',
|
|
30
|
+
char: 'c',
|
|
31
|
+
short: 's',
|
|
32
|
+
int: 'i',
|
|
33
|
+
long: 'l',
|
|
34
|
+
'long long': 'x',
|
|
35
|
+
float: 'f',
|
|
36
|
+
double: 'd',
|
|
37
|
+
bool: 'b',
|
|
38
|
+
'unsigned char': 'Uc',
|
|
39
|
+
'unsigned short': 'Us',
|
|
40
|
+
'unsigned int': 'Ui',
|
|
41
|
+
'unsigned long': 'Ul',
|
|
42
|
+
};
|
|
43
|
+
const CODE_BUILTIN: Record<string, string> = Object.fromEntries(Object.entries(BUILTIN_CODE).map(([k, v]) => [v, k]));
|
|
44
|
+
|
|
45
|
+
function mangleType(t: CppType): string {
|
|
46
|
+
const base = BUILTIN_CODE[t.base] ?? `${t.base.length}${t.base}`; // builtin code OR `<len><name>`
|
|
47
|
+
return 'P'.repeat(t.ptr) + base;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Mangle a recovered signature into its CodeWarrior symbol. */
|
|
51
|
+
export function mangle(sig: CppSig): string {
|
|
52
|
+
const qual = sig.cls ? `${sig.cls.length}${sig.cls}` : '';
|
|
53
|
+
const args = sig.params.length ? sig.params.map(mangleType).join('') : 'v'; // () ⇒ `v`
|
|
54
|
+
return `${sig.name}__${qual}F${args}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Parse one argument type off the front of `s`, returning the type and the remaining string. */
|
|
58
|
+
function parseType(s: string): { t: CppType; rest: string } {
|
|
59
|
+
let ptr = 0;
|
|
60
|
+
while (s[0] === 'P') {
|
|
61
|
+
ptr++;
|
|
62
|
+
s = s.slice(1);
|
|
63
|
+
}
|
|
64
|
+
const digits = s.match(/^(\d+)/);
|
|
65
|
+
if (digits) {
|
|
66
|
+
// a class type: <len><name>
|
|
67
|
+
const len = parseInt(digits[1], 10);
|
|
68
|
+
const name = s.slice(digits[1].length, digits[1].length + len);
|
|
69
|
+
// A length prefix that overruns the symbol is NOT a mangled type (e.g. the plain-C name
|
|
70
|
+
// `map__Fill16`) — reject rather than fabricate an empty/truncated class name.
|
|
71
|
+
if (len === 0 || name.length !== len) {
|
|
72
|
+
throw new Error(`mangle: class-name length prefix overruns the symbol at '${s}'`);
|
|
73
|
+
}
|
|
74
|
+
return { t: { base: name, ptr }, rest: s.slice(digits[1].length + len) };
|
|
75
|
+
}
|
|
76
|
+
// a builtin: greedily match the longest code (the `U`-prefixed unsigned codes are 2 chars).
|
|
77
|
+
for (const code of ['Uc', 'Us', 'Ui', 'Ul']) {
|
|
78
|
+
if (s.startsWith(code)) {
|
|
79
|
+
return { t: { base: CODE_BUILTIN[code], ptr }, rest: s.slice(2) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const one = s[0];
|
|
83
|
+
if (CODE_BUILTIN[one]) {
|
|
84
|
+
return { t: { base: CODE_BUILTIN[one], ptr }, rest: s.slice(1) };
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`mangle: unrecognized type code at '${s}'`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Demangle a CodeWarrior function symbol back into its recovered signature. Returns null if `sym`
|
|
90
|
+
* is not a mangled function name (e.g. an unmangled C symbol), so callers can treat it as plain C. */
|
|
91
|
+
export function demangle(sym: string): CppSig | null {
|
|
92
|
+
const i = sym.indexOf('__');
|
|
93
|
+
if (i <= 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const name = sym.slice(0, i);
|
|
97
|
+
let rest = sym.slice(i + 2);
|
|
98
|
+
let cls: string | undefined;
|
|
99
|
+
// An optional class qualifier precedes `F` (a leading digit = the class-name length prefix).
|
|
100
|
+
const q = rest.match(/^(\d+)/);
|
|
101
|
+
if (q) {
|
|
102
|
+
const len = parseInt(q[1], 10);
|
|
103
|
+
cls = rest.slice(q[1].length, q[1].length + len);
|
|
104
|
+
if (len === 0 || cls.length !== len) {
|
|
105
|
+
return null;
|
|
106
|
+
} // overrunning qualifier ⇒ not a mangled name
|
|
107
|
+
rest = rest.slice(q[1].length + len);
|
|
108
|
+
}
|
|
109
|
+
if (rest[0] !== 'F') {
|
|
110
|
+
return null;
|
|
111
|
+
} // not a function signature
|
|
112
|
+
rest = rest.slice(1);
|
|
113
|
+
const params: CppType[] = [];
|
|
114
|
+
if (rest === 'v') {
|
|
115
|
+
rest = '';
|
|
116
|
+
} // `Fv` = ()
|
|
117
|
+
// A type code this scheme-subset doesn't model (const `C`, reference `R`, …) means the symbol is
|
|
118
|
+
// outside our vocabulary, not malformed input — honor the documented contract and return null so
|
|
119
|
+
// the caller treats it as un-recoverable rather than crashing.
|
|
120
|
+
try {
|
|
121
|
+
while (rest.length) {
|
|
122
|
+
const { t, rest: r } = parseType(rest);
|
|
123
|
+
params.push(t);
|
|
124
|
+
rest = r;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return { name, cls, params };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Spell a CppType as C++ source (`Vec *`, `unsigned int`). */
|
|
133
|
+
export function spellType(t: CppType): string {
|
|
134
|
+
return t.base + (t.ptr ? ' ' + '*'.repeat(t.ptr) : '');
|
|
135
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// asmlift — the idiom layer: rewrite patterns AS DATA + a generic greedy driver.
|
|
2
|
+
// A pattern is a serializable object (match-DAG over the SSA def-graph → replacement);
|
|
3
|
+
// a single generic interpreter applies any of them, so a PDL-style data format and
|
|
4
|
+
// AI-generation are an incremental step, not a rewrite.
|
|
5
|
+
//
|
|
6
|
+
// Crucially, rewrites go through replaceAllUsesWith + DCE — never in-place opcode
|
|
7
|
+
// mutation of a live value.
|
|
8
|
+
import { Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
9
|
+
import { type Opcode, isDceSafe } from '../ir/opcodes';
|
|
10
|
+
import type { IrType } from '../ir/types';
|
|
11
|
+
import { T } from '../ir/types';
|
|
12
|
+
|
|
13
|
+
export type MatchNode =
|
|
14
|
+
| { op: string; attrEquals?: Record<string, number>; bindImm?: Record<string, string>; args: MatchNode[] }
|
|
15
|
+
| { bind: string } // bind this operand's VALUE to a name
|
|
16
|
+
| { same: string } // this operand must equal a previously-bound value
|
|
17
|
+
| { constImm: string }; // this operand must be a `const`; bind its numeric VALUE to an imm name
|
|
18
|
+
|
|
19
|
+
// A tiny declarative arithmetic over bound immediate names. It keeps a *computed* replacement
|
|
20
|
+
// attribute DATA rather than a JS closure, so a strength-reduced idiom whose replacement is a
|
|
21
|
+
// FUNCTION of the matched immediates — `x*(2^k+1)` from a bound shift `k`, `x*(c·2^k)` from a bound
|
|
22
|
+
// multiplier `c` and shift `k` — stays serializable/AI-generable like every other pattern.
|
|
23
|
+
// `pow2(e)` = 2**e.
|
|
24
|
+
export type ImmExpr =
|
|
25
|
+
number | { imm: string } | { op: '+' | '-' | '*'; args: [ImmExpr, ImmExpr] } | { op: 'pow2'; args: [ImmExpr] };
|
|
26
|
+
|
|
27
|
+
function evalImm(e: ImmExpr, imms: Map<string, number>): number {
|
|
28
|
+
if (typeof e === 'number') {
|
|
29
|
+
return e;
|
|
30
|
+
}
|
|
31
|
+
if ('imm' in e) {
|
|
32
|
+
const v = imms.get(e.imm);
|
|
33
|
+
if (v === undefined) {
|
|
34
|
+
throw new Error(`pattern references unbound immediate '${e.imm}'`);
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
const a = e.args.map((x) => evalImm(x, imms));
|
|
39
|
+
switch (e.op) {
|
|
40
|
+
case '+':
|
|
41
|
+
return a[0] + a[1];
|
|
42
|
+
case '-':
|
|
43
|
+
return a[0] - a[1];
|
|
44
|
+
case '*':
|
|
45
|
+
return a[0] * a[1];
|
|
46
|
+
case 'pow2':
|
|
47
|
+
return 2 ** a[0];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A replacement operand is either a previously-BOUND value (by name) or a SYNTHESIZED constant
|
|
52
|
+
// computed from the bound immediates (e.g. the derived multiplier `2^k+1`). A synthesized const
|
|
53
|
+
// is materialized as its own `const` op spliced in before the rewrite site.
|
|
54
|
+
export type ReplaceArg = string | { constImm: ImmExpr };
|
|
55
|
+
|
|
56
|
+
export interface RewritePattern {
|
|
57
|
+
id: string;
|
|
58
|
+
// `applies` is DATA, consumed generically by patternApplies — NOT an `arch ==` branch.
|
|
59
|
+
// `isa` pins the ISA; `compilers` pins which COMPILERS emit this idiom (the same shift-sequence
|
|
60
|
+
// for `/2` is produced by agbcc AND gcc, so a compiler LIST, not a single arch, is the honest
|
|
61
|
+
// predicate); `capabilities` is a hardware predicate. An absent axis means "don't constrain on it".
|
|
62
|
+
applies: { isa?: string; compilers?: string[]; capabilities?: Partial<{ hwDivide: boolean; hwFloat: boolean }> };
|
|
63
|
+
match: MatchNode; // rooted at the op result to replace
|
|
64
|
+
// NOTE: a RELATIONAL guard (a `where` clause constraining the bound immediates, e.g. "two shift
|
|
65
|
+
// amounts sum to 32") is deliberately NOT built — the one candidate, magic-number division, needs
|
|
66
|
+
// a computed divisor proof and lives as the bespoke raise/magicdiv.ts pass instead. The
|
|
67
|
+
// COMPUTED-attr half of the envelope (ImmExpr, above) IS built — earned by the
|
|
68
|
+
// multiply-by-constant idioms.
|
|
69
|
+
replaceWith: { op: string; args: ReplaceArg[]; attrs?: Record<string, number | ImmExpr>; resultType?: IrType };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Does this pattern apply to `target`? Every DECLARED axis must match: the ISA (so an idiom can be
|
|
73
|
+
* pinned to one frontend), the compiler set (so an idiom fires only for the compilers that emit
|
|
74
|
+
* it — the reason MIPS+IDO and MIPS+GCC are distinguishable despite one frontend), and every
|
|
75
|
+
* declared capability. An omitted axis is unconstrained. */
|
|
76
|
+
export function patternApplies(
|
|
77
|
+
p: RewritePattern,
|
|
78
|
+
target: { id: string; compiler: string; capabilities: { hwDivide: boolean; hwFloat: boolean } },
|
|
79
|
+
): boolean {
|
|
80
|
+
if (p.applies.isa && p.applies.isa !== target.id) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (p.applies.compilers && !p.applies.compilers.includes(target.compiler)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const req = p.applies.capabilities;
|
|
87
|
+
if (!req) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return Object.entries(req).every(([k, v]) => target.capabilities[k as keyof typeof target.capabilities] === v);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// mwcc's `x == 0` / `!x` spelling, as pure data: `cntlzw rD,rS; srwi rD,rD,5` — clz(x) is 32
|
|
94
|
+
// exactly when x == 0, so `clz(x) >> 5` IS the boolean. Folding to `icmp_eq(x, 0)` gives
|
|
95
|
+
// recovery/structuring the comparison it actually is, and re-emitting `x == 0` reproduces the
|
|
96
|
+
// cntlzw+srwi pair under mwcc (byte-exact — the iszero/notb benchmark rows). Without the fold,
|
|
97
|
+
// the transient `clz` survives to the structurer and gaps loud (see ir/opcodes.ts).
|
|
98
|
+
export const CNTLZW_EQ0: RewritePattern = {
|
|
99
|
+
id: 'cntlzw-eq0',
|
|
100
|
+
applies: { compilers: ['mwcc'] },
|
|
101
|
+
match: {
|
|
102
|
+
op: 'shr_u',
|
|
103
|
+
attrEquals: { imm: 5 },
|
|
104
|
+
args: [{ op: 'clz', args: [{ bind: 'X' }] }],
|
|
105
|
+
},
|
|
106
|
+
replaceWith: { op: 'icmp_eq', args: ['X', { constImm: 0 }], resultType: T.u(32) },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// The PPC rotate mirror, as pure data: hardware only rotates LEFT, so mwcc spells `rotr(x, n)`
|
|
110
|
+
// as `rotlw(x, 32 - n)`. Lowering that literally emits `x << (32 - n) | x >> (32 - (32 - n))`,
|
|
111
|
+
// whose inner subtraction recompiles to an extra instruction; folding to `rotr(x, n)` lets the
|
|
112
|
+
// structurer spell the clean right-rotate idiom. mwcc-gated: a thumb `ror` whose amount happens
|
|
113
|
+
// to be a source-level `32 - n` must NOT be respelled (that round-trip is unverified on agbcc).
|
|
114
|
+
export const ROTL_MIRROR: RewritePattern = {
|
|
115
|
+
id: 'rotl-mirror',
|
|
116
|
+
applies: { compilers: ['mwcc'] },
|
|
117
|
+
match: {
|
|
118
|
+
op: 'rotl',
|
|
119
|
+
args: [{ bind: 'X' }, { op: 'sub', args: [{ op: 'const', attrEquals: { value: 32 }, args: [] }, { bind: 'M' }] }],
|
|
120
|
+
},
|
|
121
|
+
replaceWith: { op: 'rotr', args: ['X', 'M'] },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Signed-division-by-2 idiom, as pure data: shr_s( add( X, shr_u(X, 31) ), 1 ) == X / 2.
|
|
125
|
+
// The lifted IR shape is ISA-NEUTRAL (shr_u/add/shr_s), and BOTH agbcc (ARMv4T) and KMC GCC
|
|
126
|
+
// (MIPS) strength-reduce signed `/2` to exactly it — even KMC despite the N64 having hardware
|
|
127
|
+
// divide (shifting is cheaper than `div` for a power of two). So the predicate is the COMPILER,
|
|
128
|
+
// not a hardware capability: gate by `compilers`, and re-emitting `x / 2` reproduces the sequence.
|
|
129
|
+
export const SDIV_POW2_2: RewritePattern = {
|
|
130
|
+
id: 'sdiv-pow2/2',
|
|
131
|
+
applies: { compilers: ['agbcc', 'gcc'] },
|
|
132
|
+
match: {
|
|
133
|
+
op: 'shr_s',
|
|
134
|
+
attrEquals: { imm: 1 },
|
|
135
|
+
args: [
|
|
136
|
+
{
|
|
137
|
+
op: 'add',
|
|
138
|
+
args: [{ bind: 'X' }, { op: 'shr_u', attrEquals: { imm: 31 }, args: [{ same: 'X' }] }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
replaceWith: { op: 'sdiv', args: ['X'], attrs: { imm: 2 }, resultType: T.s() },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// ── multiply-by-constant idioms (DIVMUL) ────────────────────────────────────────────────
|
|
146
|
+
// A compiler strength-reduces `x * C` for a small constant C into shifts + one add/sub, because a
|
|
147
|
+
// shift-add chain is cheaper than a general multiply. The reduction is COMPILER-driven and shared
|
|
148
|
+
// across every target measured (agbcc/ARM, IDO/MIPS, GCC/MIPS all emit `lsl;add` / `sll;addu` for
|
|
149
|
+
// `x*5`), so — like the /2 idiom — the honest predicate is the compiler LIST, not the ISA. The
|
|
150
|
+
// replacement multiplier is a FUNCTION of the bound shift amount (ImmExpr), not a literal.
|
|
151
|
+
// Re-emitting `x * C` lets the compiler regenerate the exact shift chain byte-for-byte.
|
|
152
|
+
const MUL_COMPILERS = ['agbcc', 'ido', 'gcc'];
|
|
153
|
+
|
|
154
|
+
// x * (2^k + 1) == (x << k) + x (`lsl rD,x,#k; add rD,rD,x`)
|
|
155
|
+
export const MUL_SHIFT_ADD: RewritePattern = {
|
|
156
|
+
id: 'mul-shift-add',
|
|
157
|
+
applies: { compilers: MUL_COMPILERS },
|
|
158
|
+
match: {
|
|
159
|
+
op: 'add',
|
|
160
|
+
args: [{ op: 'shl', bindImm: { imm: 'k' }, args: [{ bind: 'X' }] }, { same: 'X' }],
|
|
161
|
+
},
|
|
162
|
+
replaceWith: { op: 'mul', args: ['X', { constImm: { op: '+', args: [{ op: 'pow2', args: [{ imm: 'k' }] }, 1] } }] },
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// x * (2^k - 1) == (x << k) - x (`lsl rD,x,#k; sub rD,rD,x`) — sub is non-commutative.
|
|
166
|
+
export const MUL_SHIFT_SUB: RewritePattern = {
|
|
167
|
+
id: 'mul-shift-sub',
|
|
168
|
+
applies: { compilers: MUL_COMPILERS },
|
|
169
|
+
match: {
|
|
170
|
+
op: 'sub',
|
|
171
|
+
args: [{ op: 'shl', bindImm: { imm: 'k' }, args: [{ bind: 'X' }] }, { same: 'X' }],
|
|
172
|
+
},
|
|
173
|
+
replaceWith: { op: 'mul', args: ['X', { constImm: { op: '-', args: [{ op: 'pow2', args: [{ imm: 'k' }] }, 1] } }] },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// x * (c · 2^k) == (x * c) << k — the composite tail a compiler appends when the constant is not
|
|
177
|
+
// itself 2^k±1 (`x*6` = `(x*3)<<1`, `x*12` = `(x*3)<<2`). Runs AFTER the two base multiplies fold
|
|
178
|
+
// the inner `x*c`; binds the inner multiplier `c` off its `const` operand and the outer shift `k`,
|
|
179
|
+
// and folds to a single `x * (c·2^k)`. This is the operand-const bind + computed-attr combination.
|
|
180
|
+
export const MUL_SHIFT_SCALE: RewritePattern = {
|
|
181
|
+
id: 'mul-shift-scale',
|
|
182
|
+
applies: { compilers: MUL_COMPILERS },
|
|
183
|
+
match: {
|
|
184
|
+
op: 'shl',
|
|
185
|
+
bindImm: { imm: 'k' },
|
|
186
|
+
args: [{ op: 'mul', args: [{ bind: 'X' }, { constImm: 'c' }] }],
|
|
187
|
+
},
|
|
188
|
+
replaceWith: {
|
|
189
|
+
op: 'mul',
|
|
190
|
+
args: ['X', { constImm: { op: '*', args: [{ imm: 'c' }, { op: 'pow2', args: [{ imm: 'k' }] }] } }],
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** The multiply-by-constant idiom bundle (ordered: the two base folds before the composite tail). */
|
|
195
|
+
export const MUL_CONST_PATTERNS: RewritePattern[] = [MUL_SHIFT_ADD, MUL_SHIFT_SUB, MUL_SHIFT_SCALE];
|
|
196
|
+
|
|
197
|
+
// ── byte/half extension idioms ───────────────────────────────────────────────────────────────
|
|
198
|
+
// A compiler with no dedicated byte/half move lowers a narrowing cast `(u8)x` / `(s8)x` (and 16-bit)
|
|
199
|
+
// to a shift PAIR `(x << (32-w)) >> (32-w)`: LOGICAL `shr_u` for unsigned (zero-extend), ARITHMETIC
|
|
200
|
+
// `shr_s` for signed (sign-extend). agbcc (ARMv4T, no byte move) emits exactly this (`lsl #24;
|
|
201
|
+
// lsr/asr #24`). The naive lift prints `x << 24 >> 24` — but C's `>>` over the s32-typed value is
|
|
202
|
+
// ARITHMETIC, so the UNSIGNED case recompiles with `asr` where the target has `lsr`: a miscompile
|
|
203
|
+
// (tou8/zextb/tou16 nonmatch). Folding to a cast op both fixes that and reads correctly; recompiling
|
|
204
|
+
// `(u8)x` reproduces `lsl;lsr`. Gated to agbcc: on IDO/GCC the zero-extend is `andi`/`and` (not a
|
|
205
|
+
// shift pair) and `(u8)x` lowers to `andi` there — so this shift-pair shape is agbcc's alone, and the
|
|
206
|
+
// fold must not touch the other compilers (where it would change `srl`↔`andi`). `k = 32 - w`.
|
|
207
|
+
const zextPat = (w: number, k: number): RewritePattern => ({
|
|
208
|
+
id: `zext${w}`,
|
|
209
|
+
applies: { compilers: ['agbcc'] },
|
|
210
|
+
match: { op: 'shr_u', attrEquals: { imm: k }, args: [{ op: 'shl', attrEquals: { imm: k }, args: [{ bind: 'X' }] }] },
|
|
211
|
+
replaceWith: { op: 'zext', args: ['X'], attrs: { width: w } },
|
|
212
|
+
});
|
|
213
|
+
const sextPat = (w: number, k: number): RewritePattern => ({
|
|
214
|
+
id: `sext${w}`,
|
|
215
|
+
applies: { compilers: ['agbcc'] },
|
|
216
|
+
match: { op: 'shr_s', attrEquals: { imm: k }, args: [{ op: 'shl', attrEquals: { imm: k }, args: [{ bind: 'X' }] }] },
|
|
217
|
+
replaceWith: { op: 'sext', args: ['X'], attrs: { width: w } },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/** Byte/half zero- and sign-extension casts. Byte = shift by 24, half = shift by 16. The
|
|
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`). */
|
|
223
|
+
export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16), sextPat(8, 24), sextPat(16, 16)];
|
|
224
|
+
|
|
225
|
+
// 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
|
|
229
|
+
// division idiom, then the multiplies (base folds before the composite tail). Passing an explicit
|
|
230
|
+
// `patterns` (including `[]`) overrides this — `[]` runs the naive lift with no idiom folding.
|
|
231
|
+
export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
|
|
232
|
+
SDIV_POW2_2,
|
|
233
|
+
CNTLZW_EQ0,
|
|
234
|
+
ROTL_MIRROR,
|
|
235
|
+
...MUL_CONST_PATTERNS,
|
|
236
|
+
...CAST_PATTERNS,
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// Ops whose operands a compiler may emit in either order — so an idiom's match must try both
|
|
240
|
+
// (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']);
|
|
242
|
+
|
|
243
|
+
interface Binds {
|
|
244
|
+
values: Map<string, Value>;
|
|
245
|
+
imms: Map<string, number>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): boolean {
|
|
249
|
+
if ('bind' in node) {
|
|
250
|
+
b.values.set(node.bind, v);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
if ('same' in node) {
|
|
254
|
+
return b.values.get(node.same) === v;
|
|
255
|
+
}
|
|
256
|
+
if ('constImm' in node) {
|
|
257
|
+
const d = defs.get(v);
|
|
258
|
+
if (!d || d.opcode !== 'const') {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
b.imms.set(node.constImm, d.attrs.value as number);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
const d = defs.get(v);
|
|
265
|
+
if (!d || d.opcode !== node.op) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
if (node.attrEquals) {
|
|
269
|
+
for (const [k, val] of Object.entries(node.attrEquals)) {
|
|
270
|
+
if (d.attrs[k] !== val) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Bind selected immediate attributes of this op (e.g. a shift amount) for use in a computed
|
|
276
|
+
// replacement. Absent attrs fail the match rather than binding `undefined`.
|
|
277
|
+
if (node.bindImm) {
|
|
278
|
+
for (const [attr, name] of Object.entries(node.bindImm)) {
|
|
279
|
+
const val = d.attrs[attr];
|
|
280
|
+
if (typeof val !== 'number') {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
b.imms.set(name, val);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (d.operands.length !== node.args.length) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
// A commutative binary op matches its two args in EITHER order. Each order is tried on a cloned
|
|
290
|
+
// bind map so a partial (then-failed) match can't leak bindings; the first full match commits.
|
|
291
|
+
if (COMMUTATIVE.has(d.opcode) && node.args.length === 2) {
|
|
292
|
+
for (const [i, j] of [
|
|
293
|
+
[0, 1],
|
|
294
|
+
[1, 0],
|
|
295
|
+
] as const) {
|
|
296
|
+
const trial: Binds = { values: new Map(b.values), imms: new Map(b.imms) };
|
|
297
|
+
if (tryMatch(node.args[0], d.operands[i], defs, trial) && tryMatch(node.args[1], d.operands[j], defs, trial)) {
|
|
298
|
+
for (const [k, val] of trial.values) {
|
|
299
|
+
b.values.set(k, val);
|
|
300
|
+
}
|
|
301
|
+
for (const [k, val] of trial.imms) {
|
|
302
|
+
b.imms.set(k, val);
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return node.args.every((a, i) => tryMatch(a, d.operands[i], defs, b));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Apply one pattern greedily to a fixed point. Returns the number of rewrites. */
|
|
313
|
+
export function applyPattern(fn: Fn, pat: RewritePattern): number {
|
|
314
|
+
let count = 0,
|
|
315
|
+
changed = true;
|
|
316
|
+
while (changed) {
|
|
317
|
+
changed = false;
|
|
318
|
+
const defs = defOpMap(fn);
|
|
319
|
+
scan: for (const b of fn.blocks) {
|
|
320
|
+
for (let i = 0; i < b.ops.length; i++) {
|
|
321
|
+
const op = b.ops[i];
|
|
322
|
+
if (op.results.length !== 1) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const binds: Binds = { values: new Map(), imms: new Map() };
|
|
326
|
+
if (!tryMatch(pat.match, op.results[0], defs, binds)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// Materialize any synthesized-constant replacement operands as their own `const` ops,
|
|
330
|
+
// spliced in before the rewrite; bound-value operands resolve from the value binds.
|
|
331
|
+
const rw = pat.replaceWith;
|
|
332
|
+
const newRes = mkValue(rw.resultType ?? op.results[0].type);
|
|
333
|
+
const consts: Op[] = [];
|
|
334
|
+
const operands: Value[] = rw.args.map((a) => {
|
|
335
|
+
if (typeof a === 'string') {
|
|
336
|
+
// Attribute a malformed pattern HERE with its id — an unbound value would otherwise
|
|
337
|
+
// become `undefined` and detonate stages later; patterns are meant to become
|
|
338
|
+
// AI-generated data, so diagnosability is first-class.
|
|
339
|
+
const bound = binds.values.get(a);
|
|
340
|
+
if (!bound) {
|
|
341
|
+
throw new Error(`pattern '${pat.id}' replaceWith references unbound value '${a}'`);
|
|
342
|
+
}
|
|
343
|
+
return bound;
|
|
344
|
+
}
|
|
345
|
+
const cv = mkValue(T.s());
|
|
346
|
+
consts.push(mkOp('const', { results: [cv], attrs: { value: evalImm(a.constImm, binds.imms) } }));
|
|
347
|
+
return cv;
|
|
348
|
+
});
|
|
349
|
+
const attrs: Record<string, number> = {};
|
|
350
|
+
for (const [k, v] of Object.entries(rw.attrs ?? {})) {
|
|
351
|
+
attrs[k] = typeof v === 'number' ? v : evalImm(v, binds.imms);
|
|
352
|
+
}
|
|
353
|
+
const newOp = mkOp(rw.op as Opcode, { operands, results: [newRes], attrs }); // pattern data boundary: verify() rejects unknowns
|
|
354
|
+
b.ops.splice(i, 1, ...consts, newOp);
|
|
355
|
+
replaceAllUsesWith(fn, op.results[0], newRes);
|
|
356
|
+
count++;
|
|
357
|
+
changed = true;
|
|
358
|
+
break scan;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return count;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Remove effect-free ops whose single result is unused, to a fixed point. Deletability is
|
|
366
|
+
* derived from the ONE effect table in ir/opcodes.ts. */
|
|
367
|
+
export function dce(fn: Fn): void {
|
|
368
|
+
let changed = true;
|
|
369
|
+
while (changed) {
|
|
370
|
+
changed = false;
|
|
371
|
+
const used = new Set<Value>();
|
|
372
|
+
for (const b of fn.blocks) {
|
|
373
|
+
for (const op of b.ops) {
|
|
374
|
+
for (const o of op.operands) {
|
|
375
|
+
used.add(o);
|
|
376
|
+
}
|
|
377
|
+
for (const s of op.successors) {
|
|
378
|
+
for (const a of s.args) {
|
|
379
|
+
used.add(a);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const b of fn.blocks) {
|
|
385
|
+
const kept = b.ops.filter((op) => !(isDceSafe(op.opcode) && op.results.length === 1 && !used.has(op.results[0])));
|
|
386
|
+
if (kept.length !== b.ops.length) {
|
|
387
|
+
b.ops = kept;
|
|
388
|
+
changed = true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|