@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/package.json +14 -0
  4. package/src/backend/c.ts +20 -0
  5. package/src/backend/cfamily.ts +352 -0
  6. package/src/backend/cpp.ts +145 -0
  7. package/src/backend/pascal.ts +279 -0
  8. package/src/contracts.ts +131 -0
  9. package/src/detect.ts +12 -0
  10. package/src/frontend/asmdata.ts +170 -0
  11. package/src/frontend/disasm.ts +102 -0
  12. package/src/frontend/emit.ts +57 -0
  13. package/src/frontend/errors.ts +14 -0
  14. package/src/frontend/format.ts +47 -0
  15. package/src/frontend/frontend.ts +22 -0
  16. package/src/frontend/mips.ts +875 -0
  17. package/src/frontend/opaque.ts +82 -0
  18. package/src/frontend/ppc.ts +990 -0
  19. package/src/frontend/registry.ts +34 -0
  20. package/src/frontend/ssa.ts +214 -0
  21. package/src/frontend/thumb.ts +1419 -0
  22. package/src/ir/core.ts +104 -0
  23. package/src/ir/opcodes.ts +143 -0
  24. package/src/ir/parse.ts +221 -0
  25. package/src/ir/print.ts +77 -0
  26. package/src/ir/types.ts +106 -0
  27. package/src/ir/verify.ts +221 -0
  28. package/src/l3/ast.ts +301 -0
  29. package/src/l3/basecse.ts +218 -0
  30. package/src/l3/dce.ts +256 -0
  31. package/src/l3/regspell.ts +331 -0
  32. package/src/l3/reindex.ts +447 -0
  33. package/src/l3/typing.ts +145 -0
  34. package/src/mangle.ts +135 -0
  35. package/src/pattern/engine.ts +392 -0
  36. package/src/pipeline.ts +272 -0
  37. package/src/proto.ts +42 -0
  38. package/src/raise/arrays.ts +84 -0
  39. package/src/raise/const.ts +52 -0
  40. package/src/raise/errors.ts +10 -0
  41. package/src/raise/magicdiv.ts +386 -0
  42. package/src/raise/pre-recovery.ts +71 -0
  43. package/src/raise/recover.ts +215 -0
  44. package/src/raise/retsink.ts +72 -0
  45. package/src/raise/shortcircuit.ts +207 -0
  46. package/src/raise/softdiv.ts +62 -0
  47. package/src/raise/struct-arrays.ts +257 -0
  48. package/src/raise/structs.ts +223 -0
  49. package/src/rank.ts +208 -0
  50. package/src/structure/analysis.ts +410 -0
  51. package/src/structure/hazards.ts +142 -0
  52. package/src/structure/loops.ts +169 -0
  53. package/src/structure/structure.ts +1726 -0
  54. package/src/structure/switch-recover.ts +410 -0
  55. package/src/target.ts +140 -0
  56. package/src/trace.ts +233 -0
@@ -0,0 +1,331 @@
1
+ // asmlift L3 — the REGISTER-COPY re-spelling, a differ-ranked representation lever (the fourth,
2
+ // after signedness / branch sense / walk-vs-index).
3
+ //
4
+ // A compiler's register allocation leaves SOURCE-visible footprints the coalescing structurer
5
+ // legitimately erases: a phi materialized as an unconditional copy plus an IN-PLACE update on
6
+ // one arm (`adds r1, r0, #0; …; adds r1, #255` — thumb's 2-operand add-immediate REQUIRES the
7
+ // in-place form for imm > 7), a big constant staged in its own register before use, a return
8
+ // value built in a register other than the one it was computed in. Whether the source spelled
9
+ // the coalesced or the copy-carrying form is genuinely ambiguous from asm — so BOTH are emitted
10
+ // as candidates and the objdiff score referees (measured: the copy-carrying spelling is
11
+ // byte-exact on kleod's MultiplyQ8/Q4 + ReciprocalQ8/Q4 and pokeemerald's MathUtil_Mul16,
12
+ // where the coalesced form scores 3–4).
13
+ //
14
+ // Three composable rewrites, all decline-over-approximate (a shape outside the template is left
15
+ // untouched; if nothing fires, no candidate is emitted). SCOPE (adversarially learned): R1 fires
16
+ // ONLY in the top-level statement list — under a loop the "downstream rename" is temporally
17
+ // unsound (the next iteration reads the phi var BEFORE the diamond; reproduced as a wrong
18
+ // candidate outscoring a correct sibling), and under any nesting the rename cannot see the
19
+ // enclosing continuation. R2 fires only on top-level assign/return statements. Fresh vars use
20
+ // the `w*` space (pipeline naming is a*/v*/t*, reindex reserves i*), collide-checked.
21
+ // R1 — diamond → copy + in-place update: `if (cmp(E, K0)) { v = E } else { v = E ⊕ K }` (either
22
+ // arm order; E deep-equal and PURE — no calls/derefs/markers) becomes
23
+ // `v = E; w = v; if (cmp'(w, K0)) { w = w ⊕ K }` with every LATER use of `v` renamed to
24
+ // `w` (the phi variable split from the value variable — the machine's copy).
25
+ // R2 — constant-expression staging: a const-only subtree (depth ≥ 1, e.g. `128 << 9`) used as
26
+ // a bin operand materializes into its own fresh local first (`v = 128 << 9; … v / x …`) —
27
+ // the register the compiler staged the constant in.
28
+ // R3 — return assign-back: a non-var return expression lands in a fresh local first
29
+ // (`r = E; return r`). Emitted as a SEPARATE variant (with/without R3) when R1/R2 fired:
30
+ // which tail the source spelled is itself ambiguous.
31
+ import { IrType, T } from '../ir/types';
32
+ import { Expr, SFn, Stmt, exprEquals, mapExprChildren } from './ast';
33
+ import { declaredTypes, exprCType } from './typing';
34
+
35
+ const exprEq = exprEquals;
36
+
37
+ /** PURE = re-evaluable and hoistable: vars, consts, arithmetic, casts. No calls (effects), no
38
+ * derefs (memory order), no markers. */
39
+ function isPure(e: Expr): boolean {
40
+ switch (e.k) {
41
+ case 'var':
42
+ case 'const':
43
+ return true;
44
+ case 'un':
45
+ case 'cast':
46
+ return isPure(e.e);
47
+ case 'bin':
48
+ return isPure(e.l) && isPure(e.r);
49
+ default:
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /** Every mention of var `from` in an expr renamed to `to`. */
55
+ function renameVar(e: Expr, from: string, to: string): Expr {
56
+ if (e.k === 'var') {
57
+ return e.name === from ? { k: 'var', name: to } : e;
58
+ }
59
+ return mapExprChildren(e, (c) => renameVar(c, from, to));
60
+ }
61
+
62
+ function renameInStmt(s: Stmt, from: string, to: string): Stmt {
63
+ const rx = (e: Expr): Expr => renameVar(e, from, to);
64
+ switch (s.k) {
65
+ case 'assign':
66
+ return { ...s, name: s.name === from ? to : s.name, value: rx(s.value) };
67
+ case 'store':
68
+ return { ...s, lval: rx(s.lval), value: rx(s.value) };
69
+ case 'exprstmt':
70
+ return { ...s, value: rx(s.value) };
71
+ case 'return':
72
+ return s.value ? { ...s, value: rx(s.value) } : s;
73
+ case 'if':
74
+ return {
75
+ ...s,
76
+ cond: rx(s.cond),
77
+ then: s.then.map((x) => renameInStmt(x, from, to)),
78
+ else: s.else.map((x) => renameInStmt(x, from, to)),
79
+ };
80
+ case 'while':
81
+ case 'dowhile':
82
+ return { ...s, cond: rx(s.cond), body: s.body.map((x) => renameInStmt(x, from, to)) };
83
+ case 'for':
84
+ return {
85
+ ...s,
86
+ init: renameInStmt(s.init, from, to),
87
+ cond: rx(s.cond),
88
+ inc: renameInStmt(s.inc, from, to),
89
+ body: s.body.map((x) => renameInStmt(x, from, to)),
90
+ };
91
+ case 'switch':
92
+ return {
93
+ ...s,
94
+ scrutinee: rx(s.scrutinee),
95
+ cases: s.cases.map((c) => ({ ...c, body: c.body.map((x) => renameInStmt(x, from, to)) })),
96
+ default: s.default ? s.default.map((x) => renameInStmt(x, from, to)) : undefined,
97
+ };
98
+ case 'break':
99
+ case 'continue':
100
+ return s;
101
+ }
102
+ }
103
+
104
+ const FLIP: Record<string, string> = { '<': '>=', '<=': '>', '>': '<=', '>=': '<', '==': '!=', '!=': '==' };
105
+
106
+ /** A const-only tree of depth ≥ 1 (`128 << 9`, `-(1 << 4)`) — the staged-constant shape. */
107
+ function isConstExpr(e: Expr): boolean {
108
+ switch (e.k) {
109
+ case 'const':
110
+ return true;
111
+ case 'un':
112
+ return isConstExpr(e.e);
113
+ case 'bin':
114
+ return isConstExpr(e.l) && isConstExpr(e.r);
115
+ default:
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /** Apply the register-copy re-spelling. Returns 0–2 variant SFns (without/with the R3 tail);
121
+ * empty when nothing fired. Pure — never mutates the input. */
122
+ export function registerishSpellings(sfn: SFn): SFn[] {
123
+ const locals = [...sfn.locals];
124
+ const taken = new Set([...sfn.params, ...sfn.locals].map((x) => x.name));
125
+ let fresh = 0;
126
+ const freshVar = (type: IrType): string => {
127
+ let name = `w${fresh++}`;
128
+ while (taken.has(name)) {
129
+ name = `w${fresh++}`;
130
+ }
131
+ taken.add(name);
132
+ locals.push({ name, type });
133
+ return name;
134
+ };
135
+ const typeOf = (name: string): IrType => [...sfn.params, ...locals].find((x) => x.name === name)?.type ?? T.s(32);
136
+
137
+ let fired = 0;
138
+ // R1's value var — DEAD after the copy (every later read renamed to w), so R3 REUSES it for
139
+ // the tail: gcc 2.9's allocator is sensitive to the live-name count, and a fresh tail var
140
+ // scored 3 where the reused one scored 0 (measured on MultiplyQ8).
141
+ let deadValueVar: string | null = null;
142
+
143
+ // R1 over a statement list: rewrite the diamond and rename v→w in every LATER statement.
144
+ const r1List = (stmts: Stmt[]): Stmt[] => {
145
+ const out: Stmt[] = [];
146
+ for (let i = 0; i < stmts.length; i++) {
147
+ const s = stmts[i];
148
+ if (s.k === 'if' && s.then.length === 1 && s.else.length === 1) {
149
+ const m = matchDiamond(s);
150
+ if (m) {
151
+ const { v, E, updArm, upd, cond } = m;
152
+ // GUARDS BEFORE ALLOCATION (a declined shape must leave no residue — the leaked
153
+ // dead `w` even perturbed the live-name count this lever exists to reproduce):
154
+ // • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
155
+ // `if (a < v)` would compare against the POST-assignment v — reproduced);
156
+ // • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
157
+ // comparison signed flipped its sense — reproduced).
158
+ if (!isPure(E) || !isPure(cond) || condOtherMentions(cond, E, v)) {
159
+ out.push(s);
160
+ continue;
161
+ }
162
+ const wType = exprCType(E, declaredTypes({ ...sfn, locals })) ?? typeOf(v);
163
+ const w = freshVar(wType);
164
+ const condOnW = rewriteCond(cond, E, w);
165
+ if (condOnW) {
166
+ const flipped = updArm === 'else' ? flipCmp(condOnW) : condOnW;
167
+ if (flipped) {
168
+ out.push({ k: 'assign', name: v, value: E });
169
+ out.push({ k: 'assign', name: w, value: { k: 'var', name: v } });
170
+ out.push({
171
+ k: 'if',
172
+ cond: flipped,
173
+ then: [{ k: 'assign', name: w, value: renameVar(renameSubexpr(upd, E, v), v, w) }],
174
+ else: [],
175
+ });
176
+ // every LATER statement reads the phi var under its new name
177
+ const rest = stmts.slice(i + 1).map((x) => renameInStmt(x, v, w));
178
+ out.push(...r1List(rest));
179
+ fired++;
180
+ deadValueVar = v;
181
+ return out;
182
+ }
183
+ }
184
+ }
185
+ }
186
+ out.push(s); // nested constructs are NOT rewritten — see the SCOPE note
187
+ }
188
+ return out;
189
+ };
190
+
191
+ interface Diamond {
192
+ v: string;
193
+ E: Expr;
194
+ updArm: 'then' | 'else';
195
+ upd: Expr;
196
+ cond: Expr;
197
+ }
198
+ /** `if (cmp) { v = E } else { v = f(E) }` (or arms swapped), f = bin(E, const-ish). */
199
+ function matchDiamond(s: Extract<Stmt, { k: 'if' }>): Diamond | null {
200
+ if (s.then.length !== 1 || s.else.length !== 1) {
201
+ return null;
202
+ }
203
+ const a = s.then[0];
204
+ const b = s.else[0];
205
+ if (a.k !== 'assign' || b.k !== 'assign' || a.name !== b.name) {
206
+ return null;
207
+ }
208
+ const isUpdOf = (upd: Expr, base: Expr): boolean =>
209
+ upd.k === 'bin' && isPure(upd.r) && exprEq(upd.l, base) && isConstExpr(upd.r);
210
+ if (isUpdOf(b.value, a.value)) {
211
+ return { v: a.name, E: a.value, updArm: 'else', upd: b.value, cond: s.cond };
212
+ }
213
+ if (isUpdOf(a.value, b.value)) {
214
+ return { v: a.name, E: b.value, updArm: 'then', upd: a.value, cond: s.cond };
215
+ }
216
+ return null;
217
+ }
218
+ /** Does the cond's NON-E operand mention `v`? (The E side becomes the copy; the other side
219
+ * must be v-free or the hoisted assignment changes what it compares against.) */
220
+ function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
221
+ if (cond.k !== 'bin') {
222
+ return false;
223
+ }
224
+ const other = exprEq(cond.l, E) ? cond.r : exprEq(cond.r, E) ? cond.l : null;
225
+ const mentions = (e: Expr): boolean => {
226
+ if (e.k === 'var') {
227
+ return e.name === v;
228
+ }
229
+ let hit = false;
230
+ mapExprChildren(e, (c) => {
231
+ hit = hit || mentions(c);
232
+ return c;
233
+ });
234
+ return hit;
235
+ };
236
+ return other ? mentions(other) : false;
237
+ }
238
+ /** cond compares E against a pure operand → same comparison reading the named var. */
239
+ function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
240
+ if (cond.k !== 'bin' || !(cond.op in FLIP)) {
241
+ return null;
242
+ }
243
+ if (exprEq(cond.l, E) && isPure(cond.r)) {
244
+ return { k: 'bin', op: cond.op, l: { k: 'var', name }, r: cond.r };
245
+ }
246
+ if (exprEq(cond.r, E) && isPure(cond.l)) {
247
+ return { k: 'bin', op: cond.op, l: cond.l, r: { k: 'var', name } };
248
+ }
249
+ return null;
250
+ }
251
+ function flipCmp(cond: Expr): Expr | null {
252
+ if (cond.k !== 'bin') {
253
+ return null;
254
+ }
255
+ const op = FLIP[cond.op];
256
+ return op ? { k: 'bin', op: op as Extract<Expr, { k: 'bin' }>['op'], l: cond.l, r: cond.r } : null;
257
+ }
258
+ /** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
259
+ function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
260
+ if (exprEq(e, E)) {
261
+ return { k: 'var', name };
262
+ }
263
+ return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
264
+ }
265
+
266
+ // R2: stage const-expressions used as bin operands into fresh locals, at statement level.
267
+ const r2Stmt = (s: Stmt): Stmt[] => {
268
+ const staged: Stmt[] = [];
269
+ const stage = (e: Expr): Expr => {
270
+ if (e.k === 'bin') {
271
+ const l = isConstExpr(e.l) && e.l.k !== 'const' ? materialize(e.l) : stage(e.l);
272
+ const r = isConstExpr(e.r) && e.r.k !== 'const' ? materialize(e.r) : stage(e.r);
273
+ return { ...e, l, r };
274
+ }
275
+ return mapExprChildren(e, stage);
276
+ };
277
+ const materialize = (e: Expr): Expr => {
278
+ const name = freshVar(T.s(32));
279
+ staged.push({ k: 'assign', name, value: e });
280
+ fired++;
281
+ return { k: 'var', name };
282
+ };
283
+ switch (s.k) {
284
+ case 'return': {
285
+ if (!s.value) {
286
+ return [s];
287
+ }
288
+ const v = stage(s.value);
289
+ return [...staged, { ...s, value: v }];
290
+ }
291
+ case 'assign': {
292
+ const v = stage(s.value);
293
+ return [...staged, { ...s, value: v }];
294
+ }
295
+ default:
296
+ return [s];
297
+ }
298
+ };
299
+
300
+ // pass 1: R1 (may rename downstream), then R2 statement-wise
301
+ const afterR1 = r1List(sfn.body);
302
+ const afterR2 = afterR1.flatMap(r2Stmt);
303
+ if (!fired) {
304
+ return [];
305
+ }
306
+ const base: SFn = { ...sfn, locals: [...locals], body: afterR2 };
307
+
308
+ // R3 variants: the tail assign-back — a non-var return lands in a local first. WHICH local is
309
+ // itself allocator-ambiguous (gcc 2.9 wanted R1's dead value var — live-name-count sensitive;
310
+ // another allocator may want the fresh one), so BOTH tails are emitted as candidates rather
311
+ // than asserting one compiler's preference; the source dedupe collapses them when identical.
312
+ const tails: SFn[] = [];
313
+ const last = base.body[base.body.length - 1];
314
+ if (last?.k === 'return' && last.value && last.value.k !== 'var') {
315
+ const mk = (name: string): SFn => ({
316
+ ...base,
317
+ locals: [...locals],
318
+ body: [
319
+ ...base.body.slice(0, -1),
320
+ { k: 'assign', name, value: (last as Extract<Stmt, { k: 'return' }>).value! } as Stmt,
321
+ { k: 'return', value: { k: 'var', name } } as Stmt,
322
+ ],
323
+ });
324
+ if (deadValueVar) {
325
+ tails.push(mk(deadValueVar));
326
+ }
327
+ tails.push(mk(freshVar(T.s(32))));
328
+ }
329
+
330
+ return [base, ...tails];
331
+ }