@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,410 @@
1
+ // asmlift structurer — Regime-A SWITCH RECOVERY: recognise a comparison tree over a single
2
+ // scrutinee rooted at a cond_br and rebuild the `switch` — or DECLINE (null) to plain
3
+ // if-recovery, which is behaviourally identical (a clean nonmatch, never a miscompile). The
4
+ // factory takes its dependencies EXPLICITLY (`SwitchRecoverDeps`); `expr`/`structureRegion` are
5
+ // late-bound callbacks into the emission phase, so case bodies reuse the ordinary structuring
6
+ // machinery (loops/ifs inside cases, the onStack guard).
7
+ import { Block, Fn, Op, Value, successorsOf } from '../ir/core';
8
+ import { Expr, Stmt, SwitchCase } from '../l3/ast';
9
+
10
+ export interface SwitchRecoverDeps {
11
+ fn: Fn;
12
+ defs: Map<Value, Op>;
13
+ dom: Map<Block, Set<Block>>;
14
+ ipdom: Map<Block, Block | null>;
15
+ opBlock: Map<Op, Block>;
16
+ /** does this value carry a variable name? (named values are not constants) */
17
+ isNamed: (v: Value) => boolean;
18
+ /** is this opcode an integer comparison? */
19
+ isCmpOpcode: (opcode: string) => boolean;
20
+ switchAllowsNeqCase: boolean;
21
+ expr: (v: Value) => Expr;
22
+ structureRegion: (b: Block, stop: Block | null) => Stmt[];
23
+ }
24
+
25
+ export interface SwitchRecovery {
26
+ recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
27
+ /** shared with the Regime-B (`switch_br`) path in structure.ts, which throws where A declines */
28
+ caseRegionReachesSibling: (targets: Set<Block>, b: Block, merge: Block | null) => boolean;
29
+ }
30
+
31
+ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
32
+ const { fn, defs, dom, ipdom, opBlock, isNamed, isCmpOpcode, switchAllowsNeqCase, expr, structureRegion } = deps;
33
+
34
+ // --- Regime A: comparison-tree switch recovery ----------------------------------------------------
35
+ // Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
36
+ // scrutinee identity/dominance, no fall-through, concrete interval consistency, test purity.
37
+
38
+ // Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
39
+ // `250 << 2` for a large sparse case) to a number — else null.
40
+ const evalConst = (v: Value): number | null => {
41
+ if (isNamed(v)) {
42
+ return null;
43
+ } // a named variable is not a constant
44
+ const d = defs.get(v);
45
+ if (!d) {
46
+ return null;
47
+ }
48
+ if (d.opcode === 'const') {
49
+ return (d.attrs.value as number) | 0;
50
+ }
51
+ // Resolve the two operands of a binary op to constants (2-operand → both; 1-operand → operand +
52
+ // `imm` attr, which MUST be present, else decline — a missing imm would wrongly fold `and x` to 0).
53
+ const operands2 = (): [number, number] | null => {
54
+ const a = evalConst(d.operands[0]);
55
+ if (a === null) {
56
+ return null;
57
+ }
58
+ let c: number | null;
59
+ if (d.operands.length === 2) {
60
+ c = evalConst(d.operands[1]);
61
+ } else if (typeof d.attrs.imm === 'number') {
62
+ c = d.attrs.imm | 0;
63
+ } else {
64
+ return null;
65
+ }
66
+ return c === null ? null : [a, c];
67
+ };
68
+ const bin = (f: (a: number, c: number) => number): number | null => {
69
+ const p = operands2();
70
+ return p === null ? null : f(p[0], p[1]) | 0;
71
+ };
72
+ const shift = (f: (a: number, c: number) => number): number | null => {
73
+ const p = operands2();
74
+ if (p === null || p[1] < 0 || p[1] >= 32) {
75
+ return null;
76
+ } // out-of-range shift amount → decline
77
+ return f(p[0], p[1]) | 0;
78
+ };
79
+ switch (d.opcode) {
80
+ case 'shl':
81
+ return shift((a, c) => a << c);
82
+ case 'shr_u':
83
+ return shift((a, c) => a >>> c);
84
+ case 'shr_s':
85
+ return shift((a, c) => a >> c);
86
+ case 'or':
87
+ return bin((a, c) => a | c);
88
+ case 'and':
89
+ return bin((a, c) => a & c);
90
+ case 'xor':
91
+ return bin((a, c) => a ^ c);
92
+ case 'add':
93
+ return bin((a, c) => a + c);
94
+ case 'sub':
95
+ return bin((a, c) => a - c);
96
+ case 'neg': {
97
+ const a = evalConst(d.operands[0]);
98
+ return a === null ? null : -a | 0;
99
+ }
100
+ case 'not': {
101
+ const a = evalConst(d.operands[0]);
102
+ return a === null ? null : ~a | 0;
103
+ }
104
+ default:
105
+ return null;
106
+ }
107
+ };
108
+
109
+ // A "pure test block": its only computation is constants + one integer comparison feeding its
110
+ // cond_br terminator (no store/call/load/opaque — its body is DISCARDED when the tree collapses to a
111
+ // switch, so a side effect there would be lost). PRE4 (purity). The root block is exempt from the
112
+ // "only const/icmp" rule because its non-terminator ops are already emitted as sideEffects(b) before
113
+ // the switch; a non-root test block must be strictly pure.
114
+ const SIDE_EFFECTFUL = new Set(['store', 'astore', 'call', 'load', 'aload', 'opaque']);
115
+ interface TestInfo {
116
+ x: Value;
117
+ k: number;
118
+ cls: 'eq' | 'ne' | 'rel';
119
+ opcode: string;
120
+ xOnLeft: boolean;
121
+ }
122
+ const testInfo = (blk: Block, isRoot: boolean): TestInfo | null => {
123
+ const term = blk.ops[blk.ops.length - 1];
124
+ if (term.opcode !== 'cond_br') {
125
+ return null;
126
+ }
127
+ const cmp = defs.get(term.operands[0]);
128
+ if (!cmp || !isCmpOpcode(cmp.opcode)) {
129
+ return null;
130
+ }
131
+ if (!isRoot && blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode))) {
132
+ return null;
133
+ } // PRE4
134
+ // Which operand is the scrutinee, which is the constant?
135
+ const [lo, ro] = cmp.operands;
136
+ const lc = evalConst(lo),
137
+ rc = evalConst(ro);
138
+ let x: Value, k: number, xOnLeft: boolean;
139
+ if (lc === null && rc !== null) {
140
+ x = lo;
141
+ k = rc;
142
+ xOnLeft = true;
143
+ } else if (rc === null && lc !== null) {
144
+ x = ro;
145
+ k = lc;
146
+ xOnLeft = false;
147
+ } else {
148
+ return null;
149
+ } // both/neither const
150
+ const cls = cmp.opcode === 'icmp_eq' ? 'eq' : cmp.opcode === 'icmp_ne' ? 'ne' : 'rel';
151
+ return { x, k, cls, opcode: cmp.opcode, xOnLeft };
152
+ };
153
+
154
+ // Evaluate a test predicate for a CONCRETE scrutinee value — used to SIMULATE the decision tree and
155
+ // verify recovered case values (below). Returns true iff the `taken` (successors[0]) edge is followed.
156
+ // Signed/unsigned per the icmp opcode (PRE3, done concretely rather than via interval lattices).
157
+ const evalCmp = (opcode: string, xOnLeft: boolean, xv: number, k: number): boolean => {
158
+ const uns = opcode.startsWith('icmp_u');
159
+ const [xn, kn] = uns ? [xv >>> 0, k >>> 0] : [xv | 0, k | 0];
160
+ const [l, r] = xOnLeft ? [xn, kn] : [kn, xn]; // put the scrutinee where it textually appears
161
+ switch (opcode) {
162
+ case 'icmp_eq':
163
+ return l === r;
164
+ case 'icmp_ne':
165
+ return l !== r;
166
+ case 'icmp_slt':
167
+ case 'icmp_ult':
168
+ return l < r;
169
+ case 'icmp_sle':
170
+ case 'icmp_ule':
171
+ return l <= r;
172
+ case 'icmp_sgt':
173
+ case 'icmp_ugt':
174
+ return l > r;
175
+ case 'icmp_sge':
176
+ case 'icmp_uge':
177
+ return l >= r;
178
+ default:
179
+ return false;
180
+ }
181
+ };
182
+
183
+ // Can any case/default entry's region reach a SIBLING entry (switch fall-through)? Region =
184
+ // blocks strictly dominated by `b`, short of `merge`. Shared by Regime A (declines to
185
+ // if-recovery) and Regime B (throws — a jump-table has no fallback).
186
+ const caseRegionReachesSibling = (targets: Set<Block>, b: Block, merge: Block | null): boolean => {
187
+ const inRegion = (blk: Block) => blk !== merge && dom.get(blk)!.has(b);
188
+ for (const entry of targets) {
189
+ const rseen = new Set<Block>([entry]);
190
+ const q = [entry];
191
+ while (q.length) {
192
+ const cur = q.pop()!;
193
+ for (const s of successorsOf(cur)) {
194
+ if (s === entry) {
195
+ continue;
196
+ }
197
+ if (targets.has(s)) {
198
+ return true;
199
+ }
200
+ if (inRegion(s) && !rseen.has(s)) {
201
+ rseen.add(s);
202
+ q.push(s);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ return false;
208
+ };
209
+
210
+ const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
211
+ const root = testInfo(b, true);
212
+ if (!root) {
213
+ return null;
214
+ }
215
+ const scrut = root.x;
216
+ // PRE1 (scrutinee identity + dominance): the scrutinee is a single raw SSA Value that must DOMINATE
217
+ // the whole region. A block param (phi) is rejected — it is not one definition across the region.
218
+ // Params are seeded into names (isNamed); a value defined by an op has a defining block that must dominate
219
+ // b. Function params (entry params) dominate everything.
220
+ const scrutDef = defs.get(scrut);
221
+ const entryBlk = fn.blocks[0];
222
+ if (scrutDef) {
223
+ const defBlk = opBlock.get(scrutDef)!;
224
+ if (!dom.get(b)!.has(defBlk)) {
225
+ return null;
226
+ }
227
+ } else if (!entryBlk.params.includes(scrut)) {
228
+ return null; // a non-entry block param → decline
229
+ }
230
+
231
+ // Walk the test tree. `cases`: value → case-entry block. `defaultCands`: leaves reached without an
232
+ // equality pin. A test-block DAG cycle, or a `!=` case when the compiler disallows it, declines.
233
+ const cases = new Map<number, Block>();
234
+ const defaultCands = new Set<Block>();
235
+ // Skip pure forwarding blocks — a block whose only op is an unconditional `br` (no side effects, no
236
+ // params). agbcc's binary-search layout branches to the shared default through such empty `b .Ldef`
237
+ // blocks; without skipping them each becomes a DISTINCT default candidate and the whole tree declines.
238
+ const skipForward = (blk: Block): Block => {
239
+ let cur = blk;
240
+ const guard = new Set<Block>();
241
+ // Only skip a truly empty forwarding block: a lone `br` with no params AND no successor ARGS —
242
+ // an edge that carries a phi arg is NOT transparent (skipping it would drop that assignment).
243
+ while (
244
+ cur.ops.length === 1 &&
245
+ cur.ops[0].opcode === 'br' &&
246
+ cur.params.length === 0 &&
247
+ cur.ops[0].successors[0].args.length === 0 &&
248
+ !guard.has(cur)
249
+ ) {
250
+ guard.add(cur);
251
+ cur = cur.ops[0].successors[0].block;
252
+ }
253
+ return cur;
254
+ };
255
+ // Concretely SIMULATE the decision tree for a scrutinee value `xv`, returning the leaf block it
256
+ // reaches (or null on an unexpected cycle). This is PRE3 done concretely: it lets us verify each
257
+ // recovered case value actually routes to its recorded body in the ORIGINAL tree.
258
+ const simulateTree = (xv: number): Block | null => {
259
+ let cur = b;
260
+ const guard = new Set<Block>();
261
+ for (;;) {
262
+ const ti = testInfo(cur, cur === b);
263
+ if (!ti || ti.x !== scrut) {
264
+ return cur;
265
+ } // reached a leaf (case body / default)
266
+ if (guard.has(cur)) {
267
+ return null;
268
+ }
269
+ guard.add(cur);
270
+ const term = cur.ops[cur.ops.length - 1];
271
+ const taken = evalCmp(ti.opcode, ti.xOnLeft, xv, ti.k);
272
+ cur = skipForward(term.successors[taken ? 0 : 1].block);
273
+ }
274
+ };
275
+ const seen = new Set<Block>();
276
+ const work: Block[] = [b];
277
+ while (work.length) {
278
+ const blk = work.pop()!;
279
+ if (seen.has(blk)) {
280
+ return null;
281
+ } // a test-block DAG cycle → decline
282
+ seen.add(blk);
283
+ const ti = testInfo(blk, blk === b);
284
+ if (!ti || ti.x !== scrut) {
285
+ return null;
286
+ } // PRE1: every test is on the SAME Value
287
+ const term = blk.ops[blk.ops.length - 1];
288
+ const taken = skipForward(term.successors[0].block),
289
+ fall = skipForward(term.successors[1].block);
290
+ const asLeafOrTest = (child: Block, role: 'case' | 'nav', k?: number) => {
291
+ const isTest = !!testInfo(child, false) && testInfo(child, false)!.x === scrut;
292
+ if (role === 'case') {
293
+ if (isTest) {
294
+ return false;
295
+ } // a case target that's a test → decline
296
+ if (child.params.length) {
297
+ return false;
298
+ } // case entry with a phi → decline
299
+ if (cases.has(k!)) {
300
+ return false;
301
+ } // duplicate case value → decline
302
+ cases.set(k!, child);
303
+ return true;
304
+ }
305
+ // navigation edge
306
+ if (isTest) {
307
+ work.push(child);
308
+ return true;
309
+ }
310
+ defaultCands.add(child); // a non-test leaf reached by nav = default
311
+ return true;
312
+ };
313
+ if (ti.cls === 'eq') {
314
+ if (!asLeafOrTest(taken, 'case', ti.k)) {
315
+ return null;
316
+ } // x==k → taken is case k
317
+ if (!asLeafOrTest(fall, 'nav')) {
318
+ return null;
319
+ }
320
+ } else if (ti.cls === 'ne') {
321
+ if (!switchAllowsNeqCase) {
322
+ return null;
323
+ } // per-compiler gate
324
+ if (!asLeafOrTest(fall, 'case', ti.k)) {
325
+ return null;
326
+ } // x!=k → the EQUAL side (fall) is case k
327
+ if (!asLeafOrTest(taken, 'nav')) {
328
+ return null;
329
+ }
330
+ } else {
331
+ // relational → pure navigation
332
+ if (!asLeafOrTest(taken, 'nav')) {
333
+ return null;
334
+ }
335
+ if (!asLeafOrTest(fall, 'nav')) {
336
+ return null;
337
+ }
338
+ }
339
+ }
340
+
341
+ if (cases.size < 2) {
342
+ return null;
343
+ } // not worth a switch (m2c: ≥2 cases)
344
+ // The default is the single non-test leaf that is NOT a case body. 0 → no default; ≥2 distinct → decline.
345
+ const caseBlocks = new Set(cases.values());
346
+ const defaults = [...defaultCands].filter((d) => !caseBlocks.has(d));
347
+ if (defaults.length > 1) {
348
+ return null;
349
+ }
350
+ const defaultBlk = defaults[0] ?? null;
351
+ if (defaultBlk && defaultBlk.params.length) {
352
+ return null;
353
+ } // default entry with a phi → decline
354
+ // A default candidate that is ALSO a case body means a relational edge hit a case leaf → ambiguous.
355
+ if ([...defaultCands].some((d) => caseBlocks.has(d))) {
356
+ return null;
357
+ }
358
+
359
+ // PRE1 dominance of the whole region: b must dominate every case body + the default (single-entry).
360
+ for (const cb of caseBlocks) {
361
+ if (!dom.get(cb)!.has(b)) {
362
+ return null;
363
+ }
364
+ }
365
+ if (defaultBlk && !dom.get(defaultBlk)!.has(b)) {
366
+ return null;
367
+ }
368
+
369
+ // PRE2 (fall-through): only NON-fall-through switches are handled — decline if any case body
370
+ // can reach ANOTHER case body (or the default) while staying inside the region. (The SAME
371
+ // predicate serves the Regime-B path, which throws instead.)
372
+ const merge = ipdom.get(b) ?? stop;
373
+ const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
374
+ if (caseRegionReachesSibling(targets, b, merge)) {
375
+ return null;
376
+ }
377
+
378
+ // PRE3 (concrete interval consistency): a `case k` is only sound if the ORIGINAL tree
379
+ // actually routes x==k to its recorded body. A relational guard can make an `x==k` test DEAD (e.g.
380
+ // `if(x<5){ if(x==20) … }` — x==20 is unreachable under x<5); a naive switch would resurrect `case 20`
381
+ // and misroute x==20. Simulating the tree per case value catches exactly this — decline on any mismatch.
382
+ for (const [k, blk] of cases) {
383
+ if (simulateTree(k) !== blk) {
384
+ return null;
385
+ }
386
+ }
387
+
388
+ // Build the switch. Cases sorted ascending (safe: no fall-through — PRE2). Bodies delegate to the
389
+ // existing structureRegion (loops/ifs inside cases, the onStack guard — all reused).
390
+ const scrutExpr = expr(scrut);
391
+ const sortedCases = [...cases.entries()].sort((a, c) => a[0] - c[0]);
392
+ const outCases: SwitchCase[] = sortedCases.map(([k, blk]) => ({
393
+ values: [k],
394
+ body: structureRegion(blk, merge),
395
+ fallsThrough: false,
396
+ }));
397
+ const sw: Stmt = {
398
+ k: 'switch',
399
+ scrutinee: scrutExpr,
400
+ cases: outCases,
401
+ ...(defaultBlk ? { default: structureRegion(defaultBlk, merge) } : {}),
402
+ };
403
+ const out: Stmt[] = [sw];
404
+ if (merge && merge !== stop) {
405
+ out.push(...structureRegion(merge, stop));
406
+ }
407
+ return out;
408
+ };
409
+ return { recognizeSwitch, caseRegionReachesSibling };
410
+ }
package/src/target.ts ADDED
@@ -0,0 +1,140 @@
1
+ // asmlift — the Target: (isa, compiler) as first-class axes. ABI + capabilities are DATA
2
+ // consumed generically by shared passes — never a target-name branch inside a shared pass
3
+ // (m2c's `arch.arch ==` leakage).
4
+ //
5
+ // What each datum drives:
6
+ // • id → frontend dispatch (registry.ts)
7
+ // • compiler → idiom gating (patternApplies) + the report
8
+ // • argRegs / returnReg → entry-param ordering and return-value read in the frontends
9
+ // • capabilities.hwDivide → gates the MIPS hardware-divide decode (mips.ts), the soft-division
10
+ // pre-recovery pass, and idiom gating; a `div` on a target declaring no divider degrades to
11
+ // a loud opaque (exercised by packages/cli/test/matching/divmul.test.ts). `hwFloat` → idiom
12
+ // gating only (no float pass yet).
13
+ // • capabilities.endianness / flags → RESERVED hardware facts, not yet read by any pass
14
+ // (byte-addressing will consume endianness; PPC condition regs → flags).
15
+ // • compilerBehaviors.* → all consumed by the structurer (threaded via StructureOptions).
16
+ //
17
+ // `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization choices) are
18
+ // deliberately separate bags: a new compiler must set its behaviors EXPLICITLY instead of
19
+ // silently inheriting a universal that is really per-compiler. `coalesceLoopInit` already
20
+ // differs across targets (IDO true, agbcc/GCC false).
21
+ // This module is browser-pure by contract (no Node APIs, enforced by
22
+ // test/browser-safe.test.ts): the toolchain paths that COMPILE for these targets
23
+ // live in @asmlift/toolchains.
24
+ import type { StructureOptions } from './structure/structure';
25
+
26
+ export interface TargetDescription {
27
+ id: string; // the ISA — 'armv4t' / 'mips' / 'ppc'. Selects the frontend (registry.ts).
28
+ // The COMPILER is a first-class axis distinct from the ISA (matching = deoptimize to a specific
29
+ // compiler): two targets can share an ISA (⇒ one frontend) yet differ here — e.g. MIPS_IDO vs
30
+ // MIPS_GCC. Consumed by pattern gating (patternApplies) and the report. (version/flags/language
31
+ // are future axes, added when earned.)
32
+ compiler: string; // 'agbcc' / 'ido' / 'gcc' / 'mwcc'
33
+ argRegs: string[];
34
+ returnReg: string;
35
+ // HARDWARE / ISA facts — independent of the compiler.
36
+ capabilities: {
37
+ endianness: 'little' | 'big'; // RESERVED — no pass reads it yet (byte-addressing will)
38
+ hwDivide: boolean; // consumed by patternApplies (idiom gating)
39
+ hwFloat: boolean; // consumed by patternApplies (idiom gating)
40
+ flags: boolean; // RESERVED — no pass reads it yet (PPC condition regs will)
41
+ };
42
+ // COMPILER BEHAVIORS — the specific compiler's canonicalization choices, distinct from
43
+ // hardware `capabilities`. All consumed by the structurer (threaded through StructureOptions).
44
+ compilerBehaviors: {
45
+ // When a loop induction variable's initial value comes from an argument register, some
46
+ // compilers keep mutating that register across the loop (coalesce → no init copy); others
47
+ // copy to a fresh local. IDO -O2 reuses the arg register (true); agbcc/KMC-GCC allocate
48
+ // fresh (false).
49
+ coalesceLoopInit?: boolean;
50
+ // Divergent-if (both arms terminate, no join): reproduce the source branch DIRECTION by
51
+ // emitting the forward-branch-on-negated-condition (taken arm as `else`). IDO/MIPS preserves
52
+ // source direction so this must be on to be byte-exact; agbcc/GCC canonicalize either way so
53
+ // true is a safe default there. A compiler that inverts branch canonicalization sets it
54
+ // false. Absent ⇒ true; a compiler opts OUT.
55
+ preserveDivergentBranchSense?: boolean;
56
+ // Order the parallel-copy assignments at a CFG edge by the order their values are COMPUTED
57
+ // in the predecessor (vs. source/param order), matching a compiler that lays defining ops
58
+ // (and the copies reading them) out in computation order. Uniform (true) across all current
59
+ // compilers. Absent ⇒ true; a compiler opts OUT.
60
+ orderArgCopiesByComputation?: boolean;
61
+ // Regime-A switch recovery: accept an `x != K` test as a case (the EQUAL side is the case
62
+ // body). GCC freely emits `!=`; IDO prefers `==`/`<`. Absent ⇒ true (permissive); the
63
+ // decline path keeps recovery sound either way.
64
+ switchAllowsNeqCase?: boolean;
65
+ };
66
+ }
67
+
68
+ export const ARMV4T_AGBCC: TargetDescription = {
69
+ id: 'armv4t',
70
+ compiler: 'agbcc',
71
+ argRegs: ['r0', 'r1', 'r2', 'r3'],
72
+ returnReg: 'r0',
73
+ capabilities: { endianness: 'little', hwDivide: false, hwFloat: false, flags: true },
74
+ compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
75
+ };
76
+
77
+ /** MIPS-II / IDO 7.1 target. IDO is the IRIX C compiler,
78
+ * statically recompiled to run natively (ido-static-recomp). Unlike agbcc
79
+ * it emits no textual asm, so asmlift's input is the DISASSEMBLED object (`mips-linux-gnu-
80
+ * objdump -d`); the arch-agnostic objdiff scorer scores the MIPS object directly. Big-endian,
81
+ * hardware divide + FPU (N64). */
82
+ export const MIPS_IDO: TargetDescription = {
83
+ id: 'mips',
84
+ compiler: 'ido',
85
+ argRegs: ['a0', 'a1', 'a2', 'a3'],
86
+ returnReg: 'v0',
87
+ capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
88
+ // `switchAllowsNeqCase: false` — IDO's switch dispatch uses `==`/`<`, never `!=` cases;
89
+ // leaving it permissive mis-recognises `!=`-rooted if-else chains as switches.
90
+ compilerBehaviors: {
91
+ coalesceLoopInit: true,
92
+ preserveDivergentBranchSense: true,
93
+ orderArgCopiesByComputation: true,
94
+ switchAllowsNeqCase: false,
95
+ },
96
+ };
97
+
98
+ /** MIPS + KMC GCC — the SAME ISA as MIPS_IDO, a DIFFERENT compiler: `id:"mips"` reuses the
99
+ * `mips` frontend verbatim, only `compiler` varies. Same N64 hardware ⇒ identical hardware
100
+ * capabilities to IDO. */
101
+ export const MIPS_GCC: TargetDescription = {
102
+ id: 'mips',
103
+ compiler: 'gcc',
104
+ argRegs: ['a0', 'a1', 'a2', 'a3'],
105
+ returnReg: 'v0',
106
+ // KMC GCC allocates a fresh local for the loop init (coalesceLoopInit false — where it differs
107
+ // from IDO); the structuring levers take the universal default until a KMC fixture says otherwise.
108
+ capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
109
+ compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
110
+ };
111
+
112
+ /** PowerPC (GameCube/Wii) + Metrowerks CodeWarrior. The real GC/Wii matching target is
113
+ * CodeWarrior `mwcceppc` (not GCC): active decomp projects and decomp.me standardize on it.
114
+ * `-proc gekko` = the GC Gekko CPU. Big-endian, hardware divide + FPU. `flags: true`: PPC has
115
+ * condition registers (cr0–cr7), but compare→branch still fuses into a single `cond_br`
116
+ * (test/ppc-seam.test.ts), so `flags` stays a documented hardware fact, not yet an IR concern —
117
+ * real flags-as-data is deferred until a fixture reuses/combines a cr field. */
118
+ export const PPC_MWCC: TargetDescription = {
119
+ id: 'ppc',
120
+ compiler: 'mwcc',
121
+ // PPC EABI: r3–r10 pass integer/pointer arguments; r3 also returns.
122
+ argRegs: ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'],
123
+ returnReg: 'r3',
124
+ capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: true },
125
+ // CodeWarrior's structuring levers are UNKNOWN until fixtures reveal them — safe universal
126
+ // defaults; coalesceLoopInit false until a CW loop fixture says otherwise.
127
+ compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
128
+ };
129
+
130
+ /** Build the structurer's options for a target: the function's own `returnsVoid` plus every
131
+ * `compilerBehaviors` lever (they map 1:1 onto StructureOptions field names). The ONE place a
132
+ * target's compiler behaviors flow into the target-agnostic structurer — a new behavior lever
133
+ * is a field in `compilerBehaviors`, consumed automatically. */
134
+ export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
135
+ return { returnsVoid, ...t.compilerBehaviors };
136
+ }
137
+
138
+ export const C_TYPEDEFS =
139
+ 'typedef unsigned char u8;typedef unsigned short u16;typedef unsigned int u32;' +
140
+ 'typedef signed char s8;typedef short s16;typedef int s32;\n';