@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -20,9 +20,12 @@
20
20
  // genuine rotate/insert stays an opaque.
21
21
  // • CALLS (`bl`) — the callee symbol comes from the interleaved `R_PPC_REL24` relocation (an
22
22
  // unresolved `bl` in a .o encodes a 0 placeholder); arguments come from r3.. per the callee
23
- // prototype (falling back to argument-register liveness). The frame — `stwu r1`, `mflr`/`mtlr`,
24
- // r1-relative spills — is transparent to dataflow, so a value in a callee-saved register
25
- // survives the call.
23
+ // prototype (falling back to argument-register liveness). The frame bookkeeping — `stwu r1`,
24
+ // the link-register save, callee-saved saves — is transparent to dataflow, so a value in a
25
+ // callee-saved register survives the call; a word stored to the frame with a value is a
26
+ // stack-slot variable (`frameStore`). Inside the local area of a function that takes a frame
27
+ // address (`measurePpcFrame`), every access is memory through a frame object the shared audit
28
+ // proves instead.
26
29
  // • RECORD FORM (`.` = the Rc bit, e.g. `andi.`/`add.`) also sets cr0 from a signed compare of
27
30
  // the result against 0; that implicit compare is wired so a following `beq`/`bne` fuses.
28
31
  //
@@ -36,12 +39,45 @@
36
39
  // conditional-CTR forms (`bdz`/`bdnzt`/…), an unrecovered `bctr`, and a `bdnz` with no reaching
37
40
  // `mtctr`. NOTE mwcc at -O4 aggressively UNROLLS loops into a `bdnz` main loop + a remainder
38
41
  // loop; an unrolled loop recovers as the unrolled form (sound, rarely a match).
39
- import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
40
- import type { Opcode } from '../ir/opcodes';
42
+ import { ownArgumentWordsOn } from '../aggregate';
43
+ import {
44
+ Block,
45
+ Fn,
46
+ Op,
47
+ REGISTER_STRUCT,
48
+ STRUCT_HOMED,
49
+ Successor,
50
+ Value,
51
+ mkOp,
52
+ mkValue,
53
+ replaceAllUsesWith,
54
+ } from '../ir/core';
55
+ import { deletableWhenDead } from '../ir/discipline';
56
+ import { FCMP_TRUTH, NEGATED_FCMP, type Opcode, fcmpOfTruth } from '../ir/opcodes';
41
57
  import { T } from '../ir/types';
42
- import { type Prototypes, protoArity } from '../proto';
58
+ import {
59
+ type FnProto,
60
+ type Prototypes,
61
+ declaredWidth,
62
+ declaresVoidReturn,
63
+ isFloatingSpelling,
64
+ ownFirstArgumentPassed,
65
+ wordsOf,
66
+ } from '../proto';
67
+ import type { ContextGlobals } from '../proto-context';
68
+ import { registerSaveRoutineOf } from '../runtime-helpers';
69
+ import type { SymbolMap } from '../symbols';
43
70
  import type { TargetDescription } from '../target';
44
71
  import { type AsmData, readJumpTable } from './asmdata';
72
+ import { type CallDeclarations, type ParamClass, callDeclarations } from './call-plan';
73
+ import {
74
+ type Declarations,
75
+ computedOnlyToReturn,
76
+ leftInDiscardRegister,
77
+ pinDeadReads,
78
+ unreadPairHalf,
79
+ } from './dead-reads';
80
+ import { pinDeviceAccesses } from './device-pins';
45
81
  import {
46
82
  type DisasmInstr,
47
83
  parseImm,
@@ -50,17 +86,67 @@ import {
50
86
  sliceSymbol,
51
87
  } from './disasm';
52
88
  import { mkEmitKit, pushSwitchBr } from './emit';
53
- import { FrontendUnsupportedError } from './errors';
89
+ import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
90
+ import { inheritFlags } from './flags-edge';
54
91
  import { assertInputFormat } from './format';
55
- import type { Frontend } from './frontend';
92
+ import {
93
+ type ReturnHomeRule,
94
+ carriesFloatHome,
95
+ declaredPrecision,
96
+ floatHomeAt,
97
+ fpPrecision,
98
+ fpuArgSlots,
99
+ refuseBothPrecisions,
100
+ refuseFpWordCrossings,
101
+ returnHomeRule,
102
+ settleReturnHome,
103
+ writesFloatReturn,
104
+ } from './fpu';
105
+ import {
106
+ FRAME_OBJECT_AUDIT,
107
+ type FrameObjectRelift,
108
+ type FrameRange,
109
+ areaAboveOutgoing,
110
+ landsOnArea,
111
+ liftWithRelift,
112
+ } from './frame-objects';
113
+ import { type Frontend, type LiftOptions, type OwnDeclaration, ownDeclarationOf } from './frontend';
56
114
  import { makeHighHalves } from './high-half';
115
+ import {
116
+ type HoistedDispatch,
117
+ type IdiomLeft,
118
+ type StaleIdiomRead,
119
+ type TableKey,
120
+ agreedTable,
121
+ caseEntries,
122
+ enterCaseTarget,
123
+ entersBetween,
124
+ idiomLeaves,
125
+ makeTableBases,
126
+ parkedIn,
127
+ refuseReachableDispatch,
128
+ refuseStaleIdiomReads,
129
+ scaledIndexIn,
130
+ } from './jump-table';
131
+ import { makeLocalStatics, readObjectLocalObject } from './local-object';
57
132
  import { opaqueDest } from './opaque';
58
- import { unspellableReason } from './reloc-symbol';
59
- import { abiSortEntryParams } from './ssa';
60
- import { makeSsaBuilder } from './ssa';
133
+ import { classifyRelocSymbol, unspellableReason } from './reloc-symbol';
134
+ import {
135
+ type ArgSlots,
136
+ abiSortEntryParams,
137
+ incomingStackArgKey,
138
+ mintArgSlotHoles,
139
+ stackArgSlots,
140
+ stackSlotKey,
141
+ } from './ssa';
142
+ import { makeSsaBuilder, refuseSavedRegisterLeaks, registerLiveIns } from './ssa';
61
143
 
62
144
  type Instr = DisasmInstr;
63
145
 
146
+ /** The instructions that READ the carry bit (each also writes it), and every other writer of it. */
147
+ const CARRY_READERS = /^(adde|addme|addze|subfe|subfme|subfze)o?$/;
148
+ const CARRY_WRITERS = /^(addc|addco|addic|subfc|subfco|subfic|srawi|sraw|mtxer|mcrxr)$/;
149
+
64
150
  // Branch-condition mnemonic → the icmp for its TAKEN edge, split by compare signedness. PowerPC's
65
151
  // branch already names the relation (no negation fold needed). `signed` picks the row.
66
152
  const COND_SIGNED: Record<string, Opcode> = {
@@ -116,6 +202,39 @@ const isModeledBranch = (ins: Instr) => isXfer(ins) || ins.mnemonic === 'bl';
116
202
 
117
203
  const isReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
118
204
 
205
+ /** The FPU arithmetic this frontend lifts, onto the float opcodes, with the precision each computes
206
+ * in; `fmr` is a copy and has no opcode. `fneg` and `fmr` carry no precision at all — an FPR holds a
207
+ * double either way — so they take the function's (`frontend/fpu.ts` `fpPrecision`); in a function
208
+ * of only those, the floats state none (`T.fUnstated`), because the float and double spellings of a
209
+ * negation or a copy compile to one object. The compares and `fctiwz` (`FP_CROSSINGS`) are decoded
210
+ * in their own cases. Everything else in the file keeps the register-file refusal `opaqueDest`
211
+ * gives it: `frsp`, the loads and stores but an integer word's `stfd` to the frame, the other
212
+ * conversions, the fused multiply-adds, paired singles — and a record form (`fadds.`), which sets
213
+ * `cr1`. */
214
+ const FP_ARITH: Readonly<Record<string, { op: Opcode | 'copy'; width: 32 | 64 | null }>> = {
215
+ fadds: { op: 'fadd', width: 32 },
216
+ fsubs: { op: 'fsub', width: 32 },
217
+ fmuls: { op: 'fmul', width: 32 },
218
+ fdivs: { op: 'fdiv', width: 32 },
219
+ fadd: { op: 'fadd', width: 64 },
220
+ fsub: { op: 'fsub', width: 64 },
221
+ fmul: { op: 'fmul', width: 64 },
222
+ fdiv: { op: 'fdiv', width: 64 },
223
+ fneg: { op: 'fneg', width: null },
224
+ fmr: { op: 'copy', width: null },
225
+ };
226
+ const FP_ARITH_MNEMONICS: ReadonlySet<string> = new Set(Object.keys(FP_ARITH));
227
+ /** The FPU instructions that move a float's information into the integer file, none of which states
228
+ * a precision: the compares (an FPR holds a double either way, and `a > b` over two floats is the
229
+ * same `fcmpo` as over two doubles) and `fctiwz`, the truncation to an integer word. */
230
+ const FP_CROSSINGS: ReadonlySet<string> = new Set(['fcmpo', 'fcmpu', 'fctiwz']);
231
+
232
+ /** An FPU register: the token, and the key the SSA builder holds it under. ONE predicate for the
233
+ * decode above and the register-file refusal below (`fpReg`), so a token one of them reads as an
234
+ * FPR cannot be one the other does not. */
235
+ const PPC_FP_REG = /^f\d+$/;
236
+ const isFpKey = (k: string) => PPC_FP_REG.test(k);
237
+
119
238
  // Shared objdump scaffolding (frontend/disasm.ts). parseMem narrowed to `r\d+` bases — a
120
239
  // non-register base is an SDA/global placeholder assertOrdinaryMem declines.
121
240
  const parseMem = (operand: string): { off: number; base: string } => parseSharedMem(operand, /r\d+/);
@@ -138,6 +257,42 @@ const rlwinmMask = (mb: number, me: number): number => {
138
257
  // stripping, the mnemonic misses the cond tables and the branch is silently dropped).
139
258
  const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { relocs: true, hintSuffixes: true });
140
259
 
260
+ /** A compare a `cr` field holds: its operands and signedness, which the reading branch turns into a
261
+ * condition. A FLOAT compare (`fcmpo`/`fcmpu`) states instead which compare each of the field's
262
+ * three bits holds, since a `cror` can change one (`bits`); `quiet` is `fcmpu`'s, which C emits for
263
+ * `==` and `!=` alone, so an ordering bit of it is refused. */
264
+ type CrCompare = { lhs: Value; rhs: Value; signed: boolean; bits?: CrBits; quiet?: boolean };
265
+ type CrBits = { readonly lt: Opcode; readonly gt: Opcode; readonly eq: Opcode };
266
+ /** The bit of a field each branch reads, and whether it is taken when the bit is SET: `ble` is
267
+ * "branch if not greater than", which after a float compare is `!(a > b)`, true on a NaN — never
268
+ * `a <= b`. */
269
+ const BRANCH_BIT: Readonly<Record<string, { bit: keyof CrBits; set: boolean }>> = {
270
+ blt: { bit: 'lt', set: true },
271
+ bge: { bit: 'lt', set: false },
272
+ bgt: { bit: 'gt', set: true },
273
+ ble: { bit: 'gt', set: false },
274
+ beq: { bit: 'eq', set: true },
275
+ bne: { bit: 'eq', set: false },
276
+ };
277
+ /** A condition-register bit as `cror` and the `mfcr` extracts name it: its field and its place in
278
+ * the field, or null for the summary-overflow bit (`so`), which a float compare sets on a NaN and
279
+ * nothing here reads. `eq` names cr0's; `4*cr1+eq` cr1's. */
280
+ const crBitOf = (tok: string): { field: string; bit: keyof CrBits } | null => {
281
+ const m = /^(?:4\*cr([0-7])\+)?(lt|gt|eq)$/.exec(tok);
282
+ return m ? { field: `cr${m[1] ?? 0}`, bit: m[2] as keyof CrBits } : null;
283
+ };
284
+ const CR_BIT_NAMES: readonly (keyof CrBits | null)[] = ['lt', 'gt', 'eq', null];
285
+
286
+ /** What a block leaves in the condition register for a successor to inherit. `fields` holds, per
287
+ * field, the compare it holds, or the sentence saying what overwrote it. A field absent from the map
288
+ * never held a compare on the chain of edges that reached here, and `none` is why: the sentence
289
+ * from the block where that chain started. */
290
+ type CrExit = { fields: ReadonlyMap<string, CrCompare | string>; none: string };
291
+
292
+ /** The `cr` fields the EABI makes volatile across a call: cr0, cr1 and cr5–cr7. The callee may
293
+ * write any of them, so a compare in one does not survive a `bl`. cr2–cr4 are nonvolatile. */
294
+ const CR_VOLATILE = ['cr0', 'cr1', 'cr5', 'cr6', 'cr7'] as const;
295
+
141
296
  interface PpcBlock {
142
297
  startAddr: number;
143
298
  body: Instr[];
@@ -146,99 +301,214 @@ interface PpcBlock {
146
301
  }
147
302
 
148
303
  // A recovered dense-switch jump table (Regime B), keyed
149
- // by the BOUNDS branch (`bgt DEF`) whose block emits the `switch_br`. `caseAddrs[k]` is the `.text`
304
+ // by the BOUNDS branch (`bgt DEF`, or `bgtlr`) whose block emits the `switch_br`. `caseAddrs[k]` is the `.text`
150
305
  // address of case `k` (dense 0..N-1); `bctrAddr` is the elided dispatch's indirect jump. The mwcc
151
306
  // idiom:
152
307
  // cmplwi rS,N-1 ; bgt DEF (bounds)
153
308
  // lis rT,0 [ADDR16_HA @tbl] ; slwi rIdx,rS,2 ; addi rB,rT,0 [ADDR16_LO @tbl]
154
309
  // ; lwzx rV,rB,rIdx ; mtctr rV ; bctr (dispatch — table in .data)
310
+ // mwcc schedules other words into the bounds pair (`boundsCompare`), orders the dispatch's freely,
311
+ // and 2.3.3 forms the table base outside the dispatch (`hoistedTable`).
155
312
  interface PpcJT {
156
313
  scrutReg: string;
314
+ /** The bounds compare's address: nothing may enter after it, up to the `bctr`. */
315
+ cmpAddr: number;
157
316
  caseAddrs: number[];
158
- defaultAddr: number;
317
+ /** The default arm's address, or null for a `bgtlr` bound: out-of-range values return. */
318
+ defaultAddr: number | null;
159
319
  bctrAddr: number;
320
+ /** The base register and table of a dispatch whose block does not form its base (mwcc 2.3.3
321
+ * hoists the `lis`/`addi` pair out of a loop): `lift` proves the base reaches it
322
+ * (frontend/jump-table.ts). Null when the pruned block forms the base itself. */
323
+ hoisted: HoistedDispatch | null;
324
+ /** What the pruned dispatch leaves in each register it writes, `ctr` included (`idiomLeaves`). */
325
+ leaves: ReadonlyMap<string, IdiomLeft>;
326
+ }
327
+
328
+ /** The `cr` field a compare writes or a conditional branch reads: its first operand when it names
329
+ * one, else cr0. */
330
+ const crFieldOf = (ins: Instr): string => (ins.ops[0]?.startsWith('cr') ? ins.ops[0] : 'cr0');
331
+
332
+ /** Whether `ins` may write a CR field. A DENY-LIST, so an unknown writer stops the reader rather
333
+ * than being stepped past: the compares, the record forms (`.`), the CR-logical and move-to-CR
334
+ * forms, and every branch — a `bl` destroys the volatile fields. */
335
+ const mayWriteCr = (ins: Instr): boolean =>
336
+ /^(cmp|fcmp|cr|mcr|mtcr|b)/.test(ins.mnemonic) || ins.mnemonic.endsWith('.');
337
+
338
+ /** Whether `ins` may write GPR `r`. A DENY-LIST like mips.ts's `writtenReg`: an instruction writes
339
+ * its first operand unless it is a store, compare, trap, move-to-special or cache op; an update form
340
+ * (`lwzu`, `stwux`) also writes its base, `lmw rD` writes rD..r31, and any branch may (a call
341
+ * clobbers the volatile registers). */
342
+ const mayWriteGpr = (ins: Instr, r: string): boolean => {
343
+ const m = ins.mnemonic;
344
+ if (m.startsWith('b')) {
345
+ return true;
346
+ }
347
+ if (m === 'lmw') {
348
+ return /^r\d+$/.test(r) && Number(r.slice(1)) >= Number(ins.ops[0]?.slice(1));
349
+ }
350
+ if (/^(l|st)\w*ux?$/.test(m) && ins.ops.slice(1).some((o) => new RegExp(`\\b${r}\\b`).test(o))) {
351
+ return true;
352
+ }
353
+ return !/^(st|cmp|fcmp|tw|mt|dc|ic|sync|isync|eieio)/.test(m) && ins.ops[0] === r;
354
+ };
355
+
356
+ /** The bounds compare of a jump table, or null: the `cmplwi rS,N-1` the bounds branch at `j` reads,
357
+ * with its address and the case count `n` it states. Scanned back inside the branch's block, past
358
+ * instructions that write neither a CR field nor `rS` — mwcc schedules the default's value or the
359
+ * LR save between the two — and it must set the CR field the branch tests. The first CR writer met
360
+ * decides: any other one, or a write of `rS` after the compare, declines. So does a branch landing
361
+ * after the compare, up to the bounds branch itself: that path brings a CR field of its own, and the
362
+ * `switch_br` would bound it by a compare it never ran (`entersBetween`). */
363
+ function boundsCompare(instrs: Instr[], j: number, scrutReg: string): { addr: number; n: number } | null {
364
+ const crField = crFieldOf(instrs[j]);
365
+ for (let k = j - 1; k >= 0 && !isXfer(instrs[k]); k--) {
366
+ const c = instrs[k];
367
+ if (c.mnemonic === 'cmplwi' && c.ops[c.ops.length - 2] === scrutReg && crFieldOf(c) === crField) {
368
+ return entersBetween(instrs, c.addr, instrs[j].addr)
369
+ ? null
370
+ : { addr: c.addr, n: parseImm(c.ops[c.ops.length - 1]) + 1 };
371
+ }
372
+ if (mayWriteCr(c) || mayWriteGpr(c, scrutReg)) {
373
+ return null;
374
+ }
375
+ }
376
+ return null;
160
377
  }
161
378
 
379
+ /** The table a base formed OUTSIDE the dispatch block addresses, as far as the instructions say:
380
+ * every `addi rB,rX,SYM@l` in the function must name one table, or the guess is left undecided.
381
+ * mwcc 2.3.3 forms the base once at entry and dispatches from a loop. Only a guess — `lift` pairs
382
+ * the halves and proves the base reaches the dispatch (frontend/jump-table.ts). */
383
+ const hoistedTable = (instrs: Instr[], rB: string): TableKey | null =>
384
+ agreedTable(
385
+ instrs.flatMap((x) =>
386
+ x.mnemonic === 'addi' && x.ops[0] === rB && x.reloc?.type === 'R_PPC_ADDR16_LO' && parseImm(x.ops[2]) === 0
387
+ ? [{ sym: x.reloc.sym, addend: x.reloc.addend }]
388
+ : [],
389
+ ),
390
+ );
391
+
162
392
  // Recover mwcc jump tables from the dispatch idiom + the AsmData side-table. Fail-closed: any
163
- // deviation from the exact idiom, or a table that doesn't resolve to N in-function targets, declines
393
+ // deviation from the idiom, or a table that doesn't resolve to N in-function targets, declines
164
394
  // (→ the `bctr` loud-fail fires). Index IDENTITY guard: `slwi rIdx,rS,2` must be the bounds-checked
165
395
  // scrutinee scaled only by <<2 — no xor/neg/extra op.
166
396
  function recoverPpcJumpTables(instrs: Instr[], ad: AsmData): Map<number, PpcJT> {
167
397
  const out = new Map<number, PpcJT>();
168
- for (let i = 5; i < instrs.length; i++) {
398
+ for (let i = 0; i < instrs.length; i++) {
169
399
  if (instrs[i].mnemonic !== 'bctr') {
170
400
  continue;
171
401
  }
172
- const [lis, lwzx, mtctr] = [instrs[i - 5], instrs[i - 2], instrs[i - 1]];
173
- // The index is scaled and the table's low half formed in either order: mwcc 2.4.2 emits the
174
- // `slwi` first, GC/1.2.5n the `addi`. The two instructions are independent.
175
- const [slwi, addi] =
176
- instrs[i - 4].mnemonic === 'slwi' ? [instrs[i - 4], instrs[i - 3]] : [instrs[i - 3], instrs[i - 4]];
177
- if (
178
- lis.mnemonic !== 'lis' ||
179
- slwi.mnemonic !== 'slwi' ||
180
- addi.mnemonic !== 'addi' ||
181
- lwzx.mnemonic !== 'lwzx' ||
182
- mtctr.mnemonic !== 'mtctr'
183
- ) {
402
+ // THE DISPATCH BLOCK: after the bounds branch, the nearest transfer before the `bctr`.
403
+ let j = i - 1;
404
+ while (j >= 0 && !isXfer(instrs[j])) {
405
+ j--;
406
+ }
407
+ const bgt = instrs[j];
408
+ // `bgt DEF`, or `bgtlr` when the default arm is the return itself: mwcc bounds a switch whose
409
+ // default falls straight out of a leaf that way.
410
+ if (!bgt || !((bgt.mnemonic === 'bgt' && bgt.target !== undefined) || bgt.mnemonic === 'bgtlr')) {
411
+ continue;
412
+ }
413
+ // Each piece of the idiom is the LAST write of its register inside the block, so nothing between
414
+ // a def and its reader can have changed it. The order varies: mwcc 2.4.2 scales the index first,
415
+ // GC/1.2.5n forms the table's low half first.
416
+ const defIn = (r: string, before: number): number => {
417
+ for (let k = before - 1; k > j; k--) {
418
+ if (mayWriteGpr(instrs[k], r)) {
419
+ return k;
420
+ }
421
+ }
422
+ return -1;
423
+ };
424
+ const mi = i - 1;
425
+ const mtctr = instrs[mi];
426
+ if (mtctr?.mnemonic !== 'mtctr' || mi <= j) {
184
427
  continue;
185
428
  }
186
- const rV = mtctr.ops[0];
187
- if (lwzx.ops[0] !== rV) {
429
+ const li = defIn(mtctr.ops[0], mi); // rV = mem[rB + rIdx]
430
+ const lwzx = instrs[li];
431
+ if (lwzx?.mnemonic !== 'lwzx') {
188
432
  continue;
189
- } // rV = mem[rB + rIdx]
433
+ }
190
434
  const [rB, rIdx] = [lwzx.ops[1], lwzx.ops[2]];
191
- if (slwi.ops[0] !== rIdx || (slwi.ops[2] !== '2' && slwi.ops[2] !== '0x2')) {
435
+ const si = defIn(rIdx, li);
436
+ const slwi = instrs[si];
437
+ if (slwi?.mnemonic !== 'slwi' || (slwi.ops[2] !== '2' && slwi.ops[2] !== '0x2')) {
192
438
  continue;
193
439
  } // identity: rIdx = rS<<2
194
440
  const scrutReg = slwi.ops[1];
195
- if (addi.ops[0] !== rB || parseImm(addi.ops[2]) !== 0) {
441
+ const ai = defIn(rB, li);
442
+ const lisI = ai < 0 ? -1 : defIn(instrs[ai].ops[1], ai);
443
+ const hoisted = ai < 0 ? hoistedTable(instrs, rB) : null;
444
+ let table: TableKey | null = hoisted;
445
+ if (ai >= 0) {
446
+ const [lis, addi] = [instrs[lisI], instrs[ai]];
447
+ // The table's base is the same `@ha`/`@l` pair the address fold consumes and must show the
448
+ // same evidence — both relocation types, one symbol, one addend — so a dispatch is never
449
+ // recovered from two instructions that never addressed the table together. It stays a
450
+ // separate pairing: it runs before the blocks exist, and it wants the table's NAME, for
451
+ // `readJumpTable`, not its address. It skips the naming policy deliberately — a table base is
452
+ // read here as data, never written into the candidate as an identifier — so the `@N` and
453
+ // `.`-prefixed symbols the address fold must refuse (292 of the corpus's `ADDR16` relocations)
454
+ // are harmless here.
455
+ table =
456
+ addi.mnemonic === 'addi' &&
457
+ parseImm(addi.ops[2]) === 0 &&
458
+ lis?.mnemonic === 'lis' &&
459
+ lis.ops[0] === addi.ops[1] &&
460
+ lis.reloc?.type === 'R_PPC_ADDR16_HA' &&
461
+ addi.reloc?.type === 'R_PPC_ADDR16_LO' &&
462
+ addi.reloc.sym === lis.reloc.sym &&
463
+ addi.reloc.addend === lis.reloc.addend
464
+ ? { sym: lis.reloc.sym, addend: lis.reloc.addend }
465
+ : null;
466
+ }
467
+ if (table === null) {
196
468
  continue;
197
- } // rB = &table (lo) + 0
198
- const rT = addi.ops[1];
199
- // The table's base is the same `@ha`/`@l` pair the address fold consumes and must show the same
200
- // evidence — both relocation types, one symbol, one addend — so a dispatch is never recovered
201
- // from two instructions that never addressed the table together. It stays a separate pairing:
202
- // it runs before the blocks exist, and it wants the table's NAME, for `readJumpTable`, not its
203
- // address. It skips the naming policy deliberately — a table base is read here as data, never
204
- // written into the candidate as an identifier — so the `@N` and `.`-prefixed symbols the
205
- // address fold must refuse (292 of the corpus's `ADDR16` relocations) are harmless here.
206
- const tableSym = lis.reloc?.sym;
469
+ }
470
+ // The block is pruned unread once the table is recovered, so it may hold the idiom and nothing
471
+ // else, nothing may branch into it past the bounds check, and none of the idiom may rewrite the
472
+ // scrutinee before the `slwi` scales it — the `switch_br` reads it at the bounds branch.
473
+ const idiom = new Set(hoisted === null ? [lisI, si, ai, li, mi] : [si, li, mi]);
207
474
  if (
208
- lis.ops[0] !== rT ||
209
- !tableSym ||
210
- lis.reloc?.type !== 'R_PPC_ADDR16_HA' ||
211
- addi.reloc?.type !== 'R_PPC_ADDR16_LO' ||
212
- addi.reloc.sym !== tableSym ||
213
- addi.reloc.addend !== lis.reloc.addend
475
+ instrs.slice(j + 1, i).some((_, o) => !idiom.has(j + 1 + o)) ||
476
+ instrs.slice(j + 1, si).some((x) => mayWriteGpr(x, scrutReg)) ||
477
+ entersBetween(instrs, bgt.addr, instrs[i].addr)
214
478
  ) {
215
479
  continue;
216
480
  }
217
- // Bounds: the nearest preceding `cmplwi scrutReg,N-1 ; bgt DEF` guard. The `bgt` is itself a
218
- // transfer, so match the pair directly (the cmplwi sits one instruction behind it); a different
219
- // transfer before the guard ⇒ decline.
220
- let bounds: { addr: number; n: number; def: number } | null = null;
221
- for (let j = i - 6; j >= 1; j--) {
222
- const bgt = instrs[j];
223
- if (bgt.mnemonic === 'bgt' && bgt.target !== undefined) {
224
- const c = instrs[j - 1];
225
- if (c && c.mnemonic === 'cmplwi' && c.ops[c.ops.length - 2] === scrutReg) {
226
- bounds = { addr: bgt.addr, n: parseImm(c.ops[c.ops.length - 1]) + 1, def: bgt.target };
227
- }
228
- break;
229
- }
230
- if (isXfer(bgt)) {
231
- break;
232
- }
233
- }
234
- if (!bounds || bounds.n < 2) {
481
+ const bound = boundsCompare(instrs, j, scrutReg);
482
+ if (bound === null || bound.n < 2) {
235
483
  continue;
236
484
  }
237
- const caseAddrs = readJumpTable(ad, tableSym, 0, bounds.n);
485
+ const caseAddrs = readJumpTable(ad, table.sym, table.addend, bound.n);
238
486
  if (!caseAddrs) {
239
487
  continue;
240
488
  }
241
- out.set(bounds.addr, { scrutReg, caseAddrs, defaultAddr: bounds.def, bctrAddr: instrs[i].addr });
489
+ // In program order, `mtctr` writing `ctr`, the `slwi` the scaled scrutinee, every other word its
490
+ // first operand (`lis`, `addi`, `lwzx`).
491
+ const leaves = idiomLeaves(
492
+ [...idiom].sort((a, b) => a - b).map((k) => ({ reg: k === mi ? 'ctr' : instrs[k].ops[0], scaled: k === si })),
493
+ );
494
+ out.set(bgt.addr, {
495
+ scrutReg,
496
+ cmpAddr: bound.addr,
497
+ caseAddrs,
498
+ defaultAddr: bgt.target ?? null,
499
+ bctrAddr: instrs[i].addr,
500
+ hoisted: hoisted === null ? null : { reg: rB, table: hoisted },
501
+ leaves,
502
+ });
503
+ }
504
+ // A table's case is a branch target too, which `entersBetween` cannot see before the tables are
505
+ // read: one landing between a table's bounds compare and its indirect jump — its own table's or
506
+ // another's — enters that window as a branch would, and that table declines.
507
+ const cases = [...out.values()].flatMap((jt) => jt.caseAddrs);
508
+ for (const [at, jt] of out) {
509
+ if (cases.some((a) => a > jt.cmpAddr && a <= jt.bctrAddr)) {
510
+ out.delete(at);
511
+ }
242
512
  }
243
513
  return out;
244
514
  }
@@ -261,7 +531,9 @@ function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blo
261
531
  for (const a of jt.caseAddrs) {
262
532
  leaders.add(a);
263
533
  }
264
- leaders.add(jt.defaultAddr);
534
+ if (jt.defaultAddr !== null) {
535
+ leaders.add(jt.defaultAddr);
536
+ }
265
537
  }
266
538
 
267
539
  const blocks: PpcBlock[] = [];
@@ -282,6 +554,13 @@ function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blo
282
554
  const idxOf = new Map(blocks.map((b, i) => [b.startAddr, i]));
283
555
  const fallAddr = (b: PpcBlock) => (b.branch ?? b.body[b.body.length - 1]).addr + 4;
284
556
  const succIdx: number[][] = blocks.map(() => []);
557
+ /** A conditional return's taken edge: a block of its own that returns, appended past the real ones.
558
+ * Its index. */
559
+ const synthReturn = (): number => {
560
+ blocks.push({ startAddr: -1 - blocks.length, body: [], branch: null, synthReturn: true });
561
+ succIdx.push([]);
562
+ return blocks.length - 1;
563
+ };
285
564
  blocks.forEach((b, i) => {
286
565
  const br = b.branch;
287
566
  // Recovered switch: the bounds block dispatches to its case blocks + default (the `cmplwi`/`bgt`
@@ -289,7 +568,13 @@ function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blo
289
568
  // leader, so all resolve; a target that is not a block boundary makes it malformed → loud-fail.
290
569
  const jt = br ? jts.get(br.addr) : undefined;
291
570
  if (jt) {
292
- const succ = [...jt.caseAddrs, jt.defaultAddr].map((a) => idxOf.get(a));
571
+ const succ: Array<number | undefined> = jt.caseAddrs.map((a) => idxOf.get(a));
572
+ if (jt.defaultAddr !== null) {
573
+ succ.push(idxOf.get(jt.defaultAddr));
574
+ } else {
575
+ // A `bgtlr` bound: the default edge is a synthetic return, as for any conditional return.
576
+ succ.push(synthReturn());
577
+ }
293
578
  if (succ.some((x) => x === undefined)) {
294
579
  throw new PpcUnsupportedError(`cannot lift '${name}': jump-table target is not a block boundary`);
295
580
  }
@@ -313,17 +598,15 @@ function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blo
313
598
  return;
314
599
  }
315
600
  if (isCondReturn(br)) {
316
- const synth: PpcBlock = { startAddr: -1 - blocks.length, body: [], branch: null, synthReturn: true };
317
- const si = blocks.length;
318
- blocks.push(synth);
319
- succIdx.push([]);
601
+ const si = synthReturn();
320
602
  const fall = idxOf.get(br.addr + 4); // fall = instruction after the cond-return (no delay slot)
321
603
  succIdx[i] = fall !== undefined ? [si, fall] : [si];
322
604
  return;
323
605
  }
324
606
  // conditional branch to a label: [taken, fall]. Both must land on block boundaries; otherwise the
325
- // branch leaves the function (a tail branch) or targets unrecovered flow — fail LOUD and catchably
607
+ // branch falls off the function's end or targets unrecovered flow — fail LOUD and catchably
326
608
  // rather than silently dropping an edge (which surfaces as an opaque `verify` successor-count error).
609
+ // A branch out of the function, relocated or not, has already refused as a tail call (`liftPrelude`).
327
610
  const taken = idxOf.get(br.target!);
328
611
  const fall = idxOf.get(br.addr + 4);
329
612
  if (taken === undefined || fall === undefined) {
@@ -356,16 +639,804 @@ function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blo
356
639
  };
357
640
  }
358
641
 
642
+ /** The PPC EABI's small-data base registers: r13 points into `.sdata`/`.sbss` and r2 into
643
+ * `.sdata2`/`.sbss2`, both set once by the runtime's startup code and never by a compiled function.
644
+ * A compiled access names its symbol in an `R_PPC_EMB_SDA21` relocation, which this frontend reads
645
+ * instead of the base (`sdaAccess`). A base read as a VALUE — `lbzx r0,r3,r13`, which mwcc 2.4.7
646
+ * emits for `playerOrderNew[j - 1]` (MP4 `OrderPlayers`), or `lwz r3,-0x7ff0(r13)` in a linked image —
647
+ * has no C spelling here, and the SSA builder would otherwise read the def-less register as an
648
+ * invented parameter. It refuses, as MIPS refuses `gp` used as data. */
649
+ const SMALL_DATA_BASES: readonly string[] = ['r13', 'r2'];
650
+
651
+ /** The link register's save word, as an offset from the entry r1: the second word of the caller's
652
+ * frame header, after the back chain. mwcc 2.3.3 spells it `4(r1)` before the push and 2.4.x
653
+ * `N+4(r1)` after it (`r1Displacements`). */
654
+ const LR_SAVE_WORD = 4;
655
+ /** The caller's parameter area, as an offset from the entry r1: past the back chain and the link
656
+ * register's save word, the words a call takes past its eight argument registers. */
657
+ const PARAM_AREA_AT = 8;
658
+ /** Every load that takes an `off(rA)` operand: integer, multiple, update and floating-point
659
+ * (`outgoing-area-probes.test.ts` classifies every memory mnemonic of the committed probes against it). */
660
+ export const PPC_FRAME_LOAD = /^(lbz|lhz|lha|lwz|lmw|lbzu|lhzu|lhau|lwzu|lfs|lfd|lfsu|lfdu|psq_l|psq_lu)$/;
661
+
662
+ /** The register save or restore routine a `bl` calls (runtime-helpers.ts `RegisterSaveRoutines`),
663
+ * named by its `R_PPC_REL24`, or null for any other `bl`. Only the relocation names it: a `bl` with
664
+ * none — a linked image, or an objdump without `-r` — is no routine, and `case 'bl'` refuses it
665
+ * there as a call with no callee symbol. */
666
+ const saveRoutineOf = (target: TargetDescription, ins: Instr) =>
667
+ ins.mnemonic === 'bl' && ins.reloc?.type === 'R_PPC_REL24'
668
+ ? registerSaveRoutineOf(target.registerSaveRoutines, ins.reloc.sym)
669
+ : null;
670
+
671
+ /** A general register a callee must preserve: one the EABI neither passes an argument in, nor lets
672
+ * a callee destroy, nor reserves (r1, and the small-data bases). `prologueSavedRegs` counts the
673
+ * first store of ANY register in `regs`; only these are saves of the caller's value in `offsets`. */
674
+ const isCalleeSaved = (target: TargetDescription, r: string): boolean =>
675
+ isReg(r) &&
676
+ r !== 'r1' &&
677
+ !SMALL_DATA_BASES.includes(r) &&
678
+ !target.argRegs.includes(r) &&
679
+ !target.callerSaved.includes(r);
680
+
681
+ /** Whether any of `body` names a callee-saved general register (`isCalleeSaved`), as an operand or
682
+ * a memory base, that the prologue did not save — the mark of hand-written asm, since compiled
683
+ * code saves such a register before it touches it. A save routine or `stmw` names only its first
684
+ * register, and saves it, so either is in `saved`.
685
+ *
686
+ * A GLOBAL REGISTER VARIABLE'S register (`target.globalRegisterVariables`) does not count: it is
687
+ * the one callee-saved register COMPILED code names unsaved, and the live-in model reads it as that
688
+ * global (`registerLiveIns`). Counted, it would make the detector and the model disagree about one
689
+ * register, and drop the bound a compiled body's own declaration states: mwcc 2.4.2's
690
+ * `register int g __asm__("r14"); int f(int a) { int x, y; if (a) { x = a; y = a + 1; } … a * g`
691
+ * homes x and y in r4/r5, and `f(s32)` would lift as `f(s32 a0, s32 a1, s32 a2)` with no note. The
692
+ * blind side is a hand-written entry point handed inputs in r14..r28 and in argument registers
693
+ * past its declaration, which then reads those as `uninit_<reg>`; asm that names no other unsaved
694
+ * callee-saved register is byte-for-byte the compiled shape. No function of the ac, mp4 or Pikmin
695
+ * builds names an unsaved r14..r28 at all. */
696
+ const namesUnsavedCalleeSaved = (
697
+ body: readonly Instr[],
698
+ target: TargetDescription,
699
+ saved: ReadonlySet<string>,
700
+ ): boolean =>
701
+ body.some((ins) =>
702
+ ins.ops.some((op) => {
703
+ const reg = op.includes('(') ? parseMem(op).base : op;
704
+ return (
705
+ isCalleeSaved(target, reg) && !saved.has(reg) && !Object.hasOwn(target.globalRegisterVariables?.regs ?? {}, reg)
706
+ );
707
+ }),
708
+ );
709
+
710
+ /** WHAT THE PROLOGUE SAVED, AND WHERE. `regs` is the save half of the register partition
711
+ * (frontend/ssa.ts, `LiveInModel.uninitRegs`). A register the ABI passes no argument in is an
712
+ * uninitialised local only where the compiler could home a local there, which it may do only after
713
+ * saving it; without this, an uninitialised local in a callee-saved register lifts as an extra
714
+ * PARAMETER (`int x; if (s) x = k(a); return k(a) + x;` would take a third).
715
+ *
716
+ * Read off the ENTRY block, the one every path starts with: `stmw rS,D(r1)` saves rS..r31, a GPR
717
+ * save routine rNN..r31, and `stw rN,D(r1)` rN. A register counts as saved only where NO earlier
718
+ * instruction names it, as an operand or as a memory base, read or written. That is what a save
719
+ * is: the first thing the function does with the register. A write before the store makes the
720
+ * `stw` a spill; a READ before it is asm handed a live value there, which the save then preserves
721
+ * — the very case the save half exists to keep a parameter for, so it must not count.
722
+ *
723
+ * NOT Thumb's rule (`savedRegs` in frontend/thumb.ts), which ends the measurement at the end of the
724
+ * LEADING run of saves. mwcc interleaves its saves with the body's first instructions
725
+ * (`stw r31,12(r1); mr r31,r3; stw r30,8(r1)`), and the save routine's `addi r11,r1,N` need not sit
726
+ * next to its `bl`, so a leading run would drop saves the prologue really makes. The two rules
727
+ * agree on what they let through: no instruction before a counted save touches the register. The
728
+ * operand text can only over-name (a register spelt in a branch target names nothing), which
729
+ * under-states the set — the direction that keeps a parameter.
730
+ *
731
+ * `offsets` are where the saves of the caller's values lie, as entry-r1 offsets, which bound the
732
+ * local area from above (`measurePpcFrame`): each `stw` of a callee-saved register (`isCalleeSaved`)
733
+ * the same first-naming rule counts, each such word of a `stmw`, each word a save routine stores
734
+ * below its frame address (`addi r11,r1,N`), and each floating-point save (`stfd`, or the FPR
735
+ * routine), which this frontend refuses elsewhere but which still bounds where a local may lie.
736
+ * Null where a save's offset cannot be named: r1's depth at it, or the routine's frame address, is
737
+ * unknown.
738
+ *
739
+ * A SAVE IS RELOADED. In a function that returns, an `stw` of a callee-saved register that no
740
+ * reload of it from r1 answers anywhere — an `lwz` of it, an `lmw` or restore routine reaching it —
741
+ * saves nothing: the caller gets the register back as the body left it, so the word is a value.
742
+ * mwcc 2.4 stores a file-scope global register variable passed on the stack exactly so, in the
743
+ * entry block — `register int g __asm__("r14"); return k(1, …, 8, g) + g;` is `stw r14,8(r1); bl k`
744
+ * and no reload of r14 — and counting it as a save would turn the read of `g` after the call into
745
+ * `uninit_r14` and drop the ninth argument. Which word a reload reads is judged where it is
746
+ * lowered (`restoreFrom`). A function with no return keeps the entry-block reading. */
747
+ function prologueSavedRegs(
748
+ entry: readonly Instr[],
749
+ target: TargetDescription,
750
+ r1At: ReadonlyMap<Instr, number | string>,
751
+ blocks: readonly PpcBlock[],
752
+ ): { regs: ReadonlySet<string>; offsets: number[] | null } {
753
+ const code = blocks.flatMap((b) => [...b.body, ...(b.branch ? [b.branch] : [])]);
754
+ const returns = blocks.some((b) => b.synthReturn) || code.some((ins) => isReturn(ins) || isCondReturn(ins));
755
+ const reloaded = (r: string) =>
756
+ code.some((ins) => {
757
+ const routine = saveRoutineOf(target, ins);
758
+ return routine
759
+ ? routine.restore && !routine.fpr && Number(r.slice(1)) >= routine.first
760
+ : ins.ops[1]?.includes('(') === true &&
761
+ parseMem(ins.ops[1]).base === 'r1' &&
762
+ ((ins.mnemonic === 'lwz' && ins.ops[0] === r) ||
763
+ (ins.mnemonic === 'lmw' && Number(r.slice(1)) >= Number(ins.ops[0]?.slice(1))));
764
+ });
765
+ const regs = new Set<string>();
766
+ const named = new Set<string>();
767
+ const offsets: number[] = [];
768
+ let unnamed = false;
769
+ const word = (off: number | null) => {
770
+ if (off === null) {
771
+ unnamed = true;
772
+ } else {
773
+ offsets.push(off);
774
+ }
775
+ };
776
+ let frameAddress: number | null = null;
777
+ for (const ins of entry) {
778
+ const [d, s] = ins.ops;
779
+ const depth = r1At.get(ins);
780
+ const routine = saveRoutineOf(target, ins);
781
+ if (ins.mnemonic === 'addi' && d === target.registerSaveRoutines?.frameReg && s === 'r1') {
782
+ frameAddress = typeof depth === 'number' ? depth + parseImm(ins.ops[2]) : null;
783
+ } else if (routine) {
784
+ if (!routine.restore) {
785
+ const bytes = routine.fpr ? 8 : 4;
786
+ for (let k = routine.first; k <= 31; k++) {
787
+ if (!routine.fpr && !named.has(`r${k}`)) {
788
+ regs.add(`r${k}`);
789
+ }
790
+ word(frameAddress === null ? null : frameAddress - bytes * (32 - k));
791
+ }
792
+ }
793
+ } else if (/^(stw|stmw|stfd)$/.test(ins.mnemonic) && s?.includes('(') && parseMem(s).base === 'r1') {
794
+ const first = ins.mnemonic === 'stmw' ? Number(d.slice(1)) : 0;
795
+ const n = ins.mnemonic === 'stmw' ? 32 - first : 1;
796
+ // A single `stw` no reload answers, in a function that returns, saves nothing (see below).
797
+ const unanswered = ins.mnemonic === 'stw' && isCalleeSaved(target, d) && returns && !reloaded(d);
798
+ for (let k = 0; k < (unanswered ? 0 : n); k++) {
799
+ const reg = ins.mnemonic === 'stmw' ? `r${first + k}` : d;
800
+ if (ins.mnemonic !== 'stfd' && !named.has(reg)) {
801
+ regs.add(reg);
802
+ }
803
+ if (ins.mnemonic === 'stfd' || (isCalleeSaved(target, reg) && !named.has(reg))) {
804
+ word(typeof depth === 'number' ? parseMem(s).off + depth + 4 * k : null);
805
+ }
806
+ }
807
+ }
808
+ for (const op of ins.ops) {
809
+ const reg = op.includes('(') ? parseMem(op).base : op;
810
+ if (isReg(reg)) {
811
+ named.add(reg);
812
+ }
813
+ }
814
+ }
815
+ return { regs, offsets: unnamed ? null : offsets };
816
+ }
817
+
818
+ /** Where r1 stands, relative to its value at entry, just BEFORE an instruction runs: a byte count
819
+ * (0 before the frame push, -N after `stwu r1,-N(r1)`), or why it is not known there.
820
+ *
821
+ * A frame slot has to be named by its offset from the ENTRY r1, because the compilers disagree on
822
+ * which side of the push they save the link register. mwcc 2.4.x pushes first and saves at
823
+ * `N+4(r1)`; mwcc 2.3.3 saves at `4(r1)` BEFORE `stwu r1,-N(r1)` and restores from `N+4(r1)`. That is
824
+ * one word of the caller's frame spelled at two offsets, and an offset from the CURRENT r1 reads it
825
+ * as two different words.
826
+ *
827
+ * Forward dataflow over the block graph, per instruction. Only the two forms a compiler uses to
828
+ * move r1 are counted: `stwu r1,-N(r1)` from the entry value, and `addi r1,r1,N`. Anything else
829
+ * that writes r1 — a second push, an `addi r1,rX,N` from another register, an `mr r1,r11` — makes
830
+ * the displacement unknown from there on, and so does a join whose predecessors arrive at
831
+ * different depths. Unknown is not a refusal by itself: a displacement nothing reads decides
832
+ * nothing. The frame accesses and the push that DO read it refuse loud (`lift`, `entryOffset`). */
833
+ function r1Displacements(blocks: PpcBlock[], succIdx: number[][]): Map<Instr, number | string> {
834
+ const at = new Map<Instr, number | string>();
835
+ const into: Array<number | string | undefined> = blocks.map(() => undefined);
836
+ // A block whose displacement is unknown stays unknown, under the first reason found.
837
+ const meet = (bi: number, d: number | string): boolean => {
838
+ const old = into[bi];
839
+ let next = d;
840
+ if (typeof old === 'string' || old === d) {
841
+ next = old;
842
+ } else if (typeof old === 'number' && typeof d === 'number') {
843
+ const where = blocks[bi].startAddr >= 0 ? `0x${blocks[bi].startAddr.toString(16)}` : 'a synthesized return';
844
+ next = `the paths into ${where} arrive with r1 at two depths (${old} and ${d} bytes from its entry value)`;
845
+ }
846
+ into[bi] = next;
847
+ return next !== old;
848
+ };
849
+ const writesR1 = (ins: Instr) => ins.ops[0] === 'r1' && !/^(st|cmp|tw|mt|dc|ic)/.test(ins.mnemonic);
850
+ meet(0, 0);
851
+ const work = [0];
852
+ while (work.length) {
853
+ const bi = work.pop()!;
854
+ let d = into[bi]!;
855
+ for (const ins of [...blocks[bi].body, ...(blocks[bi].branch ? [blocks[bi].branch] : [])]) {
856
+ at.set(ins, d);
857
+ const where = `0x${ins.addr.toString(16)}`;
858
+ if (ins.mnemonic === 'stwu' && ins.ops[0] === 'r1' && parseMem(ins.ops[1]).base === 'r1') {
859
+ d =
860
+ d === 0
861
+ ? parseMem(ins.ops[1]).off
862
+ : `the frame push at ${where} is a second one (r1 already moved from its entry value)`;
863
+ } else if (ins.mnemonic === 'addi' && ins.ops[0] === 'r1' && ins.ops[1] === 'r1' && !ins.reloc) {
864
+ d = typeof d === 'number' ? d + parseImm(ins.ops[2]) : d;
865
+ } else if (writesR1(ins)) {
866
+ d = `'${ins.mnemonic} ${ins.ops.join(',')}' at ${where} sets r1 to a value this frontend does not track`;
867
+ }
868
+ }
869
+ for (const s of succIdx[bi]) {
870
+ if (meet(s, d)) {
871
+ work.push(s);
872
+ }
873
+ }
874
+ }
875
+ return at;
876
+ }
877
+
878
+ /** What the link-register proof knows at one point: what the link register holds (`lr`: null where
879
+ * it is still the caller's return address on every path, else the instruction that put something
880
+ * else there on some path), which general registers hold a copy of the return address on every
881
+ * path (`regs`), and whether the link register's save word does (`slot`). */
882
+ interface LrState {
883
+ readonly lr: Instr | null;
884
+ readonly regs: ReadonlySet<string>;
885
+ readonly slot: boolean;
886
+ }
887
+
888
+ /** EVERY RETURN OF THIS FUNCTION JUMPS THROUGH THE CALLER'S RETURN ADDRESS, or the lift declines —
889
+ * PowerPC's half of what `refuseReturnsNotThroughCallersLr` asks of Thumb (frontend/thumb.ts).
890
+ *
891
+ * `blr` and a conditional return (`beqlr`) read as returns from the instruction alone, and each is
892
+ * one only where the link register holds the address this function was called with. A forward
893
+ * dataflow over the blocks tracks where that address is:
894
+ * • the link register holds it at entry; a `bl` writes it (a call, and a register save or restore
895
+ * routine too), and an `mtlr rS` puts back whatever rS holds;
896
+ * • `mflr rD` copies it into rD, `mr` copies it on, and any other write to a register loses it —
897
+ * an ordinary call loses every register the EABI lets a callee destroy, a save routine none, and
898
+ * a GPR restore routine the r{first}..r31 it reloads, as `lmw` does;
899
+ * • a `stw` of a register holding it into the link register's save word (`LR_SAVE_WORD`, named
900
+ * from the entry r1 through `r1At`) puts it in that word, and a `lwz` from that word copies it
901
+ * back out. Any other store reaching those bytes, a store through r1 at an unknown depth, a call
902
+ * made before the frame push (the callee saves ITS return address in that very word), and a
903
+ * save routine whose frame address could reach it all overwrite it.
904
+ * Paths meet by agreement: the link register, a register or the word holds the address only if it
905
+ * does on every path in.
906
+ *
907
+ * So `li r3,7; mtlr r4; blr`, `lwz r0,0(r3); mtlr r0; blr`, `mflr r0; addi r0,r0,4; mtlr r0; blr`, an
908
+ * `mtlr` on one path only, and a `bl` with no saved address put back before the `blr` all decline,
909
+ * where mwcc's `mflr r0; stw r0,N+4(r1)` … `lwz r0,N+4(r1); mtlr r0; blr` (2.3.3 saves at `4(r1)`
910
+ * before the push) and a leaf that never names the link register are returns. No compiler emits a
911
+ * violation; hand-written asm can, and it would otherwise lift as a C `return`.
912
+ *
913
+ * Returned per instruction that reads the link register as the return address where it may not be
914
+ * that, with the instruction that put something else there on some path: a return, and an `mflr`
915
+ * after a `bl` (its copy would be taken for the return address, and is the address after the `bl`
916
+ * instead). Absent everywhere else. Blocks no path from the entry reaches are not asked: they never
917
+ * run.
918
+ *
919
+ * NOT ASKED: a store through a register holding a frame address (`addi r3,r1,N`) is not traced to
920
+ * the save word. A compiler addresses that word only through r1. */
921
+ function linkRegisterProof(
922
+ blocks: PpcBlock[],
923
+ succIdx: number[][],
924
+ r1At: ReadonlyMap<Instr, number | string>,
925
+ target: TargetDescription,
926
+ ): Map<Instr, Instr> {
927
+ const at = new Map<Instr, Instr>();
928
+ const destroyed = new Set(['r0', ...target.argRegs, ...target.callerSaved]);
929
+ const frameReg = target.registerSaveRoutines?.frameReg;
930
+ const join = (a: LrState, b: LrState): LrState => ({
931
+ lr: a.lr ?? b.lr,
932
+ regs: new Set([...a.regs].filter((r) => b.regs.has(r))),
933
+ slot: a.slot && b.slot,
934
+ });
935
+ const same = (a: LrState, b: LrState) =>
936
+ a.lr === b.lr && a.slot === b.slot && a.regs.size === b.regs.size && [...a.regs].every((r) => b.regs.has(r));
937
+ // The bytes `[lo, hi)` a store through r1 writes, as entry-r1 offsets, or null where r1's depth is
938
+ // not known there or the address takes an index register.
939
+ const storedRange = (ins: Instr): [number, number] | null => {
940
+ const d = r1At.get(ins);
941
+ const mem = ins.ops[1];
942
+ if (typeof d !== 'number' || !mem?.includes('(')) {
943
+ return null;
944
+ }
945
+ const mn = ins.mnemonic;
946
+ const width =
947
+ mn === 'stmw'
948
+ ? 4 * (32 - Number(ins.ops[0].slice(1)))
949
+ : /^stb/.test(mn)
950
+ ? 1
951
+ : /^sth/.test(mn)
952
+ ? 2
953
+ : /^stfd/.test(mn)
954
+ ? 8
955
+ : 4;
956
+ const lo = parseMem(mem).off + d;
957
+ return [lo, lo + width];
958
+ };
959
+ const namesR1 = (ins: Instr) => ins.ops.some((op) => (op.includes('(') ? parseMem(op).base : op) === 'r1');
960
+
961
+ const run = (bi: number, inState: LrState, record: boolean): LrState => {
962
+ let { lr, slot } = inState;
963
+ const regs = new Set(inState.regs);
964
+ let frameAddress: number | null = null;
965
+ for (const ins of [...blocks[bi].body, ...(blocks[bi].branch ? [blocks[bi].branch] : [])]) {
966
+ const mn = ins.mnemonic;
967
+ const [d, s] = ins.ops;
968
+ if (
969
+ record &&
970
+ ((mn === 'mflr' && lr?.mnemonic === 'bl') || ((isReturn(ins) || isCondReturn(ins)) && lr !== null))
971
+ ) {
972
+ at.set(ins, lr!);
973
+ }
974
+ if (mn === 'bl') {
975
+ const routine = saveRoutineOf(target, ins);
976
+ lr = ins;
977
+ if (routine) {
978
+ // A save routine stores below its frame address; a restore routine writes no memory.
979
+ const bytes = routine.fpr ? 8 : 4;
980
+ if (
981
+ !routine.restore &&
982
+ (frameAddress === null ||
983
+ (frameAddress - bytes * (32 - routine.first) < LR_SAVE_WORD + 4 && frameAddress > LR_SAVE_WORD))
984
+ ) {
985
+ slot = false;
986
+ }
987
+ // A GPR restore routine reloads r{first}..r31 from their save words, as `lmw` does: none of
988
+ // them holds the return address after it, whatever was copied into them before.
989
+ if (routine.restore && !routine.fpr) {
990
+ for (const r of [...regs]) {
991
+ if (Number(r.slice(1)) >= routine.first) {
992
+ regs.delete(r);
993
+ }
994
+ }
995
+ }
996
+ } else {
997
+ for (const r of destroyed) {
998
+ regs.delete(r);
999
+ }
1000
+ if (r1At.get(ins) === 0 || typeof r1At.get(ins) !== 'number') {
1001
+ slot = false;
1002
+ }
1003
+ }
1004
+ continue;
1005
+ }
1006
+ if (mn === 'mtlr') {
1007
+ lr = regs.has(d) ? null : ins;
1008
+ continue;
1009
+ }
1010
+ if (mn === 'mflr') {
1011
+ if (lr === null) {
1012
+ regs.add(d);
1013
+ } else {
1014
+ regs.delete(d);
1015
+ }
1016
+ continue;
1017
+ }
1018
+ if (/^st/.test(mn)) {
1019
+ if (namesR1(ins)) {
1020
+ const range = storedRange(ins);
1021
+ if (mn === 'stw' && range !== null && range[0] === LR_SAVE_WORD && regs.has(d)) {
1022
+ slot = true;
1023
+ } else if (range === null || (range[0] < LR_SAVE_WORD + 4 && range[1] > LR_SAVE_WORD)) {
1024
+ slot = false;
1025
+ }
1026
+ }
1027
+ // An update form writes its base register.
1028
+ if (/^st\w*ux?$/.test(mn)) {
1029
+ regs.delete(s?.includes('(') ? parseMem(s).base : s);
1030
+ }
1031
+ continue;
1032
+ }
1033
+ if (/^(cmp|tw|mt|dc|ic|b|sync|isync|eieio)/.test(mn)) {
1034
+ continue;
1035
+ }
1036
+ if (mn === 'lmw' || /^lsw/.test(mn)) {
1037
+ const first = mn === 'lmw' ? Number(d.slice(1)) : 0;
1038
+ for (const r of [...regs]) {
1039
+ if (Number(r.slice(1)) >= first) {
1040
+ regs.delete(r);
1041
+ }
1042
+ }
1043
+ continue;
1044
+ }
1045
+ const copies =
1046
+ (mn === 'mr' && regs.has(s)) ||
1047
+ (mn === 'or' && s === ins.ops[2] && regs.has(s)) ||
1048
+ (mn === 'lwz' &&
1049
+ slot &&
1050
+ s?.includes('(') &&
1051
+ parseMem(s).base === 'r1' &&
1052
+ typeof r1At.get(ins) === 'number' &&
1053
+ parseMem(s).off + (r1At.get(ins) as number) === LR_SAVE_WORD);
1054
+ if (frameReg !== undefined && mn === 'addi' && d === frameReg && s === 'r1' && !ins.reloc) {
1055
+ const depth = r1At.get(ins);
1056
+ frameAddress = typeof depth === 'number' ? depth + parseImm(ins.ops[2]) : null;
1057
+ } else if (d === frameReg) {
1058
+ frameAddress = null;
1059
+ }
1060
+ if (isReg(d)) {
1061
+ regs.delete(d);
1062
+ }
1063
+ // A load with update writes its base register too.
1064
+ if (/^l\w*ux?$/.test(mn) && mn !== 'lux') {
1065
+ regs.delete(s?.includes('(') ? parseMem(s).base : s);
1066
+ }
1067
+ if (copies && isReg(d)) {
1068
+ regs.add(d);
1069
+ }
1070
+ }
1071
+ return { lr, regs, slot };
1072
+ };
1073
+
1074
+ const into: Array<LrState | undefined> = blocks.map(() => undefined);
1075
+ into[0] = { lr: null, regs: new Set(), slot: false };
1076
+ const work = [0];
1077
+ while (work.length) {
1078
+ const bi = work.pop()!;
1079
+ const out = run(bi, into[bi]!, false);
1080
+ for (const s of succIdx[bi]) {
1081
+ const prev = into[s];
1082
+ const next = prev === undefined ? out : join(prev, out);
1083
+ if (prev === undefined || !same(prev, next)) {
1084
+ into[s] = next;
1085
+ work.push(s);
1086
+ }
1087
+ }
1088
+ }
1089
+ // Ask each reader once the states are settled, so a reason names the fixed point's writer.
1090
+ into.forEach((st, bi) => {
1091
+ if (st !== undefined) {
1092
+ run(bi, st, true);
1093
+ }
1094
+ });
1095
+ return at;
1096
+ }
1097
+
1098
+ /** The sub-word load and store mnemonics this frontend lifts. */
1099
+ const SUBWORD_MEM: ReadonlySet<string> = new Set(['lbz', 'lha', 'lhz', 'stb', 'sth']);
1100
+
1101
+ /** An instruction that takes a frame address as data: `addi rD,r1,k`. The register save routines'
1102
+ * `addi r11,r1,N` is frame bookkeeping (`frame-address` in `nonValues`), not a capture. */
1103
+ const takesFrameAddress = (ins: Instr, frameReg: string | undefined): boolean =>
1104
+ ins.mnemonic === 'addi' && ins.ops[1] === 'r1' && ins.ops[0] !== 'r1' && ins.ops[0] !== frameReg && !ins.reloc;
1105
+
1106
+ /** THE LOADED-WORD BOUND (`compilerBehaviors.outgoingAreaNeverLoadedFrom`, 8 on mwcc), per r1
1107
+ * depth: the lowest frame word, as an offset from the entry r1, that this function LOADS at that
1108
+ * depth from the parameter area up, past the callee's back chain and link-register words. mwcc
1109
+ * loads no word of its outgoing area, before a call or after, so a word it loads is a local's,
1110
+ * and since the area is one region at the frame bottom every word at or above it is no call's
1111
+ * stack argument. Only an instruction the entry reaches has a depth, so every load counted can
1112
+ * run; one at another depth names another frame's words. Measured once, in `liftPrelude`: the
1113
+ * undeclared-call guess (through `measurePpcFrame`'s `outgoingEnd`), the unread-store decline and
1114
+ * the frame-object audit (`loadedWords`) take this number, and it counts what the IR does not
1115
+ * show the audit — the slot reloads and the epilogue's restores. The local area's partition does
1116
+ * not: it starts at the parameter area whatever the bound, and rests on the guess and the audit.
1117
+ * `Infinity` at every depth where the target states no such offset. */
1118
+ function ppcLoadedWordBound(
1119
+ blocks: readonly PpcBlock[],
1120
+ r1At: ReadonlyMap<Instr, number | string>,
1121
+ target: TargetDescription,
1122
+ ): (depth: number) => number {
1123
+ const loadedFrom = target.compilerBehaviors.outgoingAreaNeverLoadedFrom;
1124
+ if (loadedFrom === undefined) {
1125
+ return () => Infinity;
1126
+ }
1127
+ const bounds = new Map<number, number>();
1128
+ for (const b of blocks) {
1129
+ for (const ins of b.body) {
1130
+ const depth = r1At.get(ins);
1131
+ const mem = ins.ops.find((o) => /^-?\d+\(r1\)$/.test(o));
1132
+ if (typeof depth !== 'number' || mem === undefined || !PPC_FRAME_LOAD.test(ins.mnemonic)) {
1133
+ continue;
1134
+ }
1135
+ const off = parseMem(mem).off;
1136
+ const word = off - (((off % 4) + 4) % 4);
1137
+ if (word >= loadedFrom) {
1138
+ bounds.set(depth, Math.min(bounds.get(depth) ?? Infinity, depth + word));
1139
+ }
1140
+ }
1141
+ }
1142
+ return (depth: number) => bounds.get(depth) ?? Infinity;
1143
+ }
1144
+
1145
+ /** THE LOCAL AREA of a function that takes a frame address, reaches its frame below a word, or
1146
+ * keeps a dead local there, and the size of the one frame it pushes — or null, which leaves every
1147
+ * such access the refusal it had without one. Null where none of the three happens; where the
1148
+ * function does not push exactly one frame, with `stwu r1,-N(r1)` at entry depth; where a capture
1149
+ * runs at another depth; where the target states no layout, a save's offset cannot be named, or
1150
+ * no area is left; and where an r1 access at another depth lands on the area's bytes
1151
+ * (`landsOnArea`), which the routing by post-push offset would then misname.
1152
+ *
1153
+ * The area starts where the caller's parameter area does (`PARAM_AREA_AT`), past the back chain
1154
+ * and the link-register save word, and runs to the lowest save (`compilerBehaviors.linkageLocalArea`,
1155
+ * `areaAboveOutgoing`). The measured rule puts it past the widest call's stack words too; no call
1156
+ * this frontend lowers takes any, so a call whose declaration may take some refuses here, where the
1157
+ * area is drawn: one declared with more parameter words than argument registers, with a 64-bit parameter (the EABI
1158
+ * aligns a pair to an odd register, so a packed word count does not say where the stack words
1159
+ * start), or with more floating parameters than f1..f8. So does a declared parameter that is a
1160
+ * struct or union passed by value (`ParamClass` `by-value`), which mwcc passes as the address of a
1161
+ * copy in this area (`tb(1, b)` copies `b` to r1+8 and passes `addi r4,r1,8` at -O4,p) — a copy no
1162
+ * source declares, whose address the lift would pass where the callee takes the struct — and one
1163
+ * nothing sizes (`unsized`), which may be that, a pair or a stack word.
1164
+ *
1165
+ * A CALL NOTHING DECLARES IS PROVEN CLEAR OF THE AREA ONLY BY THE LOADED-WORD BOUND
1166
+ * (`ppcLoadedWordBound`): mwcc loads no word of its outgoing area from the parameter area up, so
1167
+ * the call's stack words, if any, lie below the lowest pushed-frame word this function loads.
1168
+ * `outgoingEnd` is that word, or the parameter area's start where every call is declared: where it
1169
+ * is past the area's start, the area may hold such a word. Its lowering's guess reads it, refusing
1170
+ * a call whose guessed arity reaches seven argument registers unless `outgoingEnd` is at or below
1171
+ * the area's start (`lift`, the guess's `refuse`) — but the guess counts only the registers this function
1172
+ * sets, none it passes straight through, so `g9(a, …, h, 77)` with r3..r10 untouched passes it
1173
+ * and the word at r1+8 is g9's ninth argument, inside the area. What refuses that call is the
1174
+ * shared audit (frame-objects.ts `storedNeverRead`): the store to r1+8 reaches the call unread,
1175
+ * below the bound it takes (`loadedWords`, the same measurement), so no load proves it a local's.
1176
+ * That audit is the backstop this partition rests on for an undeclared callee; a change to the
1177
+ * guess or to the audit's survivor rule moves it.
1178
+ *
1179
+ * A word stored to the frame that no word load reads back, in a function that makes no call, is a
1180
+ * DEAD LOCAL: no callee can take it as a stack argument. A tail branch, which would hand this
1181
+ * frame's caller's parameter area to another function, refuses before this runs (`liftPrelude`).
1182
+ * It lifts as a store to a frame object, and what reaches the C is then the shared lift's, as for
1183
+ * the same store on Thumb and MIPS: a fixed-offset scalar becomes a local, which the dead-store
1184
+ * pass drops (`l3/dce.ts`) unless its value is a read it keeps (`sp8 = *a0`), so `int v; v = x;`
1185
+ * at -O0,p lifts to an empty body and a `volatile` local's stores are lost with it — no code can
1186
+ * observe that word — and only a store at a runtime index stays one, into the frame array. A dead
1187
+ * local alone draws the area only where one lies inside it. */
1188
+ function measurePpcFrame({
1189
+ name,
1190
+ blocks,
1191
+ saves,
1192
+ r1At,
1193
+ target,
1194
+ calls,
1195
+ loadedWordBound,
1196
+ fail,
1197
+ }: {
1198
+ name: string;
1199
+ blocks: readonly PpcBlock[];
1200
+ /** the prologue's save words, as entry-r1 offsets (`prologueSavedRegs`) */
1201
+ saves: readonly number[] | null;
1202
+ r1At: ReadonlyMap<Instr, number | string>;
1203
+ target: TargetDescription;
1204
+ calls: CallDeclarations;
1205
+ /** `ppcLoadedWordBound`, the one the lift reads */
1206
+ loadedWordBound: (depth: number) => number;
1207
+ fail: FrontendRefusal;
1208
+ }): { area: FrameRange; size: number; outgoingEnd: number } | null {
1209
+ const code = blocks.flatMap((b) => [...b.body, ...(b.branch ? [b.branch] : [])]);
1210
+ const bls = blocks.flatMap((b) => b.body).filter((ins) => ins.mnemonic === 'bl' && !saveRoutineOf(target, ins));
1211
+ const frameReg = target.registerSaveRoutines?.frameReg;
1212
+ const r1Mem = (ins: Instr) => {
1213
+ const last = ins.ops[ins.ops.length - 1] ?? '';
1214
+ return last.includes('(') && parseMem(last).base === 'r1' && ins.mnemonic !== 'stwu' ? parseMem(last) : undefined;
1215
+ };
1216
+ const captures = code.filter((ins) => takesFrameAddress(ins, frameReg));
1217
+ const subword = code.filter((ins) => SUBWORD_MEM.has(ins.mnemonic) && r1Mem(ins) !== undefined);
1218
+ const entryOff = (ins: Instr) => {
1219
+ const at = r1At.get(ins);
1220
+ return typeof at === 'number' ? r1Mem(ins)!.off + at : undefined;
1221
+ };
1222
+ const loadedWords = new Set(
1223
+ code.flatMap((ins) => {
1224
+ const at = r1Mem(ins) && /^(lwz|lmw)$/.test(ins.mnemonic) ? entryOff(ins) : undefined;
1225
+ return at === undefined
1226
+ ? []
1227
+ : ins.mnemonic === 'lmw'
1228
+ ? [...Array(32 - Number(ins.ops[0].slice(1))).keys()].map((k) => at + 4 * k)
1229
+ : [at];
1230
+ }),
1231
+ );
1232
+ const deadStores =
1233
+ bls.length > 0
1234
+ ? []
1235
+ : code.filter(
1236
+ (ins) => ins.mnemonic === 'stw' && r1Mem(ins) !== undefined && !loadedWords.has(entryOff(ins) ?? NaN),
1237
+ );
1238
+ if (captures.length === 0 && subword.length === 0 && deadStores.length === 0) {
1239
+ return null;
1240
+ }
1241
+ const pushes = code.filter((ins) => ins.mnemonic === 'stwu' && ins.ops[0] === 'r1');
1242
+ if (pushes.length !== 1 || r1At.get(pushes[0]) !== 0 || parseMem(pushes[0].ops[1]).base !== 'r1') {
1243
+ return null;
1244
+ }
1245
+ const size = -parseMem(pushes[0].ops[1]).off;
1246
+ if (!(size > 0) || captures.some((ins) => r1At.get(ins) !== -size) || saves === null) {
1247
+ return null;
1248
+ }
1249
+ const area = target.compilerBehaviors.linkageLocalArea
1250
+ ? areaAboveOutgoing({ frameSize: size, outgoingEnd: PARAM_AREA_AT, saves: saves.map((o) => o + size), align: 4 })
1251
+ : null;
1252
+ if (area === null) {
1253
+ return null;
1254
+ }
1255
+ for (const ins of code) {
1256
+ const off = r1Mem(ins) === undefined || r1At.get(ins) === -size ? undefined : entryOff(ins);
1257
+ if (off !== undefined && landsOnArea(area, off + size, 4)) {
1258
+ return null;
1259
+ }
1260
+ }
1261
+ // Drawn for dead stores alone, the area must hold one: a save the prologue made is no local.
1262
+ const inArea = (ins: Instr) => {
1263
+ const at = (entryOff(ins) ?? NaN) + size;
1264
+ return at >= area.from && at < area.to;
1265
+ };
1266
+ if (captures.length === 0 && subword.length === 0 && !deadStores.some(inArea)) {
1267
+ return null;
1268
+ }
1269
+ const fprArgs = target.fpu?.argRegs.length ?? 0;
1270
+ for (const ins of bls) {
1271
+ const callee = ins.reloc?.sym ?? '';
1272
+ const params = calls.declaredParams(callee);
1273
+ if (params === null) {
1274
+ continue;
1275
+ }
1276
+ const at = `'${callee}', called at 0x${ins.addr.toString(16)},`;
1277
+ const copied = params.findIndex((p) => p.kind === 'by-value');
1278
+ if (copied >= 0) {
1279
+ fail(
1280
+ `cannot lift '${name}': address-taken stack local — ${at} takes its parameter ${copied + 1} as ` +
1281
+ `'${params[copied].spelling}', which no word holds: a struct or union passed by value, which this compiler ` +
1282
+ "passes as the address of a copy in the caller's local area — not modelled",
1283
+ );
1284
+ }
1285
+ const count = (kind: ParamClass) => params.filter((p) => p.kind === kind).length;
1286
+ const unsized = params.findIndex((p) => p.kind === 'unsized');
1287
+ const why =
1288
+ count('wide') > 0
1289
+ ? 'a 64-bit parameter, which the EABI places in an aligned register pair or on the stack'
1290
+ : count('word') > target.argRegs.length
1291
+ ? `${count('word')} parameter words and the argument registers carry ${target.argRegs.length}`
1292
+ : count('float') > fprArgs
1293
+ ? `${count('float')} floating parameters and the floating-point argument registers carry ${fprArgs}`
1294
+ : unsized >= 0
1295
+ ? `its parameter ${unsized + 1} as '${params[unsized].spelling}', which nothing here sizes: a word, a ` +
1296
+ 'pair, a stack word or a by-value copy in the local area'
1297
+ : undefined;
1298
+ if (why !== undefined) {
1299
+ fail(
1300
+ `cannot lift '${name}': outgoing stack arguments not modelled — ${at} is declared with ${why}, and this ` +
1301
+ "compiler's local area starts past the stack words a call takes",
1302
+ );
1303
+ }
1304
+ }
1305
+ // Every call a declaration sizes takes no stack word, or refused above. One nothing declares
1306
+ // takes none past the loaded-word bound of the pushed frame, the most its area can reach.
1307
+ const undeclared = bls.some((ins) => calls.declaredParams(ins.reloc?.sym ?? '') === null);
1308
+ const outgoingEnd = undeclared ? Math.max(PARAM_AREA_AT, loadedWordBound(-size) + size) : PARAM_AREA_AT;
1309
+ return { area, size, outgoingEnd };
1310
+ }
1311
+
1312
+ /** What a call's lowering on this frontend carries out (`CallLowering`): no pair, no stack word, no
1313
+ * struct returned through memory. */
1314
+ const PPC_CALL_LOWERING = {
1315
+ pairs: false,
1316
+ memoryReturn: false,
1317
+ stackArgs: false,
1318
+ voidReturn: false,
1319
+ argRegisterBoundsArity: false,
1320
+ } as const;
1321
+
1322
+ /** This frontend's refusal, for the shared code that refuses on its behalf. */
1323
+ const ppcRefusal: FrontendRefusal = (message) => {
1324
+ throw new PpcUnsupportedError(message);
1325
+ };
1326
+
1327
+ /** WHAT `lift` READS OFF THE TEXT BEFORE IT DECODES AN INSTRUCTION: the control transfers it
1328
+ * refuses, the blocks (with the preheader), r1's depth at each instruction, the prologue's saves,
1329
+ * the callees' declarations, and the local area those draw (`measurePpcFrame`). `lift` and
1330
+ * `__testing.frameOf` both run it, so the area a test pins is the one the lift draws. */
1331
+ function liftPrelude(
1332
+ name: string,
1333
+ instrs: Instr[],
1334
+ target: TargetDescription,
1335
+ prototypes: Prototypes,
1336
+ jts: Map<number, PpcJT>,
1337
+ ownDeclaration?: () => OwnDeclaration | undefined,
1338
+ ) {
1339
+ const recoveredBctr = new Set([...jts.values()].map((j) => j.bctrAddr));
1340
+ // TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
1341
+ // would silently miscompile the control flow). CTR-counted loops and indirect branches land here.
1342
+ for (const ins of instrs) {
1343
+ if (ins.mnemonic.startsWith('b') && !isModeledBranch(ins)) {
1344
+ if (ins.mnemonic === 'bctr' && recoveredBctr.has(ins.addr)) {
1345
+ continue;
1346
+ } // recovered switch dispatch
1347
+ throw new PpcUnsupportedError(
1348
+ `cannot lift '${name}': unmodelled control transfer '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
1349
+ (ins.mnemonic === 'blrl' || ins.mnemonic === 'bctrl'
1350
+ ? `(an indirect call — a virtual dispatch or a call through a pointer — is not yet supported)`
1351
+ : `(CTR-counted loop or indirect branch — mwcc -O4 loop unrolling is not yet supported)`),
1352
+ );
1353
+ }
1354
+ }
1355
+ // A BRANCH OUT OF THE FUNCTION is a tail call: the callee takes over this frame's caller's
1356
+ // parameter area as its own, so a word stored there may be its stack argument, and the call
1357
+ // itself would be dropped. objdump prints one either at an address outside the function or,
1358
+ // relocated, at its own address, which would read as a loop. A conditional branch out
1359
+ // (`beq- g`, `R_PPC_REL14`) and a `bdnz` out are tail calls too.
1360
+ const tail = instrs.find(
1361
+ (i) =>
1362
+ (isUncond(i) || isCond(i) || isCtrLoop(i)) &&
1363
+ (i.reloc !== undefined || (i.target !== undefined && !instrs.some((j) => j.addr === i.target))),
1364
+ );
1365
+ if (tail !== undefined) {
1366
+ throw new PpcUnsupportedError(
1367
+ `cannot lift '${name}': the '${tail.mnemonic}' at 0x${tail.addr.toString(16)} leaves the function` +
1368
+ (tail.reloc ? ` for '${tail.reloc.sym}'` : '') +
1369
+ ' — a tail call is not modelled',
1370
+ );
1371
+ }
1372
+ const { blocks, succIdx } = toBlocks(instrs, name, jts);
1373
+ refuseReachableDispatch(name, blocks, recoveredBctr, ppcRefusal);
1374
+ const entryBody = blocks[0].body;
1375
+ // PREHEADER. A function whose first block is a branch target (`do b = b * b; while (--n);` is a
1376
+ // loop at the entry) would take its parameters as that loop header's phis. An empty block ahead
1377
+ // of it becomes the entry, and its live-ins are the function's parameters (frontend/ssa.ts
1378
+ // `ArgSlots`). It starts where the function does, which is what its label says.
1379
+ if (succIdx.some((ss) => ss.includes(0))) {
1380
+ blocks.unshift({ startAddr: blocks[0].startAddr, body: [], branch: null });
1381
+ succIdx.forEach((ss, i) => (succIdx[i] = ss.map((j) => j + 1)));
1382
+ succIdx.unshift([1]);
1383
+ }
1384
+ const r1At = r1Displacements(blocks, succIdx);
1385
+ const saves = prologueSavedRegs(entryBody, target, r1At, blocks);
1386
+ const calls = callDeclarations(name, target, prototypes, PPC_CALL_LOWERING, ppcRefusal, ownDeclaration);
1387
+ // THE LOCAL AREA, where a function takes a frame address, reaches its frame below a word, or
1388
+ // keeps a dead local there (`measurePpcFrame`, `compilerBehaviors.linkageLocalArea`): every r1
1389
+ // access inside it, any width, is MEMORY through an `laddr` the shared frame-object audit then
1390
+ // proves, as on Thumb and MIPS, and every word access outside it keeps the slot model. Null where
1391
+ // neither happens, or the frame is one this cannot partition, and then every capture keeps its
1392
+ // r1-as-data refusal and every sub-word access its own.
1393
+ const loadedWordBound = ppcLoadedWordBound(blocks, r1At, target);
1394
+ const frame = measurePpcFrame({
1395
+ name,
1396
+ blocks,
1397
+ saves: saves.offsets,
1398
+ r1At,
1399
+ target,
1400
+ calls,
1401
+ loadedWordBound,
1402
+ fail: ppcRefusal,
1403
+ });
1404
+ return { blocks, succIdx, r1At, savedRegs: saves.regs, calls, frame, loadedWordBound };
1405
+ }
1406
+
359
1407
  /** Lift disassembled PowerPC text → an L1 Fn with block-argument SSA. `asmData` (optional) supplies
360
1408
  * the data-section jump table for dense-switch (Regime-B) recovery; absent ⇒ a `bctr` dispatch
361
- * loud-fails. */
1409
+ * loud-fails.
1410
+ *
1411
+ * AT MOST TWICE, as on Thumb and MIPS (`frontend/frame-objects.ts` `liftWithRelift`): where the
1412
+ * frame-object audit answers that a callee may write the local area without bound, or that nothing
1413
+ * escapes and the per-object model does not describe it, it asks for the area kept as one object,
1414
+ * and the second lift's audit judges it so and refuses rather than asking again. */
362
1415
  export function lift(
363
1416
  name: string,
364
1417
  asm: string,
365
1418
  target: TargetDescription,
366
1419
  prototypes: Prototypes = {},
367
1420
  asmData?: AsmData,
1421
+ symbols?: SymbolMap,
1422
+ context?: ContextGlobals,
1423
+ options: LiftOptions = {},
368
1424
  ): Fn {
1425
+ return liftWithRelift(name, (oneObject) =>
1426
+ liftOnce(name, asm, target, prototypes, asmData, { symbols, context }, oneObject, options),
1427
+ );
1428
+ }
1429
+
1430
+ function liftOnce(
1431
+ name: string,
1432
+ asm: string,
1433
+ target: TargetDescription,
1434
+ prototypes: Prototypes,
1435
+ asmData: AsmData | undefined,
1436
+ declarations: Declarations,
1437
+ oneObject: FrameRange | undefined,
1438
+ { contextOwnWords, contextOwnProto }: Pick<LiftOptions, 'contextOwnWords' | 'contextOwnProto'>,
1439
+ ): Fn | FrameObjectRelift {
369
1440
  assertInputFormat('ppc', 'objdump', asm);
370
1441
  const instrs = parseDisasm(sliceSymbol(asm, name)); // ONE function only — an absent symbol declines loud
371
1442
  if (instrs.length === 0) {
@@ -375,19 +1446,27 @@ export function lift(
375
1446
  // recovered dispatch's `bctr` is subsumed into a `switch_br` (emitted from its bounds block), so
376
1447
  // it is exempted from the loud-fail below; an UNrecovered `bctr` still fails loud.
377
1448
  const jts = asmData ? recoverPpcJumpTables(instrs, asmData) : new Map<number, PpcJT>();
378
- const recoveredBctr = new Set([...jts.values()].map((j) => j.bctrAddr));
379
1449
  // A data reloc on an instruction whose decode did not TAKE it means objdump printed a LINK-TIME
380
1450
  // placeholder (`lis r4,0` + R_PPC_ADDR16_HA sym): the real value is the symbol's half, and
381
1451
  // lifting the 0 silently reads the wrong address — plausible-but-wrong C, the forbidden class.
382
1452
  // The jump-table idiom's own @tbl pair never reaches this guard: a recovered dispatch block
383
- // is the bounds branch's replaced fall-through, pruned as unreachable before decode.
1453
+ // is the bounds branch's replaced fall-through, pruned as unreachable before decode, and a
1454
+ // hoisted pair is decoded into a table base (`tableBases`).
384
1455
  //
385
1456
  // TWO refusals, because they are different problems and the reader acts on them differently. A
386
1457
  // symbol the naming policy calls unspellable (frontend/reloc-symbol.ts) is a DEAD END: recovering
387
1458
  // the address perfectly would still leave a name no C source can write, so no amount of lifting
388
1459
  // opens the row. A spellable symbol is a CAPABILITY GAP: the address is recoverable and the row
389
1460
  // waits on the fold. Saying which one a row hit is the whole value of the message.
390
- const relocSite = (ins: Instr) => `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)}`;
1461
+ //
1462
+ // An update form is decoded as the `addi` that advances its base and then the plain access
1463
+ // (`withUpdate` in `decode`); a refusal of that `addi`'s relocation, or of either one's write
1464
+ // (`write`), names the instruction the listing holds, not the one it was decoded through.
1465
+ const decodedFrom = new WeakMap<Instr, Instr>();
1466
+ const relocSite = (at: Instr) => {
1467
+ const ins = decodedFrom.get(at) ?? at;
1468
+ return `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)}`;
1469
+ };
391
1470
  /** Whether the decode of the instruction in hand has TAKEN its relocation. `decode` asks once, of
392
1471
  * every modelled instruction, so a relocation no case consumed refuses instead of being dropped.
393
1472
  * A per-case guard would only cover the cases that remember to carry one, and every other
@@ -395,12 +1474,26 @@ export function lift(
395
1474
  * `R_PPC_EMB_SDA21` is `a0 & 0`). An UNmodelled instruction refuses earlier and for a better
396
1475
  * reason: `lfs`/`lfd` under either relocation name the float gap, not this one. */
397
1476
  let relocTaken = false;
1477
+ const statics = makeLocalStatics();
398
1478
  /** The symbol a relocation names, once the naming policy has passed it. Every recovery below goes
399
1479
  * through here FIRST, so an unspellable name can never reach the declaration minter looking like
400
- * an ordinary identifier — recovering the address is only half of being able to write it down. */
1480
+ * an ordinary identifier — recovering the address is only half of being able to write it down.
1481
+ *
1482
+ * A function-scope static is the one kind no `extern` can name that a lift can still WRITE: the
1483
+ * function defines it, so its definition, read out of the side table, is recorded beside the Fn
1484
+ * under the linker name the IR keeps, and structure spells it by its source name. */
401
1485
  const spellableSym = (ins: Instr): string => {
402
1486
  relocTaken = true;
403
1487
  const sym = ins.reloc?.sym ?? '';
1488
+ if (classifyRelocSymbol(sym) === 'local-static') {
1489
+ const read = asmData
1490
+ ? readObjectLocalObject(asmData, sym, name)
1491
+ : { refused: "whose definition needs the object's data, which this lift was not given" };
1492
+ if ('refused' in read) {
1493
+ throw new PpcUnsupportedError(`${relocSite(ins)} names a function-scope static ('${sym}') ${read.refused}`);
1494
+ }
1495
+ return statics.define(read);
1496
+ }
404
1497
  const why = unspellableReason(sym);
405
1498
  if (why) {
406
1499
  throw new PpcUnsupportedError(`${relocSite(ins)} ${why}`);
@@ -413,22 +1506,18 @@ export function lift(
413
1506
  `is a link-time placeholder, not the value`,
414
1507
  );
415
1508
  };
416
- // TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
417
- // would silently miscompile the control flow). CTR-counted loops and indirect branches land here.
418
- for (const ins of instrs) {
419
- if (ins.mnemonic.startsWith('b') && !isModeledBranch(ins)) {
420
- if (ins.mnemonic === 'bctr' && recoveredBctr.has(ins.addr)) {
421
- continue;
422
- } // recovered switch dispatch
423
- throw new PpcUnsupportedError(
424
- `cannot lift '${name}': unmodelled control transfer '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
425
- (ins.mnemonic === 'blrl' || ins.mnemonic === 'bctrl'
426
- ? `(an indirect call — a virtual dispatch or a call through a pointer — is not yet supported)`
427
- : `(CTR-counted loop or indirect branch — mwcc -O4 loop unrolling is not yet supported)`),
428
- );
429
- }
430
- }
431
- const { blocks, succIdx } = toBlocks(instrs, name, jts);
1509
+ const fail = ppcRefusal;
1510
+ // the own declaration binds only a body a compiler wrote (`ownDeclared`, measured below, before
1511
+ // any call is lowered)
1512
+ const { blocks, succIdx, r1At, savedRegs, calls, frame, loadedWordBound } = liftPrelude(
1513
+ name,
1514
+ instrs,
1515
+ target,
1516
+ prototypes,
1517
+ jts,
1518
+ () => (!ownDeclared ? undefined : ownDeclarationOf(name, prototypes, contextOwnProto)),
1519
+ );
1520
+ const lrUnproven = linkRegisterProof(blocks, succIdx, r1At, target);
432
1521
 
433
1522
  const preds: number[][] = blocks.map(() => []);
434
1523
  blocks.forEach((_, i) => {
@@ -437,9 +1526,89 @@ export function lift(
437
1526
  }
438
1527
  });
439
1528
 
440
- const ssa = makeSsaBuilder(name, blocks.length, preds);
1529
+ const fpu = target.fpu;
1530
+ // A CALL IN A FUNCTION THAT COMPUTES ON FLOATS refuses — at the float instruction, so a refusal
1531
+ // the stream reaches first keeps its own reason: which FPRs a callee destroys, which it reads as
1532
+ // float arguments and which it returns in are all unmodelled, so a float live across a call — or
1533
+ // handed to one in f1 — would resolve to a value the callee overwrote or be dropped. Both scans
1534
+ // read the blocks the entry reaches: a `bl` on no path makes no call.
1535
+ const reached = blocks.flatMap((b) => b.body);
1536
+ // A register save routine is no call: it moves only the registers it saves (`saveRoutine`).
1537
+ const callInsn = reached.find((ins) => ins.mnemonic === 'bl' && !saveRoutineOf(target, ins));
1538
+
1539
+ // THE LIVE-IN MODEL is the register half only (this frontend claims no frame partition): a
1540
+ // def-less read of a register the ABI passes no argument in, and that the prologue saved or the
1541
+ // ABI lets a callee use unsaved, is an uninitialised local. `registerLiveIns` (frontend/ssa.ts)
1542
+ // composes it, as it does for Thumb. The FPU's callee-saved half is not measured, so f14..f31 keep
1543
+ // the treatment a target claiming no partition gets. The function's own declaration bounds the
1544
+ // general argument registers (`ownArgumentWordsOn`): mwcc 2.3.3 homes locals in r5..r10 past a
1545
+ // two-parameter one. The FPRs are not bounded here.
1546
+ //
1547
+ // NOT WHERE THE BODY IS HAND-WRITTEN. A declaration states a compiled function's register
1548
+ // interface, and only a compiled one's: a CodeWarrior `asm` function's type — `ksNesInit03__Fv`,
1549
+ // declared `void ()` — says nothing of the registers it really takes (r4..r8 there, set by the
1550
+ // emulator core that calls it), and bounding by it turns each into `uninit_<reg>`. The cheap
1551
+ // tell is a callee-saved register the body names without the prologue having saved it
1552
+ // (`stw r7,5612(r31)` with no save of r31), which compiled code never does: it saves before it
1553
+ // touches one. Such a body keeps every argument register a parameter, as with no declaration.
1554
+ const ownDeclared = !namesUnsavedCalleeSaved(reached, target, savedRegs);
1555
+ // Which home the function returns in (frontend/fpu.ts `returnHomeRule`), by the own declaration
1556
+ // where the body is a compiler's.
1557
+ const homeRule: ReturnHomeRule = !fpu
1558
+ ? { home: 'integer', declared: false }
1559
+ : returnHomeRule(
1560
+ ownDeclared ? ownDeclarationOf(name, prototypes, contextOwnProto) : undefined,
1561
+ reached.some((ins) => FP_CROSSINGS.has(ins.mnemonic)),
1562
+ () => writesFloatReturn(reached, FP_ARITH_MNEMONICS, (t) => (isFpKey(t) ? t : null), fpu),
1563
+ );
1564
+ const floatReturn = homeRule.home === 'float';
1565
+ const fpType = declaredPrecision(
1566
+ fpPrecision(reached, (m) => FP_ARITH[m]?.width ?? (FP_CROSSINGS.has(m) ? null : undefined)),
1567
+ ownDeclared ? ownDeclarationOf(name, prototypes, contextOwnProto) : undefined,
1568
+ );
1569
+ const liveIns = registerLiveIns(
1570
+ target,
1571
+ savedRegs,
1572
+ !ownDeclared ? undefined : (contextOwnWords ?? ownArgumentWordsOn(name, prototypes[name], target)),
1573
+ );
1574
+ const ssa = makeSsaBuilder(
1575
+ name,
1576
+ blocks.length,
1577
+ preds,
1578
+ () => ({ ...liveIns, saveSite: 'store or save routine in its entry block' }),
1579
+ (k) => (isFpKey(k) ? (fpType ?? T.f32()) : undefined),
1580
+ liveIns.globalRegs,
1581
+ );
441
1582
  const { irBlocks, readVar, writeVar, paramReg } = ssa;
442
1583
 
1584
+ /** Where an r1 access of `width` bytes at `ins` lies in the local area: its post-push offset, or
1585
+ * undefined for one outside it. An access across the area's edge refuses: half memory and half
1586
+ * slot is neither. `measurePpcFrame` refuses a frame where an access at another depth lands in
1587
+ * the area, so one inside it runs at the frame's own depth. */
1588
+ const inLocalArea = (ins: Instr, mem: string, width: number): number | undefined => {
1589
+ const depth = r1At.get(ins);
1590
+ if (frame === null || parseMem(mem).base !== 'r1' || typeof depth !== 'number') {
1591
+ return undefined;
1592
+ }
1593
+ const at = parseMem(mem).off + depth + frame.size;
1594
+ if (at >= frame.area.to || at + width <= frame.area.from) {
1595
+ return undefined;
1596
+ }
1597
+ if (at < frame.area.from || at + width > frame.area.to) {
1598
+ fail(
1599
+ `cannot lift '${name}': address-taken stack local — '${ins.mnemonic}' at 0x${ins.addr.toString(16)} reaches ` +
1600
+ `across an edge of the local area, r1+${frame.area.from} up to r1+${frame.area.to} — part memory, part slot`,
1601
+ );
1602
+ }
1603
+ return at;
1604
+ };
1605
+ // The frame offset each `laddr` this lift emitted names — function-scoped, since a register
1606
+ // holding one crosses blocks — and every capture moved into another (`addi rD,rCapture,k`
1607
+ // re-minted as the `laddr` of the sum), handed to the audit so a base only the move read is
1608
+ // dropped rather than judged as a capture (`foldMovedCaptures`).
1609
+ const laddrAt = new Map<Value, number>();
1610
+ const movedCaptures = new Set<Value>();
1611
+
443
1612
  /** The HIGH half of a relocated address, per value standing for one. The invariant, and every
444
1613
  * way a placeholder could leak past it, lives in frontend/high-half.ts — shared with
445
1614
  * frontend/mips.ts, which folds the same two-instruction address under `%hi`/`%lo`.
@@ -457,71 +1626,247 @@ export function lift(
457
1626
  hi: '@ha',
458
1627
  hiArticle: 'an',
459
1628
  lo: '@l',
460
- fail: (message) => {
461
- throw new PpcUnsupportedError(message);
462
- },
1629
+ fail,
463
1630
  });
1631
+ /** The jump-table bases a recovered dispatch reads from outside its own block — the `@l` that
1632
+ * completes one defines no value but the base (frontend/jump-table.ts). */
1633
+ const tableBases = makeTableBases(
1634
+ [...jts.values()].flatMap((j) => (j.hoisted ? [j.hoisted] : [])),
1635
+ fail,
1636
+ );
1637
+ // What an elided dispatch left at each case target's entry (jump-table.ts `caseEntries`), and the
1638
+ // markers written there, by value.
1639
+ const entries = caseEntries(
1640
+ blocks.flatMap((b, at) => {
1641
+ const jt = b.branch ? jts.get(b.branch.addr) : undefined;
1642
+ if (jt === undefined) {
1643
+ return [];
1644
+ }
1645
+ const n = jt.caseAddrs.length;
1646
+ // the default is a `bgtlr` bound's synthetic return of its own, or the default block
1647
+ return [
1648
+ { at, scrutReg: jt.scrutReg, cases: succIdx[at].slice(0, n), defaultBlock: succIdx[at][n], leaves: jt.leaves },
1649
+ ];
1650
+ }),
1651
+ preds,
1652
+ );
1653
+ const staleIdiom = new Map<Value, StaleIdiomRead>();
1654
+ const parked = parkedIn(staleIdiom);
1655
+ const scaledIndex = scaledIndexIn(staleIdiom);
1656
+ /** What this frontend defines registers with that is no argument: a pending `@ha` half and a
1657
+ * table base. NOT a marker an elided dispatch left (`staleIdiom`): a case may copy one
1658
+ * (`mr r3,r4`), and a return or a call that skipped it would drop what the case handed on. */
1659
+ const placeholders = {
1660
+ has: (v: Value) => highHalves.has(v) || tableBases.has(v),
1661
+ guardRead: (n: string, r: string, v: Value) => tableBases.guardRead(n, r, highHalves.guardRead(n, r, v)),
1662
+ };
464
1663
  /** Reading a register AS A VALUE. A register holding a high half is not one, so this refuses loud
465
1664
  * rather than handing back a plausible number standing for an address. `foldLoHalf` is the only
466
1665
  * code entitled to look at a high half, which is why it reaches `readVar` directly. */
467
- const readReg = (r: string, at: number): Value => highHalves.guardRead(name, r, readVar(r, at));
1666
+ const readReg = (r: string, at: number): Value => {
1667
+ if (SMALL_DATA_BASES.includes(r)) {
1668
+ fail(
1669
+ `cannot lift '${name}': ${r} (the small-data base) used as data — an access the relocation ` +
1670
+ `does not name is not modelled`,
1671
+ );
1672
+ }
1673
+ const v = placeholders.guardRead(name, r, readVar(r, at));
1674
+ const nv = nonValues.get(v);
1675
+ if (nv !== undefined && NON_VALUE_RULES[nv.kind].refuseAtRead) {
1676
+ refuseNonValue(nv);
1677
+ }
1678
+ return v;
1679
+ };
1680
+ /** Reading r1 (the stack pointer) as a DATA operand means frame-pointer arithmetic or an
1681
+ * address-taken local (`addi r3,r1,8` = `&local`) — not modellable without a stack abstraction,
1682
+ * and fabricating a value for r1 silently miscompiles. */
1683
+ const r1AsData = (): never =>
1684
+ fail(
1685
+ `cannot lift '${name}': stack pointer r1 used as data (address-taken local / frame arithmetic) — not supported`,
1686
+ );
1687
+ /** An `mflr` reached by a `bl` with no `mtlr` since, or a return the link-register proof could
1688
+ * not show jumps through the caller's return address (`linkRegisterProof`). */
1689
+ const refuseOverwrittenLink = (ins: Instr) => {
1690
+ const by = lrUnproven.get(ins);
1691
+ const at = (i: Instr) => `'${i.mnemonic}' at 0x${i.addr.toString(16)}`;
1692
+ if (by === undefined) {
1693
+ return;
1694
+ }
1695
+ if (ins.mnemonic === 'mflr') {
1696
+ fail(
1697
+ `cannot lift '${name}': the ${at(ins)} reads the link register the ${at(by)} overwrote on some path, ` +
1698
+ `with no 'mtlr' since — the return address is lost there`,
1699
+ );
1700
+ }
1701
+ if (by.mnemonic === 'bl') {
1702
+ fail(
1703
+ `cannot lift '${name}': the ${at(ins)} may not return through the caller's link register — the ` +
1704
+ `${at(by)} overwrote it on some path, and no 'mtlr' put the saved return address back`,
1705
+ );
1706
+ }
1707
+ fail(
1708
+ `cannot lift '${name}': the ${at(ins)} may not return through the caller's link register — on some ` +
1709
+ `path the ${at(by)} put ${by.ops[0]} there, which does not hold the return address an 'mflr' copied ` +
1710
+ `out or its reload from the link register's save word`,
1711
+ );
1712
+ };
1713
+ /** DEFINITIONS THAT ARE NOT VALUES: the frame bookkeeping this frontend erases. Each kind is
1714
+ * written into a register as an ordinary SSA definition, so SSA answers whether one reaches a use,
1715
+ * and is keyed by that value, as the high halves are (frontend/high-half.ts). Its one legitimate
1716
+ * consumer looks it up here; every other use refuses through `refuseNonValue`,
1717
+ * checked at a read (`readReg`) where the kind says so and, always, on the finished function
1718
+ * (`assertNoNonValueEscaped`), because a use can reach through a join's incoming arguments, which
1719
+ * no read sees. A new kind is one more member of the union, one rule and one refusal, and the
1720
+ * three checks cover it. The high halves keep their own module only because MIPS shares it.
1721
+ *
1722
+ * - `frame-address`: what `addi r11,r1,N` forms — its offset from the entry r1 (or why that is not
1723
+ * known there) and the address of the `addi`. Its one consumer is a register save or restore
1724
+ * routine (`saveRoutineOf`), which takes it in the target's `registerSaveRoutines.frameReg`, so
1725
+ * that register is the only destination deferred; every other `addi rD,r1,N` refuses where it
1726
+ * stands. Any other use is the frame read as data, and refuses as the `addi` itself would. One
1727
+ * no routine took refuses too, even where nothing reads it (`assertNonValuesConsumed`). A `bl`
1728
+ * to the routine that carries no relocation (a linked image, or objdump without `-r`) never gets
1729
+ * that far: it is no routine, and `case 'bl'` refuses it as a call with no callee symbol.
1730
+ * - `restored`: the caller's value a restore puts back in a callee-saved register
1731
+ * (`restoreFrom`). No C value names it, so it has no consumer.
1732
+ * - `return-address`: what `mflr rD` copies out of the link register, with the address of the
1733
+ * `mflr`. Storing it to the link register's save word is the LR save (`frameStore`), and moving
1734
+ * it back with `mtlr` is the return. Any other use is a function reading its own return address
1735
+ * (MP4's `asm { mflr retaddr }` does, to tag an allocation with its caller), which no C
1736
+ * expression spells: a store elsewhere refuses at once, anything else once the function is
1737
+ * built. A read alone does not refuse, so a copy (`mr`) still carries it to its save.
1738
+ * - `fctiwz-high`: the HIGH word an `stfd` of an `fctiwz` result stores, with the address of the
1739
+ * `stfd`. `fctiwz` leaves that half undefined, so a reload of it is no value. No consumer. */
1740
+ type NonValue =
1741
+ | { kind: 'frame-address'; off: number | string; addr: number }
1742
+ | { kind: 'restored'; reg: string; addr: number }
1743
+ | { kind: 'return-address'; addr: number }
1744
+ | { kind: 'fctiwz-high'; addr: number };
1745
+ const NON_VALUE_RULES: Record<NonValue['kind'], { refuseAtRead: boolean; mustBeConsumed: boolean }> = {
1746
+ 'frame-address': { refuseAtRead: true, mustBeConsumed: true },
1747
+ restored: { refuseAtRead: true, mustBeConsumed: false },
1748
+ 'return-address': { refuseAtRead: false, mustBeConsumed: false },
1749
+ 'fctiwz-high': { refuseAtRead: true, mustBeConsumed: false },
1750
+ };
1751
+ const nonValues = new Map<Value, NonValue>();
1752
+ /** The non-values their consumer took (only `frame-address` has one that must). */
1753
+ const consumedNonValues = new Set<Value>();
1754
+ /** The non-value of kind `k` that `v` stands for, or undefined. */
1755
+ const nonValueOf = <K extends NonValue['kind']>(v: Value, k: K): Extract<NonValue, { kind: K }> | undefined => {
1756
+ const nv = nonValues.get(v);
1757
+ return nv?.kind === k ? (nv as Extract<NonValue, { kind: K }>) : undefined;
1758
+ };
1759
+ const refuseNonValue = (nv: NonValue): never => {
1760
+ switch (nv.kind) {
1761
+ case 'frame-address':
1762
+ return r1AsData();
1763
+ case 'restored':
1764
+ return fail(
1765
+ `cannot lift '${name}': ${nv.reg} is read after the restore at 0x${nv.addr.toString(16)} put the ` +
1766
+ `caller's ${nv.reg} back — no C value names it`,
1767
+ );
1768
+ case 'return-address':
1769
+ return fail(
1770
+ `cannot lift '${name}': the return address the 'mflr' at 0x${nv.addr.toString(16)} ` +
1771
+ `copies out is used as a value — a function reading its own return address has no C spelling`,
1772
+ );
1773
+ case 'fctiwz-high':
1774
+ return fail(
1775
+ `cannot lift '${name}': the high word the 'stfd' at 0x${nv.addr.toString(16)} stores is reloaded and ` +
1776
+ `used — 'fctiwz' leaves that half of its result undefined`,
1777
+ );
1778
+ }
1779
+ };
1780
+ /** Checked on the finished function, after `finish` pruned the joins nothing reads. */
1781
+ const assertNoNonValueEscaped = (fnBlocks: Block[]) => {
1782
+ for (const b of fnBlocks) {
1783
+ for (const op of b.ops) {
1784
+ for (const v of [...op.operands, ...op.successors.flatMap((s) => s.args)]) {
1785
+ const nv = nonValues.get(v);
1786
+ if (nv !== undefined) {
1787
+ refuseNonValue(nv);
1788
+ }
1789
+ }
1790
+ }
1791
+ }
1792
+ };
1793
+ const assertNonValuesConsumed = () => {
1794
+ for (const [v, nv] of nonValues) {
1795
+ if (NON_VALUE_RULES[nv.kind].mustBeConsumed && !consumedNonValues.has(v)) {
1796
+ refuseNonValue(nv);
1797
+ }
1798
+ }
1799
+ };
468
1800
  const RET = target.returnReg;
469
1801
  const ARG_REGS = target.argRegs;
470
1802
 
471
- // Best-effort call arity when a callee has no prototype: the count of contiguous argument
472
- // registers (r3..) with a VALUE reaching the call. A prototype's `params` is authoritative when
473
- // supplied; this liveness heuristic covers the rest.
1803
+ // THE FRAME, as two kinds of word slot, each named by its offset from the ENTRY r1
1804
+ // (`r1Displacements`).
474
1805
  //
475
- // A pending `@ha` high half is a def but not a value, and it must not raise the count: a `lis`
476
- // hoisted into the prologue leaves its half in r4 across an intervening call, and counted, it
477
- // makes `strlen(s)` into `strlen(s, <half>)` — which then refuses at the read. Measured on
478
- // `pikmin:searchKanjiCode__FUs`, whose `lis r4` at 0x4 pairs only at 0x28, past the `bl strlen`.
1806
+ // A SAVE holds an entry value that is no C value: a callee-saved register's, the link register's
1807
+ // word, or the back chain the frame push stores (the caller's r1). Nothing but the caller reads it
1808
+ // back. Its restore — a load into the register it was saved from — puts the caller's value back,
1809
+ // which no C value names, so a read of the register after it refuses (`restored` in
1810
+ // `nonValues`); a load into any other register refuses at once. Function-scoped, so a save in the
1811
+ // prologue matches a restore in any epilogue.
479
1812
  //
480
- // A GAP REFUSES. The count is contiguous, so an argument register with nothing reaching it ends
481
- // it — and one is empty for two opposite reasons. Either the call really takes that few
482
- // arguments, or the register still holds this function's own untouched incoming argument, a value
483
- // the machine passes on that SSA has no definition for because nothing ever wrote it. When a
484
- // LATER argument register does hold a value the second reading is the only one left, and taking
485
- // the first drops that argument and every one after it: `ac-decomp:evw_anime_colreg_manual` sets
486
- // up seven registers for `evw_color_set` and, with r4 at its incoming value, lifts to
487
- // `evw_color_set(a0);` — its divide, its multiply and five arguments gone. Which reading it is
488
- // cannot be decided here — the function's own arity is exactly what is missing — so this refuses
489
- // and names the gap rather than guessing. A prototype answers it (`protoArity` is asked first).
490
- const fallbackArgc = (bi: number, at: number): number => {
491
- const holdsValue = (k: number) => ssa.hasReachingDef(ARG_REGS[k], bi, (v) => !highHalves.has(v));
492
- let n = 0;
493
- while (n < ARG_REGS.length && holdsValue(n)) {
494
- n++;
495
- }
496
- for (let k = n + 1; k < ARG_REGS.length; k++) {
497
- if (holdsValue(k)) {
498
- throw new PpcUnsupportedError(
499
- `cannot lift '${name}': the call at 0x${at.toString(16)} has no prototype, ${ARG_REGS[k]} holds a ` +
500
- `value and ${ARG_REGS[n]} holds none — an argument register left at its incoming value and one ` +
501
- `the call does not pass look the same here, so the argument count is not decidable`,
502
- );
503
- }
1813
+ // ONE SAVE AND ONE RESTORE, whichever instruction does it: `stmw`/`lmw`, a single `stw`/`lwz`
1814
+ // and a register save routine all go through `saveInto` and `restoreFrom`. A single `stw` is the
1815
+ // one save that may also be a spill: of a register this function already wrote, it stores a
1816
+ // value. A `stmw` or a routine has no such reading, and saving a written register refuses.
1817
+ //
1818
+ // Every other word store is a VALUE — an argument's home, or a live value spilled — and it is the
1819
+ // shared stack-slot variable (`stackSlotKey`, frontend/ssa.ts) MIPS and Thumb use. A reload into
1820
+ // any register reads it; a reload on a path that never stored it refuses in the builder; a store
1821
+ // nothing reloads refuses at the end of `lift`.
1822
+ //
1823
+ // A slot is one kind for the whole function: a word both saved and stored with a value has no
1824
+ // single reading, and refuses.
1825
+ const saveSlots = new Map<number, string>();
1826
+ // Each reload (`lwz`, `lmw`, restore routine) that restores a save, and the registers it restores:
1827
+ // a save holds only while every path restores it after its last write (`refuseSavedRegisterLeaks`).
1828
+ const restoredBy = new Map<Instr, ReadonlySet<string>>();
1829
+ // AN INCOMING STACK WORD HAS ONE NAME, `@sarg<k>`, for its loads and its stores: the EABI passes
1830
+ // integer argument word k >= 8 at entry r1 + 8 + 4(k - 8), in the caller's parameter area past
1831
+ // its back chain and link-register save word (`corpus/probe-mwcc-frame.c`, `in9`/`in10`). A read
1832
+ // no store reaches is the caller's argument through the ordinary live-in path, and a store makes
1833
+ // the parameter an assigned one, as Thumb and MIPS name it. Words only: a sub-word incoming
1834
+ // argument (`lha 10(r1)` for a `short`) refuses as a sub-word frame access, a float past f8 is
1835
+ // loaded with the unmodelled `lfs`/`lfd`, and a function reading one beside a float argument or
1836
+ // declared with a 64-bit or floating parameter refuses (`ppcArgSlots`). Undefined for every
1837
+ // other word.
1838
+ const incomingStackKey = (entryOff: number): string | undefined =>
1839
+ incomingStackArgKey(entryOff, PARAM_AREA_AT, ARG_REGS.length);
1840
+ const valueSlots = new Map<
1841
+ number,
1842
+ { addr: number; mem: string; read: boolean; stored: { v: Value; at: number }[] }
1843
+ >();
1844
+ // What each `mfcr` copied: the fields the condition register held, by the value the copy stands
1845
+ // for. Nothing defines that value — it is no integer — and only an extract of one bit of a float
1846
+ // compare's field reads it (`crBitValue`); anything else that does refuses once the SSA is finished.
1847
+ const crCopies = new Map<Value, ReadonlyMap<string, CrCompare | string>>();
1848
+ const loadedFrom = target.compilerBehaviors.outgoingAreaNeverLoadedFrom;
1849
+ const refuseMixedSlot = (ins: Instr, mem: string, off: number, kind: 'save' | 'value') => {
1850
+ if (kind === 'save' ? valueSlots.has(off) : saveSlots.has(off)) {
1851
+ throw new PpcUnsupportedError(
1852
+ `cannot lift '${name}': local stack frames not supported — '${mem}' at 0x${ins.addr.toString(16)} ` +
1853
+ `is a word this function both saves a register in and stores a value to`,
1854
+ );
504
1855
  }
505
- return n;
506
1856
  };
507
-
508
- // Frame slots (r1-relative offsets) that hold a TRANSPARENT save, and WHICH REGISTER each one
509
- // holds — a callee-saved register's entry value or the saved link register. A reload is dropped
510
- // (the value is unchanged, so the in-register SSA value already carries it) only when it restores
511
- // the very register the slot was saved from. Function-scoped so a save in the prologue block
512
- // matches a restore in a different epilogue block. Any OTHER r1 access is a genuine local spill /
513
- // address-taken stack object this frontend cannot model — those fail LOUD (below), never silently
514
- // drop, because a dropped local spill is a silent miscompile.
515
- //
516
- // THE REGISTER IS PART OF THE SLOT, not bookkeeping about it. `stw r3,8(r1)` / `lwz r4,8(r1)` is
517
- // mwcc spilling an incoming ARGUMENT and reading it back into another register — a real value
518
- // moving through memory, not a save/restore pair, and an offset-only record cannot tell the two
519
- // apart. Dropping that reload leaves the destination with no definition at all, and
520
- // `fallbackArgc`'s contiguous scan then takes every LATER argument with it.
521
- // `pikmin:__ct__7ActFreeFP4Piki` is the benchmark's inhabitant — it reads `this` back into r4 —
522
- // and 28 Mario Party 4 checkout functions share the shape, each losing an argument the relocation
523
- // fold recovered.
524
- const savedSlots = new Map<number, string>();
1857
+ /** The slot an r1-relative operand names: its offset from the entry r1. Where r1's displacement is
1858
+ * not known, no offset can be named, and a save or restore there refuses rather than being
1859
+ * matched against a slot it may not be. */
1860
+ const entryOffset = (ins: Instr, mem: string): number => {
1861
+ const d = r1At.get(ins);
1862
+ if (typeof d !== 'number') {
1863
+ throw new PpcUnsupportedError(
1864
+ `cannot lift '${name}': stack-frame access '${mem}' at 0x${ins.addr.toString(16)} — the stack ` +
1865
+ `pointer's offset from its entry value is not known there: ${d ?? 'the instruction is unreachable'}`,
1866
+ );
1867
+ }
1868
+ return parseMem(mem).off + d;
1869
+ };
525
1870
  // `stmw rS,D(r1)` saves rS..r31 into consecutive words from D; `lmw rD,D(r1)` restores the same
526
1871
  // range.
527
1872
  const frameRange = (firstReg: string, off: number): Array<{ off: number; reg: string }> => {
@@ -533,29 +1878,192 @@ export function lift(
533
1878
  return slots;
534
1879
  };
535
1880
 
1881
+ // What each filled block left in the condition register. Written as blocks are filled, so a lookup
1882
+ // finds only blocks filled earlier, which is what `inheritFlags` needs to tell "not known" from
1883
+ // "none".
1884
+ const exitCr = new Map<number, CrExit>();
1885
+ const blockLabel = (bi: number): string =>
1886
+ blocks[bi].startAddr >= 0
1887
+ ? `${name}+0x${blocks[bi].startAddr.toString(16)}`
1888
+ : `the return block '${name}' synthesizes`;
1889
+
1890
+ // A FLOAT RETURN is decided for the whole function (frontend/fpu.ts `returnHomeRule`), and then
1891
+ // r3 is scratch like any other integer register. An undecided one returns r3 here, and
1892
+ // `settleReturnHome` may hand each return f1 instead. A table base left in r3 is no return value, as a
1893
+ // pending half is none (`placeholders`); reading one would refuse, and a merge of one with a value
1894
+ // still does.
1895
+ const returnOperands = (bi: number): Value[] =>
1896
+ floatReturn
1897
+ ? [readReg(fpu!.returnReg, bi)]
1898
+ : ssa.hasReachingDef(RET, bi, (v) => !placeholders.has(v) && !parked(v, RET))
1899
+ ? [readReg(RET, bi)]
1900
+ : [];
1901
+ // A RETURN NOTHING WROTE r3 FOR STILL RETURNS r3 — the caller's value, which on PowerPC is the
1902
+ // first argument. `int f(int a, int x) { if (x) return a; … return 5; }` is `cmpwi r4,0 ; bnelr`:
1903
+ // that path neither reads nor writes r3, so `returnOperands` finds no definition and returns
1904
+ // nothing, while every other path returns a word — C that falls off the end of an `int` function,
1905
+ // and that loses `a`. Whether the function returns a word is known only once every block is
1906
+ // filled, so a bare return is recorded here and given r3's entry value after the fill
1907
+ // (`returnUntouchedArg`) when another return hands back a word. Only where r3 still holds the
1908
+ // entry value on every path into it: a path a call destroyed r3 on returns nothing a C expression
1909
+ // names, and stays bare.
1910
+ //
1911
+ // AND ONLY WHERE r3 IS AN ARGUMENT AT ALL (`r3IsAnArgument`). "r3 still holds what the caller
1912
+ // left" says nothing about whether the caller left anything: `void f(void) { if (gA & 2) return;
1913
+ // gB++; }` uses r3 as a temporary on the other path, so that path "returns a word", and filling
1914
+ // the bare return would mint a parameter the ABI never passed — `f(s32 a0)`, which contradicts a
1915
+ // declared `void f(void)` (the candidate does not compile) and a C++ `__Fv` mangling.
1916
+ const bareReturns: Array<{ op: Op; bi: number }> = [];
1917
+ const wordReturns: number[] = [];
1918
+ // What r4, the second register of an r3:r4 return, may hold at each word return where no
1919
+ // declaration pins the width (frontend/dead-reads.ts `unreadPairHalf`).
1920
+ const unreadHigh = new Set<Value>();
1921
+ // Each of those word returns: what r3 returns there and what r4 may hold (`computedPairHalf`).
1922
+ const pairReturns: PairReturn[] = [];
1923
+ // Every value a compare consumed, whether or not a branch reads the field it set: a compare
1924
+ // nothing reads leaves no op, so this is the only record that the value was used
1925
+ // (frontend/dead-reads.ts `compared`).
1926
+ const compared = new Set<Value>();
1927
+ // Every value a carry reader consumed by folding it away: the constant a `subfze` reads, and the
1928
+ // `rB - 1` a `subfe rD,rA,rB` cancels. The machine read them, and no op says so (`computedPairHalf`).
1929
+ const readWithoutOp = new Set<Value>();
1930
+ // Every `subfc`/`subfic` difference whose carry a reader turned into a 0/1 truth value: the
1931
+ // difference may be the by-product of a 32-bit compare, not the low word of a 64-bit result,
1932
+ // which `computedPairHalf` settles (`TruthDifference`). `carrySource` maps each carry compare to
1933
+ // the difference its writer computed, and `truthPending` each reader result one step short of
1934
+ // the truth value to it.
1935
+ const truthDifferences: TruthDifference[] = [];
1936
+ const carrySource = new Map<Value, Value>();
1937
+ const truthPending = new Map<Value, { diff: Value; via: 'neg' | 'and1' }>();
1938
+ // Every load into the register the compiler discards an unused read into at a level that keeps a
1939
+ // plain one (`compilerBehaviors.discardsUnusedReadsInto`, frontend/dead-reads.ts `discarded`).
1940
+ const discarded = new Set<Value>();
1941
+ const discardReg = target.compilerBehaviors.discardsUnusedReadsInto;
1942
+ // Every value written to a register other than the discard register: `leftInDiscardRegister`
1943
+ // takes out of `discarded` each load one of them derives from, which mwcc put in a home.
1944
+ const writtenElsewhere: Value[] = [];
1945
+ // A `subfze` over a register this block takes in from another block: modelled as `~rA + CA`, and
1946
+ // kept only where `ssa.finish` resolves rA to one constant on every path in (`subfze` case below).
1947
+ const joinedCarryConsts: { ins: Instr; not: Op; add: Op; ca: Value }[] = [];
1948
+ let wordReturn = false;
1949
+ /** Every return, as it is emitted. */
1950
+ const returns: { op: Op; bi: number }[] = [];
1951
+ const emitReturn = (ops: Op[], bi: number) => {
1952
+ const op = mkOp('ret', { operands: returnOperands(bi) });
1953
+ ops.push(op);
1954
+ returns.push({ op, bi });
1955
+ if (floatReturn) {
1956
+ return;
1957
+ }
1958
+ if (op.operands.length > 0) {
1959
+ wordReturn = true;
1960
+ wordReturns.push(bi);
1961
+ const high = new Set<Value>();
1962
+ unreadPairHalf(ssa, prototypes[name], ARG_REGS[1], bi, high);
1963
+ high.forEach((v) => unreadHigh.add(v));
1964
+ pairReturns.push({ word: op.operands[0], high });
1965
+ } else {
1966
+ bareReturns.push({ op, bi });
1967
+ }
1968
+ };
1969
+ /** Whether every word return hands back a value computed ONLY to be returned: on every path into
1970
+ * it, r3's last write is one (frontend/dead-reads.ts `computedOnlyToReturn`), so the function
1971
+ * returns a word. `int f(int a) { switch (gS) { case 0: return 3; … } return a; }` is `li r3,3 ;
1972
+ * blr` on every case and a bare `blr` out of range. A temporary is read again (`lwz r3,gB ; addi
1973
+ * r0,r3,1`), and a call's result and a load are excluded — a void function's call leaves its
1974
+ * callee's result in r3, and a discarded `volatile` read is a load nobody reads — so `void
1975
+ * f(void) { if (gA & 2) return; gB++; }` still answers no.
1976
+ *
1977
+ * What it cannot tell apart is an `int f(void)` with a path that falls off its end: that path
1978
+ * returns whatever the caller left in r3 too, and is lifted as returning it — the bytes' own
1979
+ * behaviour, at the cost of a parameter the source did not name. A CodeWarrior C++ symbol says
1980
+ * so where there is one (`__Fv`), and a declaration always decides first. */
1981
+ const r3WrittenToReturn = (): boolean => {
1982
+ if (wordReturns.length === 0) {
1983
+ return false;
1984
+ }
1985
+ const computed = computedOnlyToReturn(irBlocks);
1986
+ return wordReturns.every((bi) => ssa.everyReachingDef(RET, bi, computed));
1987
+ };
1988
+ /** Whether the caller passes r3, so a bare return of it hands back an argument. A declaration
1989
+ * decides when it says (`ownFirstArgumentPassed`, which reads it as the live-in bound does): a
1990
+ * non-void return and a parameter word or `this` first, or a nonzero register count — and a free
1991
+ * function of none (`g__Fv`) or a count of 0 is passed nothing, which no body evidence
1992
+ * overrides. A hand-written body (`ownDeclared` false) is read as having none. Without one, only
1993
+ * the body can show it: r3 is already an entry parameter, or a higher argument register is live
1994
+ * in, which puts an argument in r3 by position (the obligation `mintArgSlotHoles` discharges), or
1995
+ * every word return hands back a value computed only to be returned (`r3WrittenToReturn`) — then
1996
+ * the function returns a word, and a bare return of it hands back what the caller left in r3. */
1997
+ const r3IsAnArgument = (): boolean => {
1998
+ const own = prototypes[name];
1999
+ if (declaresVoidReturn(own)) {
2000
+ return false;
2001
+ }
2002
+ // a count bounds no register, but says whether any argument is passed at all
2003
+ if (typeof own?.params === 'number') {
2004
+ return own.params > 0;
2005
+ }
2006
+ const declared = ownDeclared ? ownFirstArgumentPassed(name, own, target) : undefined;
2007
+ if (declared !== undefined) {
2008
+ return declared;
2009
+ }
2010
+ const live = new Set(irBlocks[0].params.map((p) => paramReg.get(p)));
2011
+ return ARG_REGS.some((r) => live.has(r)) || r3WrittenToReturn();
2012
+ };
2013
+ /** Whether the function's own STATED declaration returns a word: then it returns one even where
2014
+ * no path writes r3, and a bare return hands back what the caller left there — `int f(int a) {
2015
+ * return a; }` is a bare `blr`. A context's own entry is not read for this (its return is a
2016
+ * header nothing checked), and neither is a hand-written body's (`ownDeclared` false). */
2017
+ const statesWordReturn = (): boolean => {
2018
+ const r = prototypes[name]?.returns;
2019
+ const width = r === undefined || isFloatingSpelling(r) ? undefined : declaredWidth(r);
2020
+ return ownDeclared && width !== undefined && width <= 32;
2021
+ };
2022
+ const returnUntouchedArg = () => {
2023
+ if (!(wordReturn || statesWordReturn()) || bareReturns.length === 0 || !r3IsAnArgument()) {
2024
+ return;
2025
+ }
2026
+ for (const { op, bi } of bareReturns) {
2027
+ const v = ssa.entryValue(RET, bi);
2028
+ if (v !== undefined) {
2029
+ op.operands = [v];
2030
+ }
2031
+ }
2032
+ };
2033
+
536
2034
  const fillBlock = (b: PpcBlock, bi: number) => {
537
2035
  const ops = irBlocks[bi].ops;
538
2036
  const succ = (j: number): Successor => ({ block: irBlocks[j], args: [] });
539
2037
 
540
2038
  // A synthetic conditional-return block: just return the current return register.
541
2039
  if (b.synthReturn) {
542
- const retOps = ssa.hasReachingDef(RET, bi) ? [readReg(RET, bi)] : [];
543
- ops.push(mkOp('ret', { operands: retOps }));
2040
+ emitReturn(ops, bi);
544
2041
  return;
545
2042
  }
2043
+ // A case target of an elided dispatch is entered with what the idiom left in its registers.
2044
+ const entry = entries.get(bi);
2045
+ if (entry !== undefined) {
2046
+ enterCaseTarget(
2047
+ entry,
2048
+ ops,
2049
+ {
2050
+ key: (r) => r,
2051
+ scrutinee: (r) => readReg(r, bi),
2052
+ dispatchScrutinee: (r) => ssa.readAtEnd(r, entry.at),
2053
+ write: (r, v) => writeVar(r, bi, v),
2054
+ },
2055
+ staleIdiom,
2056
+ );
2057
+ }
546
2058
 
547
2059
  // `lastDef` is the value most recently written to a register in this instruction — used to
548
2060
  // wire a record-form op's implicit cr0 side effect (see the `rc` handling in `decode`).
549
2061
  let lastDef: Value | null = null;
550
- // Reading r1 (the stack pointer) as a DATA operand means frame-pointer arithmetic or an
551
- // address-taken local (`addi r3,r1,8` = `&local`) — not modellable without a stack abstraction,
552
- // and fabricating a value for r1 silently miscompiles. Fail LOUD. (Frame bookkeeping never
553
- // reaches here: stwu / addi r1 / mflr / mtlr / r1-relative spills are handled before `read`.)
2062
+ // r1 read as data fails LOUD (`r1AsData`). Frame bookkeeping never reaches here: stwu / addi r1
2063
+ // / mflr / mtlr / r1-relative spills / the save routines' frame address are handled before `read`.
554
2064
  const read = (r: string): Value => {
555
2065
  if (r === 'r1') {
556
- throw new PpcUnsupportedError(
557
- `cannot lift '${name}': stack pointer r1 used as data (address-taken local / frame arithmetic) — not supported`,
558
- );
2066
+ r1AsData();
559
2067
  }
560
2068
  return readReg(r, bi);
561
2069
  };
@@ -576,43 +2084,213 @@ export function lift(
576
2084
  );
577
2085
  }
578
2086
  };
579
- // A word store/load based on r1. Returns true if it is TRANSPARENT frame bookkeeping (skip):
580
- // a save of a register with no reaching def (callee-saved entry value / saved lr), or a reload
581
- // from a recorded save slot. A store of a LIVE (reaching-def) value is a real local spill →
582
- // fail LOUD. A reload from an unrecorded slot is a genuine stack local → fail LOUD.
583
- const frameStore = (srcReg: string, mem: string): boolean => {
584
- const { base, off } = parseMem(mem);
585
- if (base !== 'r1') {
2087
+ /** The instruction a save or restore refusal names. */
2088
+ const site = (ins: Instr) =>
2089
+ `the '${ins.mnemonic}'${ins.mnemonic === 'bl' && ins.reloc ? ` to '${ins.reloc.sym}'` : ''} at 0x${ins.addr.toString(16)}`;
2090
+ /** Save `reg`'s entry value into the save slot at entry offset `off`, named `mem` in `ins`. */
2091
+ const saveInto = (ins: Instr, reg: string, off: number, mem: string) => {
2092
+ if (ssa.hasReachingDef(reg, bi)) {
2093
+ // A read leaves a definition too: the entry value, as a parameter, since `prologueSavedRegs`
2094
+ // does not count a save after a read.
2095
+ throw new PpcUnsupportedError(
2096
+ ssa.hasReachingDef(reg, bi, (v) => !paramReg.has(v))
2097
+ ? `cannot lift '${name}': ${site(ins)} saves ${reg} after this function wrote it, so '${mem}' holds a ` +
2098
+ `value, not the caller's ${reg}`
2099
+ : `cannot lift '${name}': ${site(ins)} saves ${reg} after this function read it — a value handed ` +
2100
+ `over in a register no argument is passed in, which a compiled prologue saves before any use`,
2101
+ );
2102
+ }
2103
+ refuseMixedSlot(ins, mem, off, 'save');
2104
+ saveSlots.set(off, reg);
2105
+ };
2106
+ /** Restore `reg` from the save slot at `off`: it must be the slot `reg` was saved into, and `reg`
2107
+ * then holds the caller's value again. r1 is no SSA register: its reload is the back chain the
2108
+ * frame push stored, and `r1Displacements` tracks it instead. */
2109
+ const restoreFrom = (ins: Instr, reg: string, off: number, mem: string) => {
2110
+ const saved = saveSlots.get(off);
2111
+ if (saved !== reg) {
2112
+ throw new PpcUnsupportedError(
2113
+ `cannot lift '${name}': reload of '${mem}' into ${reg}, ` +
2114
+ (saved === undefined
2115
+ ? `a word nothing saved it into (${site(ins)})`
2116
+ : `a slot ${saved} was saved into — the word holds the caller's ${saved}, which no C value ` +
2117
+ `names (${site(ins)})`),
2118
+ );
2119
+ }
2120
+ if (reg !== 'r1') {
2121
+ restoredBy.set(ins, new Set([...(restoredBy.get(ins) ?? []), reg]));
2122
+ const caller = mkValue(T.unk(32));
2123
+ nonValues.set(caller, { kind: 'restored', reg, addr: ins.addr });
2124
+ writeVar(reg, bi, caller);
2125
+ }
2126
+ };
2127
+ // A word store/load based on r1: a SAVE or a VALUE slot (see `saveSlots` above). Returns false
2128
+ // for any other base, which is ordinary memory.
2129
+ const frameStore = (ins: Instr, srcReg: string, mem: string): boolean => {
2130
+ if (parseMem(mem).base !== 'r1' || inLocalArea(ins, mem, 4) !== undefined) {
2131
+ return false;
2132
+ }
2133
+ const off = entryOffset(ins, mem);
2134
+ const isArg = ARG_REGS.includes(srcReg);
2135
+ // The LR SAVE stores the return address `mflr` copied out, and only into the link register's
2136
+ // save word, 4 bytes above the entry r1. Anywhere else the word is a value — past the push,
2137
+ // 8 bytes up is the callee's parameter area, where it is a ninth argument — and a register
2138
+ // holding the return address on some paths and another value on others is not a save at all.
2139
+ const isReturnAddress = (v: Value) => nonValueOf(v, 'return-address') !== undefined;
2140
+ if (ssa.hasReachingDef(srcReg, bi, isReturnAddress)) {
2141
+ const mixed = ssa.hasReachingDef(srcReg, bi, (v) => !isReturnAddress(v));
2142
+ if (mixed || off !== LR_SAVE_WORD) {
2143
+ throw new PpcUnsupportedError(
2144
+ `cannot lift '${name}': the store to '${mem}' at 0x${ins.addr.toString(16)} is not the link ` +
2145
+ `register's save — ${
2146
+ mixed
2147
+ ? `${srcReg} holds the return address an 'mflr' copies out on some paths and another value on others`
2148
+ : `the return address an 'mflr' copies out lands ${off} bytes from the entry r1, not in the ` +
2149
+ `save word ${LR_SAVE_WORD} bytes up, so it is used as a value`
2150
+ }`,
2151
+ );
2152
+ }
2153
+ refuseMixedSlot(ins, mem, off, 'save');
2154
+ saveSlots.set(off, srcReg);
2155
+ return true;
2156
+ }
2157
+ // Any other save stores what the register held at entry: nothing defined it on any path. An
2158
+ // argument register nothing defined holds the argument, which is a value.
2159
+ // …save a global register variable's: compiled code stores one only as a value, the outgoing
2160
+ // stack argument `k(1, …, 8, g)` is (`stw r14,8(r1)`), and the builder records no definition
2161
+ // for it (its writes are stores to the global), so "nothing defined it" is no evidence of a save.
2162
+ if (!isArg && !liveIns.globalRegs?.has(srcReg) && !ssa.hasReachingDef(srcReg, bi)) {
2163
+ saveInto(ins, srcReg, off, mem);
2164
+ return true;
2165
+ }
2166
+ refuseMixedSlot(ins, mem, off, 'value');
2167
+ // An argument moved to its home is not an argument set up for the next call.
2168
+ const v = (isArg ? ssa.entryValue(srcReg, bi) : undefined) ?? readReg(srcReg, bi);
2169
+ // An assignment to an incoming stack parameter is that parameter's SSA variable, and no call
2170
+ // made here takes the word: it is the caller's (a tail branch, whose callee would read the
2171
+ // same words as its own stack arguments, refuses in `liftPrelude`).
2172
+ const incoming = incomingStackKey(off);
2173
+ if (incoming !== undefined) {
2174
+ writeVar(incoming, bi, v);
2175
+ return true;
2176
+ }
2177
+ writeVar(stackSlotKey(off), bi, v);
2178
+ if (!valueSlots.has(off)) {
2179
+ valueSlots.set(off, { addr: ins.addr, mem, read: false, stored: [] });
2180
+ }
2181
+ valueSlots.get(off)!.stored.push({ v, at: ins.addr });
2182
+ return true;
2183
+ };
2184
+ const frameLoad = (ins: Instr, dstReg: string, mem: string): boolean => {
2185
+ if (parseMem(mem).base !== 'r1' || inLocalArea(ins, mem, 4) !== undefined) {
586
2186
  return false;
587
2187
  }
588
- if (!ssa.hasReachingDef(srcReg, bi)) {
589
- savedSlots.set(off, srcReg);
2188
+ const off = entryOffset(ins, mem);
2189
+ if (saveSlots.has(off)) {
2190
+ restoreFrom(ins, dstReg, off, mem);
590
2191
  return true;
591
2192
  }
592
- throw new PpcUnsupportedError(
593
- `cannot lift '${name}': spill of a live value to the stack ('${srcReg},${mem}') — local stack frames not supported`,
594
- );
595
- };
596
- const frameLoad = (dstReg: string, mem: string): boolean => {
597
- const { base, off } = parseMem(mem);
598
- if (base !== 'r1') {
599
- return false;
2193
+ const incoming = incomingStackKey(off);
2194
+ if (incoming !== undefined) {
2195
+ write(dstReg, readVar(incoming, bi));
2196
+ return true;
2197
+ }
2198
+ const slot = valueSlots.get(off);
2199
+ if (slot === undefined || !ssa.hasReachingDef(stackSlotKey(off), bi)) {
2200
+ throw new PpcUnsupportedError(
2201
+ `cannot lift '${name}': reload of a stack local ('${mem}') — local stack frames not supported`,
2202
+ );
2203
+ }
2204
+ slot.read = true;
2205
+ write(dstReg, readVar(stackSlotKey(off), bi));
2206
+ return true;
2207
+ };
2208
+ /** The instruction `decode` is lifting, for a refusal raised below it. */
2209
+ let decoding: Instr | null = null;
2210
+ // THE STACK POINTER IS NO VALUE A LIFT WRITES. The frame push, `addi r1,r1,N` and the reload of
2211
+ // the back chain move r1, and `r1Displacements` follows them; none reaches here. Any other write
2212
+ // (`lwz r1,4(r3)` in a longjmp, `mr r1,r10` in a context switch) would land in an SSA
2213
+ // variable nothing reads, since a read of r1 is refused as data (`read`), and the lift
2214
+ // would drop the write: a load into r1 would become a discarded volatile read. So it refuses.
2215
+ // THE SMALL-DATA BASES ARE NONE EITHER, for the same reason one step removed: an access through
2216
+ // r13 or r2 is read off its `R_PPC_EMB_SDA21` relocation (`sdaAccess`), never off the register,
2217
+ // so a write to either (`lwzu r13,4(r3)`, `mr r2,r4`) would be dropped and every later small-data
2218
+ // access would still name its symbol as if the base had not moved.
2219
+ const write = (r: string, v: Value) => {
2220
+ // the instruction the listing holds, not the plain access an update form is decoded through
2221
+ const by = () => {
2222
+ const listed = decoding === null ? null : (decodedFrom.get(decoding) ?? decoding);
2223
+ return listed === null
2224
+ ? 'an instruction'
2225
+ : `'${listed.mnemonic} ${listed.ops.join(',')}' at 0x${listed.addr.toString(16)}`;
2226
+ };
2227
+ if (r === 'r1') {
2228
+ throw new PpcUnsupportedError(
2229
+ `cannot lift '${name}': ${by()} writes the stack pointer r1 — only the frame push, 'addi r1,r1,N' ` +
2230
+ `and the back-chain reload move r1 here, and the value it would take is not one this lift tracks`,
2231
+ );
2232
+ }
2233
+ if (SMALL_DATA_BASES.includes(r)) {
2234
+ throw new PpcUnsupportedError(
2235
+ `cannot lift '${name}': ${by()} writes the small-data base ${r} — a small-data access is read ` +
2236
+ `off its relocation, not off the register, so a moved base is not one this lift tracks`,
2237
+ );
2238
+ }
2239
+ writeVar(r, bi, v);
2240
+ lastDef = v;
2241
+ if (discardReg !== undefined && r !== discardReg) {
2242
+ writtenElsewhere.push(v);
2243
+ }
2244
+ };
2245
+ // A `bl` into a register save or restore routine is frame bookkeeping, not a call: it writes no
2246
+ // register this function reads (only the link register, already copied out — which
2247
+ // `linkRegisterProof` checks), so nothing is destroyed and r0 keeps the return address
2248
+ // `mflr` put there. A SAVE records a save slot per word, at the routine's fixed offsets below the
2249
+ // frame address in r11; a RESTORE must find the same register in every word it reloads. It
2250
+ // refuses where:
2251
+ // • r11 does not hold one frame address on every path into the call, or a call destroyed it on
2252
+ // the way (`useOutsideOps`; and a frame address no routine takes refuses at the end of `lift`);
2253
+ // • r1's displacement at the `addi` is not known, so no slot can be named;
2254
+ // • the routine's number is outside r14..r31 (no such runtime routine);
2255
+ // • it moves floating-point registers (named, not modelled);
2256
+ // • a register it saves was written or read first, or a word it restores was not saved from
2257
+ // that register — `saveInto` and `restoreFrom`, the rules every save and restore keeps.
2258
+ const saveRoutine = (ins: Instr, r: NonNullable<ReturnType<typeof saveRoutineOf>>) => {
2259
+ const at = `the 'bl' to '${r.sym}' at 0x${ins.addr.toString(16)}`;
2260
+ if (r.first < 14 || r.first > 31 || ins.reloc!.addend !== 0) {
2261
+ throw new PpcUnsupportedError(`cannot lift '${name}': ${at} names no register save routine of the runtime`);
2262
+ }
2263
+ // The FPR half is named and refused. Every compiled function that calls it (doubles live
2264
+ // across a call, a 24-float leaf; 2.4.2 and 2.4.7 at -O4,p, 2.3.3 at -O4,s) already declines
2265
+ // on the unmodelled `stfd`/`lfd` or on a float live across a call, and no row calls it, so a
2266
+ // model of its 8-byte slots would be a model no lift has checked.
2267
+ if (r.fpr) {
2268
+ throw new PpcUnsupportedError(
2269
+ `cannot lift '${name}': ${at} saves or restores floating-point registers — not modelled`,
2270
+ );
2271
+ }
2272
+ const fr = target.registerSaveRoutines!.frameReg;
2273
+ const base = ssa.hasReachingDef(fr, bi, (v) => !nonValueOf(v, 'frame-address')) ? undefined : readVar(fr, bi);
2274
+ const frame = base === undefined ? undefined : nonValueOf(base, 'frame-address');
2275
+ if (frame === undefined) {
2276
+ throw new PpcUnsupportedError(
2277
+ `cannot lift '${name}': ${at} does not find one frame address ('addi ${fr},r1,N') in ${fr} on every path`,
2278
+ );
2279
+ }
2280
+ consumedNonValues.add(base!);
2281
+ // If a call between the `addi` and here destroyed r11, the routine would store through what the
2282
+ // callee left there: the builder refuses that read once it knows it was used.
2283
+ ssa.useOutsideOps(base!, bi);
2284
+ if (typeof frame.off !== 'number') {
2285
+ throw new PpcUnsupportedError(
2286
+ `cannot lift '${name}': ${at} saves below the frame address formed at 0x${frame.addr.toString(16)}, where ` +
2287
+ `the stack pointer's offset from its entry value is not known: ${frame.off}`,
2288
+ );
600
2289
  }
601
- const saved = savedSlots.get(off);
602
- if (saved === dstReg) {
603
- return true;
2290
+ for (let k = r.first; k <= 31; k++) {
2291
+ const disp = -4 * (32 - k);
2292
+ (r.restore ? restoreFrom : saveInto)(ins, `r${k}`, frame.off + disp, `${disp}(${fr})`);
604
2293
  }
605
- throw new PpcUnsupportedError(
606
- saved === undefined
607
- ? `cannot lift '${name}': reload of a stack local ('${mem}') — local stack frames not supported`
608
- : `cannot lift '${name}': reload of '${mem}' into ${dstReg}, a slot ${saved} was saved into — ` +
609
- `a load that does not restore the register the slot holds is a value read back through the ` +
610
- `stack, which is a local stack frame this frontend does not model`,
611
- );
612
- };
613
- const write = (r: string, v: Value) => {
614
- writeVar(r, bi, v);
615
- lastDef = v;
616
2294
  };
617
2295
  // Shared emitter kit (frontend/emit.ts) — the ISA-specific readers/guards stay above.
618
2296
  const kit = mkEmitKit(ops, write);
@@ -622,10 +2300,58 @@ export function lift(
622
2300
  // emit an honest `opaque` instead, which fails LOUD at assertResolved whether or not anything
623
2301
  // reads that register (see frontend/opaque.ts for the policy).
624
2302
  const emitOpaqueDest = (ins: Instr) => {
2303
+ // AN OPAQUE STANDS FOR ONE DESTINATION, `ops[0]`. An unmodelled load that writes a second
2304
+ // GPR would leave that one holding its old value with nothing marking it, which annotate mode
2305
+ // (the CLI and bench default) then prints as ordinary C: `lwzux r0,r3,r4; lbz r3,0(r3)` read
2306
+ // `*a0` where the byte is at `a0 + a1`. So refuse, as a store refuses, every load that writes
2307
+ // more than its first operand: an indexed update load (`lbzux`, `lhzux`, `lhaux`, `lwzux`,
2308
+ // which also write rA, as `mayWriteGpr` knows), `lmw` below r31 and the string loads. The FP
2309
+ // update forms keep the floating-point refusal `opaqueDest` gives them. The test is on the
2310
+ // lower-cased mnemonic, as `opaqueDest`'s classes are case-blind: the decode's cases are
2311
+ // lower case, so an upper-case `LWZU` or `LBZUX` (pasted asm; objdump and dtk never print
2312
+ // one) reaches here undecoded, and the displacement update forms are refused with the indexed.
2313
+ const mn = ins.mnemonic.toLowerCase();
2314
+ const lmwFirst = mn === 'lmw' ? Number(ins.ops[0]?.slice(1)) : NaN;
2315
+ const second = /^l(?!f)\w*ux?$/.test(mn)
2316
+ ? `its base ${mn.endsWith('x') ? ins.ops[1] : parseMem(ins.ops[1] ?? '').base}`
2317
+ : lmwFirst < 31
2318
+ ? `r${lmwFirst + 1}${lmwFirst + 1 < 31 ? '..r31' : ''}`
2319
+ : /^lsw[ix]$/.test(mn)
2320
+ ? 'the registers after it'
2321
+ : null;
2322
+ if (second !== null) {
2323
+ throw new PpcUnsupportedError(
2324
+ `cannot lift '${name} @0x${ins.addr.toString(16)}': unmodelled instruction '${ins.mnemonic}' writes ${second} ` +
2325
+ `as well as ${ins.ops[0]}, and an opaque stands for one destination`,
2326
+ );
2327
+ }
625
2328
  // storeClass: every PPC store mnemonic is st* — an unmodelled one (`stwbrx`, `sthbrx`, …)
626
2329
  // must throw, never skip (its first token is the SOURCE register).
627
2330
  const od = opaqueDest(ins.mnemonic, ins.ops, {
628
2331
  isReg,
2332
+ // The floating-point register file. `isReg` is `r\d+`, so an `fN` operand is invisible to
2333
+ // it: `fadds f1,f1,f2` reads as "no register destination" and `fcmpo cr0,f1,f2` as the same
2334
+ // thing about a CONDITION register, when both are one missing file. `stfs`/`stfd` reach
2335
+ // this arm ahead of `^st` deliberately — they are refused for the file they move, not for
2336
+ // the memory they move it to.
2337
+ //
2338
+ // NO SIGIL, WHICH IS THE OPPOSITE OF THE MIPS CHOICE, and it needs its own argument because
2339
+ // PowerPC objdump prints an FPU register bare (`f1`) and has no sigil to require. The
2340
+ // collision the MIPS predicate pays a sigil to avoid is live here in principle — objdump
2341
+ // writes a branch target as bare lower-case hex, so `f0`/`f4`/`f8` are addresses AND
2342
+ // matches. What makes it unreachable is not this pattern but the branch handling above: a
2343
+ // `b*` mnemonic in `isModeledBranch` is decoded as a transfer or a call and never falls to
2344
+ // `emitOpaqueDest`, and every other `b*` is refused by the whole-function control-transfer
2345
+ // pre-pass before a block is even filled. Those two are exhaustive over the mnemonics that
2346
+ // can carry an address, so no operand reaching this policy is one. `test/fp-refusal.test.ts`
2347
+ // pins it, and moving the FPU check into a pre-pass ahead of that one turns it red.
2348
+ fpReg: PPC_FP_REG,
2349
+ // The FPSCR moves that name no `fN` operand at all: `mtfsfi 7,0` takes a field number,
2350
+ // `mtfsb0`/`mtfsb1` a bit number, `mcrfs cr0,cr1` two condition registers. Without this
2351
+ // they read as "no register destination to degrade", which is the message that hides a
2352
+ // missing file behind the shape of the instruction. `mffs f0` and `mtfsf 0xff,f0` DO name
2353
+ // one and are `fpReg`'s.
2354
+ fpControl: /^(mtfsfi|mtfsb0|mtfsb1|mcrfs)$/i,
629
2355
  storeClass: /^st/i,
630
2356
  skipSafe: /^nop$/i,
631
2357
  context: `${name} @0x${ins.addr.toString(16)}`,
@@ -654,6 +2380,15 @@ export function lift(
654
2380
  ops.push(mkOp('gaddr', { results: [g], attrs: { sym } }));
655
2381
  return g;
656
2382
  };
2383
+ // A FRAME ADDRESS AS DATA: post-push offset k of the local area, lowered to the `laddr` the
2384
+ // frame-object audit proves (`FRAME_OBJECT_AUDIT`, frontend/frame-objects.ts `frameObjects`),
2385
+ // which also judges where it lies against the area: below it or above it refuses.
2386
+ const emitLaddr = (k: number): Value => {
2387
+ const v = mkValue(T.unk(32));
2388
+ ops.push(mkOp('laddr', { results: [v], attrs: { off: k } }));
2389
+ laddrAt.set(v, k);
2390
+ return v;
2391
+ };
657
2392
  // `lwz rD,0(0)` under an `R_PPC_EMB_SDA21` relocation is a SMALL-DATA access. Both printed
658
2393
  // fields are link-time placeholders: the linker substitutes r13/r2 for the base register and a
659
2394
  // section-relative displacement for the offset, so the address is exactly `&SYM` plus the
@@ -675,11 +2410,15 @@ export function lift(
675
2410
  // The base value + byte offset of a displacement memory operand: a small-data global, or an
676
2411
  // ordinary register base. Read BEFORE a store's source register, because read order decides
677
2412
  // block-parameter layout.
678
- const memOperand = (ins: Instr, mem: string): { base: Value; off: number } => {
2413
+ const memOperand = (ins: Instr, mem: string, width: number): { base: Value; off: number } => {
679
2414
  const sda = sdaAccess(ins, mem);
680
2415
  if (sda) {
681
2416
  return sda;
682
2417
  }
2418
+ const local = inLocalArea(ins, mem, width);
2419
+ if (local !== undefined) {
2420
+ return { base: emitLaddr(local), off: 0 };
2421
+ }
683
2422
  assertOrdinaryMem(mem);
684
2423
  const { off, base } = parseMem(mem);
685
2424
  return { base: read(base), off };
@@ -717,12 +2456,41 @@ export function lift(
717
2456
  const hi = highHalves.pair(relocSite(ins), rHi, readVar(rHi, bi), lo.sym, (h) => h.addend === lo.addend);
718
2457
  return emitGaddr(hi.sym);
719
2458
  };
2459
+ /** `addi rD,rHi,SYM@l` completing the base of a jump table a dispatch reads from outside its
2460
+ * block: the pair is proven as `foldLoHalf` proves it, and rD holds the table base. */
2461
+ const foldTableBase = (ins: Instr, rHi: string): Value | null => {
2462
+ const lo = ins.reloc!;
2463
+ const key = { sym: lo.sym, addend: lo.addend };
2464
+ if (!tableBases.wants(key)) {
2465
+ return null;
2466
+ }
2467
+ relocTaken = true;
2468
+ highHalves.pair(relocSite(ins), rHi, readVar(rHi, bi), lo.sym, (h) => h.addend === lo.addend);
2469
+ const v = mkValue(T.unk(32));
2470
+ tableBases.record(v, key, ins.addr);
2471
+ return v;
2472
+ };
720
2473
  const emitLoad = (ins: Instr, d: string, mem: string, width: number, signed: boolean) => {
721
- const { base, off } = memOperand(ins, mem);
722
- emit('load', d, [base], { off, width, signed });
2474
+ const { base, off } = memOperand(ins, mem, width);
2475
+ const v = emit('load', d, [base], { off, width, signed });
2476
+ if (d === discardReg) {
2477
+ discarded.add(v);
2478
+ }
723
2479
  };
724
2480
  const emitStore = (ins: Instr, srcReg: string, mem: string, width: number) => {
725
- const { base, off } = memOperand(ins, mem);
2481
+ const { base, off } = memOperand(ins, mem, width);
2482
+ // A register the prologue saves is saved above the local area by construction, so its
2483
+ // entry value stored inside it is a frame this does not describe.
2484
+ if (
2485
+ inLocalArea(ins, mem, width) !== undefined &&
2486
+ isCalleeSaved(target, srcReg) &&
2487
+ !ssa.hasReachingDef(srcReg, bi)
2488
+ ) {
2489
+ fail(
2490
+ `cannot lift '${name}': address-taken stack local — '${ins.mnemonic}' at 0x${ins.addr.toString(16)} stores ` +
2491
+ `the caller's ${srcReg} inside the local area, a frame this does not describe`,
2492
+ );
2493
+ }
726
2494
  ops.push(mkOp('store', { operands: [base, read(srcReg)], attrs: { off, width } }));
727
2495
  };
728
2496
  // Register+register INDEXED addressing (`lwzx rD,rA,rB` = *(rA+rB), `stwx rS,rA,rB` = *(rA+rB)=rS).
@@ -736,14 +2504,41 @@ export function lift(
736
2504
  return addr;
737
2505
  };
738
2506
  const emitLoadX = (d: string, rA: string, rB: string, width: number, signed: boolean) => {
739
- emit('load', d, [addrX(rA, rB)], { off: 0, width, signed });
2507
+ const v = emit('load', d, [addrX(rA, rB)], { off: 0, width, signed });
2508
+ if (d === discardReg) {
2509
+ discarded.add(v);
2510
+ }
740
2511
  };
741
2512
  const emitStoreX = (srcReg: string, rA: string, rB: string, width: number) => {
742
2513
  ops.push(mkOp('store', { operands: [addrX(rA, rB), read(srcReg)], attrs: { off: 0, width } }));
743
2514
  };
744
2515
 
745
2516
  // cr-field compare state, so a following branch fuses. Keyed by cr name ("cr0" default).
746
- const cmpDef = new Map<string, { lhs: Value; rhs: Value; signed: boolean }>();
2517
+ //
2518
+ // A block starts with what its predecessor left, under the edge rule both frontends share
2519
+ // (`inheritFlags`, frontend/flags-edge.ts): one predecessor, already filled, not a jump-table
2520
+ // dispatch. The edge out of a CONDITIONAL branch carries too, because a `bc` writes no CR field:
2521
+ // mwcc's switch dispatch `cmpwi r0,1; beq- case1; bge- default` reads one compare from the
2522
+ // `beq-` and then from the `bge-` on its fall-through, which begins a block of its own.
2523
+ //
2524
+ // The clobber rule is this frontend's own: a compare writes its field (a float compare too), a
2525
+ // record-form op writes cr0, a `cror` rewrites one bit of a float compare's field, and a call
2526
+ // destroys every volatile field (CR_VOLATILE). A field something else wrote holds the sentence
2527
+ // saying so rather than an entry, so the branch that reads it can name the writer. Every other
2528
+ // instruction that writes a CR field (`crxor`, `mtcrf`, …) names no register destination and is
2529
+ // refused outright by the opaque policy, so it never reaches here.
2530
+ const inherited = inheritFlags(bi, {
2531
+ preds,
2532
+ exit: exitCr,
2533
+ label: blockLabel,
2534
+ dispatches: (i) => {
2535
+ const pbr = blocks[i].branch;
2536
+ return pbr !== null && jts.has(pbr.addr);
2537
+ },
2538
+ refusesConditional: () => false,
2539
+ });
2540
+ const cmpDef = new Map<string, CrCompare | string>(typeof inherited === 'string' ? [] : inherited.fields);
2541
+ const noCmpWhy = typeof inherited === 'string' ? inherited : inherited.none;
747
2542
  // One operand-grammar normalizer for the four compare decodes: `cmpX rA,…` (cr0 implicit)
748
2543
  // or `cmpX crN,rA,…` → the cr field, the lhs register token, and the rhs token (register or
749
2544
  // immediate — the case reads/parses it).
@@ -752,11 +2547,100 @@ export function lift(
752
2547
  const args = hasCr ? cmpOps.slice(1) : cmpOps;
753
2548
  return { cr: hasCr ? cmpOps[0] : 'cr0', lhsTok: args[0], rhsTok: args[args.length - 1] };
754
2549
  };
2550
+ // An FPU register holds a float or, after `fctiwz`, an integer word (frontend/fpu.ts
2551
+ // `refuseFpWordCrossings` checks the edges); a float read refuses the word.
2552
+ const readFloat = (ins: Instr, key: string): Value => {
2553
+ const v = readReg(key, bi);
2554
+ if (v.type.kind !== 'float') {
2555
+ throw new PpcUnsupportedError(
2556
+ `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)} reads ${key} as a float, and it ` +
2557
+ 'holds an integer word',
2558
+ );
2559
+ }
2560
+ return v;
2561
+ };
2562
+ // A compare or a conversion refuses beside a call (`callInsn`), and in a function its own
2563
+ // declaration puts in both precisions (`declaredPrecision`): neither states a precision itself.
2564
+ const refuseCrossingUnmodelled = (ins: Instr) => {
2565
+ if (callInsn) {
2566
+ throw new PpcUnsupportedError(
2567
+ `cannot lift '${name}': '${ins.mnemonic}' computes on a float and '${callInsn.mnemonic}' at ` +
2568
+ `0x${callInsn.addr.toString(16)} makes a call — the floating-point registers a call reads, returns ` +
2569
+ `in and destroys are not modelled`,
2570
+ );
2571
+ }
2572
+ if (fpType === null) {
2573
+ refuseBothPrecisions(name, ins.mnemonic);
2574
+ }
2575
+ };
2576
+ // ONE BIT of an `mfcr` copy, as the 0/1 it is: bit `n` counts from the most significant, four to
2577
+ // a field (`lt`, `gt`, `eq`, `so`). `rlwinm rD,r0,K,31,31` rotates bit K-1 to the bottom and
2578
+ // `srwi rD,r0,31` takes bit 0. Only a float compare's bit is read this way here, and it is the
2579
+ // compare that bit holds (`CrBits`); anything else refuses.
2580
+ const crBitValue = (ins: Instr, copy: Value, n: number): Value => {
2581
+ const fields = crCopies.get(copy)!;
2582
+ const field = `cr${n >> 2}`;
2583
+ const bit = n < 0 ? null : CR_BIT_NAMES[n & 3];
2584
+ const held = fields.get(field);
2585
+ if (bit === null || typeof held !== 'object' || !held.bits || (held.quiet && bit !== 'eq')) {
2586
+ throw new PpcUnsupportedError(
2587
+ `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)} extracts from a copy of the ` +
2588
+ 'condition register something other than one bit a float compare set',
2589
+ );
2590
+ }
2591
+ const v = mkValue(T.unk(32));
2592
+ ops.push(mkOp(held.bits[bit], { operands: [held.lhs, held.rhs], results: [v] }));
2593
+ return v;
2594
+ };
755
2595
  const recordCmp = (cr: string, lhs: Value, rhs: Value, signed: boolean) => {
756
2596
  cmpDef.set(cr, { lhs, rhs, signed });
2597
+ compared.add(lhs).add(rhs);
2598
+ };
2599
+
2600
+ // The carry bit, XER[CA], as a 0/1 value. mwcc computes a materialised compare branch-free
2601
+ // through it (`subfc` then `subfe`/`subfze`/`adde`/`addze`), so a reader needs the comparison
2602
+ // the writer left there. Three writers are modelled, each as the compare its carry IS:
2603
+ // subfc rD,rA,rB rD = rB - rA CA = rB >=u rA
2604
+ // subfic rD,rA,K rD = K - rA CA = K >=u rA (K == 0: rA == 0)
2605
+ // addic rD,rA,K rD = rA + K CA = carry of the unsigned add (K == -1: rA != 0)
2606
+ // The compare is built at its reader, so a carry nothing reads adds no op. A carry is
2607
+ // UNKNOWN, and its reader refuses naming why, when no instruction of this block set it (a carry
2608
+ // across a block edge is not modelled), a call clobbered it, or a writer this frontend does not
2609
+ // model set it: `srawi`/`sraw` (the signed `/2^k` round-up), `addc` (the low half of a 64-bit
2610
+ // add), and the extended forms, whose own carry-out is a high word's. An `addic` carry is
2611
+ // read only as a compare (`subfe`, `subfze`): `adde`/`addze` over it is the high word of
2612
+ // mwcc 2.4.x's 64-bit add of a constant, which is not built, so they refuse it as they refuse
2613
+ // an `addc` carry.
2614
+ type Carry = { make: () => Value; addic: boolean } | { why: string };
2615
+ let carry: Carry = { why: 'nothing in its own block set it (a carry across a block edge is not modelled)' };
2616
+ const setCarry = (make: () => Value, addic = false) => {
2617
+ carry = { make, addic };
2618
+ };
2619
+ /** The carry, as the compare that set it. `wide` marks an extended add (`adde`/`addze`), which
2620
+ * over an `addic` carry is the high word of a 64-bit add. */
2621
+ const readCarry = (ins: Instr, wide = false): Value => {
2622
+ const why =
2623
+ 'why' in carry
2624
+ ? carry.why
2625
+ : wide && carry.addic
2626
+ ? 'an addic set it: an extended add over that carry is the high word of a 64-bit add, which is not modelled'
2627
+ : undefined;
2628
+ if (why !== undefined) {
2629
+ throw new PpcUnsupportedError(
2630
+ `cannot lift '${name}': the '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
2631
+ `reads the carry bit (XER[CA]), and ${why}`,
2632
+ );
2633
+ }
2634
+ return (carry as { make: () => Value }).make();
2635
+ };
2636
+ /** The value of `v` when this block defines it as a `const`. */
2637
+ const constIn = (v: Value): number | undefined => {
2638
+ const def = ops.find((o) => o.results[0] === v);
2639
+ return def?.opcode === 'const' ? (def.attrs.value as number) : undefined;
757
2640
  };
758
2641
 
759
2642
  const decode = (ins: Instr) => {
2643
+ decoding = ins;
760
2644
  const [d, s, t] = ins.ops;
761
2645
  // Record form: a trailing `.` (the Rc bit) means the op ALSO sets cr0 from a signed compare
762
2646
  // of its result against 0 (e.g. `andi.`, `addic.`). Decode the base op, then record that
@@ -766,50 +2650,210 @@ export function lift(
766
2650
  const mnem = rc ? ins.mnemonic.slice(0, -1) : ins.mnemonic;
767
2651
  lastDef = null;
768
2652
  relocTaken = false;
2653
+ // Every carry writer and every call leaves the carry unknown unless its case below models it.
2654
+ const clobberCarry = (how: string) => {
2655
+ carry = { why: `the '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ${how}` };
2656
+ };
2657
+ if (CARRY_WRITERS.test(mnem)) {
2658
+ clobberCarry('set it, a carry this frontend does not model');
2659
+ } else if (/^b.*l$/.test(mnem)) {
2660
+ clobberCarry('is a call, which clobbers it');
2661
+ }
2662
+ // A LOAD OR STORE WITH UPDATE (`lbzu`/`lhzu`/`lhau`/`lwzu`, `stbu`/`sthu`/`stwu`) addresses
2663
+ // `rA + D` and leaves that address in rA: `lbzu rD,1(r3)` is `r3 += 1; rD = *(u8 *)r3`, and
2664
+ // `stbu rS,1(r3)` is `r3 += 1; *(u8 *)r3 = rS`. mwcc emits them for a fully unrolled scan of a
2665
+ // table, a run of stores to a global array and a pre-increment pointer walk (`*++p`) at -O4,p,
2666
+ // and for the word loop of a struct copy at every level. Decoded as exactly that: the
2667
+ // `addi rA,rA,D` this frontend already lifts, then the plain access at `0(rA)`. So an `@l` displacement (`lhzu r0,gH@l(r4)`, the
2668
+ // FIRST access of a run over a `u16`/`u32` table or a store run) completes its `lis` the way
2669
+ // `addi rD,rHi,gH@l` does, and leaves `&gH` in rA; a base holding a frame address moves as a
2670
+ // capture moved by `addi` does, and the frame-object audit judges what it then reaches.
2671
+ //
2672
+ // It refuses where that reading is not the instruction's:
2673
+ // • a base field of 0 (`r0`) is the literal 0, which the ISA makes an invalid form for every
2674
+ // update form, and so is a load into its own base (rD == rA): neither has one meaning;
2675
+ // • a store of its own base (rS == rA) stores the value BEFORE the update, which an
2676
+ // `addi`-first decode would not;
2677
+ // • a base of r1 moves the stack pointer, which `r1Displacements` tracks only for the frame
2678
+ // push (`case 'stwu'`) and `addi r1,r1,N`, and so does a load into r1;
2679
+ // • a relocation other than `R_PPC_ADDR16_LO` (no compiler emits one).
2680
+ // Not decoded, and refused as unmodelled (`emitOpaqueDest` refuses the loads, which write a
2681
+ // second register an opaque cannot stand for; `opaqueDest` the stores): the INDEXED update forms
2682
+ // (`lwzux`, `stbux`, …), which in 2004 functions with an update form across pikmin,
2683
+ // ac-decomp and marioparty4 appear only in hand-written asm, and the floating-point ones
2684
+ // (`lfsu`, `stfdu`, …), whose plain forms this frontend does not lift either.
2685
+ const withUpdate = (plain: string, store: boolean) => {
2686
+ const [r, mem] = ins.ops;
2687
+ const { off, base } = parseMem(mem ?? '');
2688
+ const why =
2689
+ !isReg(base) || base === 'r0'
2690
+ ? `its base is ${base === 'r0' ? 'r0' : `'${mem}'`}, which the ISA makes an invalid form of an update`
2691
+ : base === 'r1'
2692
+ ? 'it moves the stack pointer, which only the frame push may do here'
2693
+ : !store && r === 'r1'
2694
+ ? 'it loads into the stack pointer, a value this lift does not track'
2695
+ : r === base
2696
+ ? store
2697
+ ? `it stores its own base ${r}, whose stored value is the one before the update`
2698
+ : `it loads into its own base ${r}, which the ISA makes an invalid form`
2699
+ : ins.reloc !== undefined && ins.reloc.type !== 'R_PPC_ADDR16_LO'
2700
+ ? `it carries a '${ins.reloc.type}' relocation, which no update form is decoded under`
2701
+ : undefined;
2702
+ if (why !== undefined) {
2703
+ throw new PpcUnsupportedError(
2704
+ `cannot lift '${name}': '${ins.mnemonic} ${ins.ops.join(',')}' at 0x${ins.addr.toString(16)} is a ` +
2705
+ `${store ? 'store' : 'load'} with update not modelled — ${why}`,
2706
+ );
2707
+ }
2708
+ const advance: Instr = { ...ins, mnemonic: 'addi', ops: [base, base, String(off)] };
2709
+ decodedFrom.set(advance, ins);
2710
+ decode(advance);
2711
+ // The advanced base is the update's side effect, and proves no argument (`ssa.ts`
2712
+ // `noteIncidentalWrite`).
2713
+ if (lastDef !== null) {
2714
+ ssa.noteIncidentalWrite(lastDef);
2715
+ }
2716
+ const access: Instr = { addr: ins.addr, mnemonic: plain, ops: [r, `0(${base})`] };
2717
+ decodedFrom.set(access, ins);
2718
+ decode(access);
2719
+ // The `addi` took the relocation or threw, at its own choke point.
2720
+ relocTaken = true;
2721
+ };
769
2722
  switch (mnem) {
770
2723
  case 'nop':
771
2724
  break;
772
2725
  // --- call + frame/link-register bookkeeping ---
773
2726
  // `bl <sym>`: read the argument registers (r3..), produce the return value in r3. The
774
- // callee symbol comes from the relocation (ins.reloc); caller-saved clobbering is implicit
775
- // (anything live across the call has already been moved to a callee-saved register).
2727
+ // callee symbol comes from the relocation (ins.reloc), and the caller-saved set is recorded
2728
+ // as destroyed (`calls.lower`) — a read of one past here names bytes the callee overwrote,
2729
+ // and it has a reaching definition all the same.
776
2730
  case 'bl': {
777
2731
  relocTaken = ins.reloc?.type === 'R_PPC_REL24';
778
- const sym = ins.reloc?.sym ?? 'func';
779
- const declared = protoArity(prototypes[sym]);
780
- const argc = declared ?? fallbackArgc(bi, ins.addr);
781
- const args: Value[] = [];
782
- for (let k = 0; k < argc; k++) {
783
- args.push(read(ARG_REGS[k]));
2732
+ const routine = saveRoutineOf(target, ins);
2733
+ if (routine) {
2734
+ saveRoutine(ins, routine);
2735
+ break;
2736
+ }
2737
+ // A `bl` no relocation names is a call the assembler resolved to an offset in this
2738
+ // section — into this function's own body, or to a neighbour — and has no name to call.
2739
+ if (ins.reloc?.type !== 'R_PPC_REL24') {
2740
+ throw new PpcUnsupportedError(
2741
+ `cannot lift '${name}': the 'bl' at 0x${ins.addr.toString(16)} has no callee symbol — no ` +
2742
+ `R_PPC_REL24 relocation names it, so it calls an offset in this section, which has no name to call it by`,
2743
+ );
784
2744
  }
785
- // Pushed with `tmp` rather than `emit` so the result register is written separately from
786
- // the op — r3.. are volatile under the EABI, so a GUESSED arity that counted a register
787
- // set up before an intervening call passes an argument the caller never set up
788
- // (`finish()` cuts those back — frontend/ssa.ts), and the call's OWN result is the
789
- // CALLEE's write, so `noteCall` records the clobber after it rather than before.
790
- const res = kit.tmp('call', args, { target: sym });
791
- if (declared === undefined) {
792
- ssa.recordGuessedCall(ops[ops.length - 1], bi, { argRegs: ARG_REGS, returnReg: RET });
2745
+ const sym = ins.reloc.sym;
2746
+ calls.lower({
2747
+ callee: sym,
2748
+ ssa,
2749
+ bi,
2750
+ read,
2751
+ write,
2752
+ // a pending `@ha` half and a table base are no argument
2753
+ placeholders,
2754
+ parked,
2755
+ scaledIndex,
2756
+ guess: {
2757
+ // a gap in the argument registers refuses, naming this address (`fallbackArgc`)
2758
+ at: ins.addr,
2759
+ // A GUESS THAT FILLS EVERY ARGUMENT REGISTER CANNOT SAY WHERE THE LIST ENDS. The ninth
2760
+ // argument travels in the callee's parameter area, 8 bytes above the pushed r1 (past the
2761
+ // back chain and the LR save word), and a value stored there that reaches the call is that
2762
+ // argument or a local the compiler put in the same words. Reading THAT word back does not
2763
+ // decide it on a compiler that may load its outgoing area, so the end-of-lift check for a
2764
+ // store nothing reloads is not enough here. Without a prototype this refuses; a declared
2765
+ // arity says which — and so does the loaded-word bound: mwcc loads no word of its
2766
+ // parameter area, so a word at or above the lowest one it loads is a local's.
2767
+ //
2768
+ // A guess of seven is the same question for one shape. A 64-bit argument takes an aligned
2769
+ // register pair, so one that follows seven words skips r10 and fills the first two words
2770
+ // of the parameter area (mwcc 2.3.3 builds `g(1, 2, 3, 4, 5, 6, 7, x)` with `long long x`
2771
+ // that way). A single word there cannot be an argument after seven: the eighth word would
2772
+ // have gone in r10.
2773
+ refuse: (argc) => {
2774
+ if (argc < ARG_REGS.length - 1) {
2775
+ return;
2776
+ }
2777
+ // The same question decides where the local area starts: past the widest call's
2778
+ // stack words, so a guess that cannot say whether this call takes any cannot draw it
2779
+ // — save where the loaded-word bound reaches the area's start, so that no word of the
2780
+ // area is any call's stack argument (`measurePpcFrame`'s `outgoingEnd`).
2781
+ if (frame !== null && (r1At.get(ins) !== -frame.size || frame.outgoingEnd > frame.area.from)) {
2782
+ fail(
2783
+ `cannot lift '${name}': outgoing stack arguments not modelled — nothing declares '${sym}', called at ` +
2784
+ `0x${ins.addr.toString(16)} with ${argc} argument registers filled, so whether it takes stack words ` +
2785
+ `is unknown, and this compiler's local area starts past them`,
2786
+ );
2787
+ }
2788
+ const depth = r1At.get(ins);
2789
+ const reaching = (off: number) =>
2790
+ valueSlots.has(off) && ssa.hasReachingDef(stackSlotKey(off), bi, (v) => !nonValues.has(v));
2791
+ // …save a word at or above the loaded-word bound, which is a local's
2792
+ const inParamArea = (off: number) =>
2793
+ typeof depth !== 'number' ||
2794
+ (off >= depth + PARAM_AREA_AT && off < 0 && off + 4 <= loadedWordBound(depth));
2795
+ const word = [...valueSlots.keys()].find((off) => inParamArea(off) && reaching(off));
2796
+ const passed =
2797
+ argc === ARG_REGS.length || typeof depth !== 'number'
2798
+ ? word
2799
+ : reaching(depth + PARAM_AREA_AT) &&
2800
+ reaching(depth + PARAM_AREA_AT + 4) &&
2801
+ inParamArea(depth + PARAM_AREA_AT + 4)
2802
+ ? depth + PARAM_AREA_AT
2803
+ : undefined;
2804
+ if (passed !== undefined) {
2805
+ const slot = valueSlots.get(passed)!;
2806
+ throw new PpcUnsupportedError(
2807
+ `cannot lift '${name}': outgoing stack arguments not modelled — the undeclared call to '${sym}' at ` +
2808
+ `0x${ins.addr.toString(16)} fills ${argc} argument registers, and the value ` +
2809
+ `stored to '${slot.mem}' at 0x${slot.addr.toString(16)} reaches it in the callee's ` +
2810
+ `parameter area, where an argument past the registers travels`,
2811
+ );
2812
+ }
2813
+ },
2814
+ },
2815
+ });
2816
+ for (const cr of CR_VOLATILE) {
2817
+ cmpDef.set(
2818
+ cr,
2819
+ `the call at 0x${ins.addr.toString(16)} destroyed it (the EABI does not preserve ${cr} across a call)`,
2820
+ );
793
2821
  }
794
- write(RET, res);
795
- ssa.noteCall(bi);
796
2822
  break;
797
2823
  }
798
2824
  // Stack-frame + link-register bookkeeping. `stwu r1,-N(r1)` / `addi r1,r1,N` adjust the frame
799
- // pointer; `mflr`/`mtlr` save/restore the return address. Transparent. Register saves/restores
800
- // (individual r1 spills, and `stmw`/`lmw` of a callee-saved range) are transparent ONLY when
801
- // they move an unchanged entry value — `frameStore`/`frameLoad` enforce that (a spill of a
802
- // LIVE value fails loud); `stmw`/`lmw` record/consume the slot directly.
803
- // The GENERAL form `stwu rS,D(rA)` (base ≠ r1) is a real store-with-BASE-UPDATE
804
- // (`*(rA+D)=rS; rA+=D`) — neither effect is modelled here, so loud-fail rather than drop both.
2825
+ // pointer; `mflr` copies the return address out (a `return-address` in `nonValues`) and `mtlr`
2826
+ // puts it back.
2827
+ // A word store or load on r1 is a save or a value slot (`frameStore`/`frameLoad`); `stmw`
2828
+ // records save slots.
2829
+ // The GENERAL form `stwu rS,D(rA)` (base ≠ r1) is a store with update (`withUpdate`).
2830
+ // The push itself must be the only one, taken from the entry r1: every slot is named by its
2831
+ // offset from there (`r1Displacements`), and a push from anywhere else moves that origin.
805
2832
  case 'stwu':
806
2833
  if (parseMem(s).base === 'r1') {
2834
+ if (d !== 'r1' || r1At.get(ins) !== 0) {
2835
+ throw new PpcUnsupportedError(
2836
+ `cannot lift '${name}': '${ins.mnemonic} ${ins.ops.join(',')}' at 0x${ins.addr.toString(16)} ` +
2837
+ `is not the one frame push from the entry stack pointer — ` +
2838
+ `${d !== 'r1' ? `it stores ${d}, not the back chain` : `r1 had already moved (${r1At.get(ins)})`}`,
2839
+ );
2840
+ }
2841
+ // The push also STORES: the entry r1, at its new top, which is the entry offset of `s`
2842
+ // because r1 has not moved yet. A value stored there before the push is overwritten, and
2843
+ // a load from it afterwards reads the caller's r1.
2844
+ refuseMixedSlot(ins, s, parseMem(s).off, 'save');
2845
+ saveSlots.set(parseMem(s).off, 'r1');
807
2846
  break;
808
2847
  }
809
- throw new PpcUnsupportedError(
810
- `cannot lift '${name}': stwu with update on ${parseMem(s).base} (store-with-base-update) not modelled`,
811
- );
812
- case 'mflr':
2848
+ withUpdate('stw', true);
2849
+ break;
2850
+ case 'mflr': {
2851
+ refuseOverwrittenLink(ins);
2852
+ const lr = mkValue(T.unk(32));
2853
+ nonValues.set(lr, { kind: 'return-address', addr: ins.addr });
2854
+ write(d, lr);
2855
+ break;
2856
+ }
813
2857
  case 'mtlr':
814
2858
  break;
815
2859
  // `mtctr rS` initialises the CTR loop counter — track it as the `ctr` pseudo-register so
@@ -821,21 +2865,34 @@ export function lift(
821
2865
  break;
822
2866
  case 'stmw':
823
2867
  if (parseMem(s).base === 'r1') {
824
- for (const slot of frameRange(d, parseMem(s).off)) {
825
- savedSlots.set(slot.off, slot.reg);
2868
+ if (inLocalArea(ins, s, 4 * (32 - Number(d.slice(1)))) !== undefined) {
2869
+ fail(
2870
+ `cannot lift '${name}': address-taken stack local — the 'stmw' at 0x${ins.addr.toString(16)} saves ` +
2871
+ `into the local area, a frame this does not describe`,
2872
+ );
2873
+ }
2874
+ const off = entryOffset(ins, s);
2875
+ const depth = off - parseMem(s).off;
2876
+ for (const slot of frameRange(d, off)) {
2877
+ saveInto(ins, slot.reg, slot.off, `${slot.off - depth}(r1)`);
826
2878
  }
827
2879
  break;
828
2880
  }
829
2881
  assertOrdinaryMem(s);
830
2882
  emitOpaqueDest(ins);
831
2883
  break;
832
- // The restore half. Every word it reads must be the slot the matching `stmw` wrote, for the
833
- // same register — `frameLoad` asks that question one word at a time, and refuses loud when
834
- // the answer is no, exactly as the single-register `lwz` does.
2884
+ // The restore half: `frameLoad` judges each word it reads, exactly as for a single `lwz`.
835
2885
  case 'lmw':
836
2886
  if (parseMem(s).base === 'r1') {
837
2887
  for (const slot of frameRange(d, parseMem(s).off)) {
838
- frameLoad(slot.reg, `${slot.off}(r1)`);
2888
+ const word = `${slot.off}(r1)`;
2889
+ if (inLocalArea(ins, word, 4) !== undefined) {
2890
+ fail(
2891
+ `cannot lift '${name}': address-taken stack local — the 'lmw' at 0x${ins.addr.toString(16)} ` +
2892
+ `reloads ${slot.reg} out of '${word}', inside the local area, a frame this does not describe`,
2893
+ );
2894
+ }
2895
+ frameLoad(ins, slot.reg, word);
839
2896
  }
840
2897
  break;
841
2898
  }
@@ -877,9 +2934,14 @@ export function lift(
877
2934
  `is '${s}', not the expected 0 placeholder`,
878
2935
  );
879
2936
  }
2937
+ // A jump table's own name is read as data and never written down, so the naming policy
2938
+ // does not ask it; RELA pairs a half only with a low half of the same symbol and addend,
2939
+ // and that one, with no immediate of its own, makes a table base (`foldTableBase`).
2940
+ const table = tableBases.wants({ sym: ins.reloc.sym, addend: ins.reloc.addend });
2941
+ relocTaken = true;
880
2942
  const hi = mkValue(T.unk(32));
881
2943
  highHalves.record(hi, {
882
- sym: spellableSym(ins),
2944
+ sym: table ? ins.reloc.sym : spellableSym(ins),
883
2945
  addend: ins.reloc.addend,
884
2946
  addr: ins.addr,
885
2947
  mnemonic: ins.mnemonic,
@@ -899,7 +2961,9 @@ export function lift(
899
2961
  // The STACK POINTER first, before any fold looks at the operands. `addi r1,r1,N` is frame
900
2962
  // teardown and is skipped; a relocation on it is no known compiler's output, and folding
901
2963
  // one would write an address into r1 and let the function lift as if the teardown had
902
- // happened — silent, and about the frame rather than the value.
2964
+ // happened — silent, and about the frame rather than the value. Only `addi r1,r1,N` is the
2965
+ // teardown: `addi r1,r3,N` sets r1 to another register's value, and falls through to
2966
+ // `write`, which refuses it like any other write to the stack pointer.
903
2967
  if (d === 'r1') {
904
2968
  if (ins.reloc) {
905
2969
  throw new PpcUnsupportedError(
@@ -907,15 +2971,59 @@ export function lift(
907
2971
  `adjust — an address built in r1 is not a shape this frontend models`,
908
2972
  );
909
2973
  }
2974
+ if (s === 'r1') {
2975
+ break;
2976
+ }
2977
+ }
2978
+ // A capture, `addi rD,r1,k`, is the `laddr` of the local area's offset k, where the frame
2979
+ // has one (`measurePpcFrame`); without one it reaches `read`'s r1-as-data refusal. A capture
2980
+ // moved by a constant is the capture at the sum.
2981
+ if (frame !== null && takesFrameAddress(ins, target.registerSaveRoutines?.frameReg)) {
2982
+ write(d, emitLaddr(parseImm(t) + (r1At.get(ins) as number) + frame.size));
2983
+ break;
2984
+ }
2985
+ if (
2986
+ frame !== null &&
2987
+ mnem === 'addi' &&
2988
+ !ins.reloc &&
2989
+ isReg(s) &&
2990
+ s !== 'r0' &&
2991
+ s !== 'r1' &&
2992
+ !SMALL_DATA_BASES.includes(s)
2993
+ ) {
2994
+ const raw = readVar(s, bi);
2995
+ const at = laddrAt.get(raw);
2996
+ if (at !== undefined) {
2997
+ movedCaptures.add(raw);
2998
+ write(d, emitLaddr(at + parseImm(t)));
2999
+ break;
3000
+ }
3001
+ }
3002
+ if (mnem === 'addi' && d === target.registerSaveRoutines?.frameReg && s === 'r1' && !ins.reloc) {
3003
+ const v = mkValue(T.unk(32));
3004
+ const depth = r1At.get(ins);
3005
+ nonValues.set(v, {
3006
+ kind: 'frame-address',
3007
+ off: typeof depth === 'number' ? depth + parseImm(t) : (depth ?? 'the instruction is unreachable'),
3008
+ addr: ins.addr,
3009
+ });
3010
+ write(d, v);
910
3011
  break;
911
3012
  }
912
3013
  // `addi rD,rHi,SYM@l` completes the address a `lis` began: rD = &SYM (+ the relocation's
913
- // addend). It is the only `@l` consumer modelled here. The others — `ori`, `addic`, and
914
- // the `@l`-in-a-displacement form (1,935 integer load/store sites across the three
915
- // checkouts, plus the float loads) — are left unbuilt per docs/level-tower.md's "earn the
916
- // level": no row in this benchmark reaches them. Each refuses at its own guard, and a
917
- // high half none of them consumed is caught by `readReg` or by the dangling-`@ha` check.
3014
+ // addend). It is the only `@l` consumer modelled here, and an update form's `@l`
3015
+ // displacement reaches it as the `addi` that advances its base (`withUpdate`). The others —
3016
+ // `ori`, `addic`, and the `@l`-in-a-displacement form of a PLAIN load or store (1,935
3017
+ // integer load/store sites across the three checkouts, plus the float loads) — are left
3018
+ // unbuilt per docs/level-tower.md's "earn the level": no row in this benchmark reaches
3019
+ // them. Each refuses at its own guard, and a high half none of them consumed is caught
3020
+ // by `readReg` or by the dangling-`@ha` check.
918
3021
  if (mnem === 'addi' && ins.reloc?.type === 'R_PPC_ADDR16_LO') {
3022
+ const base = parseImm(t) === 0 ? foldTableBase(ins, s) : null;
3023
+ if (base !== null) {
3024
+ write(d, base);
3025
+ break;
3026
+ }
919
3027
  const g = foldLoHalf(ins, s, t);
920
3028
  if (ins.reloc.addend === 0) {
921
3029
  write(d, g);
@@ -924,7 +3032,37 @@ export function lift(
924
3032
  }
925
3033
  break;
926
3034
  }
927
- emitBin('add', d, read(s), constVal(parseImm(t)));
3035
+ // An rA field of 0 is the literal 0, not r0: `addi rD,0,SIMM` is `li`. A disassembler that
3036
+ // prints it as `addi rD,r0,SIMM` still means the constant.
3037
+ if (mnem === 'addi' && s === 'r0') {
3038
+ write(d, constVal(parseImm(t)));
3039
+ break;
3040
+ }
3041
+ // `addi rD,rS,0` is a register move: rS + 0 is rS, as Thumb's `add rD,rS,#0` is
3042
+ // (frontend/thumb.ts). mwcc 2.3.3 spells its moves this way. Lifted as an add, the `+ 0`
3043
+ // makes a pointer an integer sum, and C++ will not pass `(u32)&table + 0` as a
3044
+ // `const char *`. `addic` sets the carry and is not a move.
3045
+ if (mnem === 'addi' && parseImm(t) === 0) {
3046
+ write(d, read(s));
3047
+ break;
3048
+ }
3049
+ {
3050
+ const a = read(s);
3051
+ const k = parseImm(t);
3052
+ emitBin('add', d, a, constVal(k));
3053
+ if (mnem === 'addic') {
3054
+ // a + K carries out exactly when a >=u 2^32 - K; K == 0 never carries.
3055
+ setCarry(
3056
+ () =>
3057
+ k === 0
3058
+ ? constVal(0)
3059
+ : k === -1
3060
+ ? kit.tmp('icmp_ne', [a, constVal(0)])
3061
+ : kit.tmp('icmp_uge', [a, constVal((2 ** 32 - (k >>> 0)) | 0)]),
3062
+ true,
3063
+ );
3064
+ }
3065
+ }
928
3066
  break;
929
3067
  // add immediate SHIFTED — the register-based `%ha` anchor: mwcc derives an absolute base
930
3068
  // from a scaled index (`addis r4,r3,-32736` = r3 + 0x80200000). The jump-table lis/addi
@@ -932,19 +3070,114 @@ export function lift(
932
3070
  // arithmetic, and a reloc-carrying one never gets here — the choke point in `decode` takes
933
3071
  // it.
934
3072
  case 'addis':
3073
+ if (s === 'r0') {
3074
+ write(d, constVal((parseImm(t) << 16) >> 0)); // `addis rD,0,SIMM` is `lis`, as for `addi`
3075
+ break;
3076
+ }
935
3077
  emitBin('add', d, read(s), constVal((parseImm(t) << 16) >> 0));
936
3078
  break;
937
3079
  case 'subf':
938
- case 'subfc':
939
3080
  case 'subfo':
940
- emitBin('sub', d, read(t), read(s));
3081
+ case 'subfc':
3082
+ case 'subfco': {
3083
+ const b = read(t);
3084
+ const a = read(s);
3085
+ const diff = emitBin('sub', d, b, a);
3086
+ if (mnem.startsWith('subfc')) {
3087
+ setCarry(() => {
3088
+ const c = kit.tmp('icmp_uge', [b, a]);
3089
+ carrySource.set(c, diff);
3090
+ return c;
3091
+ });
3092
+ }
941
3093
  break; // rD = rB - rA (reversed)
942
- case 'subfic':
943
- emitBin('sub', d, constVal(parseImm(t)), read(s));
3094
+ }
3095
+ case 'subfic': {
3096
+ const k = constVal(parseImm(t));
3097
+ const a = read(s);
3098
+ const diff = emitBin('sub', d, k, a);
3099
+ setCarry(() => {
3100
+ const c = parseImm(t) === 0 ? kit.tmp('icmp_eq', [a, constVal(0)]) : kit.tmp('icmp_uge', [k, a]);
3101
+ carrySource.set(c, diff);
3102
+ return c;
3103
+ });
944
3104
  break; // rD = imm - rA
945
- case 'neg':
946
- emitUn('neg', d, read(s));
3105
+ }
3106
+ // The carry READERS, each over the carry its block's writer left (`readCarry`):
3107
+ // subfe rD,rA,rB = ~rA + rB + CA adde rD,rA,rB = rA + rB + CA
3108
+ // subfze rD,rA = ~rA + CA addze rD,rA = rA + CA
3109
+ // Two `subfe` shapes are modelled, both of whose register halves cancel: `subfe rD,rY,rY`
3110
+ // (~y + y = -1, so CA - 1, reading no rY — mwcc writes it over a register holding anything,
3111
+ // the return address included) and `subfe rD,rA,rB` with rA = rB - 1 (~(x-1) + x = 0, so
3112
+ // CA). Any other `subfe` is the high half of a 64-bit subtract, and `subfze` over a register
3113
+ // that is no constant is a 64-bit negate: both stay unmodelled, as do `addme`/`subfme`.
3114
+ case 'subfe': {
3115
+ if (s === t) {
3116
+ const c = readCarry(ins);
3117
+ const r = emitBin('add', d, c, constVal(-1));
3118
+ const diff = carrySource.get(c);
3119
+ if (diff !== undefined) {
3120
+ truthPending.set(r, { diff, via: 'neg' }); // CA - 1; its negation is the 0/1 `rA <u rB`
3121
+ }
3122
+ break;
3123
+ }
3124
+ const b = read(t);
3125
+ const def = ops.find((o) => o.results[0] === read(s));
3126
+ if (def?.opcode === 'add' && def.operands[0] === b && constIn(def.operands[1]) === -1) {
3127
+ readWithoutOp.add(def.results[0]);
3128
+ write(d, readCarry(ins));
3129
+ break;
3130
+ }
3131
+ emitOpaqueDest(ins);
3132
+ break;
3133
+ }
3134
+ case 'subfze': {
3135
+ const x = read(s);
3136
+ const k = constIn(x);
3137
+ if (k !== undefined) {
3138
+ readWithoutOp.add(x);
3139
+ const ca = readCarry(ins);
3140
+ ~k === 0 ? write(d, ca) : emitBin('add', d, constVal(~k), ca);
3141
+ break;
3142
+ }
3143
+ // mwcc hoists the constant out of a loop (`li r3,-1` ahead of the loop, `subfze r0,r3` in
3144
+ // its body), so the register comes in through a join. Whether every path brings the same
3145
+ // constant is known only once every block is filled; the exact `~rA + CA` stands until then.
3146
+ // The value may also come down through blocks with one predecessor from the block that set
3147
+ // it, so only a value this block computes is settled here; an entry parameter is the
3148
+ // caller's value, never a constant, and the settling refuses it.
3149
+ if (bi === 0 || ops.some((o) => o.results.includes(x))) {
3150
+ emitOpaqueDest(ins);
3151
+ break;
3152
+ }
3153
+ const ca = readCarry(ins);
3154
+ const inv = mkValue(T.unk(32));
3155
+ const not = mkOp('not', { operands: [x], results: [inv] });
3156
+ ops.push(not);
3157
+ emitBin('add', d, inv, ca);
3158
+ joinedCarryConsts.push({ ins, not, add: ops[ops.length - 1], ca });
3159
+ break;
3160
+ }
3161
+ case 'adde':
3162
+ emitBin('add', d, binTmp('add', read(s), read(t)), readCarry(ins, true));
3163
+ break;
3164
+ case 'addze': {
3165
+ const c = readCarry(ins, true);
3166
+ const r = emitBin('add', d, read(s), c);
3167
+ const diff = carrySource.get(c);
3168
+ if (diff !== undefined) {
3169
+ truthPending.set(r, { diff, via: 'and1' }); // mwcc's signed compare keeps bit 0 of this
3170
+ }
3171
+ break;
3172
+ }
3173
+ case 'neg': {
3174
+ const x = read(s);
3175
+ const n = emitUn('neg', d, x);
3176
+ if (truthPending.get(x)?.via === 'neg') {
3177
+ truthDifferences.push({ diff: truthPending.get(x)!.diff, carry: x, truth: n });
3178
+ }
947
3179
  break;
3180
+ }
948
3181
  case 'mullw':
949
3182
  case 'mullwo':
950
3183
  emitBin('mul', d, read(s), read(t));
@@ -1038,6 +3271,10 @@ export function lift(
1038
3271
  emitShImm('shl', d, read(s), parseImm(t));
1039
3272
  break;
1040
3273
  case 'srwi':
3274
+ if (crCopies.has(read(s))) {
3275
+ write(d, crBitValue(ins, read(s), parseImm(t) === 31 ? 0 : -1));
3276
+ break;
3277
+ }
1041
3278
  emitShImm('shr_u', d, read(s), parseImm(t));
1042
3279
  break;
1043
3280
  case 'srawi':
@@ -1057,37 +3294,66 @@ export function lift(
1057
3294
  break;
1058
3295
  }
1059
3296
  // rotate-and-mask, rotate 0 only: `clrlwi rD,rS,n` = rS & (~0>>>n); `clrrwi` = rS & (~0<<n).
1060
- case 'clrlwi':
1061
- emitBin('and', d, read(s), constVal((0xffffffff >>> parseImm(t)) >>> 0));
3297
+ case 'clrlwi': {
3298
+ const x = read(s);
3299
+ const m = emitBin('and', d, x, constVal((0xffffffff >>> parseImm(t)) >>> 0));
3300
+ if (parseImm(t) === 31 && truthPending.get(x)?.via === 'and1') {
3301
+ truthDifferences.push({ diff: truthPending.get(x)!.diff, carry: x, truth: m });
3302
+ }
1062
3303
  break;
3304
+ }
1063
3305
  case 'clrrwi':
1064
3306
  emitBin('and', d, read(s), constVal((0xffffffff << parseImm(t)) >>> 0));
1065
3307
  break;
1066
3308
  case 'rlwinm': {
1067
3309
  const [sh, mb, me] = [parseImm(ins.ops[2]), parseImm(ins.ops[3]), parseImm(ins.ops[4])];
3310
+ if (crCopies.has(read(s))) {
3311
+ write(d, crBitValue(ins, read(s), mb === 31 && me === 31 ? (sh + 31) % 32 : -1));
3312
+ break;
3313
+ }
1068
3314
  // `rlwinm rD,rS,SH,MB,ME` = rotl(rS,SH) & mask(MB,ME). Three cases we can lower exactly:
1069
3315
  // • SH==0 — a pure masked AND.
1070
- // • right-shift EXTRACT `(x>>n)&m` — ME==31 and the field does not wrap (SH+MB>=32): the
1071
- // compiler's form for `(x>>n)&mask`, with n=32-SH and mask=mask(MB,31). Emit the shift
1072
- // then the AND — the faithful, idiomatic C that recompiles to this exact rlwinm.
3316
+ // • right-shift EXTRACT `(x>>n)&m` — the field lies in the SH bits the rotate brings round
3317
+ // from the top (MB >= 32-SH, MB <= ME): the compiler's form for `(x>>n)&mask`, with
3318
+ // n=32-SH and mask=mask(MB,ME). Emit the shift then the AND — the faithful, idiomatic C
3319
+ // that recompiles to this exact rlwinm. Where that field is the one bit x's sign bit
3320
+ // lands on (MB == ME == 32-SH) it is `(x >>u 31) << (SH-1)`: mwcc merges a shift of a
3321
+ // 0/1 value into its extract (`(a < b) << 3` ends `rlwinm r3,r0,4,28,28` on 2.4.x), and
3322
+ // the `srwi 31` the branch-free compare folds read is then whole (pattern/engine.ts
3323
+ // MWCC_COMPARE_PATTERNS).
3324
+ // • left-shift INSERT `(x&m)<<SH` — the field lies wholly in the bits the shift fills
3325
+ // (MB <= ME <= 31-SH): `(x & (mask >> SH)) << SH`, mwcc's form for a masked value moved
3326
+ // up (`(a < b) << 5` over 2.3.3's `& 1` ends `rlwinm r3,r0,5,26,26`).
1073
3327
  // • anything else (a genuine rotate / bitfield insert) stays an opaque — loud, not dropped.
1074
3328
  if (sh === 0) {
1075
3329
  emitBin('and', d, read(s), constVal(rlwinmMask(mb, me)));
1076
3330
  break;
1077
3331
  }
1078
- if (me === 31 && sh + mb >= 32) {
3332
+ if (mb === me && mb === 32 - sh && sh > 1) {
3333
+ const sign = mkValue(T.unk(32));
3334
+ ops.push(mkOp('shr_u', { operands: [read(s)], results: [sign], attrs: { imm: 31 } }));
3335
+ emitShImm('shl', d, sign, sh - 1);
3336
+ break;
3337
+ }
3338
+ if (mb <= me && me <= 31 - sh) {
3339
+ const masked = mkValue(T.unk(32));
3340
+ ops.push(mkOp('and', { operands: [read(s), constVal(rlwinmMask(mb, me) >>> sh)], results: [masked] }));
3341
+ emitShImm('shl', d, masked, sh);
3342
+ break;
3343
+ }
3344
+ if (mb <= me && sh + mb >= 32) {
1079
3345
  const shifted = mkValue(T.unk(32));
1080
3346
  ops.push(mkOp('shr_u', { operands: [read(s)], results: [shifted], attrs: { imm: (32 - sh) & 31 } }));
1081
- emitBin('and', d, shifted, constVal(rlwinmMask(mb, 31)));
3347
+ emitBin('and', d, shifted, constVal(rlwinmMask(mb, me)));
1082
3348
  break;
1083
3349
  }
1084
3350
  emitOpaqueDest(ins);
1085
3351
  break;
1086
3352
  }
1087
- // Word load/store: a transparent frame save/restore is skipped; a live-value spill or a
1088
- // stack local fails loud (frameStore/frameLoad); otherwise it is ordinary memory.
3353
+ // Word load/store: an r1-relative word is a frame slot (frameStore/frameLoad); otherwise it is
3354
+ // ordinary memory.
1089
3355
  case 'lwz':
1090
- if (frameLoad(d, s)) {
3356
+ if (frameLoad(ins, d, s)) {
1091
3357
  break;
1092
3358
  }
1093
3359
  emitLoad(ins, d, s, 4, true);
@@ -1102,7 +3368,7 @@ export function lift(
1102
3368
  emitLoad(ins, d, s, 1, false);
1103
3369
  break;
1104
3370
  case 'stw':
1105
- if (frameStore(d, s)) {
3371
+ if (frameStore(ins, d, s)) {
1106
3372
  break;
1107
3373
  }
1108
3374
  emitStore(ins, d, s, 4);
@@ -1113,6 +3379,17 @@ export function lift(
1113
3379
  case 'stb':
1114
3380
  emitStore(ins, d, s, 1);
1115
3381
  break;
3382
+ // The update forms (`withUpdate`); `stwu` is decoded with the frame push above.
3383
+ case 'lwzu':
3384
+ case 'lhzu':
3385
+ case 'lhau':
3386
+ case 'lbzu':
3387
+ withUpdate(mnem.slice(0, -1), false);
3388
+ break;
3389
+ case 'sthu':
3390
+ case 'stbu':
3391
+ withUpdate(mnem.slice(0, -1), true);
3392
+ break;
1116
3393
  // Register+register indexed forms (variable-index array access). Widths/signedness mirror the
1117
3394
  // displacement loads/stores above; `lhax` is the sign-extending halfword (algebraic).
1118
3395
  case 'lwzx':
@@ -1162,14 +3439,144 @@ export function lift(
1162
3439
  recordCmp(c.cr, read(c.lhsTok), rhs, false);
1163
3440
  break;
1164
3441
  }
3442
+ // A FLOAT COMPARE sets its field's three bits from the IEEE relation of its operands; a NaN
3443
+ // clears all three (and sets `so`). Which compare each bit holds is what a branch, a `cror`
3444
+ // or an extract then reads.
3445
+ case 'fcmpo':
3446
+ case 'fcmpu': {
3447
+ const [cr, a, bReg] = ins.ops;
3448
+ if (fpu === undefined || !/^cr[0-7]$/.test(cr ?? '') || ![a, bReg].every(isFpKey)) {
3449
+ emitOpaqueDest(ins);
3450
+ break;
3451
+ }
3452
+ refuseCrossingUnmodelled(ins);
3453
+ const lhs = readFloat(ins, a);
3454
+ const rhs = readFloat(ins, bReg);
3455
+ cmpDef.set(cr, {
3456
+ lhs,
3457
+ rhs,
3458
+ signed: true,
3459
+ bits: { lt: 'fcmp_olt', gt: 'fcmp_ogt', eq: 'fcmp_oeq' },
3460
+ quiet: mnem === 'fcmpu',
3461
+ });
3462
+ break;
3463
+ }
3464
+ // `cror bD,bA,bB` ORs two bits into a third. mwcc's `<=` and `>=` over floats set `eq` to
3465
+ // `lt | eq` or `gt | eq` of one field, which is the compare whose truth table is the OR of
3466
+ // the two (`FCMP_TRUTH`). Bits of two fields, or of an integer compare, are not modelled.
3467
+ case 'cror': {
3468
+ const [dst, x, y] = ins.ops.map(crBitOf);
3469
+ const field = dst?.field;
3470
+ const held = field === undefined ? undefined : cmpDef.get(field);
3471
+ const bits = typeof held === 'object' ? held.bits : undefined;
3472
+ if (!dst || !x || !y || x.field !== field || y.field !== field || held === undefined || !bits) {
3473
+ throw new PpcUnsupportedError(
3474
+ `cannot lift '${name}': 'cror' at 0x${ins.addr.toString(16)} — only an OR of two bits of the field a ` +
3475
+ 'float compare set, into that field, is modelled',
3476
+ );
3477
+ }
3478
+ const truth = FCMP_TRUTH[bits[x.bit]].map((t, i) => t || FCMP_TRUTH[bits[y.bit]][i]);
3479
+ const opcode = fcmpOfTruth(truth);
3480
+ if (opcode === undefined) {
3481
+ throw new PpcUnsupportedError(
3482
+ `cannot lift '${name}': 'cror' at 0x${ins.addr.toString(16)} ORs two bits into no compare of the operands`,
3483
+ );
3484
+ }
3485
+ cmpDef.set(field!, { ...(held as CrCompare), bits: { ...bits, [dst.bit]: opcode } });
3486
+ break;
3487
+ }
3488
+ case 'mfcr': {
3489
+ const copy = mkValue(T.unk(32));
3490
+ crCopies.set(copy, new Map(cmpDef));
3491
+ write(d, copy);
3492
+ break;
3493
+ }
3494
+ // `fctiwz fD,fS` truncates toward zero into the integer word of fD's low half, which reaches
3495
+ // the integer file only through memory: `stfd fD,N(r1)` and `lwz rT,N+4(r1)` (the low word,
3496
+ // big-endian). So fD holds that integer as a WORD, and its `stfd` stores it to the frame word
3497
+ // a reload reads; its high half is no value (`fctiwz-high`), so a use of its reload refuses.
3498
+ case 'fctiwz': {
3499
+ if (fpu === undefined || ins.ops.length !== 2 || !ins.ops.every(isFpKey)) {
3500
+ emitOpaqueDest(ins);
3501
+ break;
3502
+ }
3503
+ refuseCrossingUnmodelled(ins);
3504
+ const v = mkValue(T.unk(32));
3505
+ ops.push(mkOp('fptosi', { operands: [readFloat(ins, s)], results: [v] }));
3506
+ writeVar(d, bi, v);
3507
+ break;
3508
+ }
3509
+ case 'stfd': {
3510
+ const held = isFpKey(d) && fpu !== undefined ? readReg(d, bi) : undefined;
3511
+ if (held === undefined || held.type.kind === 'float' || parseMem(s).base !== 'r1') {
3512
+ emitOpaqueDest(ins);
3513
+ break;
3514
+ }
3515
+ const off = entryOffset(ins, s) + 4;
3516
+ refuseMixedSlot(ins, s, off - 4, 'value');
3517
+ refuseMixedSlot(ins, s, off, 'value');
3518
+ const high = mkValue(T.unk(32));
3519
+ nonValues.set(high, { kind: 'fctiwz-high', addr: ins.addr });
3520
+ writeVar(stackSlotKey(off - 4), bi, high);
3521
+ writeVar(stackSlotKey(off), bi, held);
3522
+ if (!valueSlots.has(off)) {
3523
+ valueSlots.set(off, { addr: ins.addr, mem: s, read: false, stored: [] });
3524
+ }
3525
+ valueSlots.get(off)!.stored.push({ v: held, at: ins.addr });
3526
+ break;
3527
+ }
3528
+ case 'fadds':
3529
+ case 'fsubs':
3530
+ case 'fmuls':
3531
+ case 'fdivs':
3532
+ case 'fadd':
3533
+ case 'fsub':
3534
+ case 'fmul':
3535
+ case 'fdiv':
3536
+ case 'fneg':
3537
+ case 'fmr': {
3538
+ const { op } = FP_ARITH[mnem];
3539
+ if (rc || fpu === undefined || ins.ops.length === 0 || !ins.ops.every(isFpKey)) {
3540
+ emitOpaqueDest(ins);
3541
+ break;
3542
+ }
3543
+ if (callInsn) {
3544
+ throw new PpcUnsupportedError(
3545
+ `cannot lift '${name}': '${ins.mnemonic}' computes on a float and '${callInsn.mnemonic}' at ` +
3546
+ `0x${callInsn.addr.toString(16)} makes a call — the floating-point registers a call reads, returns ` +
3547
+ `in and destroys are not modelled`,
3548
+ );
3549
+ }
3550
+ if (fpType === null) {
3551
+ refuseBothPrecisions(name, ins.mnemonic);
3552
+ }
3553
+ const [dst, ...srcs] = ins.ops;
3554
+ if (op === 'copy') {
3555
+ writeVar(dst, bi, readFloat(ins, srcs[0]));
3556
+ break;
3557
+ }
3558
+ const v = mkValue(fpType);
3559
+ ops.push(mkOp(op, { operands: srcs.map((k) => readFloat(ins, k)), results: [v] }));
3560
+ writeVar(dst, bi, v);
3561
+ break;
3562
+ }
1165
3563
  default:
1166
3564
  emitOpaqueDest(ins);
1167
3565
  break; // unmodelled: an honest opaque, never a silent drop
1168
3566
  }
3567
+ if (CARRY_READERS.test(mnem)) {
3568
+ clobberCarry('set it as the carry out of an extended add or subtract, which is not modelled');
3569
+ }
1169
3570
  // A record-form op sets cr0 from a signed compare of its result against 0. Wire that so the
1170
3571
  // next branch reading cr0 fuses (`andi. r0,r3,1; beq …` → `if ((a0 & 1) == 0)`).
1171
3572
  if (rc && lastDef) {
1172
3573
  cmpDef.set('cr0', { lhs: lastDef, rhs: constVal(0), signed: true });
3574
+ compared.add(lastDef);
3575
+ } else if (rc) {
3576
+ cmpDef.set(
3577
+ 'cr0',
3578
+ `'${ins.mnemonic}' at 0x${ins.addr.toString(16)} wrote it from no value this frontend models`,
3579
+ );
1173
3580
  }
1174
3581
  // THE CHOKE POINT. Everything above either took the relocation or threw; a relocation still
1175
3582
  // sitting here was dropped, and a dropped relocation is the printed placeholder standing in
@@ -1182,13 +3589,27 @@ export function lift(
1182
3589
  for (const ins of b.body) {
1183
3590
  decode(ins);
1184
3591
  }
3592
+ // No terminator writes a CR field (a `bc` reads one, `bdnz` writes CTR), so the state after the
3593
+ // body is the state at the block's exit. Recorded for EVERY block, including those whose edges
3594
+ // `inheritFlags` will refuse: the edge rule lives in one place, and a second copy of it here
3595
+ // could disagree with the first.
3596
+ exitCr.set(bi, { fields: cmpDef, none: noCmpWhy });
1185
3597
 
1186
3598
  const br = b.branch;
1187
3599
  // Recovered dense switch: the bounds block dispatches a `switch_br` over the scrutinee — N case
1188
3600
  // blocks (dense 0..N-1) then the default (last successor). The `cmplwi`/`bgt` and the elided
1189
3601
  // dispatch (`lis…lwzx;mtctr;bctr`) are subsumed.
1190
3602
  const jt = br ? jts.get(br.addr) : undefined;
3603
+ // Asked before the jump-table lowering: a table whose bound is a conditional return
3604
+ // (`cmplwi rS,N; bgtlr`) lowers that return as the switch's default edge, and it jumps through
3605
+ // whatever the link register holds there exactly as a plain `bgtlr` does.
3606
+ if (br && (isReturn(br) || isCondReturn(br))) {
3607
+ refuseOverwrittenLink(br);
3608
+ }
1191
3609
  if (jt) {
3610
+ if (jt.hoisted) {
3611
+ tableBases.demand(jt.hoisted, bi, jt.bctrAddr);
3612
+ }
1192
3613
  pushSwitchBr(
1193
3614
  ops,
1194
3615
  readReg(jt.scrutReg, bi),
@@ -1248,15 +3669,27 @@ export function lift(
1248
3669
  // The branch names its cr field as the first operand when not cr0.
1249
3670
  const crName = br.ops[0]?.startsWith('cr') ? br.ops[0] : 'cr0';
1250
3671
  const cmp = cmpDef.get(crName);
1251
- // `cmpDef` is block-local; a compare split from its branch by a block boundary is a
1252
- // cross-block cr dependency this frontend does not model. Decline loud.
3672
+ if (typeof cmp === 'string') {
3673
+ throw new PpcUnsupportedError(
3674
+ `cannot lift '${name}': conditional branch '${base}' tests ${crName}, but ${cmp}`,
3675
+ );
3676
+ }
1253
3677
  if (!cmp) {
1254
3678
  throw new PpcUnsupportedError(
1255
- `cannot lift '${name}': conditional branch '${base}' has no reaching compare (${crName}) in its block`,
3679
+ `cannot lift '${name}': conditional branch '${base}' has no reaching compare (${crName}): ${noCmpWhy}`,
1256
3680
  );
1257
3681
  }
1258
3682
  const table = !cmp.signed ? COND_UNSIGNED : COND_SIGNED;
1259
- const cond = mkCmp(ops, table[base], cmp.lhs, cmp.rhs);
3683
+ const { bit, set } = BRANCH_BIT[base];
3684
+ if (cmp.bits && cmp.quiet && bit !== 'eq') {
3685
+ throw new PpcUnsupportedError(
3686
+ `cannot lift '${name}': conditional branch '${base}' orders the operands of an 'fcmpu', which C emits ` +
3687
+ 'for == and != alone',
3688
+ );
3689
+ }
3690
+ const cond = cmp.bits
3691
+ ? mkCmp(ops, set ? cmp.bits[bit] : NEGATED_FCMP[cmp.bits[bit]], cmp.lhs, cmp.rhs)
3692
+ : mkCmp(ops, table[base], cmp.lhs, cmp.rhs);
1260
3693
  ops.push(mkOp('cond_br', { operands: [cond], successors: succIdx[bi].map((j) => succ(j)) }));
1261
3694
  return;
1262
3695
  }
@@ -1265,8 +3698,17 @@ export function lift(
1265
3698
  ops.push(mkOp('br', { attrs: { fallthrough: true }, successors: [succ(succIdx[bi][0])] }));
1266
3699
  return;
1267
3700
  }
1268
- const retOps = ssa.hasReachingDef(RET, bi) ? [readReg(RET, bi)] : [];
1269
- ops.push(mkOp('ret', { operands: retOps }));
3701
+ // A LAST block with no terminator runs off the end of the text: the input stops before the
3702
+ // function does (a dump split at a foreign label, a hand-supplied fragment), and what
3703
+ // follows is unknown. Lifting it as a return invents one. Thumb refuses the same way.
3704
+ if (!br) {
3705
+ const last = b.body[b.body.length - 1];
3706
+ fail(
3707
+ `cannot lift '${name}': control falls off the end (the block at 0x${b.startAddr.toString(16)} has no ` +
3708
+ `terminator and no successor${last ? `; its last instruction is at 0x${last.addr.toString(16)}` : ''})`,
3709
+ );
3710
+ }
3711
+ emitReturn(ops, bi);
1270
3712
  return;
1271
3713
  }
1272
3714
  // unconditional branch
@@ -1277,26 +3719,454 @@ export function lift(
1277
3719
  fillBlock(b, bi);
1278
3720
  ssa.markFilled(bi);
1279
3721
  });
3722
+ refuseSavedRegisterLeaks(
3723
+ name,
3724
+ blocks.map((b) => ({
3725
+ instrs: [...b.body, ...(b.branch ? [b.branch] : [])],
3726
+ returns: b.synthReturn === true || (b.branch !== null && (isReturn(b.branch) || isCondReturn(b.branch))),
3727
+ })),
3728
+ preds,
3729
+ new Set([...saveSlots.values()].filter((r) => isCalleeSaved(target, r))),
3730
+ // a call preserves every callee-saved register, and a restore routine is a restore
3731
+ (ins, r) => !ins.mnemonic.startsWith('b') && mayWriteGpr(ins, r),
3732
+ restoredBy,
3733
+ );
3734
+ returnUntouchedArg();
3735
+ // What f1 holds at each return, and, where the home is undecided and f1 holds a float this function
3736
+ // computed on every path into every return, that float — read now, before the SSA is finished.
3737
+ const homes = fpu ? returns.map(({ bi }) => floatHomeAt(ssa, fpu.returnReg, bi)) : [];
3738
+ const carriesFloat = carriesFloatHome(homeRule, homes);
3739
+ if (carriesFloat) {
3740
+ returns.forEach(({ op, bi }) => op.operands.push(readReg(fpu!.returnReg, bi)));
3741
+ }
1280
3742
  // A high half no `@l` ever completed — every `@l` consumer this frontend does not model (`ori`, a
1281
3743
  // float load's displacement) lands here, which is what keeps "not modelled" from becoming "not
1282
3744
  // emitted".
1283
3745
  highHalves.assertAllConsumed(name);
3746
+ tableBases.prove(name, ssa);
3747
+ refuseStaleIdiomReads(name, irBlocks, staleIdiom, ssa, prototypes[name] ?? contextOwnProto?.proto, ppcRefusal, calls);
3748
+ // A value stored to the frame that nothing reloads is either a home the body never reads or an
3749
+ // OUTGOING stack argument (`stw r4,8(r1)` before a nine-argument `bl` passes the ninth in the
3750
+ // callee's parameter area, as ac-decomp's `aQMgr_order_decide_trade_*` do). The two are the same
3751
+ // store, and dropping the second loses an argument — save a word at or above the loaded-word
3752
+ // bound of the frame it was stored in, which is no call's argument and so a dead local's.
3753
+ //
3754
+ // A STRUCT'S HOME is the other unread word that is no argument. mwcc 2.4.2 and 2.4.7 give a struct
3755
+ // a callee returned in r3 (`registerStructLocal`, call-plan.ts) a home in the frame and store the
3756
+ // register there whole even where every member read is taken off r3 itself: `s = mk1(x); gW =
3757
+ // s.x;` is `bl mk1; stw r3,gW; ...; stw r3,8(r1)`, and nothing reloads the home. A word every
3758
+ // store of which is such an image holds that struct, and is no call's stack argument where every
3759
+ // call that can run after one of those stores — later in its block, or in a block reachable from
3760
+ // it — is declared to take none: then it is the local the call assigns, which
3761
+ // `liftRegisterStructCalls` (frontend/register-struct-call.ts) declares. A call before every store
3762
+ // reads the word before it is written, so it cannot be handed the struct there.
3763
+ const imageCalls = new Map(
3764
+ irBlocks.flatMap((b) =>
3765
+ b.ops.filter((op) => op.attrs[REGISTER_STRUCT] !== undefined).map((op) => [op.results[0], op] as const),
3766
+ ),
3767
+ );
3768
+ const images = new Set(imageCalls.keys());
3769
+ // a struct with a home is one the source names (ir/core.ts `STRUCT_HOMED`)
3770
+ for (const { stored } of valueSlots.values()) {
3771
+ for (const { v } of stored) {
3772
+ const call = imageCalls.get(v);
3773
+ if (call !== undefined) {
3774
+ call.attrs = { ...call.attrs, [REGISTER_STRUCT]: STRUCT_HOMED };
3775
+ }
3776
+ }
3777
+ }
3778
+ const takesNoStackWord = (ins: Instr): boolean => {
3779
+ if (ins.mnemonic === 'bctrl' || ins.mnemonic === 'blrl') {
3780
+ return false; // nothing declares what an indirect call takes
3781
+ }
3782
+ if (ins.mnemonic !== 'bl' || saveRoutineOf(target, ins)) {
3783
+ return true;
3784
+ }
3785
+ const declared = calls.declaredCall(ins.reloc?.sym ?? '');
3786
+ return declared !== null && wordsOf(declared.widths, target) <= ARG_REGS.length;
3787
+ };
3788
+ const blockOfAddr = new Map(blocks.flatMap((b, bi) => b.body.map((ins, k) => [ins.addr, { bi, k }] as const)));
3789
+ const noCallAfterTakesStack = (addr: number): boolean => {
3790
+ const at = blockOfAddr.get(addr);
3791
+ if (at === undefined) {
3792
+ return false;
3793
+ }
3794
+ if (!blocks[at.bi].body.slice(at.k + 1).every(takesNoStackWord)) {
3795
+ return false;
3796
+ }
3797
+ const seen = new Set<number>();
3798
+ const work = [...succIdx[at.bi]];
3799
+ while (work.length > 0) {
3800
+ const bi = work.pop()!;
3801
+ if (seen.has(bi)) {
3802
+ continue;
3803
+ }
3804
+ seen.add(bi);
3805
+ if (!blocks[bi].body.every(takesNoStackWord)) {
3806
+ return false;
3807
+ }
3808
+ work.push(...succIdx[bi]);
3809
+ }
3810
+ return true;
3811
+ };
3812
+ const depthOfAddr = new Map(blocks.flatMap((b) => b.body.map((ins) => [ins.addr, r1At.get(ins)] as const)));
3813
+ for (const [off, { addr, mem, read, stored }] of valueSlots) {
3814
+ const depth = depthOfAddr.get(addr);
3815
+ const deadLocal = typeof depth === 'number' && off < 0 && off >= loadedWordBound(depth);
3816
+ const structHome = stored.length > 0 && stored.every(({ v, at }) => images.has(v) && noCallAfterTakesStack(at));
3817
+ if (!read && !deadLocal && !structHome) {
3818
+ throw new PpcUnsupportedError(
3819
+ `cannot lift '${name}': local stack frames not supported — '${mem}' is stored at 0x${addr.toString(16)} ` +
3820
+ `and never read back, and an unread home and a call's argument passed on the stack are the same store`,
3821
+ );
3822
+ }
3823
+ }
3824
+ const argSlots = ppcArgSlots(name, fpuArgSlots(name, fpu, ARG_REGS, isFpKey, fpType), ARG_REGS, prototypes[name]);
3825
+ mintArgSlotHoles(ssa, preds[0].length > 0, argSlots);
3826
+ const droppedPairHalf = computedPairHalf(
3827
+ irBlocks,
3828
+ pairReturns,
3829
+ target.compilerBehaviors.keepsUnusedReads === true
3830
+ ? (v) => paramReg.get(v) === RET && irBlocks[0].params.includes(v)
3831
+ : undefined,
3832
+ truthDifferences,
3833
+ (v) => compared.has(v) || readWithoutOp.has(v) || ssa.frameWrites.has(v),
3834
+ );
1284
3835
  ssa.finish();
3836
+ // A copy of the condition register is no integer: only a one-bit extract in its own block reads
3837
+ // it, and a read across an edge resolves to it only now.
3838
+ for (const blk of irBlocks) {
3839
+ for (const op of blk.ops) {
3840
+ if ([...op.operands, ...op.successors.flatMap((x) => x.args)].some((v) => crCopies.has(v))) {
3841
+ throw new PpcUnsupportedError(
3842
+ `cannot lift '${name}': a copy of the condition register ('mfcr') is read as an integer, which is not modelled`,
3843
+ );
3844
+ }
3845
+ }
3846
+ }
3847
+ refuseFpWordCrossings(name, ssa.fn);
3848
+ // Asked once the lift is done, so a refusal the stream meets first keeps its own reason.
3849
+ if (fpu) {
3850
+ settleReturnHome(name, ssa.fn, homeRule, returns, homes, carriesFloat, fpu, RET);
3851
+ }
3852
+ resolveJoinedCarryConsts(name, ssa.fn, joinedCarryConsts);
3853
+ if (droppedPairHalf) {
3854
+ throw new PpcUnsupportedError(
3855
+ `cannot lift '${name}': at a word return no declaration sizes, r4 holds a value this function computed ` +
3856
+ 'and nothing reads, which is the other word of a 64-bit return (r3:r4); returning r3 alone would drop it',
3857
+ );
3858
+ }
1285
3859
  highHalves.assertNoneEscaped(name, irBlocks);
3860
+ tableBases.assertNoneEscaped(name, irBlocks);
3861
+ // A non-value reaches a use through a join's incoming arguments as well as directly (`nonValues`).
3862
+ assertNoNonValueEscaped(irBlocks);
3863
+ assertNonValuesConsumed();
3864
+
3865
+ if (frame !== null) {
3866
+ // Prove every `laddr` names storage of this frame at a shape the machine states, or decline
3867
+ // (`auditFrameObjects`); an accepted function then gets the device pin the audit answers. The
3868
+ // slot model's words are named from the pushed r1, as the audit names every offset.
3869
+ const verdict = FRAME_OBJECT_AUDIT.run({
3870
+ name,
3871
+ irBlocks,
3872
+ ownedLocals: frame.area,
3873
+ declaredLocals: frame.area,
3874
+ usedSlotOffsets: new Set(
3875
+ [...valueSlots.keys()].map((o) => o + frame.size).filter((o) => o >= 0 && o < frame.size),
3876
+ ),
3877
+ // the bound of the pushed frame, named from the pushed r1 as every offset here is
3878
+ loadedWords:
3879
+ loadedFrom === undefined ? undefined : { from: loadedFrom, bound: loadedWordBound(-frame.size) + frame.size },
3880
+ capturedObjectIsTheWholeFrame: false,
3881
+ movedCaptures,
3882
+ returnsWithoutHiddenPointer: calls.returnsWithoutHiddenPointer,
3883
+ symbols: undefined,
3884
+ target,
3885
+ oneObject,
3886
+ });
3887
+ if (verdict !== undefined && 'oneObject' in verdict) {
3888
+ return verdict;
3889
+ }
3890
+ if (verdict !== undefined) {
3891
+ pinDeviceAccesses(irBlocks, verdict, target, undefined);
3892
+ }
3893
+ }
3894
+ pinDeadReads(irBlocks, target, {
3895
+ name,
3896
+ own: prototypes[name],
3897
+ atWordReturn: unreadHigh,
3898
+ atUndeclaredCall: calls.heldAtUndeclaredCall,
3899
+ compared,
3900
+ spilled: ssa.frameWrites,
3901
+ discarded: leftInDiscardRegister(irBlocks, discarded, writtenElsewhere),
3902
+ declarations,
3903
+ });
1286
3904
 
1287
- // ABI-ordered entry parameters (r3, r4, …) — a callee-saved copy can read a later argument
1288
- // register first, so sort the true entry's params by argument-register index.
1289
- const entry = irBlocks[0];
1290
- // non-ABI live-in ranks FIRST (indexOf's -1) — deliberate MIPS/PPC tie-break; Thumb's is 99/last
1291
- abiSortEntryParams(entry, preds[0].length > 0, (v) => ARG_REGS.indexOf(paramReg.get(v) ?? ''));
3905
+ abiSortEntryParams(irBlocks[0], preds[0].length > 0, paramReg, argSlots);
3906
+ ssa.fn.localObjects = statics.finish();
1292
3907
  return ssa.fn;
1293
3908
  }
1294
3909
 
3910
+ /** WHETHER A WORD RETURN'S r4 WAS COMPUTED FOR NOTHING, which the lift refuses. `returns` holds,
3911
+ * for each word return no declaration pins the width of, the word r3 returns and what r4, the
3912
+ * second register of an r3:r4 return, may hold there (frontend/dead-reads.ts `unreadPairHalf`).
3913
+ * Where one of those values is the result of an op that nothing reads — directly, or through joins
3914
+ * nothing reads — the compiler computed it for the caller: an optimizing compiler deletes a pure
3915
+ * computation nobody reads, so a value that survives in r4 with no reader is the other word of a
3916
+ * 64-bit return, and returning r3 alone drops it. mwcc's `(unsigned long long)a - b` is `subfc
3917
+ * r4,r4,r3; subfe r3,r4,r4`, whose high word is the very `subfe` a 32-bit `-(a < b)` is, and `long
3918
+ * long x = a; if (c) x = -x; return x;` joins `0 - a` into r4 below the branch; both would lift as
3919
+ * their high word.
3920
+ *
3921
+ * AT A LEVEL THAT KEEPS DEAD CODE (`entryWord` given, where `keepsUnusedReads` holds) that premise
3922
+ * fails for a parameter: mwcc -O0 keeps `b = a + 1;` in b's register r4. There a return whose r3
3923
+ * is still the entry value (`entryWord`, directly or through joins of only it) is not asked, since
3924
+ * -O0 spells a 64-bit op as both words' ops and writes r3 even where its word is unchanged (`x |
3925
+ * 0xff` is `or r4,r4,r5; or r3,r3,r0` with r0 = 0), in every 64-bit shape measured on 2.4.2, 2.4.7
3926
+ * and 2.3.3. At -O4 an untouched r3 is evidence of nothing: `((u64)a << 32) | (b + 1)` is `addi
3927
+ * r4,r4,1`. A dead parameter store beside a computed r3 (`b = a * 3; return a + 1;`) still
3928
+ * refuses.
3929
+ *
3930
+ * Asked BEFORE `ssa.finish`, while every join is still a parameter with its incoming values wired,
3931
+ * so a join the trivial-phi pass would fold is walked, not lost, and refused AFTER it, so an
3932
+ * unmodelled instruction or an unresolved `subfze` keeps its own message. `exemptValue` names a
3933
+ * value the machine did use where the lift left no op for the use: one a compare consumed (`compared`),
3934
+ * one a carry reader folded away (`readWithoutOp`: ac-decomp's `mSP_GetRealShopLevel` returns `(x
3935
+ * >= 25000)` as `li r4,-1; subfc r0,r0,r3; subfze r3,r4`), a `subfc`/`subfic` difference whose
3936
+ * carry a reader turned into a 0/1 truth value (`truthDifferences`, kept only where that reader is
3937
+ * the carry's sole consumer in its block and neither is r3's word: 2.3.3's `a + (a < b)` is
3938
+ * `subfc r4,r4,r3; subfe r0,r0,r0; neg r0,r0; add r3,r3,r0` over the dead `b`, and the signed one
3939
+ * keeps bit 0 of an `addze`; a 64-bit high word reads the borrow as `CA - 1`, which r3 returns),
3940
+ * and one stored to the frame (`frameWrites`). A function that holds an
3941
+ * unmodelled instruction is not asked: the `opaque` may read r4 without naming it, and it declines
3942
+ * under its own name. A value r4 holds that the function did not compute — its entry value, the
3943
+ * caller's `a` that `long long z(unsigned a) { return a; }` moves there — is not seen, and that
3944
+ * word return still drops the low word. */
3945
+ type PairReturn = { word: Value; high: ReadonlySet<Value> };
3946
+ /** A `subfc`/`subfic` difference (`diff`) whose carry a reader made a 0/1 truth value: `carry` is
3947
+ * the `subfe rX,rY,rY` (CA - 1) or `addze` result, `truth` the `neg` or `clrlwi 31` over it. */
3948
+ type TruthDifference = { diff: Value; carry: Value; truth: Value };
3949
+ function computedPairHalf(
3950
+ blocks: readonly Block[],
3951
+ returns: readonly PairReturn[],
3952
+ entryWord: ((v: Value) => boolean) | undefined,
3953
+ truthDifferences: readonly TruthDifference[],
3954
+ exemptValue: (v: Value) => boolean,
3955
+ ): boolean {
3956
+ if (returns.every((r) => r.high.size === 0) || blocks.some((b) => b.ops.some((o) => o.opcode === 'opaque'))) {
3957
+ return false;
3958
+ }
3959
+ const defOf = new Map<Value, Op>();
3960
+ const blockOf = new Map<Op, Block>();
3961
+ const incoming = new Map<Value, Value[]>();
3962
+ // Every reader of each value: the ops that take it as an operand, a join argument counting as one.
3963
+ const readers = new Map<Value, number>();
3964
+ const readBy = (v: Value) => readers.set(v, (readers.get(v) ?? 0) + 1);
3965
+ for (const b of blocks) {
3966
+ for (const o of b.ops) {
3967
+ blockOf.set(o, b);
3968
+ o.results.forEach((r) => defOf.set(r, o));
3969
+ o.operands.forEach(readBy);
3970
+ for (const succ of o.successors) {
3971
+ succ.args.forEach((a, i) => {
3972
+ readBy(a);
3973
+ const p = succ.block.params[i];
3974
+ incoming.set(p, [...(incoming.get(p) ?? []), a]);
3975
+ });
3976
+ }
3977
+ }
3978
+ }
3979
+ // What r3 may hold at a word return: the word itself, and every value a join of it carries in.
3980
+ const returned = new Set<Value>();
3981
+ const toReturn = returns.map((r) => r.word);
3982
+ while (toReturn.length > 0) {
3983
+ const v = toReturn.pop()!;
3984
+ if (!returned.has(v)) {
3985
+ returned.add(v);
3986
+ toReturn.push(...(incoming.get(v) ?? []));
3987
+ }
3988
+ }
3989
+ // A truth value's difference is the compare's by-product only where nothing else saw the carry:
3990
+ // the reader is its sole consumer and runs in its block, so on every path the difference takes,
3991
+ // and neither the carry nor the truth value is what r3 returns. mwcc's `u64 d = (u64)a - b; *p =
3992
+ // -(int)(d >> 32); return d;` is `subfc r4,r4,r3; subfe r3,r4,r4; neg r0,r3`: that `subfe` is
3993
+ // the high word, the `neg` beside it is a second reader, and r4 is the low word.
3994
+ const comparedDifference = new Set<Value>();
3995
+ for (const { diff, carry, truth } of truthDifferences) {
3996
+ const reader = defOf.get(truth);
3997
+ const writer = defOf.get(carry);
3998
+ if (
3999
+ reader !== undefined &&
4000
+ writer !== undefined &&
4001
+ reader.operands.filter((x) => x === carry).length === 1 &&
4002
+ readers.get(carry) === 1 &&
4003
+ !exemptValue(carry) &&
4004
+ blockOf.get(reader) === blockOf.get(writer) &&
4005
+ !returned.has(carry) &&
4006
+ !returned.has(truth)
4007
+ ) {
4008
+ comparedDifference.add(diff);
4009
+ }
4010
+ }
4011
+ const exempt = (v: Value) => exemptValue(v) || comparedDifference.has(v);
4012
+ // What some kept op reads, through the joins that carry it there.
4013
+ const read = new Set<Value>();
4014
+ const work: Value[] = [];
4015
+ const mark = (v: Value) => {
4016
+ if (!read.has(v)) {
4017
+ read.add(v);
4018
+ work.push(v);
4019
+ }
4020
+ };
4021
+ for (const b of blocks) {
4022
+ for (const o of b.ops) {
4023
+ if (!deletableWhenDead(o)) {
4024
+ o.operands.forEach(mark);
4025
+ }
4026
+ }
4027
+ }
4028
+ while (work.length > 0) {
4029
+ const v = work.pop()!;
4030
+ defOf.get(v)?.operands.forEach(mark);
4031
+ incoming.get(v)?.forEach(mark);
4032
+ }
4033
+ const seen = new Set<Value>();
4034
+ const computedForNothing = (v: Value): boolean => {
4035
+ if (seen.has(v) || read.has(v) || exempt(v)) {
4036
+ return false;
4037
+ }
4038
+ seen.add(v);
4039
+ const def = defOf.get(v);
4040
+ if (def !== undefined) {
4041
+ return def.opcode !== 'undef';
4042
+ }
4043
+ return (incoming.get(v) ?? []).some(computedForNothing);
4044
+ };
4045
+ // Whether `v` is r3's entry value on every path: directly, or through joins of nothing else.
4046
+ const entry = (v: Value, on: Set<Value>): boolean => {
4047
+ if (entryWord!(v)) {
4048
+ return true;
4049
+ }
4050
+ const ins = incoming.get(v);
4051
+ if (ins === undefined || on.has(v)) {
4052
+ return ins !== undefined;
4053
+ }
4054
+ on.add(v);
4055
+ return ins.every((x) => entry(x, on));
4056
+ };
4057
+ return returns.some(
4058
+ (r) => !(entryWord !== undefined && entry(r.word, new Set())) && [...r.high].some(computedForNothing),
4059
+ );
4060
+ }
4061
+
4062
+ /** Settle each `subfze rD,rA` whose rA came in through a join (the `subfze` decode): where every
4063
+ * path brings the same constant K — the join's incoming values followed back, each a `const` or a
4064
+ * join of its own, a cycle adding nothing — the `~rA + CA` becomes `~K + CA`, which is CA itself
4065
+ * for mwcc's `li -1`. Anything else refuses, as an unmodelled `subfze` does in its own block: over
4066
+ * a register that is no constant it is the high half of a 64-bit negate. */
4067
+ function resolveJoinedCarryConsts(
4068
+ name: string,
4069
+ fn: Fn,
4070
+ pending: readonly { ins: Instr; not: Op; add: Op; ca: Value }[],
4071
+ ): void {
4072
+ if (pending.length === 0) {
4073
+ return;
4074
+ }
4075
+ const defs = new Map<Value, Op>();
4076
+ const incoming = new Map<Value, Value[]>();
4077
+ for (const b of fn.blocks) {
4078
+ for (const o of b.ops) {
4079
+ o.results.forEach((r) => defs.set(r, o));
4080
+ for (const succ of o.successors) {
4081
+ succ.args.forEach((a, i) => {
4082
+ const p = succ.block.params[i];
4083
+ incoming.set(p, [...(incoming.get(p) ?? []), a]);
4084
+ });
4085
+ }
4086
+ }
4087
+ }
4088
+ // The one constant `v` is on every path, `null` where it is none, `undefined` where only a cycle
4089
+ // brings it (which says nothing).
4090
+ const constantOf = (v: Value, seen: Set<Value>): number | null | undefined => {
4091
+ const def = defs.get(v);
4092
+ if (def !== undefined) {
4093
+ return def.opcode === 'const' && typeof def.attrs.value === 'number' ? def.attrs.value | 0 : null;
4094
+ }
4095
+ const ins = incoming.get(v);
4096
+ if (ins === undefined) {
4097
+ return null; // an entry parameter
4098
+ }
4099
+ if (seen.has(v)) {
4100
+ return undefined;
4101
+ }
4102
+ seen.add(v);
4103
+ let k: number | undefined;
4104
+ for (const a of ins) {
4105
+ const c = constantOf(a, seen);
4106
+ if (c === null || (c !== undefined && k !== undefined && c !== k)) {
4107
+ return null;
4108
+ }
4109
+ k ??= c;
4110
+ }
4111
+ return k ?? null;
4112
+ };
4113
+ for (const { ins, not, add, ca } of pending) {
4114
+ const k = constantOf(not.operands[0], new Set());
4115
+ if (k === null || k === undefined) {
4116
+ throw new PpcUnsupportedError(
4117
+ `cannot lift '${name}': unmodelled instruction '${ins.mnemonic}' at 0x${ins.addr.toString(16)} — it reads ` +
4118
+ `${ins.ops[1]}, which is not one constant on every path into its block, and a subfze over a register is the ` +
4119
+ 'upper word of a 64-bit negate',
4120
+ );
4121
+ }
4122
+ const home = fn.blocks.find((b) => b.ops.includes(add))!;
4123
+ if (~k === 0) {
4124
+ replaceAllUsesWith(fn, add.results[0], ca);
4125
+ home.ops.splice(home.ops.indexOf(add), 1);
4126
+ home.ops.splice(home.ops.indexOf(not), 1);
4127
+ } else {
4128
+ Object.assign(not, { opcode: 'const', operands: [], attrs: { value: ~k } });
4129
+ }
4130
+ }
4131
+ }
4132
+
1295
4133
  function mkCmp(ops: Op[], opc: Opcode, l: Value, r: Value): Value {
1296
4134
  const v = mkValue(T.unk(32));
1297
4135
  ops.push(mkOp(opc, { operands: [l, r], results: [v] }));
1298
4136
  return v;
1299
4137
  }
1300
4138
 
4139
+ /** The EABI's slots: the shared register-then-stack-word rule (`stackArgSlots`), refusing a stack
4140
+ * word read beside a floating-point argument — the FPRs are a separate file, so where a float
4141
+ * parameter falls among the integer ones is not in the object — or in a function whose own
4142
+ * declaration has a 64-bit or floating parameter, which takes words the integer count does not. */
4143
+ function ppcArgSlots(name: string, regs: ArgSlots, argRegs: readonly string[], own: FnProto | undefined): ArgSlots {
4144
+ return stackArgSlots(regs, argRegs, (readKeys, top) => {
4145
+ const float = readKeys.find(isFpKey);
4146
+ const declared = Array.isArray(own?.params)
4147
+ ? own.params.find((p) => isFloatingSpelling(p) || (declaredWidth(p) ?? 0) > 32)
4148
+ : undefined;
4149
+ if (float !== undefined || declared !== undefined) {
4150
+ throw new PpcUnsupportedError(
4151
+ `cannot lift '${name}': incoming stack argument ${top + 1} is read beside ` +
4152
+ (float !== undefined ? `the floating-point argument in ${float}` : `a parameter declared '${declared}'`) +
4153
+ ` — which words that argument takes ahead of the stack words is not modelled`,
4154
+ );
4155
+ }
4156
+ });
4157
+ }
4158
+
1301
4159
  /** The PowerPC / CodeWarrior frontend, registered for the `ppc` target. */
1302
4160
  export const ppcFrontend: Frontend = { id: 'ppc', inputFormat: 'objdump', lift };
4161
+
4162
+ /** Internal surface for this module's own tests, and for nothing else (`frontend/frame-objects.ts`
4163
+ * says why it is not a plain export). */
4164
+ export const __testing = {
4165
+ PARAM_AREA_AT,
4166
+ /** the local area `lift` draws for one function of objdump text (`liftPrelude`), or its refusal */
4167
+ frameOf: (name: string, asm: string, target: TargetDescription, prototypes: Prototypes = {}, asmData?: AsmData) => {
4168
+ const instrs = parseDisasm(sliceSymbol(asm, name));
4169
+ const jts = asmData ? recoverPpcJumpTables(instrs, asmData) : new Map<number, PpcJT>();
4170
+ return liftPrelude(name, instrs, target, prototypes, jts).frame;
4171
+ },
4172
+ };