@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,990 @@
1
+ // asmlift ISA frontend — PowerPC (GameCube/Wii, Metrowerks CodeWarrior `mwcceppc`). Input is
2
+ // disassembled text (`powerpc-eabi-objdump -d --no-show-raw-insn`), parsed by the shared
3
+ // `parseDisasm` with the reloc + branch-hint options enabled.
4
+ //
5
+ // ISA facts that shape this frontend:
6
+ // • NO DELAY SLOTS — a block simply ends at its terminator.
7
+ // • CONDITION REGISTERS, FUSED — a compare (`cmpw`/`cmpwi`; unsigned `cmplw`/`cmplwi`) is tracked
8
+ // and the branch that reads it fuses into a single `cond_br icmp_*`. The branch MNEMONIC
9
+ // carries the sense (`bge` ⇒ `icmp_sge`), so no negation fold is needed; an unsigned compare
10
+ // picks the unsigned icmp row.
11
+ // • CONDITIONAL RETURN — `cmpwi r3,0; bgelr` is "if cr0≥0, return r3". The `bXXlr` forms become a
12
+ // cond_br to a SYNTHETIC return block, so the structurer sees an ordinary divergent-if.
13
+ // • EXTENDED MNEMONICS — objdump prints the simplified forms: `mr`, `li`, `subf` (reversed
14
+ // operands: `subf rD,rA,rB` = rB−rA), `not`, and the rotate-and-mask family.
15
+ // `slwi`/`srwi`/`clrlwi`/`clrrwi` and rotate-0 `rlwinm` are exact; a non-zero-rotate `rlwinm`
16
+ // lowers only as the right-shift bitfield extract `(x>>n)&mask` (ME=31, non-wrapping); a
17
+ // genuine rotate/insert stays an opaque.
18
+ // • CALLS (`bl`) — the callee symbol comes from the interleaved `R_PPC_REL24` relocation (an
19
+ // unresolved `bl` in a .o encodes a 0 placeholder); arguments come from r3.. per the callee
20
+ // prototype (falling back to argument-register liveness). The frame — `stwu r1`, `mflr`/`mtlr`,
21
+ // r1-relative spills — is transparent to dataflow, so a value in a callee-saved register
22
+ // survives the call.
23
+ // • RECORD FORM (`.` = the Rc bit, e.g. `andi.`/`add.`) also sets cr0 from a signed compare of
24
+ // the result against 0; that implicit compare is wired so a following `beq`/`bne` fuses.
25
+ //
26
+ // TRUSTWORTHINESS: an unmodelled instruction with a register destination emits an `opaque` value
27
+ // (dead ⇒ vanishes, live ⇒ fails LOUD downstream); an unmodelled CONTROL TRANSFER throws
28
+ // PpcUnsupportedError in `lift`. Never plausible-but-wrong C.
29
+ //
30
+ // Scope: straight-line + `if`/diamond integer functions (incl. the conditional-return idiom),
31
+ // non-recursive `bl` calls, recovered dense-switch jump tables, and CTR-counted `bdnz` loops
32
+ // (`mtctr` seeds a `ctr` pseudo-register). Returns through r3. Out of scope, failing loud: the
33
+ // conditional-CTR forms (`bdz`/`bdnzt`/…), an unrecovered `bctr`, and a `bdnz` with no reaching
34
+ // `mtctr`. NOTE mwcc at -O4 aggressively UNROLLS loops into a `bdnz` main loop + a remainder
35
+ // loop; an unrolled loop recovers as the unrolled form (sound, rarely a match).
36
+ import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
37
+ import type { Opcode } from '../ir/opcodes';
38
+ import { T } from '../ir/types';
39
+ import { type Prototypes, protoArity } from '../proto';
40
+ import type { TargetDescription } from '../target';
41
+ import { type AsmData, readJumpTable } from './asmdata';
42
+ import {
43
+ type DisasmInstr,
44
+ parseImm,
45
+ parseDisasm as parseSharedDisasm,
46
+ parseMem as parseSharedMem,
47
+ sliceSymbol,
48
+ } from './disasm';
49
+ import { mkEmitKit, pushSwitchBr } from './emit';
50
+ import { FrontendUnsupportedError } from './errors';
51
+ import { assertInputFormat } from './format';
52
+ import type { Frontend } from './frontend';
53
+ import { opaqueDest } from './opaque';
54
+ import { abiSortEntryParams } from './ssa';
55
+ import { makeSsaBuilder } from './ssa';
56
+
57
+ type Instr = DisasmInstr;
58
+
59
+ // Branch-condition mnemonic → the icmp for its TAKEN edge, split by compare signedness. PowerPC's
60
+ // branch already names the relation (no negation fold needed). `signed` picks the row.
61
+ const COND_SIGNED: Record<string, Opcode> = {
62
+ blt: 'icmp_slt',
63
+ ble: 'icmp_sle',
64
+ bgt: 'icmp_sgt',
65
+ bge: 'icmp_sge',
66
+ beq: 'icmp_eq',
67
+ bne: 'icmp_ne',
68
+ };
69
+ const COND_UNSIGNED: Record<string, Opcode> = {
70
+ blt: 'icmp_ult',
71
+ ble: 'icmp_ule',
72
+ bgt: 'icmp_ugt',
73
+ bge: 'icmp_uge',
74
+ beq: 'icmp_eq',
75
+ bne: 'icmp_ne',
76
+ };
77
+ const CONDS = new Set(Object.keys(COND_SIGNED));
78
+
79
+ // Out-of-scope control flow (conditional-CTR forms `bdz`/`bdnzt`/…, indirect `bctr`/`bctrl`, …).
80
+ // PowerPC branch mnemonics all start with `b`; anything not in `isModeledBranch` is a real branch
81
+ // this frontend cannot lower, and silently dropping a branch is a silent miscompile — so `lift`
82
+ // throws a catchable "out of scope" signal instead. Subclasses the shared frontend signal so
83
+ // consumers can `instanceof FrontendUnsupportedError`; `.name` kept for stable stub text.
84
+ export class PpcUnsupportedError extends FrontendUnsupportedError {
85
+ constructor(message: string) {
86
+ super(message);
87
+ this.name = 'PpcUnsupportedError';
88
+ }
89
+ }
90
+
91
+ const isReturn = (ins: Instr) => ins.mnemonic === 'blr';
92
+ const isUncond = (ins: Instr) => ins.mnemonic === 'b';
93
+ const isCond = (ins: Instr) => CONDS.has(ins.mnemonic) && ins.target !== undefined;
94
+ // Conditional return, e.g. `bgelr`/`bltlr`: a cond mnemonic with the `lr` suffix and no target.
95
+ const condReturnBase = (ins: Instr): string | null => {
96
+ if (!ins.mnemonic.endsWith('lr')) {
97
+ return null;
98
+ }
99
+ const base = 'b' + ins.mnemonic.slice(1, -2);
100
+ return CONDS.has(base) ? base : null;
101
+ };
102
+ const isCondReturn = (ins: Instr) => condReturnBase(ins) !== null;
103
+ // `bdnz L` — CTR ← CTR−1; branch to L if CTR ≠ 0. The ONLY CTR form modelled — `bdz`, the
104
+ // conditional-CTR forms (`bdnzt`/`bdzf`/…), and the indirect `bctr`/`bctrl` fail loud (below).
105
+ // A `bdnz` with no target is malformed.
106
+ const isCtrLoop = (ins: Instr) => ins.mnemonic === 'bdnz' && ins.target !== undefined;
107
+ const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins) || isCondReturn(ins) || isCtrLoop(ins);
108
+ // `bl` is a CALL (mid-block, control returns), not a block transfer, so it is modelled but not in
109
+ // isXfer. Every other `b*` mnemonic is a branch we can lower iff it is one of these forms.
110
+ const isModeledBranch = (ins: Instr) => isXfer(ins) || ins.mnemonic === 'bl';
111
+
112
+ const isReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
113
+
114
+ // Shared objdump scaffolding (frontend/disasm.ts). parseMem narrowed to `r\d+` bases — a
115
+ // non-register base is an SDA/global placeholder assertOrdinaryMem declines.
116
+ const parseMem = (operand: string): { off: number; base: string } => parseSharedMem(operand, /r\d+/);
117
+
118
+ // 32-bit mask with bits [mb..me] set (PowerPC bit numbering: 0 = MSB). Wraps when mb > me.
119
+ const rlwinmMask = (mb: number, me: number): number => {
120
+ let m = 0;
121
+ for (let b = 0; b < 32; b++) {
122
+ const set = mb <= me ? b >= mb && b <= me : b >= mb || b <= me;
123
+ if (set) {
124
+ m |= 0x80000000 >>> b;
125
+ }
126
+ }
127
+ return m >>> 0;
128
+ };
129
+
130
+ // Shared objdump scaffolding (frontend/disasm.ts), with the two PPC extras threaded as options:
131
+ // reloc lines (`-r` — the callee symbol for a `bl` whose encoded offset is a placeholder) and
132
+ // branch-prediction hint suffixes (`blt-`/`bge+` — a hint, not a different instruction; without
133
+ // stripping, the mnemonic misses the cond tables and the branch is silently dropped).
134
+ const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { relocs: true, hintSuffixes: true });
135
+
136
+ interface PpcBlock {
137
+ startAddr: number;
138
+ body: Instr[];
139
+ branch: Instr | null; // terminating transfer (or null for a pure fall-through)
140
+ synthReturn?: boolean; // a synthetic block that just returns the return register
141
+ }
142
+
143
+ // A recovered dense-switch jump table (Regime B), keyed
144
+ // by the BOUNDS branch (`bgt DEF`) whose block emits the `switch_br`. `caseAddrs[k]` is the `.text`
145
+ // address of case `k` (dense 0..N-1); `bctrAddr` is the elided dispatch's indirect jump. The mwcc
146
+ // idiom:
147
+ // cmplwi rS,N-1 ; bgt DEF (bounds)
148
+ // lis rT,0 [ADDR16_HA @tbl] ; slwi rIdx,rS,2 ; addi rB,rT,0 [ADDR16_LO @tbl]
149
+ // ; lwzx rV,rB,rIdx ; mtctr rV ; bctr (dispatch — table in .data)
150
+ interface PpcJT {
151
+ scrutReg: string;
152
+ caseAddrs: number[];
153
+ defaultAddr: number;
154
+ bctrAddr: number;
155
+ }
156
+
157
+ // Recover mwcc jump tables from the dispatch idiom + the AsmData side-table. Fail-closed: any
158
+ // deviation from the exact idiom, or a table that doesn't resolve to N in-function targets, declines
159
+ // (→ the `bctr` loud-fail fires). Index IDENTITY guard: `slwi rIdx,rS,2` must be the bounds-checked
160
+ // scrutinee scaled only by <<2 — no xor/neg/extra op.
161
+ function recoverPpcJumpTables(instrs: Instr[], ad: AsmData): Map<number, PpcJT> {
162
+ const out = new Map<number, PpcJT>();
163
+ for (let i = 5; i < instrs.length; i++) {
164
+ if (instrs[i].mnemonic !== 'bctr') {
165
+ continue;
166
+ }
167
+ const [lis, slwi, addi, lwzx, mtctr] = [instrs[i - 5], instrs[i - 4], instrs[i - 3], instrs[i - 2], instrs[i - 1]];
168
+ if (
169
+ lis.mnemonic !== 'lis' ||
170
+ slwi.mnemonic !== 'slwi' ||
171
+ addi.mnemonic !== 'addi' ||
172
+ lwzx.mnemonic !== 'lwzx' ||
173
+ mtctr.mnemonic !== 'mtctr'
174
+ ) {
175
+ continue;
176
+ }
177
+ const rV = mtctr.ops[0];
178
+ if (lwzx.ops[0] !== rV) {
179
+ continue;
180
+ } // rV = mem[rB + rIdx]
181
+ const [rB, rIdx] = [lwzx.ops[1], lwzx.ops[2]];
182
+ if (slwi.ops[0] !== rIdx || (slwi.ops[2] !== '2' && slwi.ops[2] !== '0x2')) {
183
+ continue;
184
+ } // identity: rIdx = rS<<2
185
+ const scrutReg = slwi.ops[1];
186
+ if (addi.ops[0] !== rB || parseImm(addi.ops[2]) !== 0) {
187
+ continue;
188
+ } // rB = &table (lo) + 0
189
+ const rT = addi.ops[1];
190
+ const tableSym = lis.sym; // ADDR16_HA/LO @tbl (from inline -r reloc)
191
+ if (lis.ops[0] !== rT || !tableSym || addi.sym !== tableSym) {
192
+ continue;
193
+ }
194
+ // Bounds: the nearest preceding `cmplwi scrutReg,N-1 ; bgt DEF` guard. The `bgt` is itself a
195
+ // transfer, so match the pair directly (the cmplwi sits one instruction behind it); a different
196
+ // transfer before the guard ⇒ decline.
197
+ let bounds: { addr: number; n: number; def: number } | null = null;
198
+ for (let j = i - 6; j >= 1; j--) {
199
+ const bgt = instrs[j];
200
+ if (bgt.mnemonic === 'bgt' && bgt.target !== undefined) {
201
+ const c = instrs[j - 1];
202
+ if (c && c.mnemonic === 'cmplwi' && c.ops[c.ops.length - 2] === scrutReg) {
203
+ bounds = { addr: bgt.addr, n: parseImm(c.ops[c.ops.length - 1]) + 1, def: bgt.target };
204
+ }
205
+ break;
206
+ }
207
+ if (isXfer(bgt)) {
208
+ break;
209
+ }
210
+ }
211
+ if (!bounds || bounds.n < 2) {
212
+ continue;
213
+ }
214
+ const caseAddrs = readJumpTable(ad, tableSym, 0, bounds.n);
215
+ if (!caseAddrs) {
216
+ continue;
217
+ }
218
+ out.set(bounds.addr, { scrutReg, caseAddrs, defaultAddr: bounds.def, bctrAddr: instrs[i].addr });
219
+ }
220
+ return out;
221
+ }
222
+
223
+ // Split into basic blocks and compute successor block INDICES (order [taken, fall] for a cond
224
+ // branch/return). Conditional-return branches get a synthetic return block as their taken edge.
225
+ // A recovered jump table (`jts`, keyed by bounds-branch addr) turns its bounds block into a
226
+ // `switch_br` dispatcher: the case + default addresses become leaders and its only successors.
227
+ function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blocks: PpcBlock[]; succIdx: number[][] } {
228
+ const leaders = new Set<number>(instrs.length ? [instrs[0].addr] : []);
229
+ instrs.forEach((ins, i) => {
230
+ if ((isCond(ins) || isUncond(ins) || isCtrLoop(ins)) && ins.target !== undefined) {
231
+ leaders.add(ins.target);
232
+ }
233
+ if ((isCond(ins) || isCondReturn(ins) || isCtrLoop(ins)) && instrs[i + 1]) {
234
+ leaders.add(instrs[i + 1].addr);
235
+ } // fall-through
236
+ });
237
+ for (const jt of jts.values()) {
238
+ for (const a of jt.caseAddrs) {
239
+ leaders.add(a);
240
+ }
241
+ leaders.add(jt.defaultAddr);
242
+ }
243
+
244
+ const blocks: PpcBlock[] = [];
245
+ let cur: PpcBlock | null = null;
246
+ for (const ins of instrs) {
247
+ if (cur === null || leaders.has(ins.addr)) {
248
+ cur = { startAddr: ins.addr, body: [], branch: null };
249
+ blocks.push(cur);
250
+ }
251
+ if (isXfer(ins)) {
252
+ cur.branch = ins;
253
+ cur = null;
254
+ } else {
255
+ cur.body.push(ins);
256
+ }
257
+ }
258
+
259
+ const idxOf = new Map(blocks.map((b, i) => [b.startAddr, i]));
260
+ const fallAddr = (b: PpcBlock) => (b.branch ?? b.body[b.body.length - 1]).addr + 4;
261
+ const succIdx: number[][] = blocks.map(() => []);
262
+ blocks.forEach((b, i) => {
263
+ const br = b.branch;
264
+ // Recovered switch: the bounds block dispatches to its case blocks + default (the `cmplwi`/`bgt`
265
+ // and the elided dispatch block are subsumed into a `switch_br`). Every case/default addr is a
266
+ // leader, so all resolve; a target that is not a block boundary makes it malformed → loud-fail.
267
+ const jt = br ? jts.get(br.addr) : undefined;
268
+ if (jt) {
269
+ const succ = [...jt.caseAddrs, jt.defaultAddr].map((a) => idxOf.get(a));
270
+ if (succ.some((x) => x === undefined)) {
271
+ throw new PpcUnsupportedError(`cannot lift '${name}': jump-table target is not a block boundary`);
272
+ }
273
+ succIdx[i] = succ as number[];
274
+ return;
275
+ }
276
+ if (!br) {
277
+ const j = idxOf.get(fallAddr(b));
278
+ if (j !== undefined) {
279
+ succIdx[i] = [j];
280
+ }
281
+ return;
282
+ }
283
+ if (isReturn(br)) {
284
+ succIdx[i] = [];
285
+ return;
286
+ }
287
+ if (isUncond(br)) {
288
+ const j = idxOf.get(br.target!);
289
+ succIdx[i] = j !== undefined ? [j] : [];
290
+ return;
291
+ }
292
+ if (isCondReturn(br)) {
293
+ const synth: PpcBlock = { startAddr: -1 - blocks.length, body: [], branch: null, synthReturn: true };
294
+ const si = blocks.length;
295
+ blocks.push(synth);
296
+ succIdx.push([]);
297
+ const fall = idxOf.get(br.addr + 4); // fall = instruction after the cond-return (no delay slot)
298
+ succIdx[i] = fall !== undefined ? [si, fall] : [si];
299
+ return;
300
+ }
301
+ // conditional branch to a label: [taken, fall]. Both must land on block boundaries; otherwise the
302
+ // branch leaves the function (a tail branch) or targets unrecovered flow — fail LOUD and catchably
303
+ // rather than silently dropping an edge (which surfaces as an opaque `verify` successor-count error).
304
+ const taken = idxOf.get(br.target!);
305
+ const fall = idxOf.get(br.addr + 4);
306
+ if (taken === undefined || fall === undefined) {
307
+ throw new PpcUnsupportedError(
308
+ `cannot lift '${name}': conditional branch '${br.mnemonic}' at 0x${br.addr.toString(16)} ` +
309
+ `has a target/fall-through that is not a block boundary (tail branch or unrecovered control flow)`,
310
+ );
311
+ }
312
+ succIdx[i] = [taken, fall];
313
+ });
314
+
315
+ // Prune to blocks reachable from entry (drops trailing padding). Reindex succ accordingly.
316
+ const reachable = new Set<number>();
317
+ const queue = blocks.length ? [0] : [];
318
+ while (queue.length) {
319
+ const i = queue.pop()!;
320
+ if (reachable.has(i)) {
321
+ continue;
322
+ }
323
+ reachable.add(i);
324
+ for (const s of succIdx[i]) {
325
+ queue.push(s);
326
+ }
327
+ }
328
+ const keep = blocks.map((_, i) => i).filter((i) => reachable.has(i));
329
+ const remap = new Map(keep.map((old, neu) => [old, neu]));
330
+ return {
331
+ blocks: keep.map((i) => blocks[i]),
332
+ succIdx: keep.map((i) => succIdx[i].map((s) => remap.get(s)!)),
333
+ };
334
+ }
335
+
336
+ /** Lift disassembled PowerPC text → an L1 Fn with block-argument SSA. `asmData` (optional) supplies
337
+ * the data-section jump table for dense-switch (Regime-B) recovery; absent ⇒ a `bctr` dispatch
338
+ * loud-fails. */
339
+ export function lift(
340
+ name: string,
341
+ asm: string,
342
+ target: TargetDescription,
343
+ prototypes: Prototypes = {},
344
+ asmData?: AsmData,
345
+ ): Fn {
346
+ assertInputFormat('ppc', 'objdump', asm);
347
+ const instrs = parseDisasm(sliceSymbol(asm, name)); // ONE function only — an absent symbol declines loud
348
+ if (instrs.length === 0) {
349
+ throw new PpcUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
350
+ }
351
+ // Regime B: recover mwcc jump tables from the `bctr` dispatch idiom + the AsmData table. A
352
+ // recovered dispatch's `bctr` is subsumed into a `switch_br` (emitted from its bounds block), so
353
+ // it is exempted from the loud-fail below; an UNrecovered `bctr` still fails loud.
354
+ const jts = asmData ? recoverPpcJumpTables(instrs, asmData) : new Map<number, PpcJT>();
355
+ const recoveredBctr = new Set([...jts.values()].map((j) => j.bctrAddr));
356
+ // TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
357
+ // would silently miscompile the control flow). CTR-counted loops and indirect branches land here.
358
+ for (const ins of instrs) {
359
+ if (ins.mnemonic.startsWith('b') && !isModeledBranch(ins)) {
360
+ if (ins.mnemonic === 'bctr' && recoveredBctr.has(ins.addr)) {
361
+ continue;
362
+ } // recovered switch dispatch
363
+ throw new PpcUnsupportedError(
364
+ `cannot lift '${name}': unmodelled control transfer '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
365
+ `(CTR-counted loop or indirect branch — mwcc -O4 loop unrolling is not yet supported)`,
366
+ );
367
+ }
368
+ }
369
+ const { blocks, succIdx } = toBlocks(instrs, name, jts);
370
+
371
+ const preds: number[][] = blocks.map(() => []);
372
+ blocks.forEach((_, i) => {
373
+ for (const s of succIdx[i]) {
374
+ preds[s].push(i);
375
+ }
376
+ });
377
+
378
+ const ssa = makeSsaBuilder(name, blocks.length, preds);
379
+ const { irBlocks, readVar, writeVar, paramReg } = ssa;
380
+ const RET = target.returnReg;
381
+ const ARG_REGS = target.argRegs;
382
+
383
+ // Best-effort call arity when a callee has no prototype: the count of contiguous argument
384
+ // registers (r3..) with a value reaching the call. A prototype's `params` is authoritative
385
+ // when supplied; this liveness heuristic covers the rest.
386
+ const fallbackArgc = (bi: number): number => {
387
+ let n = 0;
388
+ while (n < ARG_REGS.length && ssa.hasReachingDef(ARG_REGS[n], bi)) {
389
+ n++;
390
+ }
391
+ return n;
392
+ };
393
+
394
+ // Frame slots (r1-relative offsets) that hold a TRANSPARENT save — a callee-saved register's
395
+ // entry value or the saved link register. A reload from one of these is dropped (the value is
396
+ // unchanged, so the in-register SSA value already carries it). Function-scoped so a save in the
397
+ // prologue block matches a restore in a different epilogue block. Any OTHER r1 access is a genuine
398
+ // local spill / address-taken stack object this frontend cannot model — those fail LOUD (below),
399
+ // never silently drop, because a dropped local spill is a silent miscompile.
400
+ const savedSlots = new Set<number>();
401
+
402
+ const fillBlock = (b: PpcBlock, bi: number) => {
403
+ const ops = irBlocks[bi].ops;
404
+ const succ = (j: number): Successor => ({ block: irBlocks[j], args: [] });
405
+
406
+ // A synthetic conditional-return block: just return the current return register.
407
+ if (b.synthReturn) {
408
+ const retOps = ssa.hasReachingDef(RET, bi) ? [readVar(RET, bi)] : [];
409
+ ops.push(mkOp('ret', { operands: retOps }));
410
+ return;
411
+ }
412
+
413
+ // `lastDef` is the value most recently written to a register in this instruction — used to
414
+ // wire a record-form op's implicit cr0 side effect (see the `rc` handling in `decode`).
415
+ let lastDef: Value | null = null;
416
+ // Reading r1 (the stack pointer) as a DATA operand means frame-pointer arithmetic or an
417
+ // address-taken local (`addi r3,r1,8` = `&local`) — not modellable without a stack abstraction,
418
+ // and fabricating a value for r1 silently miscompiles. Fail LOUD. (Frame bookkeeping never
419
+ // reaches here: stwu / addi r1 / mflr / mtlr / r1-relative spills are handled before `read`.)
420
+ const read = (r: string): Value => {
421
+ if (r === 'r1') {
422
+ throw new PpcUnsupportedError(
423
+ `cannot lift '${name}': stack pointer r1 used as data (address-taken local / frame arithmetic) — not supported`,
424
+ );
425
+ }
426
+ return readVar(r, bi);
427
+ };
428
+ // Ordinary memory must be based on a real register that is not the frame pointer. A non-register
429
+ // base is an SDA/global-relative access (`stw r0,0(0)` — the base field is a 0 placeholder the
430
+ // relocation fills at link); a base of r1 that reaches here is a sub-word frame slot. Both are
431
+ // unmodelled — fail LOUD rather than fabricate a bogus pointer parameter or field.
432
+ const assertOrdinaryMem = (mem: string) => {
433
+ const { base } = parseMem(mem);
434
+ if (!isReg(base)) {
435
+ throw new PpcUnsupportedError(
436
+ `cannot lift '${name}': non-register memory base ('${mem}') — SDA/global-relative access not supported`,
437
+ );
438
+ }
439
+ if (base === 'r1') {
440
+ throw new PpcUnsupportedError(
441
+ `cannot lift '${name}': sub-word stack-frame access ('${mem}') — local stack frames not supported`,
442
+ );
443
+ }
444
+ };
445
+ // A word store/load based on r1. Returns true if it is TRANSPARENT frame bookkeeping (skip):
446
+ // a save of a register with no reaching def (callee-saved entry value / saved lr), or a reload
447
+ // from a recorded save slot. A store of a LIVE (reaching-def) value is a real local spill →
448
+ // fail LOUD. A reload from an unrecorded slot is a genuine stack local → fail LOUD.
449
+ const frameStore = (srcReg: string, mem: string): boolean => {
450
+ const { base, off } = parseMem(mem);
451
+ if (base !== 'r1') {
452
+ return false;
453
+ }
454
+ if (!ssa.hasReachingDef(srcReg, bi)) {
455
+ savedSlots.add(off);
456
+ return true;
457
+ }
458
+ throw new PpcUnsupportedError(
459
+ `cannot lift '${name}': spill of a live value to the stack ('${srcReg},${mem}') — local stack frames not supported`,
460
+ );
461
+ };
462
+ const frameLoad = (mem: string): boolean => {
463
+ const { base, off } = parseMem(mem);
464
+ if (base !== 'r1') {
465
+ return false;
466
+ }
467
+ if (savedSlots.has(off)) {
468
+ return true;
469
+ }
470
+ throw new PpcUnsupportedError(
471
+ `cannot lift '${name}': reload of a stack local ('${mem}') — local stack frames not supported`,
472
+ );
473
+ };
474
+ const write = (r: string, v: Value) => {
475
+ writeVar(r, bi, v);
476
+ lastDef = v;
477
+ };
478
+ // Shared emitter kit (frontend/emit.ts) — the ISA-specific readers/guards stay above.
479
+ const kit = mkEmitKit(ops, write);
480
+ const constVal = kit.cnst;
481
+ const emit = kit.emit;
482
+ // TRUSTWORTHINESS GUARD: an unmodelled instruction must not silently drop its destination —
483
+ // emit an honest `opaque` instead: dead ⇒ DCE'd; live ⇒ assertResolved fails LOUD (see
484
+ // frontend/opaque.ts for the policy).
485
+ const emitOpaqueDest = (ins: Instr) => {
486
+ // storeClass: every PPC store mnemonic is st* — an unmodelled one (`stwbrx`, `sthbrx`, …)
487
+ // must throw, never skip (its first token is the SOURCE register).
488
+ const od = opaqueDest(ins.mnemonic, ins.ops, {
489
+ isReg,
490
+ storeClass: /^st/,
491
+ skipSafe: /^nop$/,
492
+ context: `${name} @0x${ins.addr.toString(16)}`,
493
+ });
494
+ if (!od) {
495
+ return;
496
+ } // skip-safe only (opaqueDest throws on any other no-destination instruction)
497
+ // carry the mnemonic so annotate mode can name the gap (`ASMLIFT_ERROR("unmodelled 'xori'")`)
498
+ emit('opaque', od.dst, od.srcRegs.map(read), { mnemonic: ins.mnemonic });
499
+ };
500
+ const emitBin = kit.bin;
501
+ const emitUn = kit.un;
502
+ // Unwritten temporaries for the complemented-logic decodes: the value feeds a following op,
503
+ // it is not itself a register destination.
504
+ const binTmp = (opc: Opcode, x: Value, y: Value): Value => kit.tmp(opc, [x, y]);
505
+ const notOf = (r: string): Value => {
506
+ const v = mkValue(T.unk(32));
507
+ ops.push(mkOp('not', { operands: [read(r)], results: [v] }));
508
+ return v;
509
+ };
510
+ const emitShImm = kit.shImm;
511
+ const emitLoad = (d: string, mem: string, width: number, signed: boolean) => {
512
+ assertOrdinaryMem(mem);
513
+ const { off, base } = parseMem(mem);
514
+ emit('load', d, [read(base)], { off, width, signed });
515
+ };
516
+ const emitStore = (srcReg: string, mem: string, width: number) => {
517
+ assertOrdinaryMem(mem);
518
+ const { off, base } = parseMem(mem);
519
+ ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
520
+ };
521
+ // Register+register INDEXED addressing (`lwzx rD,rA,rB` = *(rA+rB), `stwx rS,rA,rB` = *(rA+rB)=rS).
522
+ // This is how mwcc emits EVERY variable-index array access (scalar and struct) — with rB the scaled
523
+ // index. Decode to `add(rA,rB)` + a zero-offset load/store, the exact shape recognizeArrays
524
+ // consumes (and raise/struct-arrays.ts targets), so `a[i]`
525
+ // recovers from here.
526
+ const addrX = (rA: string, rB: string): Value => {
527
+ const addr = mkValue(T.unk(32));
528
+ ops.push(mkOp('add', { operands: [read(rA), read(rB)], results: [addr] }));
529
+ return addr;
530
+ };
531
+ const emitLoadX = (d: string, rA: string, rB: string, width: number, signed: boolean) => {
532
+ emit('load', d, [addrX(rA, rB)], { off: 0, width, signed });
533
+ };
534
+ const emitStoreX = (srcReg: string, rA: string, rB: string, width: number) => {
535
+ ops.push(mkOp('store', { operands: [addrX(rA, rB), read(srcReg)], attrs: { off: 0, width } }));
536
+ };
537
+
538
+ // cr-field compare state, so a following branch fuses. Keyed by cr name ("cr0" default).
539
+ const cmpDef = new Map<string, { lhs: Value; rhs: Value; signed: boolean }>();
540
+ // One operand-grammar normalizer for the four compare decodes: `cmpX rA,…` (cr0 implicit)
541
+ // or `cmpX crN,rA,…` → the cr field, the lhs register token, and the rhs token (register or
542
+ // immediate — the case reads/parses it).
543
+ const parseCmpOps = (cmpOps: string[]): { cr: string; lhsTok: string; rhsTok: string } => {
544
+ const hasCr = cmpOps[0]?.startsWith('cr') ?? false;
545
+ const args = hasCr ? cmpOps.slice(1) : cmpOps;
546
+ return { cr: hasCr ? cmpOps[0] : 'cr0', lhsTok: args[0], rhsTok: args[args.length - 1] };
547
+ };
548
+ const recordCmp = (cr: string, lhs: Value, rhs: Value, signed: boolean) => {
549
+ cmpDef.set(cr, { lhs, rhs, signed });
550
+ };
551
+
552
+ const decode = (ins: Instr) => {
553
+ const [d, s, t] = ins.ops;
554
+ // Record form: a trailing `.` (the Rc bit) means the op ALSO sets cr0 from a signed compare
555
+ // of its result against 0 (e.g. `andi.`, `addic.`). Decode the base op, then record that
556
+ // implicit cr0 compare so a following `beq`/`bne` fuses. `mnem` is the base mnemonic the
557
+ // switch dispatches on.
558
+ const rc = ins.mnemonic.length > 1 && ins.mnemonic.endsWith('.');
559
+ const mnem = rc ? ins.mnemonic.slice(0, -1) : ins.mnemonic;
560
+ lastDef = null;
561
+ switch (mnem) {
562
+ case 'nop':
563
+ break;
564
+ // --- call + frame/link-register bookkeeping ---
565
+ // `bl <sym>`: read the argument registers (r3..), produce the return value in r3. The
566
+ // callee symbol comes from the relocation (ins.sym); caller-saved clobbering is implicit
567
+ // (anything live across the call has already been moved to a callee-saved register).
568
+ case 'bl': {
569
+ const sym = ins.sym ?? 'func';
570
+ const argc = protoArity(prototypes[sym]) ?? fallbackArgc(bi);
571
+ const args: Value[] = [];
572
+ for (let k = 0; k < argc; k++) {
573
+ args.push(read(ARG_REGS[k]));
574
+ }
575
+ emit('call', RET, args, { target: sym });
576
+ break;
577
+ }
578
+ // Stack-frame + link-register bookkeeping. `stwu r1,-N(r1)` / `addi r1,r1,N` adjust the frame
579
+ // pointer; `mflr`/`mtlr` save/restore the return address. Transparent. Register saves/restores
580
+ // (individual r1 spills, and `stmw`/`lmw` of a callee-saved range) are transparent ONLY when
581
+ // they move an unchanged entry value — `frameStore`/`frameLoad` enforce that (a spill of a
582
+ // LIVE value fails loud); `stmw`/`lmw` record/consume the slot directly.
583
+ // The GENERAL form `stwu rS,D(rA)` (base ≠ r1) is a real store-with-BASE-UPDATE
584
+ // (`*(rA+D)=rS; rA+=D`) — neither effect is modelled here, so loud-fail rather than drop both.
585
+ case 'stwu':
586
+ if (parseMem(s).base === 'r1') {
587
+ break;
588
+ }
589
+ throw new PpcUnsupportedError(
590
+ `cannot lift '${name}': stwu with update on ${parseMem(s).base} (store-with-base-update) not modelled`,
591
+ );
592
+ case 'mflr':
593
+ case 'mtlr':
594
+ break;
595
+ // `mtctr rS` initialises the CTR loop counter — track it as the `ctr` pseudo-register so
596
+ // the `bdnz` back-branch reads/decrements it. (A recovered switch dispatch's mtctr block is
597
+ // elided; a leftover mtctr is inert if nothing reads `ctr`.) `mfctr` (CTR→GPR, rare) stays
598
+ // opaque via the default case.
599
+ case 'mtctr':
600
+ writeVar('ctr', bi, read(d));
601
+ break;
602
+ case 'stmw':
603
+ if (parseMem(s).base === 'r1') {
604
+ savedSlots.add(parseMem(s).off);
605
+ break;
606
+ }
607
+ assertOrdinaryMem(s);
608
+ emitOpaqueDest(ins);
609
+ break;
610
+ case 'lmw':
611
+ if (parseMem(s).base === 'r1') {
612
+ break;
613
+ }
614
+ assertOrdinaryMem(s);
615
+ emitOpaqueDest(ins);
616
+ break;
617
+ case 'mr':
618
+ write(d, read(s));
619
+ break; // move register (or rD,rS,rS)
620
+ case 'li':
621
+ write(d, constVal(parseImm(s)));
622
+ break; // load immediate (addi rD,0,imm)
623
+ case 'lis':
624
+ write(d, constVal((parseImm(s) << 16) >> 0));
625
+ break; // load immediate shifted
626
+ case 'add':
627
+ case 'addo':
628
+ emitBin('add', d, read(s), read(t));
629
+ break;
630
+ // `addi r1,r1,N` is frame teardown (skip); any other addi is a real add-immediate.
631
+ case 'addi':
632
+ case 'addic':
633
+ if (d === 'r1') {
634
+ break;
635
+ }
636
+ emitBin('add', d, read(s), constVal(parseImm(t)));
637
+ break;
638
+ case 'subf':
639
+ case 'subfc':
640
+ case 'subfo':
641
+ emitBin('sub', d, read(t), read(s));
642
+ break; // rD = rB - rA (reversed)
643
+ case 'subfic':
644
+ emitBin('sub', d, constVal(parseImm(t)), read(s));
645
+ break; // rD = imm - rA
646
+ case 'neg':
647
+ emitUn('neg', d, read(s));
648
+ break;
649
+ case 'mullw':
650
+ case 'mullwo':
651
+ emitBin('mul', d, read(s), read(t));
652
+ break;
653
+ case 'mulli':
654
+ emitBin('mul', d, read(s), constVal(parseImm(t)));
655
+ break;
656
+ // High word of the 32x32->64 product — the magic-number division idiom: mwcc lowers `x/C`
657
+ // to `mulhw(x,M)` plus shifts/corrections. Transient decode; raise/magicdiv.ts rewrites the
658
+ // DAG to `sdiv/udiv(x, const C)`. A `mulh`/`mulhu` that escapes recovery has no C spelling
659
+ // → loud-fail. Record forms `mulhw.`/`mulhwu.` arrive as the `.`-stripped `mnem`.
660
+ case 'mulhw':
661
+ emitBin('mulh', d, read(s), read(t));
662
+ break;
663
+ case 'mulhwu':
664
+ emitBin('mulhu', d, read(s), read(t));
665
+ break;
666
+ // Hardware divide: `divw rD,rA,rB` = rA/rB (signed), `divwu` = unsigned. Unlike MIPS there
667
+ // is NO hi/lo pair — the quotient lands directly in rD. The `o` (overflow-enable) suffix is
668
+ // a flag bit, semantically identical for the quotient. Classic PPC has no hardware remainder
669
+ // op (a `%` is `divw` + `mullw` + `subf`), so no smod/umod here.
670
+ case 'divw':
671
+ case 'divwo':
672
+ emitBin('sdiv', d, read(s), read(t));
673
+ break;
674
+ case 'divwu':
675
+ case 'divwuo':
676
+ emitBin('udiv', d, read(s), read(t));
677
+ break;
678
+ case 'and':
679
+ emitBin('and', d, read(s), read(t));
680
+ break;
681
+ case 'andi':
682
+ case 'andic':
683
+ emitBin('and', d, read(s), constVal(parseImm(t)));
684
+ break;
685
+ case 'or':
686
+ emitBin('or', d, read(s), read(t));
687
+ break;
688
+ case 'ori':
689
+ emitBin('or', d, read(s), constVal(parseImm(t)));
690
+ break;
691
+ case 'xor':
692
+ emitBin('xor', d, read(s), read(t));
693
+ break;
694
+ case 'xori':
695
+ emitBin('xor', d, read(s), constVal(parseImm(t)));
696
+ break;
697
+ case 'not':
698
+ emitUn('not', d, read(s));
699
+ break; // nor rD,rS,rS
700
+ case 'nor':
701
+ s === t ? emitUn('not', d, read(s)) : emitOpaqueDest(ins);
702
+ break; // true 2-reg nor: unmodelled
703
+ // Complemented-logic forms, decoded to the idiomatic C the compiler re-emits:
704
+ // andc rD,rA,rB = rA & ~rB orc rD,rA,rB = rA | ~rB
705
+ // eqv rD,rA,rB = ~(rA ^ rB) nand rD,rA,rB = ~(rA & rB)
706
+ // Appear in branchless clamps/masks (e.g. `x & ~(x>>31)` uses andc).
707
+ case 'andc':
708
+ emitBin('and', d, read(s), notOf(t));
709
+ break;
710
+ case 'orc':
711
+ emitBin('or', d, read(s), notOf(t));
712
+ break;
713
+ case 'eqv':
714
+ emitUn('not', d, binTmp('xor', read(s), read(t)));
715
+ break;
716
+ case 'nand':
717
+ emitUn('not', d, binTmp('and', read(s), read(t)));
718
+ break;
719
+ // Sign-extend byte/halfword to 32 bits: `extsb rD,rS` = (s32)(s8)rS, `extsh` = (s32)(s16)rS.
720
+ // `sext` carries the NARROW width; structure/backend spell it `(s8)e` / `(s16)e`.
721
+ case 'extsb':
722
+ emit('sext', d, [read(s)], { width: 8 });
723
+ break;
724
+ case 'extsh':
725
+ emit('sext', d, [read(s)], { width: 16 });
726
+ break;
727
+ case 'slw':
728
+ emitBin('shl', d, read(s), read(t));
729
+ break;
730
+ case 'srw':
731
+ emitBin('shr_u', d, read(s), read(t));
732
+ break;
733
+ case 'sraw':
734
+ emitBin('shr_s', d, read(s), read(t));
735
+ break;
736
+ case 'slwi':
737
+ emitShImm('shl', d, read(s), parseImm(t));
738
+ break;
739
+ case 'srwi':
740
+ emitShImm('shr_u', d, read(s), parseImm(t));
741
+ break;
742
+ case 'srawi':
743
+ emitShImm('shr_s', d, read(s), parseImm(t));
744
+ break;
745
+ case 'rotlw':
746
+ // rotate left by register — the C rotate idiom round-trips under mwcc (verified)
747
+ emitBin('rotl', d, read(s), read(t));
748
+ break;
749
+ case 'rotlwi':
750
+ emitShImm('rotl', d, read(s), parseImm(t));
751
+ break;
752
+ case 'cntlzw': {
753
+ // count leading zeros — transient (see ir/opcodes.ts): the CNTLZW_EQ0 pattern folds
754
+ // the ==0/`!` idiom; a bare survivor gaps loud at the structurer.
755
+ emit('clz', d, [read(s)]);
756
+ break;
757
+ }
758
+ // rotate-and-mask, rotate 0 only: `clrlwi rD,rS,n` = rS & (~0>>>n); `clrrwi` = rS & (~0<<n).
759
+ case 'clrlwi':
760
+ emitBin('and', d, read(s), constVal((0xffffffff >>> parseImm(t)) >>> 0));
761
+ break;
762
+ case 'clrrwi':
763
+ emitBin('and', d, read(s), constVal((0xffffffff << parseImm(t)) >>> 0));
764
+ break;
765
+ case 'rlwinm': {
766
+ const [sh, mb, me] = [parseImm(ins.ops[2]), parseImm(ins.ops[3]), parseImm(ins.ops[4])];
767
+ // `rlwinm rD,rS,SH,MB,ME` = rotl(rS,SH) & mask(MB,ME). Three cases we can lower exactly:
768
+ // • SH==0 — a pure masked AND.
769
+ // • right-shift EXTRACT `(x>>n)&m` — ME==31 and the field does not wrap (SH+MB>=32): the
770
+ // compiler's form for `(x>>n)&mask`, with n=32-SH and mask=mask(MB,31). Emit the shift
771
+ // then the AND — the faithful, idiomatic C that recompiles to this exact rlwinm.
772
+ // • anything else (a genuine rotate / bitfield insert) stays an opaque — loud, not dropped.
773
+ if (sh === 0) {
774
+ emitBin('and', d, read(s), constVal(rlwinmMask(mb, me)));
775
+ break;
776
+ }
777
+ if (me === 31 && sh + mb >= 32) {
778
+ const shifted = mkValue(T.unk(32));
779
+ ops.push(mkOp('shr_u', { operands: [read(s)], results: [shifted], attrs: { imm: (32 - sh) & 31 } }));
780
+ emitBin('and', d, shifted, constVal(rlwinmMask(mb, 31)));
781
+ break;
782
+ }
783
+ emitOpaqueDest(ins);
784
+ break;
785
+ }
786
+ // Word load/store: a transparent frame save/restore is skipped; a live-value spill or a
787
+ // stack local fails loud (frameStore/frameLoad); otherwise it is ordinary memory.
788
+ case 'lwz':
789
+ if (frameLoad(s)) {
790
+ break;
791
+ }
792
+ emitLoad(d, s, 4, true);
793
+ break;
794
+ case 'lha':
795
+ emitLoad(d, s, 2, true);
796
+ break;
797
+ case 'lhz':
798
+ emitLoad(d, s, 2, false);
799
+ break;
800
+ case 'lbz':
801
+ emitLoad(d, s, 1, false);
802
+ break;
803
+ case 'stw':
804
+ if (frameStore(d, s)) {
805
+ break;
806
+ }
807
+ emitStore(d, s, 4);
808
+ break;
809
+ case 'sth':
810
+ emitStore(d, s, 2);
811
+ break;
812
+ case 'stb':
813
+ emitStore(d, s, 1);
814
+ break;
815
+ // Register+register indexed forms (variable-index array access). Widths/signedness mirror the
816
+ // displacement loads/stores above; `lhax` is the sign-extending halfword (algebraic).
817
+ case 'lwzx':
818
+ emitLoadX(d, s, t, 4, true);
819
+ break;
820
+ case 'lhax':
821
+ emitLoadX(d, s, t, 2, true);
822
+ break;
823
+ case 'lhzx':
824
+ emitLoadX(d, s, t, 2, false);
825
+ break;
826
+ case 'lbzx':
827
+ emitLoadX(d, s, t, 1, false);
828
+ break;
829
+ case 'stwx':
830
+ emitStoreX(d, s, t, 4);
831
+ break;
832
+ case 'sthx':
833
+ emitStoreX(d, s, t, 2);
834
+ break;
835
+ case 'stbx':
836
+ emitStoreX(d, s, t, 1);
837
+ break;
838
+ // rhs is evaluated BEFORE lhs in each case: reads create Braun-SSA phis on demand, so read
839
+ // order affects block-param layout.
840
+ case 'cmpw': {
841
+ const c = parseCmpOps(ins.ops);
842
+ const rhs = read(c.rhsTok);
843
+ recordCmp(c.cr, read(c.lhsTok), rhs, true);
844
+ break;
845
+ }
846
+ case 'cmpwi': {
847
+ const c = parseCmpOps(ins.ops);
848
+ const rhs = constVal(parseImm(c.rhsTok));
849
+ recordCmp(c.cr, read(c.lhsTok), rhs, true);
850
+ break;
851
+ }
852
+ case 'cmplw': {
853
+ const c = parseCmpOps(ins.ops);
854
+ const rhs = read(c.rhsTok);
855
+ recordCmp(c.cr, read(c.lhsTok), rhs, false);
856
+ break;
857
+ }
858
+ case 'cmplwi': {
859
+ const c = parseCmpOps(ins.ops);
860
+ const rhs = constVal(parseImm(c.rhsTok));
861
+ recordCmp(c.cr, read(c.lhsTok), rhs, false);
862
+ break;
863
+ }
864
+ default:
865
+ emitOpaqueDest(ins);
866
+ break; // unmodelled: an honest opaque, never a silent drop
867
+ }
868
+ // A record-form op sets cr0 from a signed compare of its result against 0. Wire that so the
869
+ // next branch reading cr0 fuses (`andi. r0,r3,1; beq …` → `if ((a0 & 1) == 0)`).
870
+ if (rc && lastDef) {
871
+ cmpDef.set('cr0', { lhs: lastDef, rhs: constVal(0), signed: true });
872
+ }
873
+ };
874
+
875
+ for (const ins of b.body) {
876
+ decode(ins);
877
+ }
878
+
879
+ const br = b.branch;
880
+ // Recovered dense switch: the bounds block dispatches a `switch_br` over the scrutinee — N case
881
+ // blocks (dense 0..N-1) then the default (last successor). The `cmplwi`/`bgt` and the elided
882
+ // dispatch (`lis…lwzx;mtctr;bctr`) are subsumed.
883
+ const jt = br ? jts.get(br.addr) : undefined;
884
+ if (jt) {
885
+ pushSwitchBr(
886
+ ops,
887
+ readVar(jt.scrutReg, bi),
888
+ succIdx[bi].map((j) => succ(j)),
889
+ );
890
+ return;
891
+ }
892
+ // `bdnz L`: CTR ← CTR−1; branch to L while CTR ≠ 0. Modelled as an explicit decrement of the
893
+ // `ctr` pseudo-register plus a `cond_br` on `ctr ≠ 0`, which the structurer renders as a loop.
894
+ // succIdx is [taken=back-edge, fall=exit], so the taken predicate is `ctr ≠ 0`. Loud-fail
895
+ // without a reaching `mtctr`: no recoverable trip count means no sound loop.
896
+ if (br && br.mnemonic === 'bdnz') {
897
+ if (!ssa.hasReachingDef('ctr', bi)) {
898
+ throw new PpcUnsupportedError(
899
+ `cannot lift '${name}': 'bdnz' at 0x${br.addr.toString(16)} without a reaching 'mtctr' ` +
900
+ `(CTR loop count not recoverable)`,
901
+ );
902
+ }
903
+ // CTR is VOLATILE across calls (the ABI marks it caller-saved): a `bl` or re-seeding `mtctr`
904
+ // inside the loop body clobbers the hardware CTR, making the modelled down-count wrong —
905
+ // loud-fail rather than emit a confident-but-wrong count (hand-written asm can do this even
906
+ // though a conforming compiler won't). The loop body is the natural loop of the back-edge:
907
+ // header (bdnz's target) plus every block reaching the `bdnz` latch without passing back
908
+ // through the header. The preheader holding the count `mtctr` is NOT in the body — exempt.
909
+ const header = succIdx[bi][0];
910
+ const body = new Set<number>([header]);
911
+ for (const stack = [bi]; stack.length;) {
912
+ const n = stack.pop()!;
913
+ if (body.has(n) && n !== bi) {
914
+ continue;
915
+ }
916
+ body.add(n);
917
+ if (n !== header) {
918
+ for (const p of preds[n]) {
919
+ stack.push(p);
920
+ }
921
+ }
922
+ }
923
+ for (const n of body) {
924
+ const bad = blocks[n].body.find((i) => i.mnemonic === 'bl' || i.mnemonic === 'bctrl' || i.mnemonic === 'mtctr');
925
+ if (bad) {
926
+ throw new PpcUnsupportedError(
927
+ `cannot lift '${name}': CTR loop body contains '${bad.mnemonic}' at 0x${bad.addr.toString(16)} ` +
928
+ `which clobbers CTR (loop trip count not recoverable)`,
929
+ );
930
+ }
931
+ }
932
+ const dec = mkValue(T.unk(32));
933
+ ops.push(mkOp('sub', { operands: [readVar('ctr', bi), constVal(1)], results: [dec] }));
934
+ writeVar('ctr', bi, dec);
935
+ const cond = mkCmp(ops, 'icmp_ne', dec, constVal(0));
936
+ ops.push(mkOp('cond_br', { operands: [cond], successors: succIdx[bi].map((j) => succ(j)) }));
937
+ return;
938
+ }
939
+ if (br && (isCond(br) || isCondReturn(br))) {
940
+ const base = isCondReturn(br) ? condReturnBase(br)! : br.mnemonic;
941
+ // The branch names its cr field as the first operand when not cr0.
942
+ const crName = br.ops[0]?.startsWith('cr') ? br.ops[0] : 'cr0';
943
+ const cmp = cmpDef.get(crName);
944
+ // `cmpDef` is block-local; a compare split from its branch by a block boundary is a
945
+ // cross-block cr dependency this frontend does not model. Decline loud.
946
+ if (!cmp) {
947
+ throw new PpcUnsupportedError(
948
+ `cannot lift '${name}': conditional branch '${base}' has no reaching compare (${crName}) in its block`,
949
+ );
950
+ }
951
+ const table = !cmp.signed ? COND_UNSIGNED : COND_SIGNED;
952
+ const cond = mkCmp(ops, table[base], cmp.lhs, cmp.rhs);
953
+ ops.push(mkOp('cond_br', { operands: [cond], successors: succIdx[bi].map((j) => succ(j)) }));
954
+ return;
955
+ }
956
+ if (!br || isReturn(br)) {
957
+ if (!br && succIdx[bi].length) {
958
+ ops.push(mkOp('br', { successors: [succ(succIdx[bi][0])] }));
959
+ return;
960
+ }
961
+ const retOps = ssa.hasReachingDef(RET, bi) ? [readVar(RET, bi)] : [];
962
+ ops.push(mkOp('ret', { operands: retOps }));
963
+ return;
964
+ }
965
+ // unconditional branch
966
+ ops.push(mkOp('br', { successors: [succ(succIdx[bi][0])] }));
967
+ };
968
+
969
+ blocks.forEach((b, bi) => {
970
+ fillBlock(b, bi);
971
+ ssa.markFilled(bi);
972
+ });
973
+ ssa.finish();
974
+
975
+ // ABI-ordered entry parameters (r3, r4, …) — a callee-saved copy can read a later argument
976
+ // register first, so sort the true entry's params by argument-register index.
977
+ const entry = irBlocks[0];
978
+ // non-ABI live-in ranks FIRST (indexOf's -1) — deliberate MIPS/PPC tie-break; Thumb's is 99/last
979
+ abiSortEntryParams(entry, preds[0].length > 0, (v) => ARG_REGS.indexOf(paramReg.get(v) ?? ''));
980
+ return ssa.fn;
981
+ }
982
+
983
+ function mkCmp(ops: Op[], opc: Opcode, l: Value, r: Value): Value {
984
+ const v = mkValue(T.unk(32));
985
+ ops.push(mkOp(opc, { operands: [l, r], results: [v] }));
986
+ return v;
987
+ }
988
+
989
+ /** The PowerPC / CodeWarrior frontend, registered for the `ppc` target. */
990
+ export const ppcFrontend: Frontend = { id: 'ppc', inputFormat: 'objdump', lift };