@asmlift/core 0.9.0 → 0.11.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 (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -9,7 +9,9 @@
9
9
  // `cond_br` is emitted. A `jr ra` return's delay slot (which computes the return value) runs
10
10
  // first, then the value is read. A call's (`jal`) delay slot runs before the call, so it can set
11
11
  // an argument up. A branch-likely (`beql`/`bnel`…) annuls its slot when not taken, so the slot is
12
- // a block of its own on the taken edge (`toBlocks`).
12
+ // a block of its own on the taken edge (`toBlocks`) — unless it is a copy of the word before the
13
+ // target, when the branch is retargeted there with an empty slot (`normaliseBranchLikely`, which
14
+ // does the same for a `b`/`j` whose slot is that copy).
13
15
  //
14
16
  // Comparison model: MIPS fuses compare-and-branch, so a branch lowers directly to an icmp
15
17
  // (the branch mnemonics are signed/equality only). `bltz/bgez/blez/bgtz` compare against zero; `beq/bne`
@@ -18,15 +20,22 @@
18
20
  // with no consuming branch (e.g. `return a>b` → `slt v0,a1,a0`) stays an icmp value. Unsigned
19
21
  // compares (`sltu`/`sltiu`) lower to `icmp_ult`; recover types their operands u32 so the backend
20
22
  // 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 { NEGATED_ICMP, type Opcode } from '../ir/opcodes';
23
+ import { ownArgumentWordsOn } from '../aggregate';
24
+ import { type Block, Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
25
+ import { NEGATED_FCMP, NEGATED_ICMP, type Opcode } from '../ir/opcodes';
23
26
  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 { callDeclarations } from './call-plan';
27
+ import { type Prototypes, declaresVoidReturn, ownReturnWidth, wordsOf } from '../proto';
28
+ import type { ContextGlobals } from '../proto-context';
29
+ import type { SymbolMap } from '../symbols';
30
+ import { O32_REGISTER_ALIASES, type TargetDescription } from '../target';
31
+ import { type AsmData, readJumpTable, readU32, textRelocAt } from './asmdata';
32
+ import { callDeclarations, foldByValueArgs } from './call-plan';
33
+ import { asksCalleeBody } from './callee-body';
34
+ import { pinDeadReads } from './dead-reads';
35
+ import { pinDeviceAccesses } from './device-pins';
28
36
  import {
29
37
  type DisasmInstr,
38
+ type DisasmReloc,
30
39
  parseImm,
31
40
  parseMem,
32
41
  parseDisasm as parseSharedDisasm,
@@ -36,13 +45,65 @@ import {
36
45
  import { mkEmitKit, pushSwitchBr } from './emit';
37
46
  import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
38
47
  import { assertInputFormat } from './format';
39
- import { fpPrecision, fpuArgSlots, refuseBothPrecisions, writesFloatReturn } from './fpu';
40
- import type { Frontend } from './frontend';
48
+ import {
49
+ type ReturnHomeRule,
50
+ carriesFloatHome,
51
+ floatHomeAt,
52
+ fpPrecision,
53
+ fpuArgSlots,
54
+ refuseBothPrecisions,
55
+ refuseFpWordCrossings,
56
+ returnHomeRule,
57
+ settleReturnHome,
58
+ writesFloatReturn,
59
+ } from './fpu';
60
+ import {
61
+ FRAME_OBJECT_AUDIT,
62
+ type FrameObjectRelift,
63
+ type FrameRange,
64
+ areaAboveOutgoing,
65
+ keptInRelift,
66
+ landsOnArea,
67
+ liftWithRelift,
68
+ nameMergedSretObjects,
69
+ refuseSretOutsideFrame,
70
+ } from './frame-objects';
71
+ import { type Frontend, type LiftOptions, ownDeclarationOf } from './frontend';
41
72
  import { makeHighHalves } from './high-half';
73
+ import {
74
+ type HoistedDispatch,
75
+ type IdiomLeft,
76
+ type StaleIdiomRead,
77
+ type TableKey,
78
+ agreedTable,
79
+ caseEntries,
80
+ enterCaseTarget,
81
+ entersBetween,
82
+ idiomLeaves,
83
+ makeTableBases,
84
+ parkedIn,
85
+ refuseReachableDispatch,
86
+ refuseStaleIdiomReads,
87
+ scaledIndexIn,
88
+ } from './jump-table';
42
89
  import { opaqueDest } from './opaque';
43
90
  import { MIPS_FP_REG, isSplatMips, mipsEvenFpKey, parseSplatMips } from './splat';
44
- import { abiSortEntryParams, mintArgSlotHoles, stackSlotKey } from './ssa';
45
- import { makeSsaBuilder } from './ssa';
91
+ import {
92
+ type ArgSlots,
93
+ abiSortEntryParams,
94
+ incomingStackArgKey,
95
+ mintArgSlotHoles,
96
+ stackArgSlots,
97
+ stackSlotKey,
98
+ } from './ssa';
99
+ import {
100
+ makeSsaBuilder,
101
+ recordAsmLayout,
102
+ refuseSavedRegisterLeaks,
103
+ registerLiveIns,
104
+ unifyRegisterSpellings,
105
+ } from './ssa';
106
+ import { bodyBlockAgrees } from './stackargs';
46
107
 
47
108
  type Instr = DisasmInstr;
48
109
 
@@ -70,18 +131,22 @@ const LIKELY_BASE: Record<string, string> = {
70
131
  bgtzl: 'bgtz',
71
132
  bltzl: 'bltz',
72
133
  bgezl: 'bgez',
134
+ bc1tl: 'bc1t',
135
+ bc1fl: 'bc1f',
73
136
  };
74
- // A coprocessor-1 branch tests an FP condition code (`fcc`) that `c.cond.s/d` sets. Neither the
75
- // code nor the compare is modelled, so these refuse whether or not they are also likely.
137
+ // A coprocessor-1 branch tests the FP condition code (`fcc`) that a `c.cond.s/d` sets: `bc1t` is
138
+ // taken when it is set, `bc1f` when it is clear. MIPS I-III have one code and the branch names it
139
+ // by its target alone; a MIPS IV `bc1t $fccN, L` names one of eight, which is not modelled.
76
140
  const isFpCondBranch = (m: string) => m.startsWith('bc1');
141
+ const FP_COND_BRANCH: Readonly<Record<string, boolean>> = { bc1t: true, bc1f: false };
77
142
 
78
143
  /** The FPU arithmetic this frontend lifts, onto the float opcodes (`mov.s`/`mov.d` are copies and
79
144
  * have none), with the precision each computes in: `.s` single, `.d` double, one per function
80
- * (`frontend/fpu.ts` `fpPrecision`). Every other FPU instruction — the conversions, the moves to and
81
- * from the integer file and memory, the compares — keeps the register-file refusal `opaqueDest`
82
- * gives it, and so does one of these naming an odd register (a double's second half, or a single
83
- * this frontend does not key) or a target with no `fpu` homes. `docs/floating-point.md` says what
84
- * the next layer is. */
145
+ * (`frontend/fpu.ts` `fpPrecision`). With the conversions and compares below, and `mfc1`/`mtc1`,
146
+ * that is the decode; every other FPU instruction — the moves to and from memory, the conversions
147
+ * between the precisions — keeps the register-file refusal `opaqueDest` gives it, and so does one
148
+ * of these naming an odd register (a double's second half, or a single this frontend does not
149
+ * key) or a target with no `fpu` homes. `docs/floating-point.md` says what the next layer is. */
85
150
  const FP_ARITH: Readonly<Record<string, { op: Opcode | 'copy'; width: 32 | 64 }>> = {
86
151
  'add.s': { op: 'fadd', width: 32 },
87
152
  'sub.s': { op: 'fsub', width: 32 },
@@ -96,7 +161,41 @@ const FP_ARITH: Readonly<Record<string, { op: Opcode | 'copy'; width: 32 | 64 }>
96
161
  'neg.d': { op: 'fneg', width: 64 },
97
162
  'mov.d': { op: 'copy', width: 64 },
98
163
  };
164
+ /** The moves between the FPU's file and the integer file's VALUES, onto the conversions: `trunc.w`
165
+ * truncates a float to the integer word it leaves in an FPU register (`mfc1` moves it out), and
166
+ * `cvt.s.w`/`cvt.d.w` convert the integer word `mtc1` moved in. Each is one precision, like the
167
+ * arithmetic. The `cvt.w` and `cfc1`/`ctc1` rounding-mode sequence IDO converts an UNSIGNED word
168
+ * with, `cvt.s.d`/`cvt.d.s` between the precisions, and the other roundings stay refused. */
169
+ const FP_CONVERT: Readonly<Record<string, { op: Opcode; width: 32 | 64 }>> = {
170
+ 'trunc.w.s': { op: 'fptosi', width: 32 },
171
+ 'trunc.w.d': { op: 'fptosi', width: 64 },
172
+ 'cvt.s.w': { op: 'sitofp', width: 32 },
173
+ 'cvt.d.w': { op: 'sitofp', width: 64 },
174
+ };
175
+ /** The compares that set the condition code, onto the float compares. Only the ORDERED three are
176
+ * here, because only they are what IDO, KMC and GCC 2.7.2 emit for C's relations (compiled, every
177
+ * relation in both precisions): `a > b` is `c.lt.s b, a`, `a != b` is `c.eq.s` with the branch
178
+ * sense flipped, and a NaN answers false to all three. */
179
+ const FP_COMPARE: Readonly<Record<string, { op: Opcode; width: 32 | 64 }>> = {
180
+ 'c.eq.s': { op: 'fcmp_oeq', width: 32 },
181
+ 'c.lt.s': { op: 'fcmp_olt', width: 32 },
182
+ 'c.le.s': { op: 'fcmp_ole', width: 32 },
183
+ 'c.eq.d': { op: 'fcmp_oeq', width: 64 },
184
+ 'c.lt.d': { op: 'fcmp_olt', width: 64 },
185
+ 'c.le.d': { op: 'fcmp_ole', width: 64 },
186
+ };
99
187
  const FP_ARITH_MNEMONICS: ReadonlySet<string> = new Set(Object.keys(FP_ARITH));
188
+ /** The decoded instructions that leave a FLOAT in an FPU register, which is what puts one in the
189
+ * float return home: `trunc.w` leaves an integer word there, and `mtc1` the integer it moved. */
190
+ const FP_FLOAT_WRITERS: ReadonlySet<string> = new Set([
191
+ ...FP_ARITH_MNEMONICS,
192
+ ...Object.keys(FP_CONVERT).filter((m) => FP_CONVERT[m].op === 'sitofp'),
193
+ ]);
194
+ /** The decoded instructions that carry a float's information into the INTEGER file: a `mfc1` of a
195
+ * truncated word, and a condition-code branch. Where one is present a value in `v0` may be the
196
+ * function's result, and a write to `$f0` a temporary, so neither decides the return home
197
+ * (frontend/fpu.ts `returnHomeRule`). */
198
+ const crossesToIntegers = (m: string) => m === 'mfc1' || (LIKELY_BASE[m] ?? m) in FP_COND_BRANCH;
100
199
  /** A key the SSA builder holds for the FPU's file: `mipsEvenFpKey`'s canonical spelling. */
101
200
  const isFpKey = (k: string) => /^\$f\d+$/.test(k);
102
201
 
@@ -108,6 +207,9 @@ const isStackPtr = (r: string) => r === 'sp' || r === '$sp' || r === '$29';
108
207
  // a0..a3, and argument k is the word at `16 + 4(k − 4)` above the `sp` the call is made at.
109
208
  // Compiled on ido7.1, gcc2.7.2kmc and gcc2.7.2: `g(a, b, 1, 2, 3)` stores the 3 to `16(sp)`.
110
209
  const STACK_ARGS_AT = 16;
210
+ /** Every load whose last operand is `off(base)`: integer, unaligned and FPU
211
+ * (`outgoing-area-probes.test.ts` classifies every memory mnemonic of the committed probes against it). */
212
+ export const MIPS_FRAME_LOAD = /^(lb|lbu|lh|lhu|lw|lwl|lwr|lwu|ll|ld|ldl|ldr|lwc1|ldc1)$/;
111
213
  // SSA-variable name for the stack slot at a constant `sp`-offset. Distinct namespace from the
112
214
  // register names (which are alphabetic / `$N`), so it never collides with a real register var.
113
215
  const stackSlot = stackSlotKey; // shared spelling: frontend/ssa.ts
@@ -120,10 +222,11 @@ const SUBWORD_MEM = new Set(['lb', 'lbu', 'lh', 'lhu', 'sb', 'sh']);
120
222
  const isMipsReg = (s: string | undefined): s is string =>
121
223
  /^(\$\d+|[a-z][a-z0-9]*)$/i.test(s ?? '') && !/^0x/i.test(s ?? '');
122
224
  // `jr ra`. A non-ra `jr` also lands here, but by block-fill time it is either a recovered
123
- // switch dispatch (its block is pruned as unreachable) or has already loud-failed in `lift`.
225
+ // switch dispatch (its block is pruned as unreachable, which `refuseReachableDispatch` checks) or
226
+ // has already loud-failed in `lift`.
124
227
  const isReturn = (ins: Instr) => ins.mnemonic === 'jr';
125
228
  const isUncond = (ins: Instr) => ins.mnemonic === 'b' || ins.mnemonic === 'j';
126
- const isCond = (ins: Instr) => ins.mnemonic in COND_Z || ins.mnemonic in COND_RR;
229
+ const isCond = (ins: Instr) => ins.mnemonic in COND_Z || ins.mnemonic in COND_RR || ins.mnemonic in FP_COND_BRANCH;
127
230
  const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins);
128
231
  // ANY instruction that redirects control, modelled or not: on MIPS that is the whole `b*` space
129
232
  // (minus the `break` trap) plus `j*`. `isXfer` is the subset this frontend models; this is the
@@ -177,12 +280,17 @@ const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { zer
177
280
  // THE ADDEND IS NOT ON THE RECORD. MIPS objects are REL: the relocation has no addend field and the
178
281
  // address's low bits live in the two instruction immediates, so a record that DOES carry an addend
179
282
  // is not the format assumed here, and refuses.
180
- function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData): void {
283
+ function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData, freed: ReadonlySet<number>): void {
181
284
  const byAddr = new Map(instrs.map((ins) => [ins.addr, ins]));
182
285
  for (const r of ad.relocs) {
183
286
  if (r.section !== '.text' || (r.type !== 'R_MIPS_HI16' && r.type !== 'R_MIPS_LO16' && r.type !== 'R_MIPS_26')) {
184
287
  continue;
185
288
  }
289
+ // A slot `normaliseBranchLikely` emptied holds a `nop` now; its old word's relocation
290
+ // describes the copy at the target, which carries its own.
291
+ if (freed.has(r.offset)) {
292
+ continue;
293
+ }
186
294
  const ins = byAddr.get(r.offset);
187
295
  if (!ins) {
188
296
  // A `.text` record describes the whole section, so most belong to the object's OTHER
@@ -214,8 +322,31 @@ function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData): void {
214
322
  `0x${ins.addr.toString(16)}): '${ins.reloc.type} ${ins.reloc.sym}' and '${r.type} ${r.sym}'`,
215
323
  );
216
324
  }
217
- ins.reloc = { type: r.type, sym: r.sym, addend: 0 };
325
+ ins.reloc = {
326
+ type: r.type,
327
+ sym: ins.mnemonic === 'jal' ? (textCallee(r.sym, r.offset, ad) ?? r.sym) : r.sym,
328
+ addend: 0,
329
+ };
330
+ }
331
+ }
332
+
333
+ /** THE FUNCTION A `jal` RELOCATED AGAINST `.text` ITSELF ENTERS, by name. gcc 2.7.2 and KMC relocate a
334
+ * call to a function of the same object against the section, not the function's symbol (`jal 0
335
+ * <rnd>` over `R_MIPS_26 .text`), so the record names no callee. MIPS objects are REL: the jump's
336
+ * 26-bit field holds the addend, the target's offset in the section shifted right by two. The
337
+ * symbol table names the function at that offset. Undefined, and the call keeps its section
338
+ * callee, which refuses: where `.text`'s bytes or the word are not in the dump, or where no one
339
+ * name, or more than one, starts there. Only a `jal`'s: gcc 2.7.2 and KMC relocate a `j` against
340
+ * `.text` too, and a leaf loop whose head is the entry jumps to its own function's first word, so
341
+ * naming that `j` would read the loop's back edge as a tail call. */
342
+ function textCallee(sym: string, offset: number, ad: AsmData): string | undefined {
343
+ const text = ad.sections.get('.text');
344
+ if (sym !== '.text' || text === undefined || offset + 4 > text.length) {
345
+ return undefined;
218
346
  }
347
+ const at = (readU32(text, offset, ad.bigEndian) & 0x03ffffff) * 4;
348
+ const names = [...ad.symbols].filter(([n, s]) => s.section === '.text' && s.value === at && /^[A-Za-z_]\w*$/.test(n));
349
+ return names.length === 1 ? names[0][0] : undefined;
219
350
  }
220
351
 
221
352
  interface MipsBlock {
@@ -230,61 +361,165 @@ interface MipsBlock {
230
361
  delayAnnulled: boolean;
231
362
  }
232
363
 
233
- // Mnemonics whose destination is `ops[0]` (a plain register write) — the subset the jump-table
234
- // backward trace chases (index/base/address/load). Stores, branches, `jr`, `nop`, `div`/`mult`
235
- // (hi/lo writers) are absent, so `destReg` returns null for them and the trace never mis-attributes.
236
- const WRITES_D = new Set([
237
- 'lui',
238
- 'lw',
239
- 'lh',
240
- 'lhu',
241
- 'lb',
242
- 'lbu',
243
- 'sll',
244
- 'srl',
245
- 'sra',
246
- 'sllv',
247
- 'srlv',
248
- 'srav',
249
- 'addu',
250
- 'add',
251
- 'addiu',
252
- 'addi',
253
- 'subu',
254
- 'sub',
255
- 'or',
256
- 'ori',
257
- 'and',
258
- 'andi',
259
- 'xor',
260
- 'xori',
261
- 'nor',
262
- 'move',
263
- 'li',
264
- 'slt',
265
- 'sltu',
266
- 'slti',
267
- 'sltiu',
268
- 'mflo',
269
- 'mfhi',
270
- 'mul',
271
- ]);
272
- const destReg = (ins: Instr): string | null => (WRITES_D.has(ins.mnemonic) ? ins.ops[0] : null);
364
+ // Mnemonics whose FIRST operand is a source, never a destination: the stores, the branches that
365
+ // compare it, and the moves and products that read it.
366
+ const SOURCE_FIRST = /^(s[bhwd]|sw[lr]|sc|s[wd]c1|b\w*|j|jr|mult|multu|div|divu|mt\w+|ctc1|t\w+|cache|sync)$/;
367
+
368
+ /** THE INTEGER REGISTER AN INSTRUCTION WRITES, or null — the one classifier every reader here asks
369
+ * (the jump-table trace, the `gp` skip, the frame-depth walk, the save scan, the 64-bit return
370
+ * check), so no two of them can disagree on a mnemonic.
371
+ *
372
+ * A DENY-LIST: every instruction writes its first operand unless it is one of `SOURCE_FIRST`'s.
373
+ * Every reader asks "is this the write of r?", and an unknown write answered no would be stepped
374
+ * past as though r still held what came before, so the default is the answer that stops them. A
375
+ * call writes `ra`, whatever it names first (`jal f`, and `jalr rs` with the implicit `ra`). */
376
+ const writtenReg = (ins: Instr): string | null => {
377
+ if (ins.mnemonic === 'jalr') {
378
+ return ins.ops.length > 1 ? ins.ops[0] : 'ra';
379
+ }
380
+ if (isCallInstr(ins)) {
381
+ return 'ra';
382
+ }
383
+ return SOURCE_FIRST.test(ins.mnemonic) || !isMipsReg(ins.ops[0]) ? null : ins.ops[0];
384
+ };
385
+
386
+ /** A call: every instruction that links into `ra` — `jal`, `jalr`, and the linking branches. */
387
+ const isCallInstr = (ins: Instr) => /^(jal|jalr|bal|bgezall?|bltzall?)$/.test(ins.mnemonic);
388
+
389
+ /** The register a register COPY reads — `move rD,rS`, or `addu`/`add`/`or` with `zero` beside it,
390
+ * which is how Splat and IDO spell the same move — or undefined for any other instruction. */
391
+ const copiedReg = (ins: Instr): string | undefined => {
392
+ const [, s, t] = ins.ops;
393
+ if (ins.mnemonic === 'move') {
394
+ return s;
395
+ }
396
+ if (!/^(addu|add|or)$/.test(ins.mnemonic) || s === undefined || t === undefined) {
397
+ return undefined;
398
+ }
399
+ return isZero(t) ? s : isZero(s) ? t : undefined;
400
+ };
273
401
 
274
402
  // A recovered dense-switch jump table (Regime B), keyed
275
- // by the BOUNDS branch (`beqz tmp, DEF`) whose block emits the `switch_br`. Two idioms are
276
- // handled: IDO gp-relative (`lw base,0(gp)` GOT16; `addu rV,rV,gp`
277
- // after the load) and KMC absolute (`lui base` HI16). Delay slots are honoured — the `beqz` delay
278
- // slot (which on IDO carries the DEFAULT return value `li v0,-1`) still executes before the
279
- // `switch_br`.
403
+ // by the BOUNDS branch (`beqz tmp, DEF`) whose block emits the `switch_br`. The table base is one of:
404
+ // - absolute, formed in the dispatch's block: `lui at,%hi(T) ; addu at,at,rI ; lw rV,%lo(T)(at)`,
405
+ // the table's `%lo` folded into the load's displacement (IDO, KMC and gcc 2.7.2 non-PIC, whenever
406
+ // the block forms the base itself);
407
+ // - absolute, hoisted: `lui rH,%hi(T) ; addiu rB,rH,%lo(T)` in a loop's preheader, the load at
408
+ // displacement 0 (KMC and gcc 2.7.2; `HoistedDispatch`);
409
+ // - PIC, IDO `-KPIC`: `lw at,%got(T)(gp)` GOT16 (its displacement the `%hi`), the load carrying
410
+ // the `%lo` LO16, and `addu rV,rV,gp` after it (the entries are gp-relative) (`inBlockTable`).
411
+ // The table must sit in a read-only section (`tableBaseOf`).
412
+ // Delay slots are honoured — the `beqz` delay slot (which on IDO carries the DEFAULT return value
413
+ // `li v0,-1`) still executes before the `switch_br`.
280
414
  interface MipsJT {
281
415
  scrutReg: string;
282
416
  caseAddrs: number[];
283
417
  defaultAddr: number;
284
418
  jrAddr: number;
419
+ /** The start of the bounds block when its `sltiu` runs on the edges into it, not inside it
420
+ * (`mipsBounds`); null otherwise. */
421
+ entry: number | null;
422
+ /** The base register and table of a dispatch whose block does not form its base (KMC and gcc
423
+ * 2.7.2 hoist the `lui`/`addiu` pair into a loop's preheader): `lift` proves the base reaches it
424
+ * (frontend/jump-table.ts). Null when the pruned block forms the base itself. */
425
+ hoisted: HoistedDispatch | null;
426
+ /** What the pruned dispatch leaves in each register it writes (`idiomLeaves`). */
427
+ leaves: ReadonlyMap<string, IdiomLeft>;
285
428
  }
286
429
 
287
- function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT> {
430
+ /** A MIPS jump table's bounds guard for the dispatch whose `jr` is at `i`: `sltiu tmp,scrutReg,N`
431
+ * then `beqz tmp,DEF`, where the beqz is the nearest transfer before the `jr`. `at` is the beqz's
432
+ * index.
433
+ *
434
+ * The `sltiu` is the last write of `tmp` on EVERY path into the beqz, so it is found per entry of
435
+ * the beqz's block, and every entry must run the same one: inside the block, after nothing that
436
+ * writes `tmp` or the scrutinee (IDO schedules `move s0,a1` between the two); or, where the block
437
+ * holds none, as the last word each edge into it runs. IDO rotates a `while` loop so that both of
438
+ * its entries do: the bounds `sltiu` sits in the delay slot of the loop-entry `beqz` (which runs it
439
+ * on the fall-through) and in the LIKELY slot of the latch `bnezl` (which runs it on the taken edge
440
+ * alone). An edge whose last word is not that `sltiu` — the not-taken edge of a likely branch, a
441
+ * call's return, the function's entry — leaves `tmp` unknown, and declines. `likely` holds the
442
+ * branch-likely addresses `normaliseBranchLikely` rewrote. */
443
+ function mipsBounds(
444
+ instrs: Instr[],
445
+ i: number,
446
+ scrutReg: string,
447
+ likely: ReadonlySet<number>,
448
+ ): { at: number; addr: number; n: number; def: number; entry: number | null } | null {
449
+ let k = i - 1;
450
+ while (k >= 0 && !isControlTransfer(instrs[k])) {
451
+ k--;
452
+ }
453
+ const beqz = instrs[k];
454
+ if (!beqz || beqz.mnemonic !== 'beqz' || beqz.target === undefined) {
455
+ return null;
456
+ }
457
+ const tmp = beqz.ops[0];
458
+ const isBound = (c: Instr | undefined) => c?.mnemonic === 'sltiu' && c.ops[0] === tmp && c.ops[1] === scrutReg;
459
+ const bound = (c: Instr, entry: number | null) => {
460
+ // `sltiu tmp, scrut, N` bounds `scrut < N`: the immediate IS the case count (PPC's `cmplwi`
461
+ // states the max index).
462
+ const n = parseImm(c.ops[2]);
463
+ return n < 2 ? null : { at: k, addr: beqz.addr, n, def: beqz.target!, entry };
464
+ };
465
+ const isTarget = (addr: number) => instrs.some((x) => x.target === addr);
466
+ // The beqz's block: back to a branch target, or to the word after a transfer's delay slot.
467
+ let s = k;
468
+ while (
469
+ s > 0 &&
470
+ !isTarget(instrs[s].addr) &&
471
+ !isControlTransfer(instrs[s - 1]) &&
472
+ !(s > 1 && isControlTransfer(instrs[s - 2]))
473
+ ) {
474
+ s--;
475
+ }
476
+ for (let j = k - 1; j >= s; j--) {
477
+ const c = instrs[j];
478
+ if (isBound(c)) {
479
+ return bound(c, null);
480
+ }
481
+ if (writtenReg(c) === tmp || writtenReg(c) === scrutReg) {
482
+ return null;
483
+ }
484
+ }
485
+ // No `sltiu` in the block: the last word each edge into it runs.
486
+ const last: Array<Instr | undefined> = [];
487
+ const p = instrs[s - 1];
488
+ const t = instrs[s - 2];
489
+ if (p === undefined || isControlTransfer(p)) {
490
+ return null;
491
+ }
492
+ if (t !== undefined && isControlTransfer(t)) {
493
+ // `p` is a delay slot: the fall-through edge runs it after a branch that is neither likely (its
494
+ // not-taken edge annuls the slot) nor a call (which returns here from the callee); a jump has no
495
+ // fall-through edge at all.
496
+ if (likely.has(t.addr) || isCallInstr(t)) {
497
+ return null;
498
+ }
499
+ if (!isUncond(t) && !isReturn(t)) {
500
+ last.push(p);
501
+ }
502
+ } else {
503
+ last.push(p);
504
+ }
505
+ // A taken edge runs the branch's delay slot, a likely one's included.
506
+ instrs.forEach((x, o) => {
507
+ if (x.target === instrs[s].addr) {
508
+ last.push(isCallInstr(x) ? undefined : instrs[o + 1]);
509
+ }
510
+ });
511
+ const first = last[0];
512
+ if (
513
+ first === undefined ||
514
+ !isBound(first) ||
515
+ !last.every((c) => c !== undefined && c.ops.join() === first.ops.join() && c.mnemonic === 'sltiu')
516
+ ) {
517
+ return null;
518
+ }
519
+ return bound(first, instrs[s].addr);
520
+ }
521
+
522
+ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData, likely: ReadonlySet<number>): Map<number, MipsJT> {
288
523
  // Nearest def of `reg` strictly before index `p`, not crossing a control transfer (so the trace
289
524
  // stays inside the dispatch's extended block). Returns the instruction index, or -1.
290
525
  const defBefore = (reg: string, p: number): number => {
@@ -292,14 +527,18 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
292
527
  if (isXfer(instrs[j])) {
293
528
  return -1;
294
529
  }
295
- if (destReg(instrs[j]) === reg) {
530
+ if (writtenReg(instrs[j]) === reg) {
296
531
  return j;
297
532
  }
298
533
  }
299
534
  return -1;
300
535
  };
301
- // Is `ins` a table-base materialisation (`lui rB,hi` HI16, or `lw rB,0(gp)` GOT16) with a `.text`
302
- // relocation into a data section? Returns {sym, addend} locating the table, or null.
536
+ // Is `ins` a table-base materialisation (`lui rB,hi` HI16, or `lw rB,hi(gp)` GOT16) with a `.text`
537
+ // relocation into a READ-ONLY data section? Returns {sym, addend} locating the table, or null.
538
+ // A writable table is refused: the table is read once, at its initial contents, and a switch
539
+ // lifted from it would ignore every store that retargets an entry. The compilers put a switch's
540
+ // table in `.rodata`/`.rdata`; what lands in `.data` is a non-const computed-goto label table
541
+ // (`static void *t[] = {&&a, ...}`), which its own function may write (`t[1] = &&c`).
303
542
  const tableBaseOf = (ins: Instr): { sym: string; addend: number } | null => {
304
543
  const isLui = ins.mnemonic === 'lui';
305
544
  const isGpLw = ins.mnemonic === 'lw' && ins.ops[1] !== undefined && parseMem(ins.ops[1]).base === 'gp';
@@ -311,12 +550,76 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
311
550
  return null;
312
551
  }
313
552
  const sec = ad.symbols.get(r.sym)?.section ?? r.sym; // ".rodata" section symbol → ".rodata"
314
- if (!/^\.(rodata|rdata|sdata2?|data)$/.test(sec)) {
553
+ if (!/^\.(rodata|rdata)$/.test(sec)) {
315
554
  return null;
316
555
  }
317
556
  return { sym: r.sym, addend: r.addend };
318
557
  };
319
558
 
559
+ /** The table a base formed OUTSIDE the dispatch block addresses, as far as the instructions say:
560
+ * every `addiu rB,rH,%lo` in the function, with the `lui rH,%hi` nearest before it, must name
561
+ * one table, or the guess is left undecided. KMC and gcc 2.7.2 form the base in a loop's
562
+ * preheader. Only a guess — `lift` pairs the halves and proves the base reaches the dispatch
563
+ * (frontend/jump-table.ts). The table's offset is the pair's own `%hi`/`%lo` halves, as REL
564
+ * relocations carry it. */
565
+ const hoistedTable = (rB: string): TableKey | null =>
566
+ agreedTable(
567
+ instrs.flatMap((x, q) => {
568
+ const lo = x.mnemonic === 'addiu' && x.ops[0] === rB ? textRelocAt(ad, x.addr) : null;
569
+ if (lo?.type !== 'R_MIPS_LO16') {
570
+ return [];
571
+ }
572
+ let p = q - 1;
573
+ while (p >= 0 && writtenReg(instrs[p]) !== x.ops[1]) {
574
+ p--;
575
+ }
576
+ const hi = p >= 0 && instrs[p].mnemonic === 'lui' ? tableBaseOf(instrs[p]) : null;
577
+ if (!hi || hi.sym !== lo.sym) {
578
+ return [{ sym: '', addend: NaN }];
579
+ }
580
+ return [paired(hi, parseImm(instrs[p].ops[1]), parseImm(x.ops[2]))];
581
+ }),
582
+ );
583
+ /** The table a `%hi` / `%lo` pair addresses: the high record's symbol and addend plus both
584
+ * halves' immediates, the `%lo` sign-extended — REL keeps the offset in the immediates, so a
585
+ * table 64 KiB into its section has a high half of 1 and one past 32 KiB has a negative `%lo`.
586
+ * The high half is a `lui`'s immediate (HI16) or a `lw rB,hi(gp)`'s displacement (GOT16 on the
587
+ * section symbol, whose GOT entry holds the 64 KiB page the LO16 completes). */
588
+ const paired = (hi: TableKey, hiImm: number, loImm: number): TableKey => ({
589
+ sym: hi.sym,
590
+ addend: hi.addend + (((hiImm & 0xffff) << 16) + (((loImm & 0xffff) << 16) >> 16)),
591
+ });
592
+ /** The table a base the dispatch's block forms addresses, read with the load's displacement `off`.
593
+ * The base pairs with the load: IDO, KMC and gcc 2.7.2 fold the table's `%lo` into the `lw`
594
+ * whenever the block forms the base, so `lw rV,%lo(T+K)(at)`'s displacement is the low half of
595
+ * the TABLE's address, not an index offset. One rule (`paired`) for both bases: a `lui`'s HI16,
596
+ * and IDO `-KPIC`'s GOT16 (`lw at,%got(T)(gp)`), whose displacement is the high half — `lw
597
+ * at,1(gp)` for a table 64 KiB in, then `lw rV,-24576(at)` for one at 40 KiB, or `lw rV,8(at)`
598
+ * for a later table of the unit. Refuses (null):
599
+ * - a non-zero displacement with no R_MIPS_LO16 on the load — `table[i + off/4]`, the base
600
+ * left unpaired. No compiler emits an unpaired HI16 or GOT16 for a table; displacement 0
601
+ * with no LO16 is still read at the base's own half, the address the pair gives with a
602
+ * zero `%lo`, so the hand fixtures that carry no LO16 keep lifting. An unpaired GOT16 is
603
+ * read only at immediate 0: against a global symbol it names that symbol's own GOT slot,
604
+ * not a page, so a non-zero immediate is no high half;
605
+ * - a LO16 on another symbol, or any other relocation on the load. */
606
+ const inBlockTable = (base: Instr, ld: Instr, off: number): TableKey | null => {
607
+ const hi = tableBaseOf(base);
608
+ if (!hi) {
609
+ return null;
610
+ }
611
+ const isLui = base.mnemonic === 'lui';
612
+ const hiImm = isLui ? parseImm(base.ops[1]) : parseMem(base.ops[1]).off;
613
+ const lo = textRelocAt(ad, ld.addr);
614
+ if (lo === null) {
615
+ return off === 0 && (isLui || hiImm === 0) ? paired(hi, hiImm, 0) : null;
616
+ }
617
+ if (lo.type !== 'R_MIPS_LO16' || lo.sym !== hi.sym) {
618
+ return null;
619
+ }
620
+ return paired(hi, hiImm, off);
621
+ };
622
+
320
623
  const out = new Map<number, MipsJT>();
321
624
  for (let i = 0; i < instrs.length; i++) {
322
625
  const jr = instrs[i];
@@ -326,10 +629,12 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
326
629
  let rV = jr.ops[0];
327
630
  // Optional IDO `+gp`: `addu rV, rL, gp` before the load — the loaded value is the other operand.
328
631
  let ldIdx = defBefore(rV, i);
632
+ let gpIdx = -1;
329
633
  if (ldIdx >= 0 && instrs[ldIdx].mnemonic === 'addu') {
330
634
  const [a, b] = [instrs[ldIdx].ops[1], instrs[ldIdx].ops[2]];
331
635
  if (a === 'gp' || b === 'gp') {
332
636
  rV = a === 'gp' ? b : a;
637
+ gpIdx = ldIdx;
333
638
  ldIdx = defBefore(rV, ldIdx);
334
639
  }
335
640
  }
@@ -337,9 +642,6 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
337
642
  continue;
338
643
  } // rV = *(rAddr)
339
644
  const mem = parseMem(instrs[ldIdx].ops[1]);
340
- if (mem.off !== 0) {
341
- continue;
342
- }
343
645
  const aIdx = defBefore(mem.base, ldIdx); // rAddr = base + index
344
646
  if (aIdx < 0 || instrs[aIdx].mnemonic !== 'addu') {
345
647
  continue;
@@ -347,74 +649,137 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
347
649
  const [oa, ob] = [instrs[aIdx].ops[1], instrs[aIdx].ops[2]];
348
650
  // One operand is the shifted index (`sll rIdx, scrut, 2` — identity guard), the other the table base.
349
651
  let scrutReg: string | null = null,
350
- table: { sym: string; addend: number } | null = null;
652
+ table: { sym: string; addend: number } | null = null,
653
+ sllIdx = -1,
654
+ baseIdx = -1,
655
+ hoisted: HoistedDispatch | null = null;
351
656
  for (const [idxCand, baseCand] of [
352
657
  [oa, ob],
353
658
  [ob, oa],
354
659
  ] as const) {
355
660
  const si = defBefore(idxCand, aIdx);
356
661
  const bi2 = defBefore(baseCand, aIdx);
357
- if (si < 0 || bi2 < 0) {
662
+ // A base the dispatch's block does not form is a HOISTED one (`hoistedTable`), whose `%lo`
663
+ // its own `addiu` carries: the load reads displacement 0 off it.
664
+ const out = bi2 < 0 && si >= 0 && mem.off === 0 ? hoistedTable(baseCand) : null;
665
+ if (si < 0 || (bi2 < 0 && !out)) {
358
666
  continue;
359
667
  }
360
668
  const sll = instrs[si];
361
669
  if (sll.mnemonic !== 'sll' || (sll.ops[2] !== '0x2' && sll.ops[2] !== '2')) {
362
670
  continue;
363
671
  } // index*4
364
- const tb = tableBaseOf(instrs[bi2]);
672
+ const tb = out ?? inBlockTable(instrs[bi2], instrs[ldIdx], mem.off);
365
673
  if (!tb) {
366
674
  continue;
367
675
  }
368
676
  scrutReg = sll.ops[1];
369
677
  table = tb;
678
+ [sllIdx, baseIdx] = [si, bi2];
679
+ hoisted = out ? { reg: baseCand, table: out } : null;
370
680
  break;
371
681
  }
372
682
  if (!scrutReg || !table) {
373
683
  continue;
374
684
  }
375
- // Bounds: `sltiu tmp, scrutReg, N ; beqz tmp, DEF` — the guard whose delay-slot-consuming branch
376
- // block will emit the switch_br. Scan back for the sltiu on the scrutinee.
377
- let bounds: { addr: number; n: number; def: number } | null = null;
378
- for (let j = aIdx; j >= 0; j--) {
379
- const c = instrs[j];
380
- if (c.mnemonic === 'sltiu' && c.ops[1] === scrutReg) {
381
- const tmp = c.ops[0];
382
- // the beqz reading tmp (the block terminator); it may follow the sltiu directly.
383
- for (let k = j + 1; k < instrs.length && k <= j + 3; k++) {
384
- if (instrs[k].mnemonic === 'beqz' && instrs[k].ops[0] === tmp && instrs[k].target !== undefined) {
385
- // `sltiu tmp, scrut, N` bounds `scrut < N` — the immediate IS the case count (unlike PPC's
386
- // `cmplwi rS, N-1` which uses the max index). So N = the immediate, not immediate+1.
387
- bounds = { addr: instrs[k].addr, n: parseImm(c.ops[2]), def: instrs[k].target! };
388
- break;
389
- }
390
- }
391
- break;
392
- }
685
+ // Bounds: `sltiu tmp, scrutReg, N ; beqz tmp, DEF`, the beqz the nearest transfer before the
686
+ // dispatch. Its block emits the `switch_br`, reading the scrutinee where the beqz reads `tmp`.
687
+ const bounds = mipsBounds(instrs, i, scrutReg, likely);
688
+ if (!bounds) {
689
+ continue;
393
690
  }
394
- if (!bounds || bounds.n < 2) {
691
+ // THE DISPATCH BLOCK — after the bounds branch's delay slot, through the `jr` and its own slot —
692
+ // is pruned unread once the table is recovered, so it may hold the idiom and nothing else:
693
+ // anything more would be dropped. Nothing may branch into it past the bounds check, the `jr`'s
694
+ // slot must be empty, and nothing from the bounds slot on may rewrite the scrutinee before the
695
+ // `sll` scales it (IDO's in-place `sll` IN the slot is that `sll`).
696
+ const k = bounds.at;
697
+ const idiom = new Set([ldIdx, aIdx, gpIdx, sllIdx, baseIdx]);
698
+ if (
699
+ instrs[i + 1]?.mnemonic !== 'nop' ||
700
+ entersBetween(instrs, instrs[k].addr, instrs[i + 1].addr) ||
701
+ instrs.slice(k + 2, i).some((_, o) => !idiom.has(k + 2 + o)) ||
702
+ instrs.slice(k + 1, sllIdx).some((x) => writtenReg(x) === scrutReg)
703
+ ) {
395
704
  continue;
396
705
  }
397
706
  const caseAddrs = readJumpTable(ad, table.sym, table.addend, bounds.n);
398
707
  if (!caseAddrs) {
399
708
  continue;
400
709
  }
401
- out.set(bounds.addr, { scrutReg, caseAddrs, defaultAddr: bounds.def, jrAddr: jr.addr });
710
+ // Only the words past the bounds branch's delay slot are pruned: the slot runs on both paths and
711
+ // is lifted (KMC's `sll`, IDO's in-place one, a `lui` gcc puts there).
712
+ const leaves = idiomLeaves(
713
+ [...idiom]
714
+ .filter((x) => x > k + 1)
715
+ .sort((a, b) => a - b)
716
+ .flatMap((x) => {
717
+ const r = writtenReg(instrs[x]);
718
+ return r === null ? [] : [{ reg: r, scaled: x === sllIdx }];
719
+ }),
720
+ );
721
+ out.set(bounds.addr, {
722
+ scrutReg,
723
+ caseAddrs,
724
+ defaultAddr: bounds.def,
725
+ jrAddr: jr.addr,
726
+ entry: bounds.entry,
727
+ hoisted,
728
+ leaves,
729
+ });
730
+ }
731
+ // A bound read off its block's ENTRY EDGES holds only for the edges `mipsBounds` saw. A table's
732
+ // arm is an edge too, and no table existed while it looked: one landing there declines.
733
+ const arms = new Set([...out.values()].flatMap((jt) => [...jt.caseAddrs, jt.defaultAddr]));
734
+ for (const [addr, jt] of out) {
735
+ if (jt.entry !== null && arms.has(jt.entry)) {
736
+ out.delete(addr);
737
+ }
402
738
  }
403
739
  return out;
404
740
  }
405
741
 
406
- // BRANCH-LIKELY → the ordinary branch that tests the same way, in place, returning the addresses
407
- // rewritten. The mnemonic is all that differs in the COMPARISON; what differs in the FLOW is the
408
- // delay slot, which `toBlocks` then places on the taken edge alone.
742
+ // BRANCH-LIKELY → the ordinary branch that tests the same way, in place. The mnemonic is all that
743
+ // differs in the COMPARISON; what differs in the FLOW is the delay slot, which `toBlocks` then
744
+ // places on the taken edge alone. `likely` holds the branches so placed.
745
+ //
746
+ // UNLESS THE SLOT IS THE TARGET'S OWN FIRST WORD, STOLEN — which is how IDO, KMC and GCC 2.7.2 fill
747
+ // a likely slot: `bnezl r,T; S` where the word at T-4 is also S (ido 4932 of the 4984 likely
748
+ // branches in the af splat corpus). Then the branch is retargeted to T-4 with an empty slot,
749
+ // `bnez r,T-4; nop`, and `freed` holds the slot addresses so emptied. That is exact on both paths
750
+ // WHATEVER ELSE REACHES T-4: taken runs S then T either way, and not taken annuls S either way. It
751
+ // matters because the copy otherwise becomes a block of its own on the taken edge, which reaches T
752
+ // past every join T-4 is part of — a switch arm whose `break` it skips, so the arms stop
753
+ // linearizing (ido), or a recovered switch's bounds branch that cannot annul its slot (KMC). The
754
+ // rewrite holds only while every word means what its bytes say, so it is made only when:
755
+ // - the two words are the same instruction with the same relocation — text alone cannot tell
756
+ // `lui at,0x0` against one symbol from the same text against another;
757
+ // - T-4 is not the slot itself (T = branch+8, a slot that skips only itself: nothing to share);
758
+ // - T-4 and T are both in the function, and T-4 is no transfer's delay slot — a slot runs only
759
+ // after its own transfer, so falling into one from T-8's side is not what its bytes say;
760
+ // - the slot completes no `%lo`. Its `%hi` is a register each path loads for itself (`lui v1;
761
+ // b T; lw v1,%lo(g)(v1)` against `lui v1; …; lw v1,%lo(g)(v1); T:`), and retargeted, the one
762
+ // copy at T-4 reads that register at a join, where it is the block parameter and not the half:
763
+ // the high/low fold would refuse a function the kept slot lifts. Exact either way, so the
764
+ // shape keeps its slot rather than trade a lift for a decline.
765
+ // A branch landing on the slot refuses below, rewrite or not; a recovered jump-table arm landing on
766
+ // a freed slot refuses in `lift`, where the tables exist.
409
767
  //
410
768
  // Refuses, by name, every shape that placement cannot model. A nullified slot read as an ordinary
411
769
  // always-executed one is not a cosmetic error: `absi` would return `-x` for every `x >= 0`, and a
412
770
  // store in such a slot would be performed on a path that never performs it. That is C which
413
771
  // compiles and is wrong — strictly worse than the decline it would replace.
414
- function normaliseBranchLikely(name: string, instrs: Instr[], startAddr: number): Set<number> {
772
+ function normaliseBranchLikely(
773
+ name: string,
774
+ instrs: Instr[],
775
+ startAddr: number,
776
+ relocOf: (ins: Instr) => DisasmReloc | null,
777
+ options: LiftOptions = {},
778
+ ): { likely: Set<number>; freed: Set<number> } {
415
779
  const likely = instrs.filter((ins) => ins.mnemonic in LIKELY_BASE);
416
- if (likely.length === 0) {
417
- return new Set();
780
+ const uncond = instrs.filter(isUncond);
781
+ if (likely.length === 0 && uncond.length === 0) {
782
+ return { likely: new Set(), freed: new Set() };
418
783
  }
419
784
  const refusal = (ins: Instr, why: string) =>
420
785
  new FrontendUnsupportedError(
@@ -432,6 +797,32 @@ function normaliseBranchLikely(name: string, instrs: Instr[], startAddr: number)
432
797
  const at = new Map(instrs.map((ins) => [ins.addr, ins]));
433
798
  const hex = (a: number) => `0x${a.toString(16)}`;
434
799
  const rewritten = new Set<number>();
800
+ const freed = new Set<number>();
801
+ // The words as listed, before any slot below is emptied.
802
+ const listed = new Map(at);
803
+ // `br T; S` → `br T-4; nop` when S is the word at T-4, stolen; true when it did.
804
+ const unsteal = (br: Instr, slot: Instr): boolean => {
805
+ const target = br.target!;
806
+ const stolen = at.get(target - 4);
807
+ const beforeStolen = at.get(target - 8);
808
+ if (
809
+ target === br.addr + 8 ||
810
+ stolen === undefined ||
811
+ !at.has(target) ||
812
+ (beforeStolen !== undefined && isControlTransfer(beforeStolen)) ||
813
+ !sameWord(slot, stolen, relocOf) ||
814
+ relocOf(slot)?.type === 'R_MIPS_LO16'
815
+ ) {
816
+ return false;
817
+ }
818
+ br.target = target - 4;
819
+ // The emptied slot is a fresh `nop`: the old word's relocation must not ride along with it.
820
+ const nop: Instr = { addr: slot.addr, mnemonic: 'nop', ops: [] };
821
+ instrs[instrs.indexOf(slot)] = nop;
822
+ at.set(slot.addr, nop);
823
+ freed.add(slot.addr);
824
+ return true;
825
+ };
435
826
  for (const br of likely) {
436
827
  const slot = at.get(br.addr + 4);
437
828
  const fallThrough = at.get(br.addr + 8);
@@ -468,9 +859,68 @@ function normaliseBranchLikely(name: string, instrs: Instr[], startAddr: number)
468
859
  throw refusal(br, 'another branch targets the delay slot, which would run it unconditioned');
469
860
  }
470
861
  br.mnemonic = LIKELY_BASE[br.mnemonic];
471
- rewritten.add(br.addr);
862
+ if (!unsteal(br, slot)) {
863
+ rewritten.add(br.addr);
864
+ }
865
+ }
866
+ // AN UNCONDITIONAL `b`/`j` RUNS ITS SLOT ON ITS ONE EDGE ALONE, which is all the proof above asks
867
+ // of the slot, and the same fill steals the target's first word into it (`b T; S` with S at T-4:
868
+ // ido 1234, gcc 2.7.2 1075, KMC 51 in the splat corpus). Unretargeted, the copy is the `b`'s own
869
+ // body and T-4 a separate block reaching T, so a join that the two share is split into one arm
870
+ // per copy. Asked here only where nothing else depends on the slot: not a slot already empty, not
871
+ // a `b` in some transfer's own delay slot, not a slot another branch lands on (entered there, the
872
+ // word runs and the flow falls on to the `b`'s next word, not to T), and not a `j` relocated
873
+ // against a symbol, whose printed target is a field rather than an address (it refuses below).
874
+ for (const br of uncond) {
875
+ const slot = at.get(br.addr + 4);
876
+ const prev = at.get(br.addr - 4);
877
+ const reloc = br.mnemonic === 'j' ? relocOf(br) : null;
878
+ if (
879
+ br.target === undefined ||
880
+ slot === undefined ||
881
+ slot.mnemonic === 'nop' ||
882
+ isControlTransfer(slot) ||
883
+ targets.has(slot.addr) ||
884
+ (prev === undefined ? br.addr !== startAddr : isControlTransfer(prev)) ||
885
+ (reloc !== null && !reloc.sym.startsWith('.'))
886
+ ) {
887
+ continue;
888
+ }
889
+ // A `b` THAT JUMPS OVER EXACTLY THE ONE COPY (`b T; S; S; T:`, KMC `sw_void`, ido `mergeu16`)
890
+ // would, retargeted, jump to its own next word: the two arms are each written out, and nothing
891
+ // was stolen from T. That is a fact of the jump, so it is answered here.
892
+ if (br.target - 4 === br.addr + 8) {
893
+ continue;
894
+ }
895
+ // A COMMON TAIL OF TWO OR MORE WORDS is the question the asm leaves open: `sll; b T; sra`
896
+ // against `sll; sra; T:` (ido `mergenarrow`) is one sign extension per arm, written out twice,
897
+ // or a stolen `sra` whose arms the source merged — the same object either way. Retargeted, the
898
+ // `<< 16` stays in each arm and the `>> 16` moves past the join; kept, each arm reads as the
899
+ // one `(s16)` it is. Neither reading wins on the corpus (over the af/mp3/sbk2 splat functions
900
+ // with this shape, retargeting shortens 19 lifts and lengthens 3, `func_80026D34` the
901
+ // `mergenarrow` case), so the default retargets and the kept slot is the `/kept-slot` lift
902
+ // variation the differ referees. A likely slot emptied above the `b` is no common tail; one
903
+ // emptied before T still counts, as the word it was. A `nop` is no tail at all.
904
+ const before = listed.get(br.target - 8);
905
+ const tail =
906
+ prev !== undefined && prev.mnemonic !== 'nop' && before !== undefined && sameWord(prev, before, relocOf);
907
+ if (tail && options.keepTailSlots === true) {
908
+ continue;
909
+ }
910
+ if (unsteal(br, slot) && tail) {
911
+ options.onTailSlot?.();
912
+ }
913
+ }
914
+ return { likely: rewritten, freed };
915
+ }
916
+
917
+ /** Two words are the same instruction: one spelling and one relocation (or none on either). */
918
+ function sameWord(a: Instr, b: Instr, relocOf: (ins: Instr) => DisasmReloc | null): boolean {
919
+ if (a.mnemonic !== b.mnemonic || a.ops.join() !== b.ops.join() || a.target !== b.target) {
920
+ return false;
472
921
  }
473
- return rewritten;
922
+ const [ra, rb] = [relocOf(a), relocOf(b)];
923
+ return ra === null || rb === null ? ra === rb : ra.type === rb.type && ra.sym === rb.sym && ra.addend === rb.addend;
474
924
  }
475
925
 
476
926
  // EVERY control transfer this frontend models has a delay slot — the word at `branch + 4`, which
@@ -501,6 +951,368 @@ function checkDelaySlots(name: string, instrs: Instr[]): void {
501
951
  }
502
952
  }
503
953
 
954
+ /** WHETHER THE RETURN ENDING BLOCK `bi` MAY HAND BACK A 64-BIT PAIR, read off the asm: O32 returns
955
+ * one in v0:v1, the low word in v1, and v1 is also ordinary scratch, so the question is what each
956
+ * write of v1 that reaches the return was made for. Two answers say "the low word":
957
+ *
958
+ * - NOTHING IN THE FUNCTION READS IT, on any path from it. An optimising compiler computes no
959
+ * dead value, so a write no instruction reads is one the caller reads (`lw v0,16(sp);
960
+ * lw v1,20(sp)` for `return e;`, `mflo v1` for a product). A loop's induction update is read
961
+ * round the back edge, and a value one arm of a branch reads is read, so neither counts.
962
+ * - IT IS AN `addu`/`addiu` WHOSE SUM IS COMPARED, unsigned, against what it added: the carry out
963
+ * of a 64-bit add's low word into its high one (`addu v1,a3,a0; sltu t,v1,a0`, `addiu v1,a1,5;
964
+ * sltiu t,v1,5`). A pointer bound check compares the sum against something else (`addu v1,a1,a0;
965
+ * sltu v0,v0,v1`). A function that computes a carry and returns only the high word is declined
966
+ * with the pairs — loud, and the price of one rule.
967
+ * - IT IS A COPY OF A REGISTER THAT STILL HOLDS THE SAME VALUE wherever v1 is read: the reads
968
+ * could have named the source, so the copy is made for the caller (`move v1,a3; jr ra;
969
+ * sw v1,4(a0)` for `*p = x; return x;`). A copy made because its source is about to be
970
+ * overwritten (a swap, a pointer bumped after its old value is kept), or read past a join, is
971
+ * scratch.
972
+ *
973
+ * A call ends a path either way: it destroys v1 (`clobberedByCall`), and its delay slot runs
974
+ * before it. The entry ends the backward walk, since what v1 held there is no value this
975
+ * function computed. */
976
+ function v1MayHoldLowWord(blocks: readonly MipsBlock[], preds: readonly (readonly number[])[], bi: number): boolean {
977
+ const readsV1 = (ins: Instr) => {
978
+ const dest = writtenReg(ins) !== null && writtenReg(ins) === ins.ops[0];
979
+ return ins.ops.some((o, k) => !(dest && k === 0) && (o === 'v1' || /\(v1\)$/.test(o)));
980
+ };
981
+ // Each block in execution order: a call's delay slot runs before the call.
982
+ const runs = blocks.map((b) => {
983
+ const seq = [...b.body, ...(b.branch ? [b.branch] : []), ...(b.delay ? [b.delay] : [])];
984
+ for (let k = 0; k + 1 < seq.length; k++) {
985
+ if (isCallInstr(seq[k])) {
986
+ [seq[k], seq[k + 1]] = [seq[k + 1], seq[k]];
987
+ k++;
988
+ }
989
+ }
990
+ return seq;
991
+ });
992
+ const succs = blocks.map((): number[] => []);
993
+ preds.forEach((ps, to) => ps.forEach((from) => succs[from].push(to)));
994
+ // What follows the write at `runs[at][k]` on every path, up to a call or the next write of v1:
995
+ // the instructions that read it, and whether register `src` is written meanwhile.
996
+ const after = (at: number, k: number, src: string | undefined) => {
997
+ const readers: Instr[] = [];
998
+ let srcWritten = false;
999
+ const seen = new Set<number>();
1000
+ const from = (blk: number, start: number) => {
1001
+ for (const ins of runs[blk].slice(start)) {
1002
+ if (readsV1(ins)) {
1003
+ readers.push(ins);
1004
+ }
1005
+ if (isCallInstr(ins) || writtenReg(ins) === 'v1') {
1006
+ return;
1007
+ }
1008
+ srcWritten ||= src !== undefined && writtenReg(ins) === src;
1009
+ }
1010
+ for (const n of succs[blk]) {
1011
+ if (!seen.has(n)) {
1012
+ seen.add(n);
1013
+ from(n, 0);
1014
+ }
1015
+ }
1016
+ };
1017
+ from(at, k + 1);
1018
+ return { readers, srcWritten };
1019
+ };
1020
+ const lowWord = (at: number, k: number): boolean => {
1021
+ const ins = runs[at][k];
1022
+ const [, x, y] = ins.ops;
1023
+ // A register copy's source, unless that is zero (a constant).
1024
+ const copied = copiedReg(ins);
1025
+ const src = copied === undefined || isZero(copied) ? undefined : copied;
1026
+ const { readers, srcWritten } = after(at, k, src);
1027
+ // The carry compares the sum against what was added: `sltu t,v1,X` for `addu v1,X,Y` (either
1028
+ // addend other than v1 itself), `sltiu t,v1,k` for `addiu v1,X,k`.
1029
+ const carries = (r: Instr) =>
1030
+ r.ops[1] === 'v1' &&
1031
+ ((ins.mnemonic === 'addu' && r.mnemonic === 'sltu' && r.ops[2] !== 'v1' && [x, y].includes(r.ops[2])) ||
1032
+ (ins.mnemonic === 'addiu' && r.mnemonic === 'sltiu' && parseImm(r.ops[2] ?? '') === parseImm(y ?? '')));
1033
+ // A copy is judged only where its every read follows it in its own block: a read past a join
1034
+ // is where a compiler coalesces each arm's value into one register (`addu v1,a1,zero` on one
1035
+ // arm, `ori v1,a1,0xe000` on the other), which is scratch.
1036
+ const local = readers.every((r) => runs[at].indexOf(r) > k);
1037
+ return readers.length === 0 || readers.some(carries) || (src !== undefined && !srcWritten && local);
1038
+ };
1039
+ // Every write of v1 that reaches the end of `bi`, walking back.
1040
+ const seen = new Set<number>();
1041
+ const back = (at: number, end: number): boolean => {
1042
+ for (let k = end - 1; k >= 0; k--) {
1043
+ const ins = runs[at][k];
1044
+ if (isCallInstr(ins)) {
1045
+ return false;
1046
+ }
1047
+ if (writtenReg(ins) === 'v1') {
1048
+ return lowWord(at, k);
1049
+ }
1050
+ }
1051
+ return preds[at].some((p) => !seen.has(p) && (seen.add(p), back(p, runs[p].length)));
1052
+ };
1053
+ return back(bi, runs[bi].length);
1054
+ }
1055
+
1056
+ /** THE FRAME DEPTH AT EACH INSTRUCTION: how many bytes below the `sp` the function was entered
1057
+ * with it runs, so an `off(sp)` it makes names entry-relative byte `off - depth`. Exact or absent —
1058
+ * null wherever one number per instruction is not proven, and every reader then keeps its refusal.
1059
+ *
1060
+ * Proven for the frame every compiler here builds: at most one `addiu sp,sp,-N`, in the entry
1061
+ * block (anywhere in it, IDO's leaves load before they push), and `addiu sp,sp,N` only in a block
1062
+ * that returns, after which the block runs at depth 0. The entry runs from depth 0 and every other
1063
+ * block from N, which holds because the entry runs once and every other block is reached through
1064
+ * it. Refused: any other write to `sp`, a second push, a push outside the entry or in an entry a
1065
+ * branch re-enters (the preheader's case), a pop of another size or in a block that does not
1066
+ * return. */
1067
+ function frameDepths(blocks: readonly MipsBlock[], entryReentered: boolean): ReadonlyMap<Instr, number> | null {
1068
+ const depths = new Map<Instr, number>();
1069
+ const adjust = (ins: Instr): number | null | undefined => {
1070
+ if (!isStackPtr(writtenReg(ins) ?? '')) {
1071
+ return undefined; // leaves sp alone
1072
+ }
1073
+ const by = parseImm(ins.ops[2] ?? '');
1074
+ return (ins.mnemonic === 'addiu' || ins.mnemonic === 'addi') && isStackPtr(ins.ops[1] ?? '') && Number.isFinite(by)
1075
+ ? by
1076
+ : null;
1077
+ };
1078
+ const pushes = blocks.flatMap((b) => [...b.body, ...(b.delay ? [b.delay] : [])]).filter((i) => (adjust(i) ?? 0) < 0);
1079
+ if (pushes.length > 1) {
1080
+ return null;
1081
+ }
1082
+ const size = pushes.length === 1 ? -adjust(pushes[0])! : 0;
1083
+ for (const [bi, b] of blocks.entries()) {
1084
+ let depth = bi === 0 ? 0 : size;
1085
+ for (const ins of [...b.body, ...(b.delay ? [b.delay] : [])]) {
1086
+ depths.set(ins, depth);
1087
+ const by = adjust(ins);
1088
+ if (by === undefined) {
1089
+ continue;
1090
+ }
1091
+ if (by === null) {
1092
+ return null;
1093
+ }
1094
+ if (by < 0 && (bi !== 0 || entryReentered || depth !== 0)) {
1095
+ return null;
1096
+ }
1097
+ if (by > 0 && (by !== depth || b.branch === null || !isReturn(b.branch))) {
1098
+ return null;
1099
+ }
1100
+ depth -= by;
1101
+ }
1102
+ }
1103
+ return depths;
1104
+ }
1105
+
1106
+ /** What the return proof knows at one point: which registers hold the caller's return address on
1107
+ * every path (`regs`, `ra` among them while it is untouched), the instruction that last wrote
1108
+ * something else into `ra` on some path (`lost`), the entry-`sp` offset of the word it was saved
1109
+ * in (`slot`), and `sp`'s signed offset from its entry value (`depth`, -24 after `addiu sp,sp,-24`;
1110
+ * the opposite sign to `frameDepths`' positive depth), each null where unknown. */
1111
+ interface RaState {
1112
+ readonly regs: ReadonlySet<string>;
1113
+ readonly lost: Instr | null;
1114
+ readonly slot: number | null;
1115
+ readonly depth: number | null;
1116
+ }
1117
+
1118
+ /** Bytes a store mnemonic writes. Only the stores the lowering models (`emitStore`) are named; the
1119
+ * proof treats any other instruction that writes no register and addresses memory through `sp`
1120
+ * (`swl`/`swr`, `sc`, `sd*`, `swc1`/`sdc1`, all refused earlier by `opaqueDest`'s `storeClass` or
1121
+ * the FPU arm today) as a store of unknown bytes, which overwrites the save word. */
1122
+ const STORE_BYTES: Readonly<Record<string, [before: number, width: number]>> = {
1123
+ sb: [0, 1],
1124
+ sh: [0, 2],
1125
+ sw: [0, 4],
1126
+ };
1127
+
1128
+ /** Whether `r` is one of the target's `kernelRegs` (O32's `k0`/`k1`), in any spelling. */
1129
+ const isKernelReg = (kernelRegs: readonly string[], r: string) =>
1130
+ kernelRegs.includes(r === '$26' ? 'k0' : r === '$27' ? 'k1' : r.replace(/^\$/, ''));
1131
+
1132
+ /** EVERY `jr ra` OF THIS FUNCTION JUMPS TO THE CALLER'S RETURN ADDRESS, or the lift declines — the
1133
+ * MIPS half of what `refuseReturnsNotThroughCallersLr` asks of Thumb (frontend/thumb.ts).
1134
+ *
1135
+ * `jr ra` reads as a return from its operand alone, and it is one only where `ra` holds the address
1136
+ * this function was called with. It reads `ra` when it runs, before its delay slot. A forward
1137
+ * dataflow over the blocks tracks where that address is:
1138
+ * • `ra` holds it at entry; a call (`jal`) writes `ra`, runs its delay slot, and only then
1139
+ * destroys every register O32 lets a callee destroy (and its home area), so a slot's write is
1140
+ * exposed to the callee; a slot that writes `ra` refuses, since the callee then returns
1141
+ * somewhere other than the code after the call; a copy (`move`, `addu`/`or` with `zero`)
1142
+ * carries it on; any other write to a register — `lw ra,0(a0)`, `addiu ra,ra,8`, `lui ra` —
1143
+ * loses it;
1144
+ * • a `sw` of a register holding it through `sp` puts it in that word, named by its offset from
1145
+ * the entry `sp` (this walk follows `addiu sp,sp,N`; any other write to `sp` makes the depth
1146
+ * unknown), and a `lw` from that word copies it back out. Any other store reaching those bytes,
1147
+ * a store through `sp` at an unknown depth or of unknown width (`STORE_BYTES`), and a call whose
1148
+ * home area (`[sp, sp+16)` at the call) holds the word overwrite it.
1149
+ * Paths meet by agreement: a register or the word holds the address only if it does on every path
1150
+ * in, and a depth the paths disagree on is unknown.
1151
+ *
1152
+ * So `move ra,a1; jr ra`, `lw ra,0(a0)`, `addiu ra,ra,8`, `lui ra,0x8000`, a `move ra` on one path
1153
+ * only, and a `jal` whose `ra` nothing reloaded all decline, where every compiler's `sw ra,N(sp)` …
1154
+ * `lw ra,N(sp)` and a leaf that never names `ra` are returns. A reload from a word another
1155
+ * register was saved in already refuses (`emitLoad`). No compiler emits a violation; hand-written
1156
+ * asm can, and it would otherwise lift as a C `return`. A tail call (`j` to another function) and an indirect
1157
+ * `jr` already refuse in `lift`, so `jr ra` is the only exit asked.
1158
+ *
1159
+ * A copy into a kernel register (`kernelRegs`, O32's `k0`/`k1`) holds nothing: an exception handler
1160
+ * may overwrite it at any instruction, so `move k0,ra` … `move ra,k0` declines.
1161
+ *
1162
+ * NOT ASKED: a store through a register holding a frame address is not traced to the save word.
1163
+ * A compiler addresses that word only through `sp`. */
1164
+ function refuseReturnsNotThroughCallersRa(
1165
+ name: string,
1166
+ blocks: readonly MipsBlock[],
1167
+ succIdx: readonly number[][],
1168
+ callerSaved: readonly string[],
1169
+ kernelRegs: readonly string[],
1170
+ ): void {
1171
+ const canon = (r: string) => (r === '$ra' || r === '$31' ? 'ra' : r);
1172
+ const join = (a: RaState, b: RaState): RaState => {
1173
+ const regs = new Set([...a.regs].filter((r) => b.regs.has(r)));
1174
+ const depth = a.depth !== null && a.depth === b.depth ? a.depth : null;
1175
+ return {
1176
+ regs,
1177
+ lost: regs.has('ra') ? null : ((a.regs.has('ra') ? null : a.lost) ?? (b.regs.has('ra') ? null : b.lost)),
1178
+ slot: depth !== null && a.slot !== null && a.slot === b.slot ? a.slot : null,
1179
+ depth,
1180
+ };
1181
+ };
1182
+ const same = (a: RaState, b: RaState) =>
1183
+ a.lost === b.lost &&
1184
+ a.slot === b.slot &&
1185
+ a.depth === b.depth &&
1186
+ a.regs.size === b.regs.size &&
1187
+ [...a.regs].every((r) => b.regs.has(r));
1188
+
1189
+ const run = (bi: number, inState: RaState): RaState => {
1190
+ const regs = new Set(inState.regs);
1191
+ let { lost, slot, depth } = inState;
1192
+ const b = blocks[bi];
1193
+ const step = (ins: Instr): void => {
1194
+ const mn = ins.mnemonic;
1195
+ if (mn === 'jr' && canon(ins.ops[0] ?? '') === 'ra' && !regs.has('ra')) {
1196
+ throw new FrontendUnsupportedError(
1197
+ `cannot lift '${name}': the 'jr ra' at 0x${ins.addr.toString(16)} may not return through the caller's ` +
1198
+ `return address — ` +
1199
+ (lost === null
1200
+ ? `ra does not hold it on some path`
1201
+ : `on some path the '${lost.mnemonic}' at 0x${lost.addr.toString(16)} wrote ra, and no reload from ` +
1202
+ `the word it was saved in put it back`),
1203
+ );
1204
+ }
1205
+ const store = STORE_BYTES[mn];
1206
+ if (store !== undefined) {
1207
+ const mem = ins.ops[1] ?? '';
1208
+ if (mem.includes('(') && isStackPtr(parseMem(mem).base)) {
1209
+ const at = depth === null ? null : parseMem(mem).off + depth;
1210
+ if (mn === 'sw' && at !== null && regs.has(canon(ins.ops[0]))) {
1211
+ slot = at;
1212
+ } else if (at === null || (slot !== null && at - store[0] < slot + 4 && at - store[0] + store[1] > slot)) {
1213
+ slot = null;
1214
+ }
1215
+ }
1216
+ return;
1217
+ }
1218
+ const w = writtenReg(ins);
1219
+ if (w === null) {
1220
+ if (ins.ops.some((op) => op.includes('(') && isStackPtr(parseMem(op).base))) {
1221
+ slot = null;
1222
+ }
1223
+ return;
1224
+ }
1225
+ const dest = canon(w);
1226
+ const mem = ins.ops[1] ?? '';
1227
+ const reload =
1228
+ mn === 'lw' &&
1229
+ slot !== null &&
1230
+ depth !== null &&
1231
+ mem.includes('(') &&
1232
+ isStackPtr(parseMem(mem).base) &&
1233
+ parseMem(mem).off + depth === slot;
1234
+ const copy = copiedReg(ins);
1235
+ const holds = !isKernelReg(kernelRegs, dest) && (reload || (copy !== undefined && regs.has(canon(copy))));
1236
+ if (isStackPtr(dest)) {
1237
+ depth =
1238
+ depth !== null &&
1239
+ /^addiu?$/.test(mn) &&
1240
+ isStackPtr(ins.ops[1] ?? '') &&
1241
+ /^-?(0x)?[0-9a-f]+$/i.test(ins.ops[2] ?? '')
1242
+ ? depth + parseImm(ins.ops[2])
1243
+ : null;
1244
+ }
1245
+ if (holds) {
1246
+ regs.add(dest);
1247
+ } else {
1248
+ regs.delete(dest);
1249
+ if (dest === 'ra') {
1250
+ lost = ins;
1251
+ }
1252
+ }
1253
+ if (dest === 'ra' && holds) {
1254
+ lost = null;
1255
+ }
1256
+ };
1257
+ // A call links `ra`, runs its delay slot, then enters the callee (the doc above). The lowering
1258
+ // refuses a `jal` whose slot is not the next word of its block, and every other linking transfer
1259
+ // before `toBlocks`, so this walk only asserts it.
1260
+ const seq = [...b.body, ...(b.branch ? [b.branch] : []), ...(b.delay ? [b.delay] : [])];
1261
+ for (let k = 0; k < seq.length; k++) {
1262
+ const call = seq[k];
1263
+ if (!isCallInstr(call)) {
1264
+ step(call);
1265
+ continue;
1266
+ }
1267
+ regs.delete('ra');
1268
+ lost = call;
1269
+ const delay = seq[k + 1];
1270
+ if (delay?.addr !== call.addr + 4) {
1271
+ throw new Error(
1272
+ `internal: the '${call.mnemonic}' at 0x${call.addr.toString(16)} in '${name}' reached the return proof ` +
1273
+ `without its delay slot as the next instruction of its block, which the lowering's fill loop refuses`,
1274
+ );
1275
+ }
1276
+ const w = writtenReg(delay);
1277
+ if (w !== null && canon(w) === 'ra') {
1278
+ throw new FrontendUnsupportedError(
1279
+ `cannot lift '${name}': the '${delay.mnemonic}' at 0x${delay.addr.toString(16)} in the delay slot of the ` +
1280
+ `'${call.mnemonic}' at 0x${call.addr.toString(16)} writes ra, so the callee may not return through the ` +
1281
+ `caller's return address to the code after the call`,
1282
+ );
1283
+ }
1284
+ step(delay);
1285
+ k++;
1286
+ for (const r of callerSaved) {
1287
+ regs.delete(r);
1288
+ }
1289
+ if (slot !== null && (depth === null || (slot >= depth && slot < depth + STACK_ARGS_AT))) {
1290
+ slot = null;
1291
+ }
1292
+ }
1293
+ return { regs, lost, slot, depth };
1294
+ };
1295
+
1296
+ if (blocks.length === 0) {
1297
+ return;
1298
+ }
1299
+ const into: Array<RaState | undefined> = blocks.map(() => undefined);
1300
+ into[0] = { regs: new Set(['ra']), lost: null, slot: null, depth: 0 };
1301
+ const work = [0];
1302
+ while (work.length) {
1303
+ const bi = work.pop()!;
1304
+ const out = run(bi, into[bi]!);
1305
+ for (const s of succIdx[bi]) {
1306
+ const prev = into[s];
1307
+ const next = prev === undefined ? out : join(prev, out);
1308
+ if (prev === undefined || !same(prev, next)) {
1309
+ into[s] = next;
1310
+ work.push(s);
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+
504
1316
  // Split the instruction stream into basic blocks. Delay slots are consumed into their
505
1317
  // branching block; leaders are the entry, every branch target, and each conditional branch's
506
1318
  // fall-through. A branch-LIKELY's slot is not consumed — it becomes its own block (see
@@ -618,14 +1430,38 @@ function toBlocks(
618
1430
  }
619
1431
 
620
1432
  /** Lift disassembled MIPS text → an L1 Fn with block-argument SSA. `prototypes` declares each
621
- * callee's arity (`frontend/call-plan.ts`). */
1433
+ * callee's arity (`frontend/call-plan.ts`).
1434
+ *
1435
+ * AT MOST TWICE, as on Thumb (`frontend/frame-objects.ts` `liftWithRelift`): where the frame-object audit answers
1436
+ * that a callee may write the local area without bound, or that nothing escapes and the per-object
1437
+ * model does not describe it, it asks for the area kept as one object, and the second lift's audit
1438
+ * judges it so and refuses rather than asking again. */
622
1439
  export function lift(
623
1440
  name: string,
624
1441
  asm: string,
625
1442
  target: TargetDescription,
626
1443
  prototypes: Prototypes = {},
627
1444
  asmData?: AsmData,
1445
+ symbols?: SymbolMap,
1446
+ context?: ContextGlobals,
1447
+ options: LiftOptions = {},
628
1448
  ): Fn {
1449
+ return liftWithRelift(name, (oneObject) =>
1450
+ liftOnce(name, asm, target, prototypes, asmData, symbols, context, oneObject, options),
1451
+ );
1452
+ }
1453
+
1454
+ function liftOnce(
1455
+ name: string,
1456
+ asm: string,
1457
+ target: TargetDescription,
1458
+ prototypes: Prototypes,
1459
+ asmData: AsmData | undefined,
1460
+ symbols: SymbolMap | undefined,
1461
+ context: ContextGlobals | undefined,
1462
+ oneObject: FrameRange | undefined,
1463
+ options: LiftOptions,
1464
+ ): Fn | FrameObjectRelift {
629
1465
  // Two input dialects reach this frontend: `objdump -d` text (IDO/KMC — no compiler-emitted asm),
630
1466
  // and Splat-disassembled `.s` (pmret-style N64 projects). Splat is normalised to the SAME
631
1467
  // DisasmInstr[] shape (frontend/splat.ts) so everything below is dialect-agnostic.
@@ -639,38 +1475,56 @@ export function lift(
639
1475
  if (instrs.length === 0) {
640
1476
  throw new FrontendUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
641
1477
  }
1478
+ // $30 has two names, and everything below keys a register by its spelling.
1479
+ unifyRegisterSpellings(instrs, O32_REGISTER_ALIASES);
642
1480
  // Headerless input — a raw fragment, and every Splat listing — says where the function starts
643
1481
  // only by its first line.
644
1482
  const startAddr = (splat ? undefined : symbolStart(sliced)) ?? instrs[0].addr;
645
- // An FP condition-code branch is a DIFFERENT gap from a branch-likely, and saying so is what lets
646
- // each be worked on alone: `bc1fl` is both, and the condition code blocks it either way. Asked
647
- // FIRST, before the likely rewrite, so that a function carrying both reports the FP gap rather
648
- // than whatever the likely placement happens to say about the other branch.
1483
+ // An FP condition-code branch this frontend cannot read — one naming a MIPS IV condition code, or
1484
+ // any on a target with no FPU homes — is a DIFFERENT gap from a branch-likely, and saying so is
1485
+ // what lets each be worked on alone. Asked FIRST, before the likely rewrite, so that a function
1486
+ // carrying both reports the FP gap rather than whatever the likely placement says about it.
649
1487
  for (const ins of instrs) {
650
- if (isFpCondBranch(ins.mnemonic)) {
1488
+ const plain = LIKELY_BASE[ins.mnemonic] ?? ins.mnemonic;
1489
+ if (isFpCondBranch(ins.mnemonic) && !(target.fpu && plain in FP_COND_BRANCH && ins.ops.length === 1)) {
651
1490
  throw new FrontendUnsupportedError(
652
1491
  `cannot lift '${name}': floating-point condition-code branch '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
653
- `— the FP condition code is not modelled`,
1492
+ '— only the one condition code a `c.cond` sets, named by the target alone, is modelled',
654
1493
  );
655
1494
  }
656
1495
  }
657
1496
  // BRANCH-LIKELY is rewritten to its ordinary branch FIRST, so everything downstream — jump-table
658
1497
  // recovery's block-boundary walk included — sees one branch vocabulary. What stays special is the
659
1498
  // slot, and `toBlocks` is where that is placed.
660
- const likelyAddrs = normaliseBranchLikely(name, instrs, startAddr);
1499
+ // A word's relocation is on the word in Splat text, and in the object's table for objdump.
1500
+ const relocOf = (ins: Instr): DisasmReloc | null =>
1501
+ splat ? (ins.reloc ?? null) : asmData ? textRelocAt(asmData, ins.addr) : null;
1502
+ const { likely: likelyAddrs, freed: freedSlots } = normaliseBranchLikely(name, instrs, startAddr, relocOf, options);
661
1503
  // Regime B: recover jump tables from the `jr`-dispatch idiom + the AsmData table. A recovered
662
1504
  // dispatch's `jr` is subsumed into a `switch_br` (emitted from its bounds block), so it is
663
1505
  // exempted from the loud-fail below; an UNrecovered `jr <non-ra>` still fails loud.
664
- const jts = asmData ? recoverMipsJumpTables(instrs, asmData) : new Map<number, MipsJT>();
1506
+ const jts = asmData ? recoverMipsJumpTables(instrs, asmData, likelyAddrs) : new Map<number, MipsJT>();
665
1507
  const recoveredJr = new Set([...jts.values()].map((j) => j.jrAddr));
666
- // THE TWO WAYS A RECOVERED TABLE MEETS AN ANNULLED SLOT, neither of which `normaliseBranchLikely`
667
- // can see: the table does not exist while it runs. (1) The dispatch's bounds branch emits a
668
- // `switch_br`, which runs its delay slot unconditionally — a nullified slot is not that. (2) An
669
- // arm of the table lands ON a nullified slot, which would run the slot with no branch
670
- // conditioning it and then jump to that branch's target: `case 1:` would take the taken arm's
671
- // value where the hardware runs the slot and falls on through. Neither idiom has ever been seen
672
- // in the corpus; refuse rather than answer either one wrong.
1508
+ // THE WAYS A RECOVERED TABLE MEETS A LIKELY SLOT, none of which `normaliseBranchLikely` can see:
1509
+ // the table does not exist while it runs. (1) The dispatch's bounds branch emits a `switch_br`,
1510
+ // which runs its delay slot unconditionally — a nullified slot is not that. KMC writes this
1511
+ // bounds branch as `beqzl at,DEF` with DEF's first word in the slot (`sw_h3`..`sw_h5`), and that
1512
+ // one was retargeted to an ordinary branch before it got here; a slot that is NOT the default's
1513
+ // stolen word still refuses. (2) An arm of the table lands ON a nullified slot, which would run
1514
+ // the slot with no branch conditioning it and then jump to that branch's target: `case 1:` would
1515
+ // take the taken arm's value where the hardware runs the slot and falls on through. (3) An arm
1516
+ // lands on a slot the rewrite EMPTIED, a likely branch's or a `b`/`j`'s, which would run the
1517
+ // `nop` where the hardware runs the word it replaced. (2) and (3) have never been seen in the
1518
+ // corpus; refuse rather than answer any of them wrong.
673
1519
  const jtArms = new Set([...jts.values()].flatMap((jt) => [...jt.caseAddrs, jt.defaultAddr]));
1520
+ for (const addr of freedSlots) {
1521
+ if (jtArms.has(addr)) {
1522
+ throw new FrontendUnsupportedError(
1523
+ `cannot lift '${name}': a recovered switch arm lands on the delay slot at 0x${addr.toString(16)}, ` +
1524
+ 'which the stolen-slot rewrite of a branch-likely or `b`/`j` emptied',
1525
+ );
1526
+ }
1527
+ }
674
1528
  for (const addr of likelyAddrs) {
675
1529
  if (jts.has(addr)) {
676
1530
  throw new FrontendUnsupportedError(
@@ -687,7 +1541,7 @@ export function lift(
687
1541
  // text spells the halves and writes its own records). Runs AFTER jump-table recovery, so that
688
1542
  // reads `.text` relocs pristine and a recovered table base is decided before any of this.
689
1543
  if (!splat && asmData) {
690
- attachMipsRelocs(name, instrs, asmData);
1544
+ attachMipsRelocs(name, instrs, asmData, freedSlots);
691
1545
  }
692
1546
  // TRUSTWORTHINESS: fail LOUD on a control transfer this frontend cannot model — the `opaque`
693
1547
  // path cannot catch these (implicit or no register destination). A `jalr` clobbers `v0`
@@ -725,10 +1579,16 @@ export function lift(
725
1579
  }
726
1580
  checkDelaySlots(name, instrs);
727
1581
  const { blocks, succAddrs } = toBlocks(instrs, jts, likelyAddrs);
1582
+ // `isReturn` reads every surviving non-`ra` `jr` as a return, which holds only while each
1583
+ // recovered dispatch's block is pruned (frontend/jump-table.ts).
1584
+ refuseReachableDispatch(name, blocks, recoveredJr, (m) => {
1585
+ throw new FrontendUnsupportedError(m);
1586
+ });
728
1587
  // PREHEADER, as in frontend/ppc.ts: an entry that is a branch target gets an empty block ahead of
729
1588
  // it, whose live-ins are the function's parameters. One word below the function, so it falls
730
1589
  // through to it.
731
- if (blocks.some((b) => succAddrs.get(b)!.includes(blocks[0].startAddr))) {
1590
+ const entryReentered = blocks.some((b) => succAddrs.get(b)!.includes(blocks[0].startAddr));
1591
+ if (entryReentered) {
732
1592
  const ph: MipsBlock = {
733
1593
  startAddr: blocks[0].startAddr - 4,
734
1594
  body: [],
@@ -752,31 +1612,87 @@ export function lift(
752
1612
  }
753
1613
  });
754
1614
 
755
- // NO FRAME PARTITION IS CLAIMED, so every def-less slot read refuses (frontend/ssa.ts,
756
- // LiveInModel). This frontend has no frame bound at all — `addiu sp,sp,±N` is transparent and
757
- // every word sp-relative access becomes `sp@<rawOff>` — so its slot keys span O32's CALLER-owned
758
- // register-parameter home area and the incoming stack arguments above it, where a def-less read
759
- // is argument 5, not an uninitialised local.
1615
+ // THE SLOT MODEL CLAIMS ONE PARTITION, `ownedLocals` (`ownedFrame`, below): a def-less read of a
1616
+ // word this function provably owns is an uninitialised local, and any other def-less slot read
1617
+ // refuses (frontend/ssa.ts, LiveInModel) unless `emitLoad` places it. It declares no
1618
+ // `declaredLocals`, so no slot is stamped a declaration rank. A slot key is the RAW `sp@<off>`;
1619
+ // what the offset names is read through the frame depth (`frameDepths`, below): a word at or past
1620
+ // entry + 16 is incoming stack argument k, read and written as `@sarg<k>` (`incomingStackKey`;
1621
+ // O32's `<pentry offset="16">` in `mips32be.cspec`), and `[0,16)` at entry is the caller-owned
1622
+ // home of a0..a3, which is in neither range. A function that takes a frame address gets a local
1623
+ // area as well (`measureO32Frame`, `compilerBehaviors.o32LocalArea`), whose accesses are memory
1624
+ // through an `laddr` the shared frame-object audit proves, as on Thumb.
760
1625
  //
761
- // Claiming one needs the frame SIZE those offsets are measured against, which is not computed
762
- // here, plus ensureParam for the register half. The ranges themselves are known: O32 reserves
763
- // `[0,16)` as the caller-owned home area (in NEITHER range — caller-owned, but not an argument)
764
- // with stack arguments from 16 up, which is what `mips32be.cspec`'s `<localrange>` and stack
765
- // `<pentry offset="16">` encode.
766
- const fpType = fpPrecision(instrs, (m) => FP_ARITH[m]?.width);
767
- const ssa = makeSsaBuilder(name, blocks.length, preds, undefined, (k) =>
768
- isFpKey(k) ? (fpType ?? T.f32()) : undefined,
1626
+ // THE REGISTER HALF of the live-in model IS claimed: a def-less read of a register O32 passes no
1627
+ // argument in, and that is scratch or the prologue saved (`prologueSavedRegs`), is an
1628
+ // uninitialised local (`registerLiveIns`, frontend/ssa.ts, as on Thumb and PPC). The saves are
1629
+ // measured HERE, before any block is lowered, because the builder evaluates the model once, at
1630
+ // its first use, while `saveSlots` below fills progressively. The function's own declaration
1631
+ // bounds the argument registers (`ownArgumentWordsOn`): KMC and gcc 2.7.2 home a local in a2/a3
1632
+ // past a two-parameter one.
1633
+ const fpType = fpPrecision(instrs, (m) => FP_ARITH[m]?.width ?? FP_CONVERT[m]?.width ?? FP_COMPARE[m]?.width);
1634
+ const savedRegs = prologueSavedRegs(
1635
+ [...blocks[0].body, ...(blocks[0].delay ? [blocks[0].delay] : [])],
1636
+ target,
1637
+ instrs,
1638
+ );
1639
+ // The frame half is measured further down (`ownedFrame`), once the frame depths are; a model the
1640
+ // builder asks for before that claims no frame, which only refuses more.
1641
+ let ownedFrameOf: (() => { from: number; to: number } | undefined) | undefined;
1642
+ const liveIns = registerLiveIns(
1643
+ target,
1644
+ savedRegs,
1645
+ options.contextOwnWords ?? ownArgumentWordsOn(name, prototypes[name], target),
1646
+ );
1647
+ const ssa = makeSsaBuilder(
1648
+ name,
1649
+ blocks.length,
1650
+ preds,
1651
+ () => {
1652
+ const owned = ownedFrameOf?.();
1653
+ return {
1654
+ ...liveIns,
1655
+ saveSite: 'store in its entry block',
1656
+ ...(owned === undefined ? {} : { ownedLocals: owned }),
1657
+ };
1658
+ },
1659
+ (k) => (isFpKey(k) ? (fpType ?? T.f32()) : undefined),
1660
+ liveIns.globalRegs,
769
1661
  );
770
1662
  const { irBlocks, readVar, writeVar, paramReg } = ssa;
1663
+ // Where the compiler put each block (ir/core.ts `AsmLayout`). A delay slot runs in its branch's
1664
+ // block, so it is part of the body; a branch-likely's slot is a block of its own already.
1665
+ ssa.fn.asmLayout = recordAsmLayout(
1666
+ irBlocks,
1667
+ blocks.map((b) => ({
1668
+ body: b.delay ? [...b.body, b.delay] : b.body,
1669
+ endsInJump: b.branch !== null && isUncond(b.branch),
1670
+ isCall: (ins) => isCallInstr(ins as Instr),
1671
+ })),
1672
+ );
771
1673
  const RET = target.returnReg;
1674
+ // Whether this function's own declaration says its return fits one word (`v1MayHoldLowWord`):
1675
+ // void, or a scalar or pointer no wider than 32 bits. An aggregate says nothing here. A declared
1676
+ // WIDER return is refused for every frontend after the lift (pipeline.ts
1677
+ // `refuseNarrowReturnsOfWideDeclaration`), so this only clears the asm rule, never requires it.
1678
+ const own = prototypes[name];
1679
+ const ownWidth = ownReturnWidth(own);
1680
+ const ownReturnFitsAWord = declaresVoidReturn(own) || (ownWidth !== undefined && ownWidth <= 32);
772
1681
  const ARG_REGS = target.argRegs;
773
1682
  const fpu = target.fpu;
774
- const floatReturn = writesFloatReturn(
775
- blocks.flatMap((b) => (b.delay ? [...b.body, b.delay] : b.body)),
776
- FP_ARITH_MNEMONICS,
777
- mipsEvenFpKey,
778
- fpu,
779
- );
1683
+ const bodyInstrs = blocks.flatMap((b) => (b.delay ? [...b.body, b.delay] : b.body));
1684
+ // Which home the function returns in (frontend/fpu.ts `returnHomeRule`). The branches count: a
1685
+ // condition-code branch is how a compare's answer reaches the integer file.
1686
+ const homeRule: ReturnHomeRule = !fpu
1687
+ ? { home: 'integer', declared: false }
1688
+ : returnHomeRule(
1689
+ ownDeclarationOf(name, prototypes, options.contextOwnProto),
1690
+ [...bodyInstrs, ...blocks.flatMap((b) => (b.branch ? [b.branch] : []))].some((ins) =>
1691
+ crossesToIntegers(ins.mnemonic),
1692
+ ),
1693
+ () => writesFloatReturn(bodyInstrs, FP_FLOAT_WRITERS, mipsEvenFpKey, fpu),
1694
+ );
1695
+ const floatReturn = homeRule.home === 'float';
780
1696
 
781
1697
  // The pending `%hi` halves of this function's global addresses, keyed by the SSA VALUE each `lui`
782
1698
  // defines (frontend/high-half.ts holds the invariant and why a register-keyed map cannot answer
@@ -788,15 +1704,64 @@ export function lift(
788
1704
  throw new FrontendUnsupportedError(message);
789
1705
  };
790
1706
  const highHalves = makeHighHalves({ hi: '%hi', hiArticle: 'a', lo: '%lo', fail });
791
- // O32 passes the words past a0..a3 on the stack and returns a word in v0. This lowering builds no
792
- // register pair and receives no struct through a hidden pointer, so the plan refuses both. A call
793
- // through an argument register bounds no arity here: a leading float leaves a0 free.
1707
+ /** The jump-table bases a recovered dispatch reads from outside its own block — the `%lo` that
1708
+ * completes one defines no value but the base (frontend/jump-table.ts). */
1709
+ const tableBases = makeTableBases(
1710
+ [...jts.values()].flatMap((j) => (j.hoisted ? [j.hoisted] : [])),
1711
+ fail,
1712
+ );
1713
+ /** What this frontend defines registers with that is no argument: a pending `%hi` half and a
1714
+ * table base. */
1715
+ const placeholders = {
1716
+ has: (v: Value) => highHalves.has(v) || tableBases.has(v),
1717
+ guardRead: (n: string, r: string, v: Value) => tableBases.guardRead(n, r, highHalves.guardRead(n, r, v)),
1718
+ };
1719
+ // A table entry naming an address no block of this function starts at (another function's
1720
+ // code, read off a table that is not this dispatch's) declines here, before any arm is built.
1721
+ const armBlock = (addr: number): number => {
1722
+ const j = idxOf.get(addr);
1723
+ if (j === undefined) {
1724
+ throw new FrontendUnsupportedError(`cannot lift '${name}': jump-table target is not a block boundary`);
1725
+ }
1726
+ return j;
1727
+ };
1728
+ // What an elided dispatch left at each case target's entry (jump-table.ts `caseEntries`), and the
1729
+ // markers written there, by value. Not a placeholder: a case may copy one (`move v0,at`), and a
1730
+ // return or a call that skipped it would drop what the case handed on.
1731
+ const entries = caseEntries(
1732
+ blocks.flatMap((b, at) => {
1733
+ const jt = b.branch ? jts.get(b.branch.addr) : undefined;
1734
+ return jt === undefined
1735
+ ? []
1736
+ : [
1737
+ {
1738
+ at,
1739
+ scrutReg: jt.scrutReg,
1740
+ cases: jt.caseAddrs.map(armBlock),
1741
+ defaultBlock: armBlock(jt.defaultAddr),
1742
+ leaves: jt.leaves,
1743
+ },
1744
+ ];
1745
+ }),
1746
+ preds,
1747
+ );
1748
+ const staleIdiom = new Map<Value, StaleIdiomRead>();
1749
+ const parked = parkedIn(staleIdiom);
1750
+ const scaledIndex = scaledIndexIn(staleIdiom);
1751
+ // O32 passes the words past a0..a3 on the stack, returns a word in v0, and returns every struct
1752
+ // and union through a hidden pointer in a0 (`aggregateReturn: 'memory'`). This lowering builds no
1753
+ // register pair, so the plan refuses one. A call through an argument register bounds no arity
1754
+ // here: a leading float leaves a0 free.
794
1755
  const calls = callDeclarations(
795
1756
  name,
796
1757
  target,
797
1758
  prototypes,
798
- { pairs: false, memoryReturn: false, stackArgs: true, voidReturn: true, argRegisterBoundsArity: false },
1759
+ { pairs: false, memoryReturn: true, stackArgs: true, voidReturn: true, argRegisterBoundsArity: false },
799
1760
  fail,
1761
+ () => ownDeclarationOf(name, prototypes, options.contextOwnProto),
1762
+ options.calleeBodyBound,
1763
+ options.entryReadCalls,
1764
+ options.stackSized,
800
1765
  );
801
1766
  // The first instruction naming the FPU's file, a data register or the control register. A call
802
1767
  // in a function that has one refuses: which floating-point registers a callee reads, returns in
@@ -806,18 +1771,6 @@ export function lift(
806
1771
  (ins) => /^(cfc1|ctc1)$/i.test(ins.mnemonic) || ins.ops.some((o) => MIPS_FP_REG.test(o)),
807
1772
  );
808
1773
 
809
- // SOUNDNESS GUARD. The word stack-slot model (emitLoad/emitStore) is safe ONLY when every
810
- // sp-relative access in the function is word-width. If a SUB-WORD sp access aliases a word slot
811
- // (`sw a0,4(sp)` then `lbu v0,4(sp)`), routing the word store to an SSA slot while the sub-word
812
- // reload stays on the memory path DROPS the store and reads uninitialised memory — a silent
813
- // miscompile that ALSO masks the struct-overlap loud-fail (raise/structs.ts). So if ANY sub-word
814
- // sp access exists, disable slot-modelling for the whole function: everything sp-relative falls
815
- // back to the memory path, which loud-fails on a genuine overlap instead of miscompiling.
816
- const spSlotSafe = !instrs.some(
817
- (ins) =>
818
- SUBWORD_MEM.has(ins.mnemonic) && ins.ops.length > 0 && isStackPtr(parseMem(ins.ops[ins.ops.length - 1]).base),
819
- );
820
-
821
1774
  // THE FRAME A CALL'S STACK ARGUMENTS ARE STAGED IN: the bytes one `addiu sp,sp,-N` pushed. Its
822
1775
  // outgoing area is `[16, N)`; a word at or above N is this function's own incoming home area, which
823
1776
  // IDO stores a0..a3 to, so it is never an argument of a call made here. Undefined where the
@@ -830,6 +1783,24 @@ export function lift(
830
1783
  parseImm(ins.ops[2] ?? '') < 0,
831
1784
  );
832
1785
  const frameSize = pushes.length === 1 ? -parseImm(pushes[0].ops[2]) : undefined;
1786
+ const depthAt = frameDepths(blocks, entryReentered);
1787
+ // AN INCOMING STACK WORD HAS ONE NAME, `@sarg<k>`, for its loads AND its stores: O32 passes
1788
+ // argument word k >= 4 at entry sp + 16 + 4(k-4), above the caller's 16-byte home of a0..a3, and
1789
+ // a word slot there is keyed by that entry-relative position (`frameDepths`), never by the raw
1790
+ // `sp@<off>` of the depth it was reached at. A function that assigns to its stack parameter (IDO
1791
+ // stores it back in place: `if (a) e = b;` is `beqz a0; sw a1,40(sp)`) is then the same SSA
1792
+ // variable as one that assigns to a register parameter: a read no store reaches is the caller's
1793
+ // argument through the ordinary live-in path, and one a store reaches on some paths is the merge
1794
+ // of the two. The store keeps no memory effect, which is sound because nothing else reads the
1795
+ // word: the caller never reads its outgoing area back (`compilerBehaviors` on all three O32
1796
+ // compilers, measured), a sub-word or FPU access to it turns the slot model off or refuses
1797
+ // (`spSlotSafe`, the FP arm), and taking its address refuses (it is outside the local area).
1798
+ // Undefined for every other word, and wherever the depth walk could not measure the frame.
1799
+ const incomingStackKey = (ins: Instr, off: number): string | undefined => {
1800
+ const depth = depthAt?.get(ins);
1801
+ const entryOff = depth === undefined ? undefined : off - depth;
1802
+ return entryOff === undefined ? undefined : incomingStackArgKey(entryOff, STACK_ARGS_AT, ARG_REGS.length);
1803
+ };
833
1804
 
834
1805
  // THE FRAME'S SAVES. A word store of `ra`, or of a register O32 does not let a callee destroy,
835
1806
  // made while the register still holds what the caller left in it, is a SAVE: the word holds the
@@ -841,7 +1812,73 @@ export function lift(
841
1812
  // epilogue restore in any block.
842
1813
  const saveSlots = new Map<number, string>();
843
1814
  const valueSlots = new Set<number>();
1815
+ // WHETHER EACH OUTGOING-AREA WORD IS PENDING, per block, as lowered so far: a store to it
1816
+ // (`emitStore`) makes it pending, and a call that took it as a stack argument (`lowerJal`'s
1817
+ // `stackWord`) ends it, so it is pending for no later call until it is stored again. Kept per
1818
+ // STORE, not per value: a value one call took and that is stored again for a later call, at the
1819
+ // same word or another, is staged for that call all the same, and keyed by the value it would
1820
+ // hide a word the call is handed (frontend/stackargs.ts ends its pending words per offset too).
1821
+ const outgoingWordState: Map<number, 'stored' | 'taken'>[] = blocks.map(() => new Map());
1822
+ // Whether the state of word `off` some path brings into this point of block `b` is `want`: the
1823
+ // walk `ssa.hasReachingDef` makes, over this state, so a block not filled yet is passed through.
1824
+ // `here` is `b`'s state at this point (its state so far, by default); met again round a loop, `b`
1825
+ // is its state at its end, which a back edge brings.
1826
+ const outgoingWordReaches = (
1827
+ off: number,
1828
+ b: number,
1829
+ want: 'stored' | 'taken',
1830
+ here: ReadonlyMap<number, 'stored' | 'taken'> = outgoingWordState[b],
1831
+ ): boolean => {
1832
+ const seen = new Set<number>();
1833
+ const walk = (at: number, own: 'stored' | 'taken' | undefined): boolean => {
1834
+ if (own !== undefined) {
1835
+ return own === want;
1836
+ }
1837
+ if (seen.has(at)) {
1838
+ return false;
1839
+ }
1840
+ seen.add(at);
1841
+ return preds[at].some((p) => walk(p, outgoingWordState[p].get(off)));
1842
+ };
1843
+ return walk(b, here.get(off));
1844
+ };
1845
+ // EACH CALL ITS CALLEE'S BODY LICENSED, judged again once every block is filled: while a call is
1846
+ // lowered, a block not filled yet — a loop's latch, or the call's own block met round a back
1847
+ // edge — holds no state, so a word the call itself takes is not seen reaching its loop head and a
1848
+ // word staged once before the loop passes for every iteration's. Judged again on the finished
1849
+ // state (frontend/stackargs.ts reaches the same answer by its fixpoint), a word of the block some
1850
+ // path brings in taken, or a word past it some path brings in stored, refuses the lift.
1851
+ const bodyLicences = new Map<
1852
+ Instr,
1853
+ { bi: number; here: ReadonlyMap<number, 'stored' | 'taken'>; n: number; callee: string }
1854
+ >();
1855
+ // EACH CALL A DECLARATION SIZED, AND THE STACK WORDS IT TOOK, judged again the same way: its
1856
+ // arity is the declaration's, so a word past its block says nothing, but a word it takes that
1857
+ // some path brings in already taken by an earlier call — licensed or declared — is a word the
1858
+ // earlier callee owned and may have written, not the value stored there (frontend/stackargs.ts
1859
+ // ends a pending word at the call that takes it for either kind of call).
1860
+ const declaredTakes = new Map<
1861
+ Instr,
1862
+ { bi: number; here: ReadonlyMap<number, 'stored' | 'taken'>; offs: number[]; callee: string }
1863
+ >();
1864
+ const refuseTakenWord = (callee: string, addr: number, off: number): never => {
1865
+ throw new FrontendUnsupportedError(
1866
+ `cannot lift '${name}': outgoing stack arguments not modelled — the call to '${callee}' at ` +
1867
+ `0x${addr.toString(16)} takes the word at ${off}(sp) as a stack argument, but it reaches the call on ` +
1868
+ 'some path already taken by an earlier call and not stored again, so what the earlier callee left there ' +
1869
+ 'is not known',
1870
+ );
1871
+ };
844
1872
  const restores = new Map<Value, { reg: string; addr: number }>();
1873
+ // Each `lw` that restores a save, and the register it restores: a save holds only while every path
1874
+ // restores it after its last write (`refuseSavedRegisterLeaks`).
1875
+ const restoredBy = new Map<Instr, ReadonlySet<string>>();
1876
+ // Every load into the register the compiler discards an unused read into at a level that keeps a
1877
+ // plain one (`compilerBehaviors.discardsUnusedReadsInto`, frontend/dead-reads.ts `discarded`):
1878
+ // IDO's `lw zero`, which `write` drops, so the load's value is recorded here or nowhere.
1879
+ const discarded = new Set<Value>();
1880
+ const discardReg = target.compilerBehaviors.discardsUnusedReadsInto;
1881
+ const discards = (r: string) => discardReg !== undefined && (r === discardReg || (isZero(r) && isZero(discardReg)));
845
1882
  // A `$`-spelt operand is an FPU register or a bare number, and the target's list names neither.
846
1883
  const savedByCallee = (r: string) =>
847
1884
  r === 'ra' || (!r.startsWith('$') && !target.callerSaved.includes(r) && !['zero', 'sp', 'gp'].includes(r));
@@ -854,6 +1891,173 @@ export function lift(
854
1891
  }
855
1892
  };
856
1893
 
1894
+ const code = blocks.flatMap((b) => [...b.body, ...(b.delay ? [b.delay] : [])]);
1895
+ // THE LOADED-WORD BOUND (`compilerBehaviors.outgoingAreaNeverLoadedFrom`, 16 on all three O32
1896
+ // toolchains): the lowest frame word this function LOADS at the frame's own depth from that
1897
+ // offset up, or `Infinity`. No word at or above it is any call's stack argument (the field says
1898
+ // why); below 16 a load proves nothing, since the compilers load back the a1..a3 home words they
1899
+ // stage a by-value aggregate in. Every block here is entry-reachable, so every load counted can
1900
+ // run; a load at another depth names other storage under the same raw offset and is not counted.
1901
+ // Read once, before the frame is partitioned: it caps where an unsized call's outgoing area can
1902
+ // end (`o32OutgoingArea`'s `widest`, which the local area's partition and the owned range of an
1903
+ // uninitialised read take), the a0..a3 guess below weighs only the words beneath it, and the
1904
+ // frame-object audit takes it too (`loadedWords`), counting what the IR does not show the audit:
1905
+ // the slot reloads and the epilogue's `lw ra`.
1906
+ const loadedFrom = target.compilerBehaviors.outgoingAreaNeverLoadedFrom;
1907
+ const loadedWordBound = ((): number => {
1908
+ if (loadedFrom === undefined || frameSize === undefined || depthAt === null) {
1909
+ return Infinity;
1910
+ }
1911
+ let bound = Infinity;
1912
+ for (const ins of code) {
1913
+ const last = ins.ops[ins.ops.length - 1] ?? '';
1914
+ if (!MIPS_FRAME_LOAD.test(ins.mnemonic) || !last.includes('(') || depthAt.get(ins) !== frameSize) {
1915
+ continue;
1916
+ }
1917
+ const { base, off } = parseMem(last);
1918
+ const word = off - (((off % 4) + 4) % 4);
1919
+ if (isStackPtr(base) && word >= loadedFrom) {
1920
+ bound = Math.min(bound, word);
1921
+ }
1922
+ }
1923
+ return bound;
1924
+ })();
1925
+
1926
+ // THE LOCAL AREA, where a function takes a frame address (`measureO32Frame`): every `off(sp)`
1927
+ // access inside it is MEMORY, read and written through an `laddr` the frame-object audit then
1928
+ // proves, and every one outside it keeps the slot model below. Null where no frame address is
1929
+ // taken, or the frame is one this cannot partition, and then every capture keeps the
1930
+ // sp-as-data refusal.
1931
+ /** The stack words a `jal`'s callee takes, by its declaration; null where nothing declares it. */
1932
+ const stackWordsOf = (callee: string | undefined): number | null => {
1933
+ const declared = callee === undefined ? null : calls.declaredCall(callee);
1934
+ return declared === null ? null : Math.max(0, wordsOf(declared.widths, target) - ARG_REGS.length);
1935
+ };
1936
+ const frame = measureO32Frame({
1937
+ name,
1938
+ code,
1939
+ entry: [...blocks[0].body, ...(blocks[0].delay ? [blocks[0].delay] : [])],
1940
+ depthAt,
1941
+ frameSize,
1942
+ rule: target.compilerBehaviors.o32LocalArea,
1943
+ stackWordsOf,
1944
+ savedByCallee,
1945
+ splat,
1946
+ loadedWordBound,
1947
+ });
1948
+ /** Whether `off(sp)` at `ins` is an access inside the local area, refusing one that straddles its
1949
+ * edge: half memory and half slot is neither. */
1950
+ const inLocalArea = (ins: Instr, off: number, width: number): boolean => {
1951
+ // The area is in post-push offsets, and `off` is the access's own: before the push (IDO's
1952
+ // leaves load first) or after the pop it names the byte `size - depth` higher, so an incoming
1953
+ // stack word read at entry as `lw t6,16(sp)` is not the local a later `0(sp)` stores, whatever
1954
+ // the raw numbers say. `measureO32Frame` refuses an access at another depth whose TRUE position
1955
+ // lies in the area, so one at another depth is never in it.
1956
+ if (frame === null || depthAt?.get(ins) !== frame.size) {
1957
+ return false;
1958
+ }
1959
+ if (off >= frame.area.to || off + width <= frame.area.from) {
1960
+ return false;
1961
+ }
1962
+ if (off < frame.area.from || off + width > frame.area.to) {
1963
+ throw new FrontendUnsupportedError(
1964
+ `cannot lift '${name}': address-taken stack local — '${ins.mnemonic}' at 0x${ins.addr.toString(16)} reaches across an edge of the ` +
1965
+ `local area, sp+${frame.area.from} up to sp+${frame.area.to} — part memory, part slot`,
1966
+ );
1967
+ }
1968
+ // …and in a relift, only the bytes the first audit asked to keep as one object are memory: a
1969
+ // word of the area outside them is one it handed back to the slot model (`keptInRelift`)
1970
+ return (
1971
+ oneObject === undefined ||
1972
+ keptInRelift(oneObject, off, width, () => `'${ins.mnemonic}' at 0x${ins.addr.toString(16)}`, name)
1973
+ );
1974
+ };
1975
+
1976
+ // SOUNDNESS GUARD. The word stack-slot model (emitLoad/emitStore) is safe ONLY when every
1977
+ // sp-relative access it keys is word-width. If a SUB-WORD sp access aliases a word slot
1978
+ // (`sw a0,4(sp)` then `lbu v0,4(sp)`), routing the word store to an SSA slot while the sub-word
1979
+ // reload stays on the memory path DROPS the store and reads uninitialised memory — a silent
1980
+ // miscompile that ALSO masks the struct-overlap loud-fail (raise/structs.ts). So if ANY sub-word
1981
+ // sp access exists OUTSIDE the local area, disable slot-modelling for the whole function:
1982
+ // everything sp-relative falls back to the memory path, which loud-fails on a genuine overlap
1983
+ // instead of miscompiling. One inside it is memory like every access there (`inLocalArea`).
1984
+ const spSlotSafe = !code.some((ins) => {
1985
+ if (!SUBWORD_MEM.has(ins.mnemonic) || ins.ops.length === 0) {
1986
+ return false;
1987
+ }
1988
+ const { base, off } = parseMem(ins.ops[ins.ops.length - 1]);
1989
+ return isStackPtr(base) && !inLocalArea(ins, off, ins.mnemonic.includes('b') ? 1 : 2);
1990
+ });
1991
+
1992
+ // THE FRAME HALF of the live-in model (`LiveInModel.ownedLocals`), in the raw `sp@<off>` keys
1993
+ // the slot model uses: the words of the one frame this function pushed that it OWNS, so that a
1994
+ // def-less read of one is an uninitialised local — IDO homes `int r; if (a > 0) r = a*2;` in
1995
+ // `4(sp)` of an 8-byte leaf frame and reads it back on the path that never stored it. A word no
1996
+ // store reaches on ANY path is the same local (`uninit_spill` reads `36(sp)` and `0(sp)`, which
1997
+ // no instruction stores), as on Thumb (`lowerLoad`).
1998
+ //
1999
+ // WHAT ELSE COULD HAVE WRITTEN THE WORD is the whole soundness of it, and the claim is made only
2000
+ // where the answer is "nothing":
2001
+ // • another function holding a frame address — none is taken (`takesFrameAddress`), so the
2002
+ // frame-object audit has nothing to retract;
2003
+ // • a CALLEE, which O32 lets write the bottom of this frame: it stores its a0..a3 into the
2004
+ // home words `[0,16)` (IDO: `sw a2,40(sp)` in its own frame) and assigns a stack parameter in
2005
+ // place (`compilerBehaviors.localsAboveOutgoingArea`'s probe). So a function that calls owns
2006
+ // only the words past its widest call's outgoing area, `[16 + 4·words, frameSize)`. One whose
2007
+ // callee no declaration sizes owns the words from the loaded-word bound up, which no call's
2008
+ // stack argument reaches (`o32OutgoingArea`'s `widest`), and claims nothing where it loads
2009
+ // no frame word from 16 up. A leaf owns the whole frame;
2010
+ // • the same raw offset at ANOTHER depth — the key is the raw offset, so a word reached before
2011
+ // the push or after the pop names other storage under the same spelling. Every `sp` access
2012
+ // must run at the frame's own depth, save the incoming stack words that are keyed `@sarg<k>`.
2013
+ // The caller's home `[frameSize, frameSize + 16)` is in no range, so a def-less read there still
2014
+ // refuses. Sub-word frame traffic turns the slot model off (`spSlotSafe`) and claims nothing.
2015
+ //
2016
+ // A RELIFT takes a frame address by construction, and claims the local area alone: there the
2017
+ // bytes kept as one object are memory and key no slot, and every word outside them is one the
2018
+ // first audit handed back (`reliftRange`) only where no escape is unbounded, no copy reaches it
2019
+ // and no index runs, so nothing but its own direct accesses reaches it. kmc homes `u32 r; if (i)
2020
+ // r = gA[9]; … if (i) gA[10] = r;` in such a word beside two copied arrays, and reads it on the
2021
+ // path that never stored it.
2022
+ let ownedMemo: { range: { from: number; to: number } | undefined } | undefined;
2023
+ const ownedFrame = (): { from: number; to: number } | undefined =>
2024
+ (ownedMemo ??= { range: measureOwnedFrame() }).range;
2025
+ const measureOwnedFrame = (): { from: number; to: number } | undefined => {
2026
+ if (frameSize === undefined || depthAt === null || !spSlotSafe) {
2027
+ return undefined;
2028
+ }
2029
+ if (oneObject === undefined ? code.some(takesFrameAddress) : frame === null) {
2030
+ return undefined;
2031
+ }
2032
+ for (const ins of code) {
2033
+ const last = ins.ops[ins.ops.length - 1] ?? '';
2034
+ if (!last.includes('(') || !isStackPtr(parseMem(last).base) || depthAt.get(ins) === frameSize) {
2035
+ continue;
2036
+ }
2037
+ if (ins.mnemonic === 'lw' || ins.mnemonic === 'sw') {
2038
+ if (incomingStackKey(ins, parseMem(last).off) !== undefined) {
2039
+ continue;
2040
+ }
2041
+ }
2042
+ return undefined;
2043
+ }
2044
+ // A declaration the call lowering cannot carry out refuses THERE, in its own words; measured
2045
+ // here first, it claims nothing rather than pre-empting that refusal with another.
2046
+ let outgoing: ReturnType<typeof o32OutgoingArea>;
2047
+ try {
2048
+ outgoing = o32OutgoingArea(code, stackWordsOf, splat, loadedWordBound);
2049
+ } catch (e) {
2050
+ if (e instanceof FrontendUnsupportedError) {
2051
+ return undefined;
2052
+ }
2053
+ throw e;
2054
+ }
2055
+ const from = Math.max(outgoing.widest, oneObject === undefined || frame === null ? 0 : frame.area.from);
2056
+ const to = oneObject === undefined || frame === null ? frameSize : Math.min(frameSize, frame.area.to);
2057
+ return from < to ? { from, to } : undefined;
2058
+ };
2059
+ ownedFrameOf = ownedFrame;
2060
+
857
2061
  // Hardware divide state (capabilities.hwDivide). MIPS `div`/`divu rs,rt` set the hi/lo pair
858
2062
  // implicitly; a later `mflo`/`mfhi` reads the quotient/remainder. FUNCTION-scoped: GCC schedules
859
2063
  // the `mflo` into a SEPARATE block after the trap-check branch (`bnez rt; break 7`), so a
@@ -862,9 +2066,39 @@ export function lift(
862
2066
  // overwrites the same hi/lo pair, so it clears this (below) — whichever divide/multiply ran most
863
2067
  // recently owns the next `mf*`, the true hardware semantics.
864
2068
  let divState: { rs: Value; rt: Value; signed: boolean } | null = null;
2069
+ // The frame offset each `laddr` this lift emitted names, and the deferred `sp + index` sums
2070
+ // (`emitLaddr`, `materialize`) — function-scoped, since a register holding one crosses blocks.
2071
+ const laddrAt = new Map<Value, number>();
2072
+ // …and of those, the ones a direct `off(sp)` access in the local area was lowered to, not a capture
2073
+ const directAccesses = new Set<Value>();
2074
+ const frameSums = new Map<Value, FrameSum>();
2075
+ // Every capture this lift moved into another (`addiu rD,rCapture,k` re-minted as the `laddr` of
2076
+ // the sum, or `addu` deferring one into a `FrameSum`), handed to the audit as Thumb hands its
2077
+ // own: a base only the move read names no object, so the audit drops it rather than judging a
2078
+ // dead `laddr` as a capture (`foldMovedCaptures`).
2079
+ const movedCaptures = new Set<Value>();
865
2080
 
2081
+ /** Every return, as it is emitted: `settleReturnHome` may hand it the float home instead. */
2082
+ const returns: { op: Op; bi: number }[] = [];
2083
+ /** The words an `mtc1` moved into an FPU register (`emitFpMove`). */
2084
+ const movedIn = new Set<Value>();
866
2085
  const fillBlock = (b: MipsBlock, bi: number) => {
867
2086
  const ops = irBlocks[bi].ops;
2087
+ // A case target of an elided dispatch is entered with what the idiom left in its registers.
2088
+ const entry = entries.get(bi);
2089
+ if (entry !== undefined) {
2090
+ enterCaseTarget(
2091
+ entry,
2092
+ ops,
2093
+ {
2094
+ key: (r) => r,
2095
+ scrutinee: (r) => readVar(r, bi),
2096
+ dispatchScrutinee: (r) => ssa.readAtEnd(r, entry.at),
2097
+ write: (r, v) => writeVar(r, bi, v),
2098
+ },
2099
+ staleIdiom,
2100
+ );
2101
+ }
868
2102
  // Materialise the address of a named global — the same `gaddr` op the Thumb frontend emits; the
869
2103
  // structurer lowers a load/store through it to `SYM` (scalar) or `((T *)&SYM)[i]` (aggregate).
870
2104
  const emitGaddr = (sym: string): Value => {
@@ -877,15 +2111,23 @@ export function lift(
877
2111
  return constVal(0);
878
2112
  }
879
2113
  // Reading `sp` as a DATA operand means frame-pointer arithmetic or an address-taken local
880
- // (`addiu a0,sp,8` = `&local`) — not modellable without a stack abstraction. Fabricating a
881
- // value for `sp` invents a PHANTOM leading parameter that shifts every real argument — a
882
- // silent miscompile. Fail LOUD instead, mirroring the PPC frontend's r1. Frame setup/teardown
883
- // (`addiu sp,sp,±N`) and word spill/reload slots are handled before `read`.
2114
+ // (`addiu a0,sp,8` = `&local`). Fabricating a value for `sp` invents a PHANTOM leading
2115
+ // parameter that shifts every real argument — a silent miscompile. Fail LOUD instead,
2116
+ // mirroring the PPC frontend's r1. Frame setup/teardown (`addiu sp,sp,±N`), word spill/reload
2117
+ // slots and, where `measureO32Frame` drew a local area, frame captures are handled before `read`.
884
2118
  if (isStackPtr(r)) {
885
2119
  throw new FrontendUnsupportedError(
886
2120
  `cannot lift '${name}': stack pointer used as data (address-taken local / frame arithmetic) — local stack frames not supported`,
887
2121
  );
888
2122
  }
2123
+ // A KERNEL REGISTER (`target.kernelRegs`) holds nothing this function wrote: an exception
2124
+ // handler may overwrite it between any two instructions.
2125
+ if (isKernelReg(target.kernelRegs ?? [], r)) {
2126
+ throw new FrontendUnsupportedError(
2127
+ `cannot lift '${name}': ${r} read as a value — the ABI reserves it to the kernel, and an exception ` +
2128
+ 'handler may overwrite it at any instruction',
2129
+ );
2130
+ }
889
2131
  // `gp` writes (the PIC prologue) are skipped as transparent; a `gp` READ that survives here is a
890
2132
  // PIC/small-data global access (`lw x,off(gp)`) this frontend does not model — the recovered
891
2133
  // jump-table dispatch's own `lw …,0(gp)` is elided, so reaching this is a genuine decline, not a
@@ -895,9 +2137,14 @@ export function lift(
895
2137
  `cannot lift '${name}': gp used as data (PIC / small-data global access) — not supported`,
896
2138
  );
897
2139
  }
898
- // A register holding the high half of an address is NOT a value. The legitimate consumers
899
- // (`addiu %lo`, a `%lo` load/store base) reach the half through `foldLoHalf` instead.
2140
+ // A register holding the high half of an address is NOT a value. Its legitimate consumers
2141
+ // (`addiu %lo`, a `%lo` load/store base, and `addu` adding a runtime index between the
2142
+ // halves) reach the half without this read.
900
2143
  const v = readVar(r, bi);
2144
+ const sum = frameSums.get(v);
2145
+ if (sum !== undefined) {
2146
+ return materialize(sum, 0);
2147
+ }
901
2148
  const restore = restores.get(v);
902
2149
  if (restore) {
903
2150
  throw new FrontendUnsupportedError(
@@ -905,8 +2152,40 @@ export function lift(
905
2152
  `caller's ${restore.reg} into it — no C value names it`,
906
2153
  );
907
2154
  }
908
- return highHalves.guardRead(name, r, v);
2155
+ return placeholders.guardRead(name, r, v);
909
2156
  };
2157
+ // A FRAME ADDRESS AS DATA. `addiu rD,sp,k` and `move rD,sp` name frame offset k, and lower to
2158
+ // an `laddr` the frame-object audit proves. `addu rD,sp,rX` — in either operand order — names
2159
+ // `sp + rX`, a runtime index from the frame base, and that sum is DEFERRED: the access through
2160
+ // it says which object it indexes, since `lw v0,24(v0)` after `addu v0,sp,t6` is `a[i]` on an
2161
+ // array at sp+24 (IDO), not at sp+0. So the register holds a `FrameSum`, and each use builds
2162
+ // `laddr(base + displacement) + index` there — an access at its displacement, any other use at
2163
+ // 0. A capture or sum moved by a constant is the same capture at the sum. Where a frame address
2164
+ // lies against the local area is the shared audit's to judge (`FRAME_OBJECT_AUDIT`,
2165
+ // frontend/frame-objects.ts `frameObjects`), as on Thumb: below it or above it refuses.
2166
+ const emitLaddr = (k: number): Value => {
2167
+ const v = mkValue(T.unk(32));
2168
+ ops.push(mkOp('laddr', { results: [v], attrs: { off: k } }));
2169
+ laddrAt.set(v, k);
2170
+ return v;
2171
+ };
2172
+ const materialize = (sum: FrameSum, displacement: number): Value => {
2173
+ const v = mkValue(T.unk(32));
2174
+ ops.push(mkOp('add', { operands: [emitLaddr(sum.base + displacement), sum.index], results: [v] }));
2175
+ return v;
2176
+ };
2177
+ const deferSum = (base: number, index: Value): Value => {
2178
+ const v = mkValue(T.unk(32));
2179
+ frameSums.set(v, { base, index });
2180
+ return v;
2181
+ };
2182
+ // What a register holds when it may be a frame address, unguarded — `read` then guards and
2183
+ // materializes everything else.
2184
+ const rawFrame = (r: string | undefined): Value | undefined =>
2185
+ frame === null || r === undefined || isZero(r) || isStackPtr(r) || r === 'gp' ? undefined : readVar(r, bi);
2186
+ /** Whether `r` holds a pending `%hi` half here (indexed or not), unguarded. */
2187
+ const halfIn = (r: string | undefined): boolean =>
2188
+ r !== undefined && !isZero(r) && !isStackPtr(r) && r !== 'gp' && highHalves.has(readVar(r, bi));
910
2189
  // `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
911
2190
  //
912
2191
  // Keyed by the SSA VALUE the compare produced, never by its register. Keying by register is the
@@ -937,6 +2216,10 @@ export function lift(
937
2216
  // DELIBERATELY (unlike divState): a cross-block `mult`/`mflo` pair has no observed inhabitant,
938
2217
  // and the miss degrades to a LOUD opaque, never silence.
939
2218
  let mulState: { rs: Value; rt: Value; signed: boolean } | null = null;
2219
+ // The FP condition code a `c.cond` set in THIS block, as the compare it holds. Block-local like
2220
+ // `mulState`: every compare and branch the compilers emit sit in one block, and a branch with
2221
+ // none set here refuses rather than read a code across an edge.
2222
+ let fcc: { opcode: Opcode; lhs: Value; rhs: Value; value: Value } | null = null;
940
2223
  const emitCmp = (opc: Opcode, d: string, lhs: Value, rhs: Value) => {
941
2224
  const v = mkValue(T.unk(32));
942
2225
  ops.push(mkOp(opc, { operands: [lhs, rhs], results: [v] }));
@@ -950,6 +2233,14 @@ export function lift(
950
2233
  // Set by every case entitled to fold a relocation, cleared per instruction, and answered by
951
2234
  // `relocPlaceholder`.
952
2235
  let relocTaken = false;
2236
+ // The words loads into the discard register read whose pair half has not come yet (`emitLoad`):
2237
+ // each one's base register, or the symbol a relocation names, and its offset. IDO's -O1
2238
+ // scheduler puts frame reloads, argument loads and stores between the halves, a store through
2239
+ // the pair's own base among them (`s->a; s->b = x; g(x);` is `lw zero,0(t6); sw t7,8(t6);
2240
+ // lw a0,28(sp); jal g; lw at,4(t6)`). No access makes the `at` word anything but the pair half
2241
+ // — IDO never gives `at` a C value — so a word stays pending across every one of them. What ends
2242
+ // one: a write to its base register, which moves the address the half would read, and a call.
2243
+ let firstWords: { reg: string | undefined; sym: string | undefined; off: number }[] = [];
953
2244
  // EVERY immediate this frontend turns into a value goes through here. objdump prints them as
954
2245
  // numbers, so a non-numeric one means the operand is not an immediate at all — a relocation
955
2246
  // spelling the reader failed to resolve, a label — and bare `parseImm` answers NaN, which
@@ -968,20 +2259,33 @@ export function lift(
968
2259
  const decode = (ins: Instr) => {
969
2260
  const [d, s, t] = ins.ops;
970
2261
  relocTaken = false;
2262
+ if (ins.mnemonic.startsWith('jal')) {
2263
+ firstWords = [];
2264
+ } else if (ins.mnemonic !== 'lw') {
2265
+ const w = writtenReg(ins);
2266
+ firstWords = firstWords.filter((f) => f.reg === undefined || f.reg !== w);
2267
+ }
971
2268
  // The GOT/small-data base register `gp` is set up by IDO's PIC prologue (`lui gp; addiu gp,gp,lo;
972
2269
  // addu gp,gp,t9`, an `_gp_disp` HI16/LO16 pair). It is a RELOCATION base, never program data — and
973
2270
  // reading `t9` for the `addu` would fabricate a phantom leading parameter (like the sp/r1 guards).
974
2271
  // Its only real use, the GOT table-base `lw ...,0(gp)` in a jump-table dispatch, is subsumed by the
975
2272
  // recovered `switch_br` (that block is elided). So a write to `gp` is transparent: skip it.
976
- if (destReg(ins) === 'gp') {
2273
+ if (writtenReg(ins) === 'gp') {
977
2274
  return;
978
2275
  }
979
2276
  switch (ins.mnemonic) {
980
2277
  case 'nop':
981
2278
  break;
982
- case 'move':
983
- write(d, read(s));
984
- break; // pseudo: addu/or rD,rS,zero
2279
+ case 'move': {
2280
+ // pseudo: addu/or rD,rS,zero
2281
+ if (frame !== null && isStackPtr(s)) {
2282
+ write(d, emitLaddr(0));
2283
+ break;
2284
+ }
2285
+ const raw = rawFrame(s);
2286
+ write(d, raw !== undefined && frameSums.has(raw) ? raw : read(s));
2287
+ break;
2288
+ }
985
2289
  case 'li':
986
2290
  write(d, constVal(imm(ins, s)));
987
2291
  break; // pseudo: load immediate
@@ -1030,10 +2334,39 @@ export function lift(
1030
2334
  if (isStackPtr(d)) {
1031
2335
  break;
1032
2336
  }
2337
+ if (frame !== null && ins.reloc === undefined) {
2338
+ if (isStackPtr(s)) {
2339
+ write(d, emitLaddr(imm(ins, t)));
2340
+ break;
2341
+ }
2342
+ const raw = rawFrame(s)!;
2343
+ const at = raw === undefined ? undefined : laddrAt.get(raw);
2344
+ if (at !== undefined) {
2345
+ movedCaptures.add(raw!);
2346
+ write(d, emitLaddr(at + imm(ins, t)));
2347
+ break;
2348
+ }
2349
+ const sum = raw === undefined ? undefined : frameSums.get(raw);
2350
+ if (sum !== undefined) {
2351
+ write(d, deferSum(sum.base + imm(ins, t), sum.index));
2352
+ break;
2353
+ }
2354
+ }
1033
2355
  // `addiu rD, rHi, %lo(SYM)` completes a global's address begun by a `lui %hi(SYM)`:
1034
2356
  // rD = &SYM (+ a byte offset into it). Emits the shared `gaddr` op.
1035
2357
  if (ins.reloc?.type === 'R_MIPS_LO16') {
1036
- const { base: g, off } = foldLoHalf(ins, s, imm(ins, t));
2358
+ const at = pairLoHalf(ins, s, imm(ins, t));
2359
+ // The base of a jump table a dispatch reads from outside its block is no value but the
2360
+ // table base (frontend/jump-table.ts), which only that dispatch may take — and a table
2361
+ // base is never indexed.
2362
+ const table = { sym: ins.reloc.sym, addend: at.off };
2363
+ if (at.index === undefined && tableBases.wants(table)) {
2364
+ const base = mkValue(T.unk(32));
2365
+ tableBases.record(base, table, ins.addr);
2366
+ write(d, base);
2367
+ break;
2368
+ }
2369
+ const { base: g, off } = addressAt(ins, at);
1037
2370
  // `&SYM` (offset 0), or `&SYM + N` for a byte offset into the global. The `add` tree is
1038
2371
  // folded byte-correctly by memAccess when this address is a load/store base; if it
1039
2372
  // instead ESCAPES as a value, `assertDerefsTyped` declines it (the byte offset would
@@ -1048,9 +2381,47 @@ export function lift(
1048
2381
  emitBin('add', d, read(s), constVal(imm(ins, t)));
1049
2382
  break;
1050
2383
  case 'addu':
1051
- case 'add':
2384
+ case 'add': {
2385
+ // `addu rD,sp,zero` is Splat's spelling of objdump's `move rD,sp`: the frame base.
2386
+ if (frame !== null && isStackPtr(copiedReg(ins) ?? '')) {
2387
+ write(d, emitLaddr(0));
2388
+ break;
2389
+ }
2390
+ if (frame !== null && t !== undefined && isStackPtr(s) !== isStackPtr(t) && !isZero(isStackPtr(s) ? t : s)) {
2391
+ write(d, deferSum(0, read(isStackPtr(s) ? t : s)));
2392
+ break;
2393
+ }
2394
+ const [rs, rt] = [rawFrame(s), rawFrame(t)];
2395
+ const capturedAt = (v: Value | undefined) => (v === undefined ? undefined : laddrAt.get(v));
2396
+ const [cs, ct] = [capturedAt(rs), capturedAt(rt)];
2397
+ if ((cs === undefined) !== (ct === undefined)) {
2398
+ movedCaptures.add((cs === undefined ? rt : rs)!);
2399
+ write(d, deferSum((cs ?? ct)!, read(cs === undefined ? s : t)));
2400
+ break;
2401
+ }
2402
+ // `lui rH,%hi(S); addu rH,rH,idx; l*/s* x,%lo(S)(rH)` is ido's, KMC's and GCC 2.7.2's
2403
+ // `S[i]`: a runtime byte index added BETWEEN the halves. rD is still no value — it is an
2404
+ // indexed half (frontend/high-half.ts `index`) that only its `%lo` completes, to
2405
+ // `gaddr(S) + idx` at the halves' combined offset. Either operand order is accepted, as
2406
+ // `addu` commutes (every compiler measured puts rH first). Two halves added together
2407
+ // refuse: two addresses added is not an address. A COPY (`addu rD,rH,zero`) is no index:
2408
+ // it reads the half as a value and refuses, as `move` and `or rD,rH,zero` do.
2409
+ const [hs, ht] = copiedReg(ins) === undefined ? [halfIn(s), halfIn(t)] : [false, false];
2410
+ if (hs && ht) {
2411
+ throw new FrontendUnsupportedError(
2412
+ `${site(ins)} adds a high half to another high half — the sum of two addresses is not an address this frontend can write down`,
2413
+ );
2414
+ }
2415
+ if (hs || ht) {
2416
+ const idx = read(hs ? t! : s);
2417
+ const v = mkValue(T.unk(32));
2418
+ highHalves.index(v, readVar(hs ? s : t!, bi), idx, { addr: ins.addr, mnemonic: ins.mnemonic });
2419
+ write(d, v);
2420
+ break;
2421
+ }
1052
2422
  emitBin('add', d, read(s), read(t));
1053
2423
  break;
2424
+ }
1054
2425
  case 'subu':
1055
2426
  case 'sub':
1056
2427
  emitBin('sub', d, read(s), read(t));
@@ -1108,6 +2479,10 @@ export function lift(
1108
2479
  emitBin('and', d, read(s), constVal(imm(ins, t)));
1109
2480
  break;
1110
2481
  case 'or':
2482
+ if (frame !== null && isStackPtr(copiedReg(ins) ?? '')) {
2483
+ write(d, emitLaddr(0)); // `or rD,sp,zero`, IDO's spelling of the same move
2484
+ break;
2485
+ }
1111
2486
  isZero(t) ? write(d, read(s)) : emitBin('or', d, read(s), read(t));
1112
2487
  break;
1113
2488
  case 'ori':
@@ -1219,6 +2594,24 @@ export function lift(
1219
2594
  case 'mov.d':
1220
2595
  emitFpArith(ins);
1221
2596
  break;
2597
+ case 'mfc1':
2598
+ case 'mtc1':
2599
+ emitFpMove(ins);
2600
+ break;
2601
+ case 'trunc.w.s':
2602
+ case 'trunc.w.d':
2603
+ case 'cvt.s.w':
2604
+ case 'cvt.d.w':
2605
+ emitFpConvert(ins);
2606
+ break;
2607
+ case 'c.eq.s':
2608
+ case 'c.lt.s':
2609
+ case 'c.le.s':
2610
+ case 'c.eq.d':
2611
+ case 'c.lt.d':
2612
+ case 'c.le.d':
2613
+ emitFpCompare(ins);
2614
+ break;
1222
2615
  default:
1223
2616
  emitOpaqueDest(ins);
1224
2617
  break; // unmodelled: an honest opaque, never a silent drop
@@ -1312,31 +2705,112 @@ export function lift(
1312
2705
  const [dst, ...srcs] = keys as string[];
1313
2706
  const { op } = FP_ARITH[ins.mnemonic];
1314
2707
  if (op === 'copy') {
1315
- write(dst, read(srcs[0]));
2708
+ write(dst, readFloat(ins, srcs[0]));
1316
2709
  return;
1317
2710
  }
1318
2711
  const v = mkValue(fpType);
1319
- ops.push(mkOp(op, { operands: srcs.map(read), results: [v] }));
2712
+ ops.push(mkOp(op, { operands: srcs.map((k) => readFloat(ins, k)), results: [v] }));
1320
2713
  write(dst, v);
1321
2714
  };
2715
+ // AN FPU REGISTER HOLDS A FLOAT OR AN INTEGER WORD, and which is a fact about the instruction
2716
+ // that last wrote it: `trunc.w` and `mtc1` leave a word, everything else a float. Each read says
2717
+ // which it expects and refuses the other, so a word is never computed on as a float, nor a
2718
+ // float's bits moved out as an integer.
2719
+ const readFloat = (ins: Instr, key: string): Value => {
2720
+ const v = read(key);
2721
+ if (v.type.kind !== 'float') {
2722
+ throw new FrontendUnsupportedError(`${site(ins)} reads ${key} as a float, and it holds an integer word`);
2723
+ }
2724
+ return v;
2725
+ };
2726
+ const readWord = (ins: Instr, key: string): Value => {
2727
+ const v = read(key);
2728
+ if (v.type.kind === 'float') {
2729
+ throw new FrontendUnsupportedError(
2730
+ `${site(ins)} reads ${key} as an integer word, and it holds a float — moving a float's bits is not modelled`,
2731
+ );
2732
+ }
2733
+ return v;
2734
+ };
2735
+ // The keys of an instruction's FPU operands, or the register-file refusal where it names an odd
2736
+ // half or the target has no float homes.
2737
+ const fpOperands = (ins: Instr, at: readonly number[]): string[] | null => {
2738
+ const keys = at.map((i) => mipsEvenFpKey(ins.ops[i] ?? ''));
2739
+ if (fpu === undefined || keys.some((k) => k === null)) {
2740
+ emitOpaqueDest(ins);
2741
+ return null;
2742
+ }
2743
+ if (fpType === null) {
2744
+ refuseBothPrecisions(name, ins.mnemonic);
2745
+ }
2746
+ return keys as string[];
2747
+ };
2748
+ // `mfc1 rt, fs` / `mtc1 rt, fs`: an integer word between the files, unchanged. Which words came
2749
+ // IN is kept for `settleReturnHome`: the IR holds both homes as one value.
2750
+ const emitFpMove = (ins: Instr) => {
2751
+ const keys = fpOperands(ins, [1]);
2752
+ if (keys === null) {
2753
+ return;
2754
+ }
2755
+ if (ins.mnemonic === 'mfc1') {
2756
+ write(ins.ops[0], readWord(ins, keys[0]));
2757
+ } else {
2758
+ const v = read(ins.ops[0]);
2759
+ movedIn.add(v);
2760
+ write(keys[0], v);
2761
+ }
2762
+ };
2763
+ const emitFpConvert = (ins: Instr) => {
2764
+ const keys = fpOperands(ins, [0, 1]);
2765
+ if (keys === null) {
2766
+ return;
2767
+ }
2768
+ const { op } = FP_CONVERT[ins.mnemonic];
2769
+ const toInt = op === 'fptosi';
2770
+ const v = mkValue(toInt ? T.unk(32) : fpType!);
2771
+ ops.push(mkOp(op, { operands: [toInt ? readFloat(ins, keys[1]) : readWord(ins, keys[1])], results: [v] }));
2772
+ write(keys[0], v);
2773
+ };
2774
+ const emitFpCompare = (ins: Instr) => {
2775
+ const keys = fpOperands(ins, [0, 1]);
2776
+ if (keys === null) {
2777
+ return;
2778
+ }
2779
+ const opcode = FP_COMPARE[ins.mnemonic].op;
2780
+ const [lhs, rhs] = keys.map((k) => readFloat(ins, k));
2781
+ const value = mkValue(T.unk(32));
2782
+ ops.push(mkOp(opcode, { operands: [lhs, rhs], results: [value] }));
2783
+ fcc = { opcode, lhs, rhs, value };
2784
+ };
1322
2785
  // `%lo` completes the address a `lui %hi` began. The pairing asks SSA — through `readVar`, not
1323
2786
  // `read`, which refuses a half — what `rHi` holds HERE, so a pair separated by unrelated
1324
2787
  // instructions folds while a register reused between the halves does not, and a half that
1325
2788
  // arrives only through a merge or a loop header comes back as the block parameter standing for
1326
2789
  // the join, which is not a half and refuses.
1327
- const foldLoHalf = (ins: Instr, rHi: string, loImm: number): { base: Value; off: number } => {
2790
+ const foldLoHalf = (ins: Instr, rHi: string, loImm: number): { base: Value; off: number } =>
2791
+ addressAt(ins, pairLoHalf(ins, rHi, loImm));
2792
+ /** `&SYM + off` (`+ index` for an indexed half), for the `%lo` at `ins` that completed it. `off`
2793
+ * may be negative: `S[i - k]` puts the constant element in the `%lo`, whether the index is added
2794
+ * between the halves (`lw v0,-4(v0)`, ido, KMC and GCC 2.7.2) or after a base completed first
2795
+ * (`addiu v0,v0,%lo(S)-4; addu v0,a0,v0`, KMC and GCC 2.7.2), and the address is exactly
2796
+ * `S + off` either way. */
2797
+ const addressAt = (ins: Instr, { off, index }: { off: number; index?: Value }): { base: Value; off: number } => {
2798
+ const g = emitGaddr(ins.reloc!.sym);
2799
+ if (index === undefined) {
2800
+ return { base: g, off };
2801
+ }
2802
+ const v = mkValue(T.unk(32));
2803
+ ops.push(mkOp('add', { operands: [g, index], results: [v] }));
2804
+ return { base: v, off };
2805
+ };
2806
+ /** The byte offset into its symbol of the address the `%lo` at `ins` completes, the pair proven,
2807
+ * and the runtime byte index an indexed half added between the halves. */
2808
+ const pairLoHalf = (ins: Instr, rHi: string, loImm: number): { off: number; index?: Value } => {
1328
2809
  relocTaken = true;
1329
- const lo = ins.reloc!;
1330
- const hi = highHalves.pair(site(ins), rHi, readVar(rHi, bi), lo.sym);
2810
+ // `addressAt` adds the index back, so this consumer takes an indexed half.
2811
+ const hi = highHalves.pair(site(ins), rHi, readVar(rHi, bi), ins.reloc!.sym, undefined, true);
1331
2812
  // The addend is split across the two instruction immediates because MIPS is REL.
1332
- const off = hi.addend + loImm;
1333
- if (off < 0) {
1334
- throw new FrontendUnsupportedError(
1335
- `${site(ins)} completes '${lo.sym}' at ` +
1336
- `byte offset ${off} — an address BELOW the symbol (an index-biased array base) is not yet modelled`,
1337
- );
1338
- }
1339
- return { base: emitGaddr(lo.sym), off };
2813
+ return { off: hi.addend + loImm, index: hi.index };
1340
2814
  };
1341
2815
  // A displacement memory operand whose base is a `%hi` half: the global's address plus the
1342
2816
  // access offset. A non-`%lo` instruction returns null (the caller falls through to the ordinary
@@ -1348,15 +2822,70 @@ export function lift(
1348
2822
  const { off, base } = parseMem(mem);
1349
2823
  return foldLoHalf(ins, base, off);
1350
2824
  };
2825
+ // The address an access in the local area, or through a frame sum, is made at — undefined for
2826
+ // every other access. A register the prologue SAVES may not be stored there: the save area is
2827
+ // outside it by construction, so a save inside it is a frame this does not describe.
2828
+ const frameAccess = (ins: Instr, base: string, off: number, width: number, stored?: string) => {
2829
+ if (isStackPtr(base)) {
2830
+ if (!inLocalArea(ins, off, width)) {
2831
+ return undefined;
2832
+ }
2833
+ if (stored !== undefined && savedByCallee(stored) && ssa.holdsEntryValue(stored, bi)) {
2834
+ throw new FrontendUnsupportedError(
2835
+ `cannot lift '${name}': address-taken stack local — '${ins.mnemonic}' at 0x${ins.addr.toString(16)} saves the caller's ${stored} inside the local area, a frame this does not describe`,
2836
+ );
2837
+ }
2838
+ const at = emitLaddr(off);
2839
+ directAccesses.add(at);
2840
+ return at;
2841
+ }
2842
+ const raw = rawFrame(base);
2843
+ const sum = raw === undefined ? undefined : frameSums.get(raw);
2844
+ return sum === undefined ? undefined : materialize(sum, off);
2845
+ };
2846
+ // IDO reads a `volatile` doubleword nothing uses into the register PAIR its discard register
2847
+ // heads: `lw zero,0(a0)` then `lw at,4(a0)` (O32 pairs a register with the next one, and `at`
2848
+ // follows `zero`), measured at -O1 and -O0 for `volatile long long` and `volatile double`
2849
+ // through a pointer, an index and a global (`lui at` re-formed between the words), the `at`
2850
+ // word in the `jr`'s delay slot at -O1. So a word load into `at` is discarded too where it reads
2851
+ // the word above one the discard register took, at the same address, with no write to that
2852
+ // address's base register and no call between (`firstWords`); anywhere else `at` is the
2853
+ // assembler's own temporary.
2854
+ const loaded = (d: string, res: Value, discard: boolean) => {
2855
+ if (discard) {
2856
+ discarded.add(res);
2857
+ }
2858
+ write(d, res);
2859
+ };
1351
2860
  const emitLoad = (ins: Instr, d: string, mem: string, width: number, signed: boolean) => {
2861
+ const sym = ins.reloc?.type === 'R_MIPS_LO16' ? ins.reloc.sym : undefined;
2862
+ const { off: wordOff, base: wordBase } = parseMem(mem);
2863
+ const word = { reg: sym === undefined ? wordBase : undefined, sym, off: wordOff };
2864
+ const first =
2865
+ d === 'at' && width === 4
2866
+ ? firstWords.find((f) => word.reg === f.reg && word.sym === f.sym && word.off === f.off + 4)
2867
+ : undefined;
2868
+ const discard = discards(d) || first !== undefined;
2869
+ firstWords = firstWords.filter((f) => f !== first && (f.reg === undefined || f.reg !== d));
2870
+ if (discards(d) && width === 4) {
2871
+ firstWords.push(word);
2872
+ }
1352
2873
  const g = globalBase(ins, mem);
1353
2874
  if (g) {
1354
2875
  const res = mkValue(T.unk(32));
1355
2876
  ops.push(mkOp('load', { operands: [g.base], results: [res], attrs: { off: g.off, width, signed } }));
1356
- write(d, res);
2877
+ loaded(d, res, discard);
1357
2878
  return;
1358
2879
  }
1359
2880
  const { off, base } = parseMem(mem);
2881
+ // An access inside the local area, or through a frame sum, is MEMORY (`inLocalArea`).
2882
+ const at = frameAccess(ins, base, off, width);
2883
+ if (at !== undefined) {
2884
+ const res = mkValue(T.unk(32));
2885
+ ops.push(mkOp('load', { operands: [at], results: [res], attrs: { off: 0, width, signed } }));
2886
+ loaded(d, res, discard);
2887
+ return;
2888
+ }
1360
2889
  // A word reload from a stack slot is transparent to dataflow — the SAME value spilled — so
1361
2890
  // route it through the slot SSA variable, not a `load` through `sp` (which would make `sp` a
1362
2891
  // spurious pointer parameter). Compiler spills/reloads are always word-width; sub-word `sp`
@@ -1372,22 +2901,33 @@ export function lift(
1372
2901
  }
1373
2902
  const caller = mkValue(T.unk(32));
1374
2903
  restores.set(caller, { reg: d, addr: ins.addr });
2904
+ restoredBy.set(ins, new Set([d]));
1375
2905
  write(d, caller);
1376
2906
  return;
1377
2907
  }
1378
2908
  // SOUNDNESS GUARD (mirrors PPC frameLoad): only route through the slot SSA var if that slot was
1379
2909
  // actually STORED (has a reaching def). A word `lw` from an sp offset that was NEVER spilled is an
1380
- // incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local — neither modelled.
1381
- // Without this, readVar would FABRICATE a phantom entry parameter for the slot, silently emitting a
1382
- // function of wrong arity that returns the wrong argument. Loud-fail instead of miscompiling.
2910
+ // incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local. Without this,
2911
+ // readVar would FABRICATE a phantom entry parameter for the slot, silently emitting a function
2912
+ // of wrong arity that returns the wrong argument.
1383
2913
  //
1384
- // Those two cases are SEPARABLE, and the Thumb frontend now separates them (frontend/thumb.ts,
1385
- // incomingArgIndex): a slot at or above the callee's own frame cannot have been written by this
1386
- // function, so it is an incoming argument; below the frame top it is a local. Doing the same here
1387
- // needs O32's own frame rule — the 16-byte home area means a stack argument is NOT simply "above
1388
- // the frame", so the Thumb arithmetic does not carry over — plus ssa.ensureParam for the register
1389
- // half. Until then this stays one loud decline for both.
1390
- if (!ssa.hasReachingDef(stackSlot(off), bi)) {
2914
+ // The two are told apart by where the word is (`incomingStackKey`): a word at or past entry +
2915
+ // 16 is that argument, `@sarg<k>`, a parameter through the ordinary live-in path, as on
2916
+ // Thumb, merged with whatever this function stored there. Everything else keeps the
2917
+ // decline: a word of this frame, of the home area, or at an unproven depth. A sub-word or
2918
+ // floating-point incoming argument never reaches here (the `spSlotSafe` guard and the FP
2919
+ // arm).
2920
+ const incoming = incomingStackKey(ins, off);
2921
+ if (incoming !== undefined) {
2922
+ write(d, readVar(incoming, bi));
2923
+ return;
2924
+ }
2925
+ // A word of the frame this function OWNS (`ownedFrame`) that no store reaches is an
2926
+ // uninitialised local, which `readVar` answers with `undef` through the live-in model.
2927
+ const owned = ownedFrame();
2928
+ const ownsWord =
2929
+ owned !== undefined && depthAt?.get(ins) === frameSize && off >= owned.from && off + 4 <= owned.to;
2930
+ if (!ownsWord && !ssa.hasReachingDef(stackSlot(off), bi)) {
1391
2931
  throw new FrontendUnsupportedError(
1392
2932
  `cannot lift '${name}': load from stack slot sp@${off} that was never stored ` +
1393
2933
  `(stack-passed argument beyond the 4 register args, or an address-taken/uninitialised local) — not modelled`,
@@ -1398,7 +2938,7 @@ export function lift(
1398
2938
  }
1399
2939
  const res = mkValue(T.unk(32));
1400
2940
  ops.push(mkOp('load', { operands: [read(base)], results: [res], attrs: { off, width, signed } }));
1401
- write(d, res);
2941
+ loaded(d, res, discard);
1402
2942
  };
1403
2943
  const emitStore = (ins: Instr, srcReg: string, mem: string, width: number) => {
1404
2944
  const g = globalBase(ins, mem);
@@ -1407,18 +2947,32 @@ export function lift(
1407
2947
  return;
1408
2948
  }
1409
2949
  const { off, base } = parseMem(mem);
2950
+ const at = frameAccess(ins, base, off, width, srcReg);
2951
+ if (at !== undefined) {
2952
+ ops.push(mkOp('store', { operands: [at, read(srcReg)], attrs: { off: 0, width } }));
2953
+ return;
2954
+ }
1410
2955
  // A word spill to a stack slot (an argument home slot `sw a0,0(sp)`, or a local): record the
1411
2956
  // slot's value in SSA, do NOT emit a `store` through `sp`. A never-reloaded spill (the ABI
1412
2957
  // home-slot store) then has no uses and simply drops. See isStackPtr / spSlotSafe.
1413
2958
  if (spSlotSafe && isStackPtr(base) && width === 4) {
1414
- if (savedByCallee(srcReg) && ssa.holdsEntryValue(srcReg, bi)) {
2959
+ // A global register variable's register is never saved — compiled code stores it only as a
2960
+ // value, the outgoing stack argument `k(1, 2, 3, 4, g)` is (`sw s0,16(sp)`) — and the builder
2961
+ // records no definition for one (its writes are stores to the global), so "still holds the
2962
+ // entry value" is no evidence of a save there.
2963
+ if (savedByCallee(srcReg) && !liveIns.globalRegs?.has(srcReg) && ssa.holdsEntryValue(srcReg, bi)) {
1415
2964
  refuseMixedSlot(ins, mem, off, 'save');
1416
2965
  saveSlots.set(off, srcReg);
1417
2966
  return;
1418
2967
  }
1419
2968
  refuseMixedSlot(ins, mem, off, 'value');
1420
2969
  valueSlots.add(off);
1421
- writeVar(stackSlot(off), bi, read(srcReg));
2970
+ // an assignment to an incoming stack parameter is that parameter's SSA variable
2971
+ const incoming = incomingStackKey(ins, off);
2972
+ writeVar(incoming ?? stackSlot(off), bi, read(srcReg));
2973
+ if (incoming === undefined) {
2974
+ outgoingWordState[bi].set(off, 'stored');
2975
+ }
1422
2976
  return;
1423
2977
  }
1424
2978
  ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
@@ -1430,7 +2984,9 @@ export function lift(
1430
2984
  // argument k ≥ 4 lives, and the guess that cannot see past a3.
1431
2985
  const lowerJal = (ins: Instr) => {
1432
2986
  const at = `0x${ins.addr.toString(16)}`;
1433
- const callee = splat ? ins.ops[0] : ins.reloc?.type === 'R_MIPS_26' ? ins.reloc.sym : undefined;
2987
+ // this block's outgoing words as the call is reached, before it takes any
2988
+ const wordsHere = new Map(outgoingWordState[bi]);
2989
+ const callee = jalCallee(ins, splat);
1434
2990
  if (callee?.startsWith('.')) {
1435
2991
  throw new FrontendUnsupportedError(
1436
2992
  `${site(ins)} is relocated against the section '${callee}' — a callee known only by its offset ` +
@@ -1456,12 +3012,64 @@ export function lift(
1456
3012
  'where its outgoing stack arguments end is not known',
1457
3013
  );
1458
3014
  }
3015
+ // A CALL NOTHING DECLARES TAKES THE WORDS ITS CALLEE'S BODY LOADS, where exactly those reach
3016
+ // it staged (frontend/stackargs.ts, "THE CALLEE'S BODY": the same rule, `bodyBlockAgrees`,
3017
+ // asked here of SSA's reaching definitions, which O32 has when its calls are lowered):
3018
+ // the body loads incoming word n-1, so the call takes at least n stack words, and a word past
3019
+ // them that nothing staged is no argument. Licensed only where the words `[16, 16+4n)` are
3020
+ // written on every path to the call beneath the loaded-word bound and inside the frame, and
3021
+ // no other word of the area there reaches it; asked only once a word does. KNOWN GAP: a
3022
+ // frame with a local area (`frame`) is not licensed, since that area is partitioned off the
3023
+ // DECLARED widest call before any call is lowered (`stackWordsOf` above), so such a call keeps
3024
+ // the refusals below.
3025
+ // A word an earlier call took as its stack argument is pending for no later one until it is
3026
+ // stored again (`outgoingWordState`), as frontend/stackargs.ts's dataflow ends it at the call
3027
+ // whose block held it. A block not filled yet holds no state, so each licence is judged again
3028
+ // once every block is (`bodyLicences`).
3029
+ const licensedStackWords = (): number | undefined => {
3030
+ if (frame !== null || !asksCalleeBody(Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined)) {
3031
+ return undefined;
3032
+ }
3033
+ const reaching = [...valueSlots].filter(
3034
+ (off) =>
3035
+ off >= STACK_ARGS_AT &&
3036
+ off < frameSize &&
3037
+ off < loadedWordBound &&
3038
+ ssa.hasReachingDef(stackSlot(off), bi) &&
3039
+ outgoingWordReaches(off, bi, 'stored', wordsHere),
3040
+ );
3041
+ if (reaching.length === 0) {
3042
+ return undefined;
3043
+ }
3044
+ // O32 can skip an argument register for an 8-aligned argument, so the plan's count — every
3045
+ // register, then the block — holds only where each register is one this call sets up: a
3046
+ // register still at its entry value on some path may be the skipped word, and taken as an
3047
+ // argument it is a parameter this function does not have. (The callee's half of the same
3048
+ // proof is frontend/callee-body.ts `readsEveryArgReg`.)
3049
+ if (ARG_REGS.some((r) => !ssa.writtenOnEveryPath(r, bi, () => false))) {
3050
+ return undefined;
3051
+ }
3052
+ const n = options.calleeStackWords?.(callee) ?? 0;
3053
+ return bodyBlockAgrees(
3054
+ n,
3055
+ reaching.map((off) => off - STACK_ARGS_AT),
3056
+ (o) =>
3057
+ ssa.writtenOnEveryPath(stackSlot(STACK_ARGS_AT + o), bi, () => false) &&
3058
+ !outgoingWordReaches(STACK_ARGS_AT + o, bi, 'taken', wordsHere),
3059
+ )
3060
+ ? (bodyLicences.set(ins, { bi, here: wordsHere, n, callee }), n)
3061
+ : undefined;
3062
+ };
3063
+ const took: number[] = [];
1459
3064
  calls.lower({
1460
3065
  callee,
1461
3066
  ssa,
1462
3067
  bi,
1463
3068
  read,
1464
3069
  write,
3070
+ get stackWords() {
3071
+ return licensedStackWords();
3072
+ },
1465
3073
  // ARGUMENT WORD k ≥ 4 travels in `16+4(k-4)(sp)`, inside the one frame this function pushed
1466
3074
  stackWord: (k) => {
1467
3075
  const off = STACK_ARGS_AT + 4 * (k - ARG_REGS.length);
@@ -1477,21 +3085,68 @@ export function lift(
1477
3085
  `${off}(sp), and no value stored there reaches the call`,
1478
3086
  );
1479
3087
  }
3088
+ // a word an earlier call took is the earlier callee's, not the value stored there, on the
3089
+ // blocks filled so far; the rest are judged once every block is (`declaredTakes`)
3090
+ if (outgoingWordReaches(off, bi, 'taken', wordsHere)) {
3091
+ refuseTakenWord(callee, ins.addr, off);
3092
+ }
3093
+ took.push(off);
3094
+ outgoingWordState[bi].set(off, 'taken');
1480
3095
  return readVar(stackSlot(off), bi);
1481
3096
  },
1482
- // a pending `%hi` half is not an argument
1483
- highHalves,
3097
+ // a pending `%hi` half and a table base are no argument
3098
+ placeholders,
3099
+ parked,
3100
+ scaledIndex,
1484
3101
  guess: {
1485
3102
  // a gap in the argument registers refuses, naming this address (`fallbackArgc`)
1486
3103
  at: ins.addr,
1487
3104
  // A GUESS THAT FILLS a0..a3 CANNOT SAY WHERE THE LIST ENDS: a word of the outgoing area that
1488
3105
  // reaches the call is argument 5 onward, or a local the compiler put there.
1489
3106
  refuse: (argc) => {
3107
+ // …and with a local area a guess says less still. A word staged for the call above the
3108
+ // home area may be MEMORY there, which the slot test below cannot see; and a word
3109
+ // staged below it reaches the call whether or not a3 is set, since an 8-aligned
3110
+ // argument (`long long`, a struct holding one) skips a3 for the stack. Where the
3111
+ // loaded-word bound reaches the local area, no word of it is a stack argument, and the
3112
+ // slot test below sees every word that can be one.
3113
+ if (frame !== null) {
3114
+ if (argc >= ARG_REGS.length && loadedWordBound > frame.area.from) {
3115
+ throw new FrontendUnsupportedError(
3116
+ `cannot lift '${name}': outgoing stack arguments not modelled — the undeclared call to '${callee}' ` +
3117
+ `at ${at} fills a0..a3 in a frame whose local area holds a captured address, so where its ` +
3118
+ 'stack words end is not known',
3119
+ );
3120
+ }
3121
+ const staged = [...valueSlots]
3122
+ .filter(
3123
+ (off) =>
3124
+ off >= STACK_ARGS_AT &&
3125
+ off < frame.area.from &&
3126
+ off < loadedWordBound &&
3127
+ ssa.hasReachingDef(stackSlot(off), bi),
3128
+ )
3129
+ .sort((x, y) => x - y)[0];
3130
+ if (staged !== undefined) {
3131
+ throw new FrontendUnsupportedError(
3132
+ `cannot lift '${name}': outgoing stack arguments not modelled — the word stored to ${staged}(sp), ` +
3133
+ `below the local area, reaches the undeclared call to '${callee}' at ${at}, where a stack ` +
3134
+ 'argument travels',
3135
+ );
3136
+ }
3137
+ }
1490
3138
  if (argc < ARG_REGS.length) {
1491
3139
  return;
1492
3140
  }
3141
+ // …save a word at or above the loaded-word bound, which is a local's
1493
3142
  const passed = [...valueSlots]
1494
- .filter((off) => off >= STACK_ARGS_AT && off < frameSize && ssa.hasReachingDef(stackSlot(off), bi))
3143
+ .filter(
3144
+ (off) =>
3145
+ off >= STACK_ARGS_AT &&
3146
+ off < frameSize &&
3147
+ off < loadedWordBound &&
3148
+ ssa.hasReachingDef(stackSlot(off), bi),
3149
+ )
1495
3150
  .sort((x, y) => x - y)[0];
1496
3151
  if (passed !== undefined) {
1497
3152
  throw new FrontendUnsupportedError(
@@ -1503,6 +3158,9 @@ export function lift(
1503
3158
  },
1504
3159
  },
1505
3160
  });
3161
+ if (took.length > 0 && !bodyLicences.has(ins)) {
3162
+ declaredTakes.set(ins, { bi, here: wordsHere, offs: took, callee });
3163
+ }
1506
3164
  // hi and lo are the callee's to overwrite
1507
3165
  divState = null;
1508
3166
  mulState = null;
@@ -1543,20 +3201,45 @@ export function lift(
1543
3201
  return { block: irBlocks[j], args: [] };
1544
3202
  };
1545
3203
 
1546
- // Recovered dense switch: run the `beqz` delay slot first — on IDO it computes the DEFAULT
1547
- // return value (`li v0,-1`, read by the default block); on KMC it is the now-dead index shift —
1548
- // then dispatch a `switch_br` over the scrutinee (N case blocks, dense 0..N-1, + default).
3204
+ // `bc1t` is taken on the code a compare in this block set, `bc1f` on its logical NOT, which is
3205
+ // the UNORDERED compare (`NEGATED_FCMP`): `!(a < b)` is true on a NaN, and `a >= b` is not. A
3206
+ // compare in the delay slot would set the code after the branch read it.
3207
+ const fpCondValue = (ins: Instr): Value => {
3208
+ if (fcc === null) {
3209
+ throw new FrontendUnsupportedError(`${site(ins)} tests a condition code no compare in its block set`);
3210
+ }
3211
+ if (b.delay && b.delay.mnemonic in FP_COMPARE) {
3212
+ throw new FrontendUnsupportedError(`${site(ins)} has a compare in its delay slot`);
3213
+ }
3214
+ if (FP_COND_BRANCH[ins.mnemonic]) {
3215
+ return fcc.value;
3216
+ }
3217
+ const v = mkValue(T.unk(32));
3218
+ ops.push(mkOp(NEGATED_FCMP[fcc.opcode], { operands: [fcc.lhs, fcc.rhs], results: [v] }));
3219
+ return v;
3220
+ };
3221
+
3222
+ // Recovered dense switch: dispatch a `switch_br` over the scrutinee (N case blocks, dense
3223
+ // 0..N-1, + default). The scrutinee is the value the `sltiu` bounded, so it is read BEFORE the
3224
+ // `beqz` delay slot runs, as a conditional branch reads its operands: IDO can scale the index
3225
+ // IN PLACE there (`sll t6,t6,2`), and reading after the slot would switch on `k << 2` over the
3226
+ // unscaled labels 0..N-1. The slot still runs, because it can also compute the DEFAULT value
3227
+ // (IDO's `li v0,-1`, read by the default block).
1549
3228
  const jt = br ? jts.get(br.addr) : undefined;
1550
3229
  if (jt) {
3230
+ const scrut = readVar(jt.scrutReg, bi);
1551
3231
  if (b.delay) {
1552
3232
  decode(b.delay);
1553
3233
  }
1554
- pushSwitchBr(ops, readVar(jt.scrutReg, bi), [...jt.caseAddrs, jt.defaultAddr].map(succ));
3234
+ if (jt.hoisted) {
3235
+ tableBases.demand(jt.hoisted, bi, jt.jrAddr);
3236
+ }
3237
+ pushSwitchBr(ops, scrut, [...jt.caseAddrs, jt.defaultAddr].map(succ));
1555
3238
  return;
1556
3239
  }
1557
3240
 
1558
3241
  if (br && isCond(br)) {
1559
- const cond = condValue(br, ops, read, constVal, cmpDef);
3242
+ const cond = br.mnemonic in FP_COND_BRANCH ? fpCondValue(br) : condValue(br, ops, read, constVal, cmpDef);
1560
3243
  // A NULLIFIED slot is not decoded here: it is its own block on the taken edge, and the
1561
3244
  // not-taken edge skips past it (toBlocks). Every pass after this one sees ordinary
1562
3245
  // conditional execution.
@@ -1593,14 +3276,37 @@ export function lift(
1593
3276
  ops.push(mkOp('br', { attrs: to === next ? { fallthrough: true } : {}, successors: [succ(to)] }));
1594
3277
  return;
1595
3278
  }
1596
- // A FLOAT RETURN is decided for the whole function (frontend/fpu.ts `writesFloatReturn`), and
1597
- // then `v0` is scratch like any other integer register.
1598
- const retOps = floatReturn
1599
- ? [read(fpu!.returnReg)]
1600
- : ssa.holdsValue(RET, bi, (v) => !highHalves.has(v))
1601
- ? [readVar(RET, bi)]
1602
- : [];
1603
- ops.push(mkOp('ret', { operands: retOps }));
3279
+ // A FLOAT RETURN is decided for the whole function (frontend/fpu.ts `returnHomeRule`), and
3280
+ // then `v0` is scratch like any other integer register. An undecided one returns `v0` here,
3281
+ // and `settleReturnHome` may hand the return the float home instead.
3282
+ //
3283
+ // NOR ANYTHING AT ALL WHERE THE FUNCTION'S OWN DECLARATION IS VOID. Then `v0` is scratch on every
3284
+ // path: a loop can leave a value in it on one path while a void callee destroyed it on another
3285
+ // (`for (..) if (t[i] == 7) { Ping(i); return; }`), and reading it would join the callee's
3286
+ // leftover into a return the declaration says carries nothing — refused as a stale read
3287
+ // (ssa.ts `refuseStaleCallerSavedReads`), and dropped unread after the lift anyway (pipeline.ts
3288
+ // `dropVoidReturnOperands`). The same declaration `ownReturnFitsAWord` reads.
3289
+ const returnsWord =
3290
+ !floatReturn &&
3291
+ !declaresVoidReturn(own) &&
3292
+ ssa.holdsValue(RET, bi, (v) => !placeholders.has(v) && !parked(v, RET));
3293
+ // A 64-BIT INTEGER RETURN COMES BACK IN v0:v1, the high word in v0, and this lowering returns
3294
+ // one word: returning v0 alone hands back the high word with the low one dropped, a function
3295
+ // that compiles and returns the wrong value (`long long f(int a, int b, int c, int d,
3296
+ // long long e) { return e; }` read as `return a4;`). Where v1 was last written for the caller
3297
+ // (`v1MayHoldLowWord`) this refuses, unless the function's own declaration says the return
3298
+ // fits a word. Modelling the pair is the capability this stands in for.
3299
+ if (returnsWord && !ownReturnFitsAWord && v1MayHoldLowWord(blocks, preds, bi)) {
3300
+ throw new FrontendUnsupportedError(
3301
+ `cannot lift '${name}': the return at 0x${br.addr.toString(16)} may hand back the halves of a 64-bit pair ` +
3302
+ 'in v0:v1 (v1 is written for the caller, and nothing declares a return no wider than a word), and this ' +
3303
+ 'lowering returns one word',
3304
+ );
3305
+ }
3306
+ const retOps = floatReturn ? [read(fpu!.returnReg)] : returnsWord ? [readVar(RET, bi)] : [];
3307
+ const ret = mkOp('ret', { operands: retOps });
3308
+ ops.push(ret);
3309
+ returns.push({ op: ret, bi });
1604
3310
  return;
1605
3311
  }
1606
3312
  // unconditional branch
@@ -1611,11 +3317,71 @@ export function lift(
1611
3317
  fillBlock(b, bi);
1612
3318
  ssa.markFilled(bi);
1613
3319
  });
3320
+ for (const [ins, { bi, here, n, callee }] of bodyLicences) {
3321
+ for (const off of valueSlots) {
3322
+ if (off < STACK_ARGS_AT || off >= (frameSize ?? 0) || off >= loadedWordBound) {
3323
+ continue;
3324
+ }
3325
+ const taken = off < STACK_ARGS_AT + 4 * n;
3326
+ if (outgoingWordReaches(off, bi, taken ? 'taken' : 'stored', here)) {
3327
+ throw new FrontendUnsupportedError(
3328
+ `cannot lift '${name}': outgoing stack arguments not modelled — the call to '${callee}' at ` +
3329
+ `0x${ins.addr.toString(16)} takes the ${n} stack ` +
3330
+ `word(s) its callee's body loads, but ${
3331
+ taken
3332
+ ? `the word at ${off}(sp) reaches it on some path already taken by a call and not stored again`
3333
+ : `the word at ${off}(sp), past them, reaches it stored on some path`
3334
+ } — the block staged for it is not the same on every path, so its arguments are not known`,
3335
+ );
3336
+ }
3337
+ }
3338
+ }
3339
+ for (const [ins, { bi, here, offs, callee }] of declaredTakes) {
3340
+ for (const off of offs) {
3341
+ if (outgoingWordReaches(off, bi, 'taken', here)) {
3342
+ refuseTakenWord(callee, ins.addr, off);
3343
+ }
3344
+ }
3345
+ }
3346
+ refuseSavedRegisterLeaks(
3347
+ name,
3348
+ blocks.map((b) => ({
3349
+ instrs: [...b.body, ...(b.delay ? [b.delay] : [])],
3350
+ returns: b.branch !== null && isReturn(b.branch) && b.branch.ops[0] === 'ra',
3351
+ })),
3352
+ preds,
3353
+ new Set([...saveSlots.values()].filter((r) => r !== 'ra')),
3354
+ (ins, r) => writtenReg(ins) === r,
3355
+ restoredBy,
3356
+ );
3357
+ // What the float home holds at each return, and, where the home is undecided and holds a float on
3358
+ // every path into every return, that float — read now, before the SSA is finished.
3359
+ const homes = fpu ? returns.map(({ bi }) => floatHomeAt(ssa, fpu.returnReg, bi)) : [];
3360
+ const carriesFloat = carriesFloatHome(homeRule, homes);
3361
+ if (carriesFloat) {
3362
+ returns.forEach(({ op, bi }) => op.operands.push(readVar(fpu!.returnReg, bi)));
3363
+ }
1614
3364
  highHalves.assertAllConsumed(name);
1615
- const argSlots = fpuArgSlots(name, fpu, ARG_REGS, isFpKey, fpType);
3365
+ tableBases.prove(name, ssa);
3366
+ refuseStaleIdiomReads(
3367
+ name,
3368
+ irBlocks,
3369
+ staleIdiom,
3370
+ ssa,
3371
+ prototypes[name] ?? options.contextOwnProto?.proto,
3372
+ fail,
3373
+ calls,
3374
+ );
3375
+ const argSlots = o32ArgSlots(name, fpuArgSlots(name, fpu, ARG_REGS, isFpKey, fpType), ARG_REGS);
1616
3376
  mintArgSlotHoles(ssa, preds[0].length > 0, argSlots);
1617
3377
  ssa.finish();
3378
+ refuseFpWordCrossings(name, ssa.fn);
3379
+ // Asked once the lift is done, so a refusal the stream meets first keeps its own reason.
3380
+ if (fpu) {
3381
+ settleReturnHome(name, ssa.fn, homeRule, returns, homes, carriesFloat, fpu, RET, movedIn);
3382
+ }
1618
3383
  highHalves.assertNoneEscaped(name, irBlocks);
3384
+ tableBases.assertNoneEscaped(name, irBlocks);
1619
3385
  for (const blk of irBlocks) {
1620
3386
  const used = [
1621
3387
  ...blk.params,
@@ -1630,10 +3396,319 @@ export function lift(
1630
3396
  }
1631
3397
  }
1632
3398
 
3399
+ // A struct passed by value becomes the one object its words hold (call-plan.ts `foldByValueArgs`)
3400
+ // before the audit judges the frame objects, as on Thumb.
3401
+ foldByValueArgs(name, irBlocks, calls, { symbols, context });
3402
+ nameMergedSretObjects(irBlocks);
3403
+
3404
+ if (frame !== null) {
3405
+ refuseEscapedFrameSums(name, irBlocks, frameSums);
3406
+ // Prove every `laddr` names storage of this frame at a shape the machine states, or decline
3407
+ // (`auditFrameObjects`); an accepted function then gets the device pin the audit answers.
3408
+ const verdict = FRAME_OBJECT_AUDIT.run({
3409
+ name,
3410
+ irBlocks,
3411
+ ownedLocals: frame.area,
3412
+ declaredLocals: frame.area,
3413
+ usedSlotOffsets: new Set([...valueSlots].filter((o) => o < frame.size)),
3414
+ loadedWords: loadedFrom === undefined ? undefined : { from: loadedFrom, bound: loadedWordBound },
3415
+ capturedObjectIsTheWholeFrame: false,
3416
+ movedCaptures,
3417
+ directAccesses,
3418
+ returnsWithoutHiddenPointer: calls.returnsWithoutHiddenPointer,
3419
+ symbols,
3420
+ target,
3421
+ oneObject,
3422
+ });
3423
+ if (verdict !== undefined && 'oneObject' in verdict) {
3424
+ return verdict;
3425
+ }
3426
+ if (verdict !== undefined) {
3427
+ pinDeviceAccesses(irBlocks, verdict, target, symbols);
3428
+ }
3429
+ }
3430
+ refuseSretOutsideFrame(name, irBlocks, target);
3431
+ pinDeadReads(irBlocks, target, {
3432
+ name,
3433
+ own: prototypes[name],
3434
+ atUndeclaredCall: calls.heldAtUndeclaredCall,
3435
+ spilled: ssa.frameWrites,
3436
+ discarded,
3437
+ declarations: { symbols, context },
3438
+ });
3439
+
3440
+ // Asked once everything else lifted, so a function that declines for another reason keeps that message.
3441
+ refuseReturnsNotThroughCallersRa(
3442
+ name,
3443
+ blocks,
3444
+ blocks.map((b) => succAddrs.get(b)!.flatMap((a) => (idxOf.has(a) ? [idxOf.get(a)!] : []))),
3445
+ target.callerSaved,
3446
+ target.kernelRegs ?? [],
3447
+ );
1633
3448
  abiSortEntryParams(irBlocks[0], preds[0].length > 0, paramReg, argSlots);
1634
3449
  return ssa.fn;
1635
3450
  }
1636
3451
 
3452
+ /** `sp + index`, deferred to its use (`materialize`): the frame offset it starts from and the index. */
3453
+ interface FrameSum {
3454
+ readonly base: number;
3455
+ readonly index: Value;
3456
+ }
3457
+
3458
+ /** A frame sum is a VALUE no op defines — every use builds its own `laddr` and `add` — so one that
3459
+ * reached a phi, or anything else the fill did not materialize, refuses. */
3460
+ function refuseEscapedFrameSums(name: string, irBlocks: readonly Block[], sums: ReadonlyMap<Value, FrameSum>): void {
3461
+ for (const blk of irBlocks) {
3462
+ for (const op of blk.ops) {
3463
+ if ([...op.operands, ...op.successors.flatMap((x) => x.args)].some((v) => sums.has(v))) {
3464
+ throw new FrontendUnsupportedError(
3465
+ `cannot lift '${name}': address-taken stack local — a frame address plus a runtime index reaches \`${op.opcode}\` across a join, ` +
3466
+ 'which object it indexes is not known there',
3467
+ );
3468
+ }
3469
+ }
3470
+ }
3471
+ }
3472
+
3473
+ /** An instruction that takes a frame address as data: `addiu rD,sp,k`, `move rD,sp`, or `addu
3474
+ * rD,sp,rX` in either operand order. */
3475
+ function takesFrameAddress(ins: Instr): boolean {
3476
+ const [d, s, t] = ins.ops;
3477
+ if (d === undefined || isStackPtr(d)) {
3478
+ return false;
3479
+ }
3480
+ if (ins.mnemonic === 'addiu' || ins.mnemonic === 'addi' || ins.mnemonic === 'move') {
3481
+ return isStackPtr(s ?? '');
3482
+ }
3483
+ if (ins.mnemonic === 'or') {
3484
+ return isStackPtr(copiedReg(ins) ?? '');
3485
+ }
3486
+ return (
3487
+ (ins.mnemonic === 'addu' || ins.mnemonic === 'add') && t !== undefined && isStackPtr(s ?? '') !== isStackPtr(t)
3488
+ );
3489
+ }
3490
+
3491
+ /** THE REGISTERS THE PROLOGUE SAVES: a word store to `sp` of a register O32 makes a callee
3492
+ * preserve, in the entry block (its delay slot included — the slot of a branch-likely is its own
3493
+ * block, so never this one) before any instruction NAMES that register, read or written. What it
3494
+ * stores is then what the caller left there, which is the half of `LiveInModel.uninitRegs` the
3495
+ * frontend owes: a compiler homes a local in a callee-saved register only after saving it, so a
3496
+ * def-less read of a saved one is that local, uninitialised, and of an unsaved one is a global
3497
+ * register variable's or refuses (`registerLiveIns`). "Nothing named it first" is PPC's condition
3498
+ * (`prologueSavedRegs` there): a read before the store would be a use of the caller's value. A
3499
+ * re-entered entry gets an empty preheader as `blocks[0]`, so it measures no save at all.
3500
+ *
3501
+ * A SAVE IS RELOADED. In a function that returns, a store no `lw` of the same register from `sp`
3502
+ * ever answers saves nothing: the caller gets the register back as the body left it, so the word
3503
+ * is a value. gcc 2.7.2 and KMC put exactly such a store in the entry block for a file-scope
3504
+ * global register variable passed on the stack — `register int g __asm__("$16"); return k(1, 2,
3505
+ * 3, 4, g) + g;` is `sw s0,16(sp); jal k` and no reload of s0 — and counting it as a save would
3506
+ * turn the read of `g` after the call into `uninit_s0` and drop the fifth argument. Any `lw` of the
3507
+ * register from `sp` will do, whatever its word: which word a restore reads is judged where it is
3508
+ * lowered. A function with no return keeps the entry-block reading: nothing reaches a caller. */
3509
+ function prologueSavedRegs(
3510
+ entry: readonly Instr[],
3511
+ target: TargetDescription,
3512
+ code: readonly Instr[],
3513
+ ): ReadonlySet<string> {
3514
+ const saved = new Set<string>();
3515
+ const named = new Set<string>();
3516
+ const calleeSaved = (r: string) => isMipsReg(r) && !r.startsWith('$') && !target.callerSaved.includes(r);
3517
+ const returns = code.some((ins) => ins.mnemonic === 'jr' && ins.ops[0] === 'ra');
3518
+ const reloaded = (r: string) =>
3519
+ code.some((ins) => ins.mnemonic === 'lw' && ins.ops[0] === r && isStackPtr(parseMem(ins.ops[1] ?? '').base));
3520
+ for (const ins of entry) {
3521
+ const [src, mem] = ins.ops;
3522
+ if (ins.mnemonic === 'sw' && src !== undefined && mem !== undefined && isStackPtr(parseMem(mem).base)) {
3523
+ if (calleeSaved(src) && !named.has(src) && (!returns || reloaded(src))) {
3524
+ saved.add(src);
3525
+ }
3526
+ }
3527
+ for (const op of ins.ops) {
3528
+ const reg = op.includes('(') ? parseMem(op).base : op;
3529
+ if (isMipsReg(reg)) {
3530
+ named.add(reg);
3531
+ }
3532
+ }
3533
+ const w = writtenReg(ins);
3534
+ if (w !== null) {
3535
+ named.add(w);
3536
+ }
3537
+ }
3538
+ return saved;
3539
+ }
3540
+
3541
+ /** The local area of a function that takes a frame address (`o32LocalArea`), and the depth every
3542
+ * access it routes runs at — or null, which leaves every capture its sp-as-data refusal. Null
3543
+ * where nothing takes a frame address; where the depth walk is unproven or the frame is not one
3544
+ * push; where a capture runs at another depth; where the target states no layout or leaves no
3545
+ * area; and where an `sp` access at another depth lands on the area's bytes, which the routing by
3546
+ * raw offset would then misname.
3547
+ *
3548
+ * The outgoing area ends past the widest call's stack words, read off each callee's declaration
3549
+ * (`stackWordsOf`). A CALL NO DECLARATION SIZES refuses here, where the number is computed, rather
3550
+ * than relying on its own lowering to refuse it: gcc stages `fp(1, 2, 3, 4, x)`'s fifth word at
3551
+ * sp+16 and puts the local at sp+24, so an extent read without that call would draw the area over
3552
+ * the staged word. A call through a register (`jalr`) or a linking branch always refuses. A `jal`
3553
+ * to an undeclared callee refuses where the area depends on its extent — where drawing the
3554
+ * outgoing area as far as it can reach would move it: up to the lowest save, or to the
3555
+ * loaded-word bound (`o32OutgoingArea`'s `widest`) where that is lower. That is gcc's layout,
3556
+ * whose locals start where the outgoing area ends: `tl(1, 2, 3, (long long)x)` to an
3557
+ * unprototyped `tl` skips a3 and stages x at sp+16..24, which no guess of a0..a3 sees. IDO's area
3558
+ * starts above the saves, which the outgoing area lies below whatever its extent, so there the
3559
+ * undeclared call costs nothing.
3560
+ * The saves are the prologue's: a store of `ra` or a callee-saved register in the entry block
3561
+ * before anything writes it. */
3562
+ /** THE OUTGOING ARGUMENT AREA OF AN O32 FRAME, the part of it a callee may write: the a0..a3 home
3563
+ * words `[0,16)` and the widest call's stack words past them, by each callee's declaration. `end`
3564
+ * is where it stops, 0 in a leaf. `unsized` is the first call no declaration sizes (a `jal` nothing
3565
+ * declares, or a call through a register), counted in `end` as taking no stack words. `widest`
3566
+ * is the most the area can reach: `end` where every call is sized, else no further than the
3567
+ * loaded-word bound (`compilerBehaviors.outgoingAreaNeverLoadedFrom`: the compiler loads no word
3568
+ * of its outgoing area from that offset up, so the lowest word there this function loads is a
3569
+ * local's, and so is every word above it), and `Infinity` where no load bounds it. Each caller
3570
+ * reads the one number: `measureO32Frame` refuses where it could move the local area, and
3571
+ * `ownedFrame` claims the frame only from it up. */
3572
+ function o32OutgoingArea(
3573
+ code: readonly Instr[],
3574
+ stackWordsOf: (callee: string | undefined) => number | null,
3575
+ splat: boolean,
3576
+ loadedWordBound: number,
3577
+ ): { end: number; unsized: Instr | undefined; widest: number } {
3578
+ const calls = code.filter(isCallInstr);
3579
+ const words = calls.map((ins) => (ins.mnemonic === 'jal' ? stackWordsOf(jalCallee(ins, splat)) : null));
3580
+ const end = calls.length === 0 ? 0 : STACK_ARGS_AT + 4 * Math.max(0, ...words.map((w) => w ?? 0));
3581
+ const unsized = calls.find((_, k) => words[k] === null);
3582
+ return { end, unsized, widest: unsized === undefined ? end : Math.max(end, loadedWordBound) };
3583
+ }
3584
+
3585
+ export function measureO32Frame({
3586
+ name,
3587
+ code,
3588
+ entry,
3589
+ depthAt,
3590
+ frameSize,
3591
+ rule,
3592
+ stackWordsOf,
3593
+ savedByCallee,
3594
+ splat,
3595
+ loadedWordBound = Infinity,
3596
+ }: {
3597
+ name: string;
3598
+ code: readonly Instr[];
3599
+ entry: readonly Instr[];
3600
+ depthAt: ReadonlyMap<Instr, number> | null;
3601
+ frameSize: number | undefined;
3602
+ rule: TargetDescription['compilerBehaviors']['o32LocalArea'];
3603
+ /** The stack words a `jal`'s callee takes, by its declaration; null where nothing declares it. */
3604
+ stackWordsOf: (callee: string | undefined) => number | null;
3605
+ savedByCallee: (r: string) => boolean;
3606
+ splat: boolean;
3607
+ /** The lowest frame word from 16 up this function loads (`lift`'s loaded-word bound), which caps
3608
+ * how far an unsized call's stack words can reach; `Infinity` where nothing bounds it. */
3609
+ loadedWordBound?: number;
3610
+ }): { area: FrameRange; size: number } | null {
3611
+ const captures = code.filter(takesFrameAddress);
3612
+ if (captures.length === 0 || depthAt === null || frameSize === undefined) {
3613
+ return null;
3614
+ }
3615
+ if (captures.some((ins) => depthAt.get(ins) !== frameSize)) {
3616
+ return null;
3617
+ }
3618
+ const unsized = code.find((ins) => isCallInstr(ins) && ins.mnemonic !== 'jal');
3619
+ if (unsized !== undefined) {
3620
+ throw new FrontendUnsupportedError(
3621
+ `cannot lift '${name}': outgoing stack arguments not modelled — the call '${unsized.mnemonic}' at ` +
3622
+ `0x${unsized.addr.toString(16)} has no declaration to size its stack words by, in a frame whose local ` +
3623
+ 'area holds a captured address',
3624
+ );
3625
+ }
3626
+ const written = new Set<string>();
3627
+ const saves: number[] = [];
3628
+ for (const ins of entry) {
3629
+ const mem = ins.mnemonic === 'sw' ? parseMem(ins.ops[1] ?? '') : undefined;
3630
+ if (mem !== undefined && isStackPtr(mem.base) && savedByCallee(ins.ops[0]) && !written.has(ins.ops[0])) {
3631
+ saves.push(mem.off);
3632
+ } else if (writtenReg(ins) !== null) {
3633
+ written.add(writtenReg(ins)!);
3634
+ }
3635
+ }
3636
+ const {
3637
+ end: outgoingEnd,
3638
+ unsized: undeclared,
3639
+ widest: widestEnd,
3640
+ } = o32OutgoingArea(code, stackWordsOf, splat, loadedWordBound);
3641
+ const area = o32LocalArea(rule, { frameSize, outgoingEnd, saves });
3642
+ if (area === null) {
3643
+ return null;
3644
+ }
3645
+ if (undeclared !== undefined) {
3646
+ const widest = o32LocalArea(rule, {
3647
+ frameSize,
3648
+ outgoingEnd: Math.max(outgoingEnd, Math.min(frameSize, widestEnd, ...saves)),
3649
+ saves,
3650
+ });
3651
+ if (widest?.from !== area.from || widest.to !== area.to) {
3652
+ throw new FrontendUnsupportedError(
3653
+ `cannot lift '${name}': outgoing stack arguments not modelled — nothing declares the callee of the \`jal\` at ` +
3654
+ `0x${undeclared.addr.toString(16)}, so how many stack words it takes is unknown, and this compiler's local ` +
3655
+ 'area starts past them',
3656
+ );
3657
+ }
3658
+ }
3659
+ for (const ins of code) {
3660
+ const last = ins.ops[ins.ops.length - 1] ?? '';
3661
+ const depth = depthAt.get(ins);
3662
+ if (!/\(/.test(last) || !isStackPtr(parseMem(last).base) || depth === frameSize) {
3663
+ continue;
3664
+ }
3665
+ if (landsOnArea(area, parseMem(last).off - depth! + frameSize, 4)) {
3666
+ return null;
3667
+ }
3668
+ }
3669
+ return { area, size: frameSize };
3670
+ }
3671
+
3672
+ /** A `jal`'s callee: the Splat operand, or the R_MIPS_26 record's symbol. */
3673
+ function jalCallee(ins: Instr, splat: boolean): string | undefined {
3674
+ return splat ? ins.ops[0] : ins.reloc?.type === 'R_MIPS_26' ? ins.reloc.sym : undefined;
3675
+ }
3676
+
3677
+ /** THE LOCAL AREA OF AN O32 FRAME, `[from, to)` in post-push `sp` offsets: where this compiler
3678
+ * keeps the locals and spill temps (`compilerBehaviors.o32LocalArea`), so where a captured frame
3679
+ * address may name a local and nothing else does. Below it is the outgoing area, which ends at
3680
+ * `outgoingEnd` — 0 in a function that makes no call, else the 16-byte home of a0..a3 and then
3681
+ * every stack word a call takes; above it, or below it on IDO, the registers the prologue saved at
3682
+ * `saves`. Null where the target states no layout, or the area is empty. */
3683
+ function o32LocalArea(
3684
+ rule: TargetDescription['compilerBehaviors']['o32LocalArea'],
3685
+ { frameSize, outgoingEnd, saves }: { frameSize: number; outgoingEnd: number; saves: readonly number[] },
3686
+ ): FrameRange | null {
3687
+ if (rule === 'above-outgoing-area') {
3688
+ return areaAboveOutgoing({ frameSize, outgoingEnd, saves, align: 8 });
3689
+ }
3690
+ if (rule === 'above-saved-registers') {
3691
+ const area = { from: Math.max(outgoingEnd, ...saves.map((o) => o + 4)), to: frameSize };
3692
+ return area.from < area.to ? area : null;
3693
+ }
3694
+ return null;
3695
+ }
3696
+
3697
+ /** O32's slots: the shared register-then-stack-word rule (`stackArgSlots`), with a floating-point
3698
+ * register read beside a stack word refused — where a float shadows the integer slots ahead of
3699
+ * the stack words is not modelled here. */
3700
+ function o32ArgSlots(name: string, regs: ArgSlots, argRegs: readonly string[]): ArgSlots {
3701
+ return stackArgSlots(regs, argRegs, (readKeys, top) => {
3702
+ const float = readKeys.find(isFpKey);
3703
+ if (float !== undefined) {
3704
+ throw new FrontendUnsupportedError(
3705
+ `cannot lift '${name}': ${float} carries a floating-point argument beside the incoming stack argument ` +
3706
+ `${top + 1} — which integer slots the float takes ahead of the stack words is not modelled`,
3707
+ );
3708
+ }
3709
+ });
3710
+ }
3711
+
1637
3712
  // Build the "branch taken" condition value for a conditional branch (emitting the icmp op).
1638
3713
  function condValue(
1639
3714
  br: Instr,
@@ -1668,3 +3743,7 @@ function condValue(
1668
3743
 
1669
3744
  /** The MIPS-II / IDO frontend, registered for the `mips` target. */
1670
3745
  export const mipsFrontend: Frontend = { id: 'mips', inputFormat: 'objdump', lift };
3746
+
3747
+ /** Internal surface for this module's own tests, and for nothing else (`frontend/frame-objects.ts`
3748
+ * says why it is not a plain export). */
3749
+ export const __testing = { o32LocalArea, refuseReturnsNotThroughCallersRa };