@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,875 @@
1
+ // asmlift ISA frontend — MIPS-II (IDO 7.1, N64). Input is DISASSEMBLED text
2
+ // (`mips-linux-gnu-objdump -d --no-show-raw-insn`), since IDO emits no textual asm.
3
+ //
4
+ // The MIPS-specific concern is the DELAY SLOT: the instruction textually AFTER a control
5
+ // transfer executes BEFORE the transfer takes effect (on both the taken and fall-through
6
+ // paths). It is therefore lifted into the branching block, sequenced right before the branch —
7
+ // EXCEPT that a conditional branch reads its comparison operands as of the branch, so those
8
+ // SSA values are captured BEFORE the delay slot runs, then the delay slot executes, then the
9
+ // `cond_br` is emitted. A `jr ra` return's delay slot (which computes the return value) runs
10
+ // first, then the value is read. Branch-likely ops (`beql`/`bnel`…, which annul the delay slot
11
+ // when not taken) and calls (`jal`) are out of scope — both loud-fail (the control-transfer
12
+ // pre-scan in `lift`).
13
+ //
14
+ // Comparison model: MIPS fuses compare-and-branch, so a branch lowers directly to an icmp
15
+ // (the branch mnemonics are signed/equality only). `bltz/bgez/blez/bgtz` compare against zero; `beq/bne`
16
+ // compare two registers; `beqz/bnez` test a register (or fold a preceding `slt` — `slt at,rs,
17
+ // rt; beqz at,L` means "branch when !(rs<rt)", i.e. the negated compare). A materialised `slt`
18
+ // with no consuming branch (e.g. `return a>b` → `slt v0,a1,a0`) stays an icmp value. Unsigned
19
+ // compares (`sltu`/`sltiu`) lower to `icmp_ult`; recover types their operands u32 so the backend
20
+ // re-emits `sltu` (the operator is the same `<` — the signedness lives in the operand types).
21
+ import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
22
+ import type { Opcode } from '../ir/opcodes';
23
+ import { T } from '../ir/types';
24
+ import type { Prototypes } from '../proto';
25
+ import type { TargetDescription } from '../target';
26
+ import { type AsmData, readJumpTable, textRelocAt } from './asmdata';
27
+ import { type DisasmInstr, parseImm, parseMem, parseDisasm as parseSharedDisasm, sliceSymbol } from './disasm';
28
+ import { mkEmitKit, pushSwitchBr } from './emit';
29
+ import { FrontendUnsupportedError } from './errors';
30
+ import { assertInputFormat } from './format';
31
+ import type { Frontend } from './frontend';
32
+ import { opaqueDest } from './opaque';
33
+ import { abiSortEntryParams } from './ssa';
34
+ import { makeSsaBuilder } from './ssa';
35
+
36
+ type Instr = DisasmInstr;
37
+
38
+ // A control transfer with its comparison opcode for "branch taken" (against a second register
39
+ // or, for the *z forms, against zero).
40
+ const COND_Z: Record<string, Opcode> = {
41
+ beqz: 'icmp_eq',
42
+ bnez: 'icmp_ne',
43
+ blez: 'icmp_sle',
44
+ bgtz: 'icmp_sgt',
45
+ bltz: 'icmp_slt',
46
+ bgez: 'icmp_sge',
47
+ };
48
+ const COND_RR: Record<string, Opcode> = { beq: 'icmp_eq', bne: 'icmp_ne' };
49
+ // Negated icmp opcode (for the `slt …; beqz` "branch when false" fold).
50
+ const NEG_ICMP: Record<string, Opcode> = {
51
+ icmp_slt: 'icmp_sge',
52
+ icmp_sge: 'icmp_slt',
53
+ icmp_sgt: 'icmp_sle',
54
+ icmp_sle: 'icmp_sgt',
55
+ icmp_ult: 'icmp_uge',
56
+ icmp_uge: 'icmp_ult',
57
+ icmp_ugt: 'icmp_ule',
58
+ icmp_ule: 'icmp_ugt',
59
+ icmp_eq: 'icmp_ne',
60
+ icmp_ne: 'icmp_eq',
61
+ };
62
+
63
+ const isZero = (r: string) => r === 'zero' || r === '$0';
64
+ // The stack pointer (`$29`). A `sw/lw` through it is not a store/load through a data pointer — it
65
+ // is a spill/reload of a stack SLOT (an argument home slot or a local). See emitStore/emitLoad.
66
+ const isStackPtr = (r: string) => r === 'sp' || r === '$sp' || r === '$29';
67
+ // SSA-variable name for the stack slot at a constant `sp`-offset. Distinct namespace from the
68
+ // register names (which are alphabetic / `$N`), so it never collides with a real register var.
69
+ const stackSlot = (off: number) => `sp@${off}`;
70
+ // Sub-word memory mnemonics (widths 1 and 2). Used by the `spSlotSafe` guard in `lift`: a sub-word
71
+ // `sp`-relative access means the word stack-slot model is unsafe for that function.
72
+ const SUBWORD_MEM = new Set(['lb', 'lbu', 'lh', 'lhu', 'sb', 'sh']);
73
+ // A MIPS register operand — a named reg (`a0`,`v0`,`t7`,`at`,`sp`,`ra`,`zero`) or `$N`. Excludes
74
+ // immediates, hex, memory `off(base)`, and branch targets, so the unhandled-op guard only taints a
75
+ // genuine register destination.
76
+ const isMipsReg = (s: string | undefined): s is string =>
77
+ /^(\$\d+|[a-z][a-z0-9]*)$/i.test(s ?? '') && !/^0x/i.test(s ?? '');
78
+ // `jr ra`. A non-ra `jr` also lands here, but by block-fill time it is either a recovered
79
+ // switch dispatch (its block is pruned as unreachable) or has already loud-failed in `lift`.
80
+ const isReturn = (ins: Instr) => ins.mnemonic === 'jr';
81
+ const isUncond = (ins: Instr) => ins.mnemonic === 'b' || ins.mnemonic === 'j';
82
+ const isCond = (ins: Instr) => ins.mnemonic in COND_Z || ins.mnemonic in COND_RR;
83
+ const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins);
84
+
85
+ // Shared objdump scaffolding (frontend/disasm.ts): parseImm/parseMem/parseDisasm. MIPS needs no
86
+ // reloc or hint-suffix handling; register-scaled indices are materialised by IDO as explicit
87
+ // `sll`+`addu` before the access, so no `base+index` addressing form appears in parseMem input.
88
+ const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm);
89
+
90
+ interface MipsBlock {
91
+ startAddr: number;
92
+ body: Instr[]; // computation instructions (excludes the branch and its delay slot)
93
+ branch: Instr | null; // terminating control transfer, or null for a pure fall-through
94
+ delay: Instr | null; // delay-slot instruction (executes before the transfer)
95
+ }
96
+
97
+ // Mnemonics whose destination is `ops[0]` (a plain register write) — the subset the jump-table
98
+ // backward trace chases (index/base/address/load). Stores, branches, `jr`, `nop`, `div`/`mult`
99
+ // (hi/lo writers) are absent, so `destReg` returns null for them and the trace never mis-attributes.
100
+ const WRITES_D = new Set([
101
+ 'lui',
102
+ 'lw',
103
+ 'lh',
104
+ 'lhu',
105
+ 'lb',
106
+ 'lbu',
107
+ 'sll',
108
+ 'srl',
109
+ 'sra',
110
+ 'sllv',
111
+ 'srlv',
112
+ 'srav',
113
+ 'addu',
114
+ 'add',
115
+ 'addiu',
116
+ 'addi',
117
+ 'subu',
118
+ 'sub',
119
+ 'or',
120
+ 'ori',
121
+ 'and',
122
+ 'andi',
123
+ 'xor',
124
+ 'xori',
125
+ 'nor',
126
+ 'move',
127
+ 'li',
128
+ 'slt',
129
+ 'sltu',
130
+ 'slti',
131
+ 'sltiu',
132
+ 'mflo',
133
+ 'mfhi',
134
+ 'mul',
135
+ ]);
136
+ const destReg = (ins: Instr): string | null => (WRITES_D.has(ins.mnemonic) ? ins.ops[0] : null);
137
+
138
+ // A recovered dense-switch jump table (Regime B), keyed
139
+ // by the BOUNDS branch (`beqz tmp, DEF`) whose block emits the `switch_br`. Two idioms are
140
+ // handled: IDO gp-relative (`lw base,0(gp)` GOT16; `addu rV,rV,gp`
141
+ // after the load) and KMC absolute (`lui base` HI16). Delay slots are honoured — the `beqz` delay
142
+ // slot (which on IDO carries the DEFAULT return value `li v0,-1`) still executes before the
143
+ // `switch_br`.
144
+ interface MipsJT {
145
+ scrutReg: string;
146
+ caseAddrs: number[];
147
+ defaultAddr: number;
148
+ jrAddr: number;
149
+ }
150
+
151
+ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT> {
152
+ // Nearest def of `reg` strictly before index `p`, not crossing a control transfer (so the trace
153
+ // stays inside the dispatch's extended block). Returns the instruction index, or -1.
154
+ const defBefore = (reg: string, p: number): number => {
155
+ for (let j = p - 1; j >= 0; j--) {
156
+ if (isXfer(instrs[j])) {
157
+ return -1;
158
+ }
159
+ if (destReg(instrs[j]) === reg) {
160
+ return j;
161
+ }
162
+ }
163
+ return -1;
164
+ };
165
+ // Is `ins` a table-base materialisation (`lui rB,hi` HI16, or `lw rB,0(gp)` GOT16) with a `.text`
166
+ // relocation into a data section? Returns {sym, addend} locating the table, or null.
167
+ const tableBaseOf = (ins: Instr): { sym: string; addend: number } | null => {
168
+ const isLui = ins.mnemonic === 'lui';
169
+ const isGpLw = ins.mnemonic === 'lw' && ins.ops[1] !== undefined && parseMem(ins.ops[1]).base === 'gp';
170
+ if (!isLui && !isGpLw) {
171
+ return null;
172
+ }
173
+ const r = textRelocAt(ad, ins.addr);
174
+ if (!r) {
175
+ return null;
176
+ }
177
+ const sec = ad.symbols.get(r.sym)?.section ?? r.sym; // ".rodata" section symbol → ".rodata"
178
+ if (!/^\.(rodata|rdata|sdata2?|data)$/.test(sec)) {
179
+ return null;
180
+ }
181
+ return { sym: r.sym, addend: r.addend };
182
+ };
183
+
184
+ const out = new Map<number, MipsJT>();
185
+ for (let i = 0; i < instrs.length; i++) {
186
+ const jr = instrs[i];
187
+ if (jr.mnemonic !== 'jr' || jr.ops[0] === 'ra') {
188
+ continue;
189
+ }
190
+ let rV = jr.ops[0];
191
+ // Optional IDO `+gp`: `addu rV, rL, gp` before the load — the loaded value is the other operand.
192
+ let ldIdx = defBefore(rV, i);
193
+ if (ldIdx >= 0 && instrs[ldIdx].mnemonic === 'addu') {
194
+ const [a, b] = [instrs[ldIdx].ops[1], instrs[ldIdx].ops[2]];
195
+ if (a === 'gp' || b === 'gp') {
196
+ rV = a === 'gp' ? b : a;
197
+ ldIdx = defBefore(rV, ldIdx);
198
+ }
199
+ }
200
+ if (ldIdx < 0 || instrs[ldIdx].mnemonic !== 'lw') {
201
+ continue;
202
+ } // rV = *(rAddr)
203
+ const mem = parseMem(instrs[ldIdx].ops[1]);
204
+ if (mem.off !== 0) {
205
+ continue;
206
+ }
207
+ const aIdx = defBefore(mem.base, ldIdx); // rAddr = base + index
208
+ if (aIdx < 0 || instrs[aIdx].mnemonic !== 'addu') {
209
+ continue;
210
+ }
211
+ const [oa, ob] = [instrs[aIdx].ops[1], instrs[aIdx].ops[2]];
212
+ // One operand is the shifted index (`sll rIdx, scrut, 2` — identity guard), the other the table base.
213
+ let scrutReg: string | null = null,
214
+ table: { sym: string; addend: number } | null = null;
215
+ for (const [idxCand, baseCand] of [
216
+ [oa, ob],
217
+ [ob, oa],
218
+ ] as const) {
219
+ const si = defBefore(idxCand, aIdx);
220
+ const bi2 = defBefore(baseCand, aIdx);
221
+ if (si < 0 || bi2 < 0) {
222
+ continue;
223
+ }
224
+ const sll = instrs[si];
225
+ if (sll.mnemonic !== 'sll' || (sll.ops[2] !== '0x2' && sll.ops[2] !== '2')) {
226
+ continue;
227
+ } // index*4
228
+ const tb = tableBaseOf(instrs[bi2]);
229
+ if (!tb) {
230
+ continue;
231
+ }
232
+ scrutReg = sll.ops[1];
233
+ table = tb;
234
+ break;
235
+ }
236
+ if (!scrutReg || !table) {
237
+ continue;
238
+ }
239
+ // Bounds: `sltiu tmp, scrutReg, N ; beqz tmp, DEF` — the guard whose delay-slot-consuming branch
240
+ // block will emit the switch_br. Scan back for the sltiu on the scrutinee.
241
+ let bounds: { addr: number; n: number; def: number } | null = null;
242
+ for (let j = aIdx; j >= 0; j--) {
243
+ const c = instrs[j];
244
+ if (c.mnemonic === 'sltiu' && c.ops[1] === scrutReg) {
245
+ const tmp = c.ops[0];
246
+ // the beqz reading tmp (the block terminator); it may follow the sltiu directly.
247
+ for (let k = j + 1; k < instrs.length && k <= j + 3; k++) {
248
+ if (instrs[k].mnemonic === 'beqz' && instrs[k].ops[0] === tmp && instrs[k].target !== undefined) {
249
+ // `sltiu tmp, scrut, N` bounds `scrut < N` — the immediate IS the case count (unlike PPC's
250
+ // `cmplwi rS, N-1` which uses the max index). So N = the immediate, not immediate+1.
251
+ bounds = { addr: instrs[k].addr, n: parseImm(c.ops[2]), def: instrs[k].target! };
252
+ break;
253
+ }
254
+ }
255
+ break;
256
+ }
257
+ }
258
+ if (!bounds || bounds.n < 2) {
259
+ continue;
260
+ }
261
+ const caseAddrs = readJumpTable(ad, table.sym, table.addend, bounds.n);
262
+ if (!caseAddrs) {
263
+ continue;
264
+ }
265
+ out.set(bounds.addr, { scrutReg, caseAddrs, defaultAddr: bounds.def, jrAddr: jr.addr });
266
+ }
267
+ return out;
268
+ }
269
+
270
+ // Split the instruction stream into basic blocks. Delay slots are consumed into their
271
+ // branching block; leaders are the entry, every branch target, and each conditional branch's
272
+ // fall-through. Unreachable trailing blocks (padding `nop`s) are dropped.
273
+ function toBlocks(
274
+ instrs: Instr[],
275
+ jts: Map<number, MipsJT>,
276
+ ): { blocks: MipsBlock[]; succAddrs: Map<MipsBlock, number[]> } {
277
+ const consumed = new Set<number>();
278
+ instrs.forEach((ins, i) => {
279
+ if (isXfer(ins)) {
280
+ consumed.add(i + 1);
281
+ }
282
+ });
283
+
284
+ const leaders = new Set<number>(instrs.length ? [instrs[0].addr] : []);
285
+ instrs.forEach((ins, i) => {
286
+ if ((isCond(ins) || isUncond(ins)) && ins.target !== undefined) {
287
+ leaders.add(ins.target);
288
+ }
289
+ if (isCond(ins) && instrs[i + 2]) {
290
+ leaders.add(instrs[i + 2].addr);
291
+ } // fall-through
292
+ });
293
+ // A recovered jump table makes its case + default targets leaders (the bounds block's `switch_br`
294
+ // successors); the dispatch block then becomes unreachable and is pruned below.
295
+ for (const jt of jts.values()) {
296
+ for (const a of jt.caseAddrs) {
297
+ leaders.add(a);
298
+ }
299
+ leaders.add(jt.defaultAddr);
300
+ }
301
+
302
+ const blocks: MipsBlock[] = [];
303
+ let cur: MipsBlock | null = null;
304
+ for (let i = 0; i < instrs.length; i++) {
305
+ if (consumed.has(i)) {
306
+ continue;
307
+ } // delay slot: handled with its branch
308
+ const ins = instrs[i];
309
+ if (cur === null || leaders.has(ins.addr)) {
310
+ cur = { startAddr: ins.addr, body: [], branch: null, delay: null };
311
+ blocks.push(cur);
312
+ }
313
+ if (isXfer(ins)) {
314
+ cur.branch = ins;
315
+ cur.delay = instrs[i + 1] ?? null;
316
+ cur = null;
317
+ } else {
318
+ cur.body.push(ins);
319
+ }
320
+ }
321
+
322
+ // Successor addresses per block (before reachability pruning).
323
+ const succAddrs = new Map<MipsBlock, number[]>();
324
+ for (const b of blocks) {
325
+ const br = b.branch;
326
+ const jt = br ? jts.get(br.addr) : undefined;
327
+ if (jt) {
328
+ succAddrs.set(b, [...jt.caseAddrs, jt.defaultAddr]);
329
+ continue;
330
+ } // switch_br dispatcher
331
+ if (!br) {
332
+ const lastBody = b.body[b.body.length - 1];
333
+ succAddrs.set(b, lastBody ? [lastBody.addr + 4] : []); // fall into next block
334
+ } else if (isReturn(br)) {
335
+ succAddrs.set(b, []);
336
+ } else if (isUncond(br)) {
337
+ succAddrs.set(b, br.target !== undefined ? [br.target] : []);
338
+ } else {
339
+ const fall = (b.delay ? b.delay.addr : br.addr) + 4; // instruction after the delay slot
340
+ succAddrs.set(b, br.target !== undefined ? [br.target, fall] : [fall]);
341
+ }
342
+ }
343
+
344
+ // Keep only blocks reachable from the entry (drops trailing padding blocks).
345
+ const byAddr = new Map(blocks.map((b) => [b.startAddr, b]));
346
+ const reachable = new Set<MipsBlock>();
347
+ const queue: MipsBlock[] = blocks.length ? [blocks[0]] : [];
348
+ while (queue.length) {
349
+ const b = queue.pop()!;
350
+ if (reachable.has(b)) {
351
+ continue;
352
+ }
353
+ reachable.add(b);
354
+ for (const a of succAddrs.get(b)!) {
355
+ const nb = byAddr.get(a);
356
+ if (nb) {
357
+ queue.push(nb);
358
+ }
359
+ }
360
+ }
361
+ return { blocks: blocks.filter((b) => reachable.has(b)), succAddrs };
362
+ }
363
+
364
+ /** Lift disassembled MIPS text → an L1 Fn with block-argument SSA. `prototypes` reserved for
365
+ * call arity (calls are a later milestone). */
366
+ export function lift(
367
+ name: string,
368
+ asm: string,
369
+ target: TargetDescription,
370
+ _prototypes: Prototypes = {},
371
+ asmData?: AsmData,
372
+ ): Fn {
373
+ assertInputFormat('mips', 'objdump', asm);
374
+ const instrs = parseDisasm(sliceSymbol(asm, name)); // ONE function only — an absent symbol declines loud
375
+ if (instrs.length === 0) {
376
+ throw new FrontendUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
377
+ }
378
+ // Regime B: recover jump tables from the `jr`-dispatch idiom + the AsmData table. A recovered
379
+ // dispatch's `jr` is subsumed into a `switch_br` (emitted from its bounds block), so it is
380
+ // exempted from the loud-fail below; an UNrecovered `jr <non-ra>` still fails loud.
381
+ const jts = asmData ? recoverMipsJumpTables(instrs, asmData) : new Map<number, MipsJT>();
382
+ const recoveredJr = new Set([...jts.values()].map((j) => j.jrAddr));
383
+ // TRUSTWORTHINESS: fail LOUD on a control transfer this frontend cannot model — the `opaque`
384
+ // path cannot catch these (implicit or no register destination). `jal`/`jalr` clobber `v0`
385
+ // implicitly, so dropping a call fabricates `v0` from a stale value; a `jr` to anything but `ra`
386
+ // (jump table / computed goto) is not a plain return. Calls are a later milestone; until then
387
+ // they are a catchable "out of scope" signal, mirroring the PPC frontend.
388
+ for (const ins of instrs) {
389
+ if (ins.mnemonic === 'jal' || ins.mnemonic === 'jalr') {
390
+ throw new FrontendUnsupportedError(
391
+ `cannot lift '${name}': function call '${ins.mnemonic}' at 0x${ins.addr.toString(16)} — MIPS calls not yet modelled`,
392
+ );
393
+ }
394
+ if (ins.mnemonic === 'jr' && ins.ops[0] !== 'ra' && !recoveredJr.has(ins.addr)) {
395
+ throw new FrontendUnsupportedError(
396
+ `cannot lift '${name}': indirect jump 'jr ${ins.ops[0] ?? ''}' at 0x${ins.addr.toString(16)} — jump tables / tail calls not supported`,
397
+ );
398
+ }
399
+ // CATCH-ALL (mirrors the PPC denylist): an unmodelled control-transfer mnemonic would otherwise
400
+ // fall through to `emitOpaqueDest` and have its BRANCH silently dropped (no register dest for
401
+ // the opaque guard to catch). Bites the branch-LIKELY forms (`beql`/`bnel`/`b*zl`, which annul
402
+ // the delay slot when not taken) and coprocessor branches (`bc1t`/`bc1f`…). `break` is a trap,
403
+ // not a branch.
404
+ const isBranchish = (ins.mnemonic[0] === 'b' && ins.mnemonic !== 'break') || ins.mnemonic[0] === 'j';
405
+ if (isBranchish && !isXfer(ins) && ins.mnemonic !== 'jal' && ins.mnemonic !== 'jalr') {
406
+ throw new FrontendUnsupportedError(
407
+ `cannot lift '${name}': unmodelled control transfer '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
408
+ `— branch-likely / coprocessor branch not supported`,
409
+ );
410
+ }
411
+ }
412
+ const { blocks, succAddrs } = toBlocks(instrs, jts);
413
+ const idxOf = new Map(blocks.map((b, i) => [b.startAddr, i]));
414
+
415
+ // CFG predecessors by block index.
416
+ const preds: number[][] = blocks.map(() => []);
417
+ blocks.forEach((b, i) => {
418
+ for (const a of succAddrs.get(b)!) {
419
+ const j = idxOf.get(a);
420
+ if (j !== undefined) {
421
+ preds[j].push(i);
422
+ }
423
+ }
424
+ });
425
+
426
+ const ssa = makeSsaBuilder(name, blocks.length, preds);
427
+ const { irBlocks, readVar, writeVar, paramReg } = ssa;
428
+ const RET = target.returnReg;
429
+ const ARG_REGS = target.argRegs;
430
+
431
+ // SOUNDNESS GUARD. The word stack-slot model (emitLoad/emitStore) is safe ONLY when every
432
+ // sp-relative access in the function is word-width. If a SUB-WORD sp access aliases a word slot
433
+ // (`sw a0,4(sp)` then `lbu v0,4(sp)`), routing the word store to an SSA slot while the sub-word
434
+ // reload stays on the memory path DROPS the store and reads uninitialised memory — a silent
435
+ // miscompile that ALSO masks the struct-overlap loud-fail (raise/structs.ts). So if ANY sub-word
436
+ // sp access exists, disable slot-modelling for the whole function: everything sp-relative falls
437
+ // back to the memory path, which loud-fails on a genuine overlap instead of miscompiling.
438
+ const spSlotSafe = !instrs.some(
439
+ (ins) =>
440
+ SUBWORD_MEM.has(ins.mnemonic) && ins.ops.length > 0 && isStackPtr(parseMem(ins.ops[ins.ops.length - 1]).base),
441
+ );
442
+
443
+ // Hardware divide state (capabilities.hwDivide). MIPS `div`/`divu rs,rt` set the hi/lo pair
444
+ // implicitly; a later `mflo`/`mfhi` reads the quotient/remainder. FUNCTION-scoped: GCC schedules
445
+ // the `mflo` into a SEPARATE block after the trap-check branch (`bnez rt; break 7`), so a
446
+ // block-local record would see `null` at the cross-block `mflo` and emit an opaque `?`. The div's
447
+ // operand Values are SSA-global, so consuming them in a later block is sound. A `mult`/`multu`
448
+ // overwrites the same hi/lo pair, so it clears this (below) — whichever divide/multiply ran most
449
+ // recently owns the next `mf*`, the true hardware semantics.
450
+ let divState: { rs: Value; rt: Value; signed: boolean } | null = null;
451
+
452
+ const fillBlock = (b: MipsBlock, bi: number) => {
453
+ const ops = irBlocks[bi].ops;
454
+ const read = (r: string): Value => {
455
+ if (isZero(r)) {
456
+ return constVal(0);
457
+ }
458
+ // Reading `sp` as a DATA operand means frame-pointer arithmetic or an address-taken local
459
+ // (`addiu a0,sp,8` = `&local`) — not modellable without a stack abstraction. Fabricating a
460
+ // value for `sp` invents a PHANTOM leading parameter that shifts every real argument — a
461
+ // silent miscompile. Fail LOUD instead, mirroring the PPC frontend's r1. Frame setup/teardown
462
+ // (`addiu sp,sp,±N`) and word spill/reload slots are handled before `read`.
463
+ if (isStackPtr(r)) {
464
+ throw new FrontendUnsupportedError(
465
+ `cannot lift '${name}': stack pointer used as data (address-taken local / frame arithmetic) — local stack frames not supported`,
466
+ );
467
+ }
468
+ // `gp` writes (the PIC prologue) are skipped as transparent; a `gp` READ that survives here is a
469
+ // PIC/small-data global access (`lw x,off(gp)`) this frontend does not model — the recovered
470
+ // jump-table dispatch's own `lw …,0(gp)` is elided, so reaching this is a genuine decline, not a
471
+ // switch. Fail LOUD rather than fabricate a phantom `gp` parameter (mirrors the sp guard above).
472
+ if (r === 'gp') {
473
+ throw new FrontendUnsupportedError(
474
+ `cannot lift '${name}': gp used as data (PIC / small-data global access) — not supported`,
475
+ );
476
+ }
477
+ return readVar(r, bi);
478
+ };
479
+ const write = (r: string, v: Value) => {
480
+ if (!isZero(r)) {
481
+ writeVar(r, bi, v);
482
+ }
483
+ };
484
+ // Shared emitter kit (frontend/emit.ts) — the ISA-specific readers/guards stay above.
485
+ const kit = mkEmitKit(ops, write);
486
+ const constVal = kit.cnst;
487
+ const emitBin = kit.bin;
488
+ // `divState` (the hardware-divide hi/lo record) is FUNCTION-scoped — see its declaration above
489
+ // the per-block loop. It materializes the typed `sdiv`/`udiv` (lo) or `smod`/`umod` (hi) at the
490
+ // `mf*` that consumes it, so an unused half never emits a dead op.
491
+ // Hardware multiply state: `mult`/`multu rs,rt` (2-operand — BOTH are sources, no register
492
+ // dest) set the hi/lo pair; a following `mflo`/`mfhi` reads the product low/high word. Distinct
493
+ // from the 3-operand MIPS32 pseudo `mul rd,rs,rt` (writes rd directly). Block-local
494
+ // DELIBERATELY (unlike divState): a cross-block `mult`/`mflo` pair has no observed inhabitant,
495
+ // and the miss degrades to a LOUD opaque, never silence.
496
+ let mulState: { rs: Value; rt: Value; signed: boolean } | null = null;
497
+ // `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
498
+ const cmpDef = new Map<string, { value: Value; opcode: string; lhs: Value; rhs: Value }>();
499
+ const emitCmp = (opc: Opcode, d: string, lhs: Value, rhs: Value) => {
500
+ const v = mkValue(T.unk(32));
501
+ ops.push(mkOp(opc, { operands: [lhs, rhs], results: [v] }));
502
+ write(d, v);
503
+ cmpDef.set(d, { value: v, opcode: opc, lhs, rhs });
504
+ };
505
+
506
+ const decode = (ins: Instr) => {
507
+ const [d, s, t] = ins.ops;
508
+ // The GOT/small-data base register `gp` is set up by IDO's PIC prologue (`lui gp; addiu gp,gp,lo;
509
+ // addu gp,gp,t9`, an `_gp_disp` HI16/LO16 pair). It is a RELOCATION base, never program data — and
510
+ // reading `t9` for the `addu` would fabricate a phantom leading parameter (like the sp/r1 guards).
511
+ // Its only real use, the GOT table-base `lw ...,0(gp)` in a jump-table dispatch, is subsumed by the
512
+ // recovered `switch_br` (that block is elided). So a write to `gp` is transparent: skip it.
513
+ if (destReg(ins) === 'gp') {
514
+ return;
515
+ }
516
+ switch (ins.mnemonic) {
517
+ case 'nop':
518
+ break;
519
+ case 'move':
520
+ write(d, read(s));
521
+ break; // pseudo: addu/or rD,rS,zero
522
+ case 'li':
523
+ write(d, constVal(parseImm(s)));
524
+ break; // pseudo: load immediate
525
+ // `lui rD, hi` loads the 16-bit immediate into the UPPER half (mirrors PPC `lis`). Alone it
526
+ // is the high half of a 32-bit literal; the following `ori`/`addiu` supplies the low half
527
+ // and raise/const.ts folds the const/const pair into one 32-bit const — the form that
528
+ // recompiles to this exact `lui;ori`.
529
+ case 'lui':
530
+ write(d, constVal((parseImm(s) << 16) >> 0));
531
+ break;
532
+ case 'addiu':
533
+ case 'addi':
534
+ // `addiu sp,sp,±N` is frame setup/teardown — transparent to dataflow (the stack-slot model
535
+ // keys slots by literal sp-offset, so the frame base never needs a value). Skip it,
536
+ // mirroring PPC's `addi r1`. Any OTHER read of sp falls through to `read`, which loud-fails.
537
+ if (isStackPtr(d)) {
538
+ break;
539
+ }
540
+ if (isZero(s)) {
541
+ write(d, constVal(parseImm(t)));
542
+ break;
543
+ } // li idiom
544
+ emitBin('add', d, read(s), constVal(parseImm(t)));
545
+ break;
546
+ case 'addu':
547
+ case 'add':
548
+ emitBin('add', d, read(s), read(t));
549
+ break;
550
+ case 'subu':
551
+ case 'sub':
552
+ emitBin('sub', d, read(s), read(t));
553
+ break;
554
+ case 'mul':
555
+ emitBin('mul', d, read(s), read(t));
556
+ break; // MIPS32 3-operand: rd = rs*rt
557
+ case 'mult':
558
+ case 'multu': // 2-operand: hi/lo = d*s (both sources)
559
+ mulState = { rs: read(d), rt: read(s), signed: ins.mnemonic === 'mult' };
560
+ divState = null;
561
+ break; // overwrites hi/lo
562
+ // Hardware divide: `div zero,rs,rt` (raw two-operand form; `zero` rd = no pseudo mflo).
563
+ // Record the operands; the following mflo/mfhi picks quotient vs remainder. The 3-operand
564
+ // pseudo (`div rd,rs,rt`, rd≠zero) additionally writes the quotient to rd. GATED on
565
+ // `capabilities.hwDivide`: a `div` on a target that declares no hardware divider is a
566
+ // genuine anomaly, so it degrades to a loud `opaque` rather than being silently modelled.
567
+ case 'div':
568
+ case 'divu': {
569
+ if (!target.capabilities.hwDivide) {
570
+ divState = null;
571
+ emitOpaqueDest(ins);
572
+ break;
573
+ }
574
+ const signed = ins.mnemonic === 'div';
575
+ divState = { rs: read(s), rt: read(t), signed };
576
+ if (!isZero(d)) {
577
+ emitBin(signed ? 'sdiv' : 'udiv', d, divState.rs, divState.rt);
578
+ }
579
+ break;
580
+ }
581
+ case 'mflo': // quotient, or product low word
582
+ if (divState) {
583
+ emitBin(divState.signed ? 'sdiv' : 'udiv', d, divState.rs, divState.rt);
584
+ } else if (mulState) {
585
+ emitBin('mul', d, mulState.rs, mulState.rt);
586
+ } else {
587
+ emitOpaqueDest(ins);
588
+ }
589
+ break;
590
+ case 'mfhi': // remainder, or product HIGH word (magic-div)
591
+ if (divState) {
592
+ emitBin(divState.signed ? 'smod' : 'umod', d, divState.rs, divState.rt);
593
+ } else if (mulState) {
594
+ emitBin(mulState.signed ? 'mulh' : 'mulhu', d, mulState.rs, mulState.rt);
595
+ } // → magicdiv
596
+ else {
597
+ emitOpaqueDest(ins);
598
+ }
599
+ break;
600
+ case 'and':
601
+ emitBin('and', d, read(s), read(t));
602
+ break;
603
+ case 'andi':
604
+ emitBin('and', d, read(s), constVal(parseImm(t)));
605
+ break;
606
+ case 'or':
607
+ isZero(t) ? write(d, read(s)) : emitBin('or', d, read(s), read(t));
608
+ break;
609
+ case 'ori':
610
+ emitBin('or', d, read(s), constVal(parseImm(t)));
611
+ break;
612
+ case 'xor':
613
+ emitBin('xor', d, read(s), read(t));
614
+ break;
615
+ case 'xori':
616
+ emitBin('xor', d, read(s), constVal(parseImm(t)));
617
+ break;
618
+ // `nor rD, x, zero` / `nor rD, zero, x` = ~x (GCC emits the zero in EITHER operand — e.g.
619
+ // its branchless `x<0?0:x` uses `nor v0,zero,a0`; IDO tends to put zero second).
620
+ case 'nor': {
621
+ if (isZero(t)) {
622
+ emitUn('not', d, read(s));
623
+ break;
624
+ }
625
+ if (isZero(s)) {
626
+ emitUn('not', d, read(t));
627
+ break;
628
+ }
629
+ // true 2-source nor: rD = ~(rS | rT), two ops.
630
+ const orRes = mkValue(T.unk(32));
631
+ ops.push(mkOp('or', { operands: [read(s), read(t)], results: [orRes] }));
632
+ emitUn('not', d, orRes);
633
+ break;
634
+ }
635
+ case 'sll':
636
+ emitShImm('shl', d, read(s), t);
637
+ break;
638
+ case 'srl':
639
+ emitShImm('shr_u', d, read(s), t);
640
+ break;
641
+ case 'sra':
642
+ emitShImm('shr_s', d, read(s), t);
643
+ break;
644
+ // Variable shift `<op>v rD, rT, rS` = rD = rT <shift> rS: VALUE is rT (=s), AMOUNT is rS
645
+ // (=t) — value-then-amount, unlike `slt rD,rS,rT`.
646
+ case 'sllv':
647
+ emitBin('shl', d, read(s), read(t));
648
+ break; // rD = rT << rS
649
+ case 'srlv':
650
+ emitBin('shr_u', d, read(s), read(t));
651
+ break;
652
+ case 'srav':
653
+ emitBin('shr_s', d, read(s), read(t));
654
+ break;
655
+ case 'negu':
656
+ case 'neg':
657
+ emitUn('neg', d, read(s));
658
+ break;
659
+ case 'not':
660
+ emitUn('not', d, read(s));
661
+ break; // pseudo (nor rD,rS,zero)
662
+ case 'slt':
663
+ emitCmp('icmp_slt', d, read(s), read(t));
664
+ break;
665
+ case 'slti':
666
+ emitCmp('icmp_slt', d, read(s), constVal(parseImm(t)));
667
+ break;
668
+ case 'sltu':
669
+ emitCmp('icmp_ult', d, read(s), read(t));
670
+ break;
671
+ case 'sltiu':
672
+ emitCmp('icmp_ult', d, read(s), constVal(parseImm(t)));
673
+ break;
674
+ // typed memory: `off(base)` addressing. Width/signedness come from the mnemonic; the
675
+ // base is typed a pointer-to-element during recovery, mirroring the Thumb frontend.
676
+ case 'lw':
677
+ emitLoad(d, s, 4, true);
678
+ break;
679
+ case 'lh':
680
+ emitLoad(d, s, 2, true);
681
+ break;
682
+ case 'lhu':
683
+ emitLoad(d, s, 2, false);
684
+ break;
685
+ case 'lb':
686
+ emitLoad(d, s, 1, true);
687
+ break;
688
+ case 'lbu':
689
+ emitLoad(d, s, 1, false);
690
+ break;
691
+ case 'sw':
692
+ emitStore(d, s, 4);
693
+ break; // d = source reg, s = off(base)
694
+ case 'sh':
695
+ emitStore(d, s, 2);
696
+ break;
697
+ case 'sb':
698
+ emitStore(d, s, 1);
699
+ break;
700
+ default:
701
+ emitOpaqueDest(ins);
702
+ break; // unmodelled: an honest opaque, never a silent drop
703
+ }
704
+ };
705
+ // TRUSTWORTHINESS GUARD (mirrors the PPC frontend): an unmodelled instruction must not silently
706
+ // drop its destination register — emit an honest `opaque`: dead ⇒ it vanishes; live ⇒
707
+ // assertResolved fails LOUD (see frontend/opaque.ts for the policy).
708
+ const emitOpaqueDest = (ins: Instr) => {
709
+ // storeClass: unmodelled MIPS stores — incl. the unaligned pair swl/swr and the FPU stores,
710
+ // whose FIRST token is a register (a SOURCE, not a dest) that would otherwise fabricate an
711
+ // opaque write to it while dropping the real memory write.
712
+ // skipSafe `break`: the compiler-emitted divide-by-zero guard trap inside the hw-divide
713
+ // idiom (KMC GCC `break 0x7`); recompiling the recovered `/` regenerates it, so it is
714
+ // transparent by the same modelling as the divide itself (byte-exactness proven by the
715
+ // hw-divide suites). Any other no-destination effect (syscall, cache, sync) throws.
716
+ const od = opaqueDest(ins.mnemonic, ins.ops, {
717
+ isReg: isMipsReg,
718
+ isZero,
719
+ storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)$/,
720
+ skipSafe: /^(nop|ssnop|break)$/,
721
+ context: `${name} @0x${ins.addr.toString(16)}`,
722
+ });
723
+ if (!od) {
724
+ return;
725
+ } // $zero write or skip-safe (opaqueDest threw otherwise)
726
+ const res = mkValue(T.unk(32));
727
+ // carry the mnemonic so annotate mode can name the gap (`ASMLIFT_ERROR("unmodelled 'lwl'")`)
728
+ ops.push(mkOp('opaque', { operands: od.srcRegs.map(read), results: [res], attrs: { mnemonic: ins.mnemonic } }));
729
+ write(od.dst, res);
730
+ };
731
+ const emitUn = kit.un;
732
+ const emitShImm = (opc: Opcode, d: string, x: Value, sa: string) => kit.shImm(opc, d, x, parseImm(sa));
733
+ const emitLoad = (d: string, mem: string, width: number, signed: boolean) => {
734
+ const { off, base } = parseMem(mem);
735
+ // A word reload from a stack slot is transparent to dataflow — the SAME value spilled — so
736
+ // route it through the slot SSA variable, not a `load` through `sp` (which would make `sp` a
737
+ // spurious pointer parameter). Compiler spills/reloads are always word-width; sub-word `sp`
738
+ // access is not spill output, so it stays on the memory path.
739
+ if (spSlotSafe && isStackPtr(base) && width === 4) {
740
+ // SOUNDNESS GUARD (mirrors PPC frameLoad): only route through the slot SSA var if that slot was
741
+ // actually STORED (has a reaching def). A word `lw` from an sp offset that was NEVER spilled is an
742
+ // incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local — neither modelled.
743
+ // Without this, readVar would FABRICATE a phantom entry parameter for the slot, silently emitting a
744
+ // function of wrong arity that returns the wrong argument. Loud-fail instead of miscompiling.
745
+ if (!ssa.hasReachingDef(stackSlot(off), bi)) {
746
+ throw new FrontendUnsupportedError(
747
+ `cannot lift '${name}': load from stack slot sp@${off} that was never stored ` +
748
+ `(stack-passed argument beyond the 4 register args, or an address-taken/uninitialised local) — not modelled`,
749
+ );
750
+ }
751
+ write(d, readVar(stackSlot(off), bi));
752
+ return;
753
+ }
754
+ const res = mkValue(T.unk(32));
755
+ ops.push(mkOp('load', { operands: [read(base)], results: [res], attrs: { off, width, signed } }));
756
+ write(d, res);
757
+ };
758
+ const emitStore = (srcReg: string, mem: string, width: number) => {
759
+ const { off, base } = parseMem(mem);
760
+ // A word spill to a stack slot (an argument home slot `sw a0,0(sp)`, or a local): record the
761
+ // slot's value in SSA, do NOT emit a `store` through `sp`. A never-reloaded spill (the ABI
762
+ // home-slot store) then has no uses and simply drops. See isStackPtr / spSlotSafe.
763
+ if (spSlotSafe && isStackPtr(base) && width === 4) {
764
+ writeVar(stackSlot(off), bi, read(srcReg));
765
+ return;
766
+ }
767
+ ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
768
+ };
769
+
770
+ for (const ins of b.body) {
771
+ decode(ins);
772
+ }
773
+
774
+ // Terminator. For a conditional branch, capture the comparison operands from the register
775
+ // state BEFORE the delay slot runs, then run the delay slot, then emit the cond_br.
776
+ const br = b.branch;
777
+ // A branch target must land on a block boundary. If it does not (an out-of-range / mid-instruction
778
+ // target — a tail branch out of the function, or flow this frontend hasn't recovered), fail LOUD
779
+ // and catchably here rather than building a successor to `undefined` and surfacing as the opaque
780
+ // internal `verify` error "successor of 'cond_br' is not a block of this fn".
781
+ const succ = (addr: number): Successor => {
782
+ const j = idxOf.get(addr);
783
+ if (j === undefined) {
784
+ throw new FrontendUnsupportedError(
785
+ `cannot lift '${name}': branch to 0x${addr.toString(16)} is not a block boundary ` +
786
+ `(out-of-range / mid-instruction target — tail branch or unrecovered control flow)`,
787
+ );
788
+ }
789
+ return { block: irBlocks[j], args: [] };
790
+ };
791
+
792
+ // Recovered dense switch: run the `beqz` delay slot first — on IDO it computes the DEFAULT
793
+ // return value (`li v0,-1`, read by the default block); on KMC it is the now-dead index shift —
794
+ // then dispatch a `switch_br` over the scrutinee (N case blocks, dense 0..N-1, + default).
795
+ const jt = br ? jts.get(br.addr) : undefined;
796
+ if (jt) {
797
+ if (b.delay) {
798
+ decode(b.delay);
799
+ }
800
+ pushSwitchBr(ops, readVar(jt.scrutReg, bi), [...jt.caseAddrs, jt.defaultAddr].map(succ));
801
+ return;
802
+ }
803
+
804
+ if (br && isCond(br)) {
805
+ const cond = condValue(br, ops, read, constVal, cmpDef);
806
+ if (b.delay) {
807
+ decode(b.delay);
808
+ }
809
+ const fall = (b.delay ? b.delay.addr : br.addr) + 4;
810
+ ops.push(mkOp('cond_br', { operands: [cond], successors: [succ(br.target!), succ(fall)] }));
811
+ return;
812
+ }
813
+ if (b.delay) {
814
+ decode(b.delay);
815
+ } // unconditional / return: delay slot just executes first
816
+
817
+ if (!br || isReturn(br)) {
818
+ const retOps = ssa.hasReachingDef(RET, bi) ? [readVar(RET, bi)] : [];
819
+ if (!br) {
820
+ ops.push(mkOp('br', { successors: [succ(succAddrs.get(b)![0])] }));
821
+ } // fall-through
822
+ else {
823
+ ops.push(mkOp('ret', { operands: retOps }));
824
+ }
825
+ return;
826
+ }
827
+ // unconditional branch
828
+ ops.push(mkOp('br', { successors: [succ(br.target!)] }));
829
+ };
830
+
831
+ blocks.forEach((b, bi) => {
832
+ fillBlock(b, bi);
833
+ ssa.markFilled(bi);
834
+ });
835
+ ssa.finish();
836
+
837
+ // ABI-ordered entry parameters (a0, a1, …) — a callee-saved copy can read a later argument
838
+ // register first. Only the true entry (no predecessors) is sorted; a loop header's phis are
839
+ // index-aligned with predecessor args and must not be reordered.
840
+ const entry = irBlocks[0];
841
+ // non-ABI live-in ranks FIRST (indexOf's -1) — deliberate MIPS/PPC tie-break; Thumb's is 99/last
842
+ abiSortEntryParams(entry, preds[0].length > 0, (v) => ARG_REGS.indexOf(paramReg.get(v) ?? ''));
843
+ return ssa.fn;
844
+ }
845
+
846
+ // Build the "branch taken" condition value for a conditional branch (emitting the icmp op).
847
+ function condValue(
848
+ br: Instr,
849
+ ops: Op[],
850
+ read: (r: string) => Value,
851
+ constVal: (n: number) => Value,
852
+ cmpDef: Map<string, { value: Value; opcode: string; lhs: Value; rhs: Value }>,
853
+ ): Value {
854
+ const mk = (opc: Opcode, l: Value, r: Value): Value => {
855
+ const v = mkValue(T.unk(32));
856
+ ops.push(mkOp(opc, { operands: [l, r], results: [v] }));
857
+ return v;
858
+ };
859
+ if (br.mnemonic in COND_RR) {
860
+ return mk(COND_RR[br.mnemonic], read(br.ops[0]), read(br.ops[1]));
861
+ }
862
+ // *z forms compare a register against zero — except beqz/bnez may fold a preceding `slt`.
863
+ const rs = br.ops[0];
864
+ const folded = cmpDef.get(rs);
865
+ if (folded && br.mnemonic === 'bnez') {
866
+ return folded.value;
867
+ } // branch when slt is true
868
+ if (folded && br.mnemonic === 'beqz') {
869
+ return mk(NEG_ICMP[folded.opcode], folded.lhs, folded.rhs);
870
+ } // …when false
871
+ return mk(COND_Z[br.mnemonic], read(rs), constVal(0));
872
+ }
873
+
874
+ /** The MIPS-II / IDO frontend, registered for the `mips` target. */
875
+ export const mipsFrontend: Frontend = { id: 'mips', inputFormat: 'objdump', lift };