@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  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 +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -20,9 +20,10 @@
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`).
26
27
  // • RECORD FORM (`.` = the Rc bit, e.g. `andi.`/`add.`) also sets cr0 from a signed compare of
27
28
  // the result against 0; that implicit compare is wired so a following `beq`/`bne` fuses.
28
29
  //
@@ -39,9 +40,10 @@
39
40
  import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
40
41
  import type { Opcode } from '../ir/opcodes';
41
42
  import { T } from '../ir/types';
42
- import { type Prototypes, protoArity } from '../proto';
43
+ import type { Prototypes } from '../proto';
43
44
  import type { TargetDescription } from '../target';
44
45
  import { type AsmData, readJumpTable } from './asmdata';
46
+ import { callDeclarations } from './call-plan';
45
47
  import {
46
48
  type DisasmInstr,
47
49
  parseImm,
@@ -50,13 +52,16 @@ import {
50
52
  sliceSymbol,
51
53
  } from './disasm';
52
54
  import { mkEmitKit, pushSwitchBr } from './emit';
53
- import { FrontendUnsupportedError } from './errors';
55
+ import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
56
+ import { inheritFlags } from './flags-edge';
54
57
  import { assertInputFormat } from './format';
58
+ import { fpPrecision, fpuArgSlots, refuseBothPrecisions, writesFloatReturn } from './fpu';
55
59
  import type { Frontend } from './frontend';
56
60
  import { makeHighHalves } from './high-half';
61
+ import { makeLocalStatics, readObjectLocalObject } from './local-object';
57
62
  import { opaqueDest } from './opaque';
58
- import { unspellableReason } from './reloc-symbol';
59
- import { abiSortEntryParams } from './ssa';
63
+ import { classifyRelocSymbol, unspellableReason } from './reloc-symbol';
64
+ import { abiSortEntryParams, mintArgSlotHoles, stackSlotKey } from './ssa';
60
65
  import { makeSsaBuilder } from './ssa';
61
66
 
62
67
  type Instr = DisasmInstr;
@@ -116,6 +121,33 @@ const isModeledBranch = (ins: Instr) => isXfer(ins) || ins.mnemonic === 'bl';
116
121
 
117
122
  const isReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
118
123
 
124
+ /** The FPU arithmetic this frontend lifts, onto the float opcodes, with the precision each computes
125
+ * in; `fmr` is a copy and has no opcode. `fneg` and `fmr` carry no precision at all — an FPR holds a
126
+ * double either way — so they take the function's (`frontend/fpu.ts` `fpPrecision`); in a function
127
+ * of only those, the floats state none (`T.fUnstated`), because the float and double spellings of a
128
+ * negation or a copy compile to one object. Everything else in the file keeps the register-file refusal `opaqueDest` gives it: `frsp`, the
129
+ * loads and stores, the conversions, the compares, the fused multiply-adds, paired singles — and a
130
+ * record form (`fadds.`), which sets `cr1`. */
131
+ const FP_ARITH: Readonly<Record<string, { op: Opcode | 'copy'; width: 32 | 64 | null }>> = {
132
+ fadds: { op: 'fadd', width: 32 },
133
+ fsubs: { op: 'fsub', width: 32 },
134
+ fmuls: { op: 'fmul', width: 32 },
135
+ fdivs: { op: 'fdiv', width: 32 },
136
+ fadd: { op: 'fadd', width: 64 },
137
+ fsub: { op: 'fsub', width: 64 },
138
+ fmul: { op: 'fmul', width: 64 },
139
+ fdiv: { op: 'fdiv', width: 64 },
140
+ fneg: { op: 'fneg', width: null },
141
+ fmr: { op: 'copy', width: null },
142
+ };
143
+ const FP_ARITH_MNEMONICS: ReadonlySet<string> = new Set(Object.keys(FP_ARITH));
144
+
145
+ /** An FPU register: the token, and the key the SSA builder holds it under. ONE predicate for the
146
+ * decode above and the register-file refusal below (`fpReg`), so a token one of them reads as an
147
+ * FPR cannot be one the other does not. */
148
+ const PPC_FP_REG = /^f\d+$/;
149
+ const isFpKey = (k: string) => PPC_FP_REG.test(k);
150
+
119
151
  // Shared objdump scaffolding (frontend/disasm.ts). parseMem narrowed to `r\d+` bases — a
120
152
  // non-register base is an SDA/global placeholder assertOrdinaryMem declines.
121
153
  const parseMem = (operand: string): { off: number; base: string } => parseSharedMem(operand, /r\d+/);
@@ -138,6 +170,20 @@ const rlwinmMask = (mb: number, me: number): number => {
138
170
  // stripping, the mnemonic misses the cond tables and the branch is silently dropped).
139
171
  const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { relocs: true, hintSuffixes: true });
140
172
 
173
+ /** A compare a `cr` field holds: its operands and signedness, which the reading branch turns into a
174
+ * condition. */
175
+ type CrCompare = { lhs: Value; rhs: Value; signed: boolean };
176
+
177
+ /** What a block leaves in the condition register for a successor to inherit. `fields` holds, per
178
+ * field, the compare it holds, or the sentence saying what overwrote it. A field absent from the map
179
+ * never held a compare on the chain of edges that reached here, and `none` is why: the sentence
180
+ * from the block where that chain started. */
181
+ type CrExit = { fields: ReadonlyMap<string, CrCompare | string>; none: string };
182
+
183
+ /** The `cr` fields the EABI makes volatile across a call: cr0, cr1 and cr5–cr7. The callee may
184
+ * write any of them, so a compare in one does not survive a `bl`. cr2–cr4 are nonvolatile. */
185
+ const CR_VOLATILE = ['cr0', 'cr1', 'cr5', 'cr6', 'cr7'] as const;
186
+
141
187
  interface PpcBlock {
142
188
  startAddr: number;
143
189
  body: Instr[];
@@ -356,6 +402,71 @@ function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blo
356
402
  };
357
403
  }
358
404
 
405
+ /** The link register's save word, as an offset from the entry r1: the second word of the caller's
406
+ * frame header, after the back chain. mwcc 2.3.3 spells it `4(r1)` before the push and 2.4.x
407
+ * `N+4(r1)` after it (`r1Displacements`). */
408
+ const LR_SAVE_WORD = 4;
409
+
410
+ /** Where r1 stands, relative to its value at entry, just BEFORE an instruction runs: a byte count
411
+ * (0 before the frame push, -N after `stwu r1,-N(r1)`), or why it is not known there.
412
+ *
413
+ * A frame slot has to be named by its offset from the ENTRY r1, because the compilers disagree on
414
+ * which side of the push they save the link register. mwcc 2.4.x pushes first and saves at
415
+ * `N+4(r1)`; mwcc 2.3.3 saves at `4(r1)` BEFORE `stwu r1,-N(r1)` and restores from `N+4(r1)`. That is
416
+ * one word of the caller's frame spelled at two offsets, and an offset from the CURRENT r1 reads it
417
+ * as two different words.
418
+ *
419
+ * Forward dataflow over the block graph, per instruction. Only the two forms a compiler uses to
420
+ * move r1 are counted: `stwu r1,-N(r1)` from the entry value, and `addi r1,r1,N`. Anything else
421
+ * that writes r1 — a second push, an `addi r1,rX,N` from another register, an `mr r1,r11` — makes
422
+ * the displacement unknown from there on, and so does a join whose predecessors arrive at
423
+ * different depths. Unknown is not a refusal by itself: a displacement nothing reads decides
424
+ * nothing. The frame accesses and the push that DO read it refuse loud (`lift`, `entryOffset`). */
425
+ function r1Displacements(blocks: PpcBlock[], succIdx: number[][]): Map<Instr, number | string> {
426
+ const at = new Map<Instr, number | string>();
427
+ const into: Array<number | string | undefined> = blocks.map(() => undefined);
428
+ // A block whose displacement is unknown stays unknown, under the first reason found.
429
+ const meet = (bi: number, d: number | string): boolean => {
430
+ const old = into[bi];
431
+ let next = d;
432
+ if (typeof old === 'string' || old === d) {
433
+ next = old;
434
+ } else if (typeof old === 'number' && typeof d === 'number') {
435
+ const where = blocks[bi].startAddr >= 0 ? `0x${blocks[bi].startAddr.toString(16)}` : 'a synthesized return';
436
+ next = `the paths into ${where} arrive with r1 at two depths (${old} and ${d} bytes from its entry value)`;
437
+ }
438
+ into[bi] = next;
439
+ return next !== old;
440
+ };
441
+ const writesR1 = (ins: Instr) => ins.ops[0] === 'r1' && !/^(st|cmp|tw|mt|dc|ic)/.test(ins.mnemonic);
442
+ meet(0, 0);
443
+ const work = [0];
444
+ while (work.length) {
445
+ const bi = work.pop()!;
446
+ let d = into[bi]!;
447
+ for (const ins of [...blocks[bi].body, ...(blocks[bi].branch ? [blocks[bi].branch] : [])]) {
448
+ at.set(ins, d);
449
+ const where = `0x${ins.addr.toString(16)}`;
450
+ if (ins.mnemonic === 'stwu' && ins.ops[0] === 'r1' && parseMem(ins.ops[1]).base === 'r1') {
451
+ d =
452
+ d === 0
453
+ ? parseMem(ins.ops[1]).off
454
+ : `the frame push at ${where} is a second one (r1 already moved from its entry value)`;
455
+ } else if (ins.mnemonic === 'addi' && ins.ops[0] === 'r1' && ins.ops[1] === 'r1' && !ins.reloc) {
456
+ d = typeof d === 'number' ? d + parseImm(ins.ops[2]) : d;
457
+ } else if (writesR1(ins)) {
458
+ d = `'${ins.mnemonic} ${ins.ops.join(',')}' at ${where} sets r1 to a value this frontend does not track`;
459
+ }
460
+ }
461
+ for (const s of succIdx[bi]) {
462
+ if (meet(s, d)) {
463
+ work.push(s);
464
+ }
465
+ }
466
+ }
467
+ return at;
468
+ }
469
+
359
470
  /** Lift disassembled PowerPC text → an L1 Fn with block-argument SSA. `asmData` (optional) supplies
360
471
  * the data-section jump table for dense-switch (Regime-B) recovery; absent ⇒ a `bctr` dispatch
361
472
  * loud-fails. */
@@ -395,12 +506,26 @@ export function lift(
395
506
  * `R_PPC_EMB_SDA21` is `a0 & 0`). An UNmodelled instruction refuses earlier and for a better
396
507
  * reason: `lfs`/`lfd` under either relocation name the float gap, not this one. */
397
508
  let relocTaken = false;
509
+ const statics = makeLocalStatics();
398
510
  /** The symbol a relocation names, once the naming policy has passed it. Every recovery below goes
399
511
  * 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. */
512
+ * an ordinary identifier — recovering the address is only half of being able to write it down.
513
+ *
514
+ * A function-scope static is the one kind no `extern` can name that a lift can still WRITE: the
515
+ * function defines it, so its definition, read out of the side table, is recorded beside the Fn
516
+ * under the linker name the IR keeps, and structure spells it by its source name. */
401
517
  const spellableSym = (ins: Instr): string => {
402
518
  relocTaken = true;
403
519
  const sym = ins.reloc?.sym ?? '';
520
+ if (classifyRelocSymbol(sym) === 'local-static') {
521
+ const read = asmData
522
+ ? readObjectLocalObject(asmData, sym, name)
523
+ : { refused: "whose definition needs the object's data, which this lift was not given" };
524
+ if ('refused' in read) {
525
+ throw new PpcUnsupportedError(`${relocSite(ins)} names a function-scope static ('${sym}') ${read.refused}`);
526
+ }
527
+ return statics.define(read);
528
+ }
404
529
  const why = unspellableReason(sym);
405
530
  if (why) {
406
531
  throw new PpcUnsupportedError(`${relocSite(ins)} ${why}`);
@@ -429,6 +554,17 @@ export function lift(
429
554
  }
430
555
  }
431
556
  const { blocks, succIdx } = toBlocks(instrs, name, jts);
557
+ // PREHEADER. A function whose first block is a branch target (`do b = b * b; while (--n);` is a
558
+ // loop at the entry) would take its parameters as that loop header's phis. An empty block ahead
559
+ // of it becomes the entry, and its live-ins are the function's parameters (frontend/ssa.ts
560
+ // `ArgSlots`). It starts where the function does, which is what its label says.
561
+ if (succIdx.some((ss) => ss.includes(0))) {
562
+ blocks.unshift({ startAddr: blocks[0].startAddr, body: [], branch: null });
563
+ succIdx.forEach((ss, i) => (succIdx[i] = ss.map((j) => j + 1)));
564
+ succIdx.unshift([1]);
565
+ }
566
+
567
+ const r1At = r1Displacements(blocks, succIdx);
432
568
 
433
569
  const preds: number[][] = blocks.map(() => []);
434
570
  blocks.forEach((_, i) => {
@@ -437,8 +573,32 @@ export function lift(
437
573
  }
438
574
  });
439
575
 
440
- const ssa = makeSsaBuilder(name, blocks.length, preds);
576
+ const fpu = target.fpu;
577
+ // A CALL IN A FUNCTION THAT COMPUTES ON FLOATS refuses — at the float instruction, so a refusal
578
+ // the stream reaches first keeps its own reason: which FPRs a callee destroys, which it reads as
579
+ // float arguments and which it returns in are all unmodelled, so a float live across a call — or
580
+ // handed to one in f1 — would resolve to a value the callee overwrote or be dropped. Both scans
581
+ // read the blocks the entry reaches: a `bl` on no path makes no call.
582
+ const reached = blocks.flatMap((b) => b.body);
583
+ const callInsn = reached.find((ins) => ins.mnemonic === 'bl');
584
+ const floatReturn = writesFloatReturn(reached, FP_ARITH_MNEMONICS, (t) => (isFpKey(t) ? t : null), fpu);
585
+ const fpType = fpPrecision(reached, (m) => FP_ARITH[m]?.width);
586
+
587
+ const ssa = makeSsaBuilder(name, blocks.length, preds, undefined, (k) =>
588
+ isFpKey(k) ? (fpType ?? T.f32()) : undefined,
589
+ );
441
590
  const { irBlocks, readVar, writeVar, paramReg } = ssa;
591
+ /** This frontend's refusal, for the shared code that refuses on its behalf. */
592
+ const fail: FrontendRefusal = (message) => {
593
+ throw new PpcUnsupportedError(message);
594
+ };
595
+ const calls = callDeclarations(
596
+ name,
597
+ target,
598
+ prototypes,
599
+ { pairs: false, memoryReturn: false, stackArgs: false, voidReturn: false, argRegisterBoundsArity: false },
600
+ fail,
601
+ );
442
602
 
443
603
  /** The HIGH half of a relocated address, per value standing for one. The invariant, and every
444
604
  * way a placeholder could leak past it, lives in frontend/high-half.ts — shared with
@@ -457,71 +617,61 @@ export function lift(
457
617
  hi: '@ha',
458
618
  hiArticle: 'an',
459
619
  lo: '@l',
460
- fail: (message) => {
461
- throw new PpcUnsupportedError(message);
462
- },
620
+ fail,
463
621
  });
464
622
  /** Reading a register AS A VALUE. A register holding a high half is not one, so this refuses loud
465
623
  * rather than handing back a plausible number standing for an address. `foldLoHalf` is the only
466
624
  * code entitled to look at a high half, which is why it reaches `readVar` directly. */
467
625
  const readReg = (r: string, at: number): Value => highHalves.guardRead(name, r, readVar(r, at));
626
+ /** The return address `mflr rD` copies out of the link register, per value standing for one, with
627
+ * the address of the `mflr`. Like a high half it is a definition and not a value: storing it to
628
+ * the link register's save word is the LR save (`frameStore`), and moving it back with `mtlr` is
629
+ * the return. Any other use is a function reading its own return address (MP4's
630
+ * `asm { mflr retaddr }` does, to tag an allocation with its caller), which no C expression
631
+ * spells, so that use refuses: a store at once, anything else once the function is built. */
632
+ const returnAddresses = new Map<Value, number>();
468
633
  const RET = target.returnReg;
469
634
  const ARG_REGS = target.argRegs;
470
635
 
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.
636
+ // THE FRAME, as two kinds of word slot, each named by its offset from the ENTRY r1
637
+ // (`r1Displacements`).
474
638
  //
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`.
639
+ // A SAVE holds an entry value that is no C value: a callee-saved register's, the link register's
640
+ // word, or the back chain the frame push stores (the caller's r1). Nothing but the caller reads it
641
+ // back, so its restore — a load into the register it was saved from — is dropped, and a load into
642
+ // any other register refuses. Function-scoped, so a save in the prologue matches a restore in any
643
+ // epilogue.
479
644
  //
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++;
645
+ // Every other word store is a VALUE — an argument's home, or a live value spilled — and it is the
646
+ // shared stack-slot variable (`stackSlotKey`, frontend/ssa.ts) MIPS and Thumb use. A reload into
647
+ // any register reads it; a reload on a path that never stored it refuses in the builder; a store
648
+ // nothing reloads refuses at the end of `lift`.
649
+ //
650
+ // A slot is one kind for the whole function: a word both saved and stored with a value has no
651
+ // single reading, and refuses.
652
+ const saveSlots = new Map<number, string>();
653
+ const valueSlots = new Map<number, { addr: number; mem: string; read: boolean }>();
654
+ const refuseMixedSlot = (ins: Instr, mem: string, off: number, kind: 'save' | 'value') => {
655
+ if (kind === 'save' ? valueSlots.has(off) : saveSlots.has(off)) {
656
+ throw new PpcUnsupportedError(
657
+ `cannot lift '${name}': local stack frames not supported — '${mem}' at 0x${ins.addr.toString(16)} ` +
658
+ `is a word this function both saves a register in and stores a value to`,
659
+ );
495
660
  }
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
- }
661
+ };
662
+ /** The slot an r1-relative operand names: its offset from the entry r1. Where r1's displacement is
663
+ * not known, no offset can be named, and a save or restore there refuses rather than being
664
+ * matched against a slot it may not be. */
665
+ const entryOffset = (ins: Instr, mem: string): number => {
666
+ const d = r1At.get(ins);
667
+ if (typeof d !== 'number') {
668
+ throw new PpcUnsupportedError(
669
+ `cannot lift '${name}': stack-frame access '${mem}' at 0x${ins.addr.toString(16)} — the stack ` +
670
+ `pointer's offset from its entry value is not known there: ${d ?? 'the instruction is unreachable'}`,
671
+ );
504
672
  }
505
- return n;
673
+ return parseMem(mem).off + d;
506
674
  };
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>();
525
675
  // `stmw rS,D(r1)` saves rS..r31 into consecutive words from D; `lmw rD,D(r1)` restores the same
526
676
  // range.
527
677
  const frameRange = (firstReg: string, off: number): Array<{ off: number; reg: string }> => {
@@ -533,13 +683,27 @@ export function lift(
533
683
  return slots;
534
684
  };
535
685
 
686
+ // What each filled block left in the condition register. Written as blocks are filled, so a lookup
687
+ // finds only blocks filled earlier, which is what `inheritFlags` needs to tell "not known" from
688
+ // "none".
689
+ const exitCr = new Map<number, CrExit>();
690
+ const blockLabel = (bi: number): string =>
691
+ blocks[bi].startAddr >= 0
692
+ ? `${name}+0x${blocks[bi].startAddr.toString(16)}`
693
+ : `the return block '${name}' synthesizes`;
694
+
695
+ // A FLOAT RETURN is decided for the whole function (frontend/fpu.ts `writesFloatReturn`), and then
696
+ // r3 is scratch like any other integer register.
697
+ const returnOperands = (bi: number): Value[] =>
698
+ floatReturn ? [readReg(fpu!.returnReg, bi)] : ssa.hasReachingDef(RET, bi) ? [readReg(RET, bi)] : [];
699
+
536
700
  const fillBlock = (b: PpcBlock, bi: number) => {
537
701
  const ops = irBlocks[bi].ops;
538
702
  const succ = (j: number): Successor => ({ block: irBlocks[j], args: [] });
539
703
 
540
704
  // A synthetic conditional-return block: just return the current return register.
541
705
  if (b.synthReturn) {
542
- const retOps = ssa.hasReachingDef(RET, bi) ? [readReg(RET, bi)] : [];
706
+ const retOps = returnOperands(bi);
543
707
  ops.push(mkOp('ret', { operands: retOps }));
544
708
  return;
545
709
  }
@@ -576,39 +740,75 @@ export function lift(
576
740
  );
577
741
  }
578
742
  };
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') {
743
+ // A word store/load based on r1: a SAVE or a VALUE slot (see `saveSlots` above). Returns false
744
+ // for any other base, which is ordinary memory.
745
+ const frameStore = (ins: Instr, srcReg: string, mem: string): boolean => {
746
+ if (parseMem(mem).base !== 'r1') {
586
747
  return false;
587
748
  }
588
- if (!ssa.hasReachingDef(srcReg, bi)) {
589
- savedSlots.set(off, srcReg);
749
+ const off = entryOffset(ins, mem);
750
+ const isArg = ARG_REGS.includes(srcReg);
751
+ // The LR SAVE stores the return address `mflr` copied out, and only into the link register's
752
+ // save word, 4 bytes above the entry r1. Anywhere else the word is a value — past the push,
753
+ // 8 bytes up is the callee's parameter area, where it is a ninth argument — and a register
754
+ // holding the return address on some paths and another value on others is not a save at all.
755
+ if (ssa.hasReachingDef(srcReg, bi, (v) => returnAddresses.has(v))) {
756
+ const mixed = ssa.hasReachingDef(srcReg, bi, (v) => !returnAddresses.has(v));
757
+ if (mixed || off !== LR_SAVE_WORD) {
758
+ throw new PpcUnsupportedError(
759
+ `cannot lift '${name}': the store to '${mem}' at 0x${ins.addr.toString(16)} is not the link ` +
760
+ `register's save — ${
761
+ mixed
762
+ ? `${srcReg} holds the return address an 'mflr' copies out on some paths and another value on others`
763
+ : `the return address an 'mflr' copies out lands ${off} bytes from the entry r1, not in the ` +
764
+ `save word ${LR_SAVE_WORD} bytes up, so it is used as a value`
765
+ }`,
766
+ );
767
+ }
768
+ refuseMixedSlot(ins, mem, off, 'save');
769
+ saveSlots.set(off, srcReg);
590
770
  return true;
591
771
  }
592
- throw new PpcUnsupportedError(
593
- `cannot lift '${name}': spill of a live value to the stack ('${srcReg},${mem}') — local stack frames not supported`,
594
- );
772
+ // Any other save stores what the register held at entry: nothing defined it on any path. An
773
+ // argument register nothing defined holds the argument, which is a value.
774
+ if (!isArg && !ssa.hasReachingDef(srcReg, bi)) {
775
+ refuseMixedSlot(ins, mem, off, 'save');
776
+ saveSlots.set(off, srcReg);
777
+ return true;
778
+ }
779
+ refuseMixedSlot(ins, mem, off, 'value');
780
+ // An argument moved to its home is not an argument set up for the next call.
781
+ const v = (isArg ? ssa.entryValue(srcReg, bi) : undefined) ?? readReg(srcReg, bi);
782
+ writeVar(stackSlotKey(off), bi, v);
783
+ if (!valueSlots.has(off)) {
784
+ valueSlots.set(off, { addr: ins.addr, mem, read: false });
785
+ }
786
+ return true;
595
787
  };
596
- const frameLoad = (dstReg: string, mem: string): boolean => {
597
- const { base, off } = parseMem(mem);
598
- if (base !== 'r1') {
788
+ const frameLoad = (ins: Instr, dstReg: string, mem: string): boolean => {
789
+ if (parseMem(mem).base !== 'r1') {
599
790
  return false;
600
791
  }
601
- const saved = savedSlots.get(off);
792
+ const off = entryOffset(ins, mem);
793
+ const saved = saveSlots.get(off);
602
794
  if (saved === dstReg) {
603
795
  return true;
604
796
  }
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
- );
797
+ if (saved !== undefined) {
798
+ throw new PpcUnsupportedError(
799
+ `cannot lift '${name}': reload of '${mem}' into ${dstReg}, a slot ${saved} was saved into — the ` +
800
+ `word holds the caller's ${saved}, which no C value names`,
801
+ );
802
+ }
803
+ const slot = valueSlots.get(off);
804
+ if (slot === undefined || !ssa.hasReachingDef(stackSlotKey(off), bi)) {
805
+ throw new PpcUnsupportedError(
806
+ `cannot lift '${name}': reload of a stack local ('${mem}') — local stack frames not supported`,
807
+ );
808
+ }
809
+ slot.read = true;
810
+ write(dstReg, readVar(stackSlotKey(off), bi));
811
+ return true;
612
812
  };
613
813
  const write = (r: string, v: Value) => {
614
814
  writeVar(r, bi, v);
@@ -626,6 +826,29 @@ export function lift(
626
826
  // must throw, never skip (its first token is the SOURCE register).
627
827
  const od = opaqueDest(ins.mnemonic, ins.ops, {
628
828
  isReg,
829
+ // The floating-point register file. `isReg` is `r\d+`, so an `fN` operand is invisible to
830
+ // it: `fadds f1,f1,f2` reads as "no register destination" and `fcmpo cr0,f1,f2` as the same
831
+ // thing about a CONDITION register, when both are one missing file. `stfs`/`stfd` reach
832
+ // this arm ahead of `^st` deliberately — they are refused for the file they move, not for
833
+ // the memory they move it to.
834
+ //
835
+ // NO SIGIL, WHICH IS THE OPPOSITE OF THE MIPS CHOICE, and it needs its own argument because
836
+ // PowerPC objdump prints an FPU register bare (`f1`) and has no sigil to require. The
837
+ // collision the MIPS predicate pays a sigil to avoid is live here in principle — objdump
838
+ // writes a branch target as bare lower-case hex, so `f0`/`f4`/`f8` are addresses AND
839
+ // matches. What makes it unreachable is not this pattern but the branch handling above: a
840
+ // `b*` mnemonic in `isModeledBranch` is decoded as a transfer or a call and never falls to
841
+ // `emitOpaqueDest`, and every other `b*` is refused by the whole-function control-transfer
842
+ // pre-pass before a block is even filled. Those two are exhaustive over the mnemonics that
843
+ // can carry an address, so no operand reaching this policy is one. `test/fp-refusal.test.ts`
844
+ // pins it, and moving the FPU check into a pre-pass ahead of that one turns it red.
845
+ fpReg: PPC_FP_REG,
846
+ // The FPSCR moves that name no `fN` operand at all: `mtfsfi 7,0` takes a field number,
847
+ // `mtfsb0`/`mtfsb1` a bit number, `mcrfs cr0,cr1` two condition registers. Without this
848
+ // they read as "no register destination to degrade", which is the message that hides a
849
+ // missing file behind the shape of the instruction. `mffs f0` and `mtfsf 0xff,f0` DO name
850
+ // one and are `fpReg`'s.
851
+ fpControl: /^(mtfsfi|mtfsb0|mtfsb1|mcrfs)$/i,
629
852
  storeClass: /^st/i,
630
853
  skipSafe: /^nop$/i,
631
854
  context: `${name} @0x${ins.addr.toString(16)}`,
@@ -743,7 +966,30 @@ export function lift(
743
966
  };
744
967
 
745
968
  // 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 }>();
969
+ //
970
+ // A block starts with what its predecessor left, under the edge rule both frontends share
971
+ // (`inheritFlags`, frontend/flags-edge.ts): one predecessor, already filled, not a jump-table
972
+ // dispatch. The edge out of a CONDITIONAL branch carries too, because a `bc` writes no CR field:
973
+ // mwcc's switch dispatch `cmpwi r0,1; beq- case1; bge- default` reads one compare from the
974
+ // `beq-` and then from the `bge-` on its fall-through, which begins a block of its own.
975
+ //
976
+ // The clobber rule is this frontend's own: a compare writes its field, a record-form op writes
977
+ // cr0, and a call destroys every volatile field (CR_VOLATILE). A field something else wrote holds
978
+ // the sentence saying so rather than an entry, so the branch that reads it can name the writer.
979
+ // Every other instruction that writes a CR field (`crxor`, `mtcrf`, `fcmpu`, …) names no
980
+ // register destination and is refused outright by the opaque policy, so it never reaches here.
981
+ const inherited = inheritFlags(bi, {
982
+ preds,
983
+ exit: exitCr,
984
+ label: blockLabel,
985
+ dispatches: (i) => {
986
+ const pbr = blocks[i].branch;
987
+ return pbr !== null && jts.has(pbr.addr);
988
+ },
989
+ refusesConditional: () => false,
990
+ });
991
+ const cmpDef = new Map<string, CrCompare | string>(typeof inherited === 'string' ? [] : inherited.fields);
992
+ const noCmpWhy = typeof inherited === 'string' ? inherited : inherited.none;
747
993
  // One operand-grammar normalizer for the four compare decodes: `cmpX rA,…` (cr0 implicit)
748
994
  // or `cmpX crN,rA,…` → the cr field, the lhs register token, and the rhs token (register or
749
995
  // immediate — the case reads/parses it).
@@ -771,45 +1017,102 @@ export function lift(
771
1017
  break;
772
1018
  // --- call + frame/link-register bookkeeping ---
773
1019
  // `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).
1020
+ // callee symbol comes from the relocation (ins.reloc), and the caller-saved set is recorded
1021
+ // as destroyed (`calls.lower`) — a read of one past here names bytes the callee overwrote,
1022
+ // and it has a reaching definition all the same.
776
1023
  case 'bl': {
777
1024
  relocTaken = ins.reloc?.type === 'R_PPC_REL24';
778
1025
  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]));
1026
+ calls.lower({
1027
+ callee: sym,
1028
+ ssa,
1029
+ bi,
1030
+ read,
1031
+ write,
1032
+ // a pending `@ha` half is not an argument
1033
+ highHalves,
1034
+ guess: {
1035
+ // a gap in the argument registers refuses, naming this address (`fallbackArgc`)
1036
+ at: ins.addr,
1037
+ // A GUESS THAT FILLS EVERY ARGUMENT REGISTER CANNOT SAY WHERE THE LIST ENDS. The ninth
1038
+ // argument travels in the callee's parameter area, 8 bytes above the pushed r1 (past the
1039
+ // back chain and the LR save word), and a value stored there that reaches the call is that
1040
+ // argument or a local the compiler put in the same words. Reading the word back, before or
1041
+ // after the call, does not decide it, so the end-of-lift check for a store nothing reloads
1042
+ // is not enough here. Without a prototype this refuses; a declared arity says which.
1043
+ //
1044
+ // A guess of seven is the same question for one shape. A 64-bit argument takes an aligned
1045
+ // register pair, so one that follows seven words skips r10 and fills the first two words
1046
+ // of the parameter area (mwcc 2.3.3 builds `g(1, 2, 3, 4, 5, 6, 7, x)` with `long long x`
1047
+ // that way). A single word there cannot be an argument after seven: the eighth word would
1048
+ // have gone in r10.
1049
+ refuse: (argc) => {
1050
+ if (argc < ARG_REGS.length - 1) {
1051
+ return;
1052
+ }
1053
+ const depth = r1At.get(ins);
1054
+ const reaching = (off: number) => valueSlots.has(off) && ssa.hasReachingDef(stackSlotKey(off), bi);
1055
+ const inParamArea = (off: number) => typeof depth !== 'number' || (off >= depth + 8 && off < 0);
1056
+ const word = [...valueSlots.keys()].find((off) => inParamArea(off) && reaching(off));
1057
+ const passed =
1058
+ argc === ARG_REGS.length || typeof depth !== 'number'
1059
+ ? word
1060
+ : reaching(depth + 8) && reaching(depth + 12)
1061
+ ? depth + 8
1062
+ : undefined;
1063
+ if (passed !== undefined) {
1064
+ const slot = valueSlots.get(passed)!;
1065
+ throw new PpcUnsupportedError(
1066
+ `cannot lift '${name}': outgoing stack arguments not modelled — the undeclared call to '${sym}' at ` +
1067
+ `0x${ins.addr.toString(16)} fills ${argc} argument registers, and the value ` +
1068
+ `stored to '${slot.mem}' at 0x${slot.addr.toString(16)} reaches it in the callee's ` +
1069
+ `parameter area, where an argument past the registers travels`,
1070
+ );
1071
+ }
1072
+ },
1073
+ },
1074
+ });
1075
+ for (const cr of CR_VOLATILE) {
1076
+ cmpDef.set(
1077
+ cr,
1078
+ `the call at 0x${ins.addr.toString(16)} destroyed it (the EABI does not preserve ${cr} across a call)`,
1079
+ );
784
1080
  }
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 });
793
- }
794
- write(RET, res);
795
- ssa.noteCall(bi);
796
1081
  break;
797
1082
  }
798
1083
  // 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.
1084
+ // pointer; `mflr` copies the return address out (`returnAddresses`) and `mtlr` puts it back.
1085
+ // A word store or load on r1 is a save or a value slot (`frameStore`/`frameLoad`); `stmw`
1086
+ // records save slots.
803
1087
  // The GENERAL form `stwu rS,D(rA)` (base ≠ r1) is a real store-with-BASE-UPDATE
804
1088
  // (`*(rA+D)=rS; rA+=D`) — neither effect is modelled here, so loud-fail rather than drop both.
1089
+ // The push itself must be the only one, taken from the entry r1: every slot is named by its
1090
+ // offset from there (`r1Displacements`), and a push from anywhere else moves that origin.
805
1091
  case 'stwu':
806
1092
  if (parseMem(s).base === 'r1') {
1093
+ if (d !== 'r1' || r1At.get(ins) !== 0) {
1094
+ throw new PpcUnsupportedError(
1095
+ `cannot lift '${name}': '${ins.mnemonic} ${ins.ops.join(',')}' at 0x${ins.addr.toString(16)} ` +
1096
+ `is not the one frame push from the entry stack pointer — ` +
1097
+ `${d !== 'r1' ? `it stores ${d}, not the back chain` : `r1 had already moved (${r1At.get(ins)})`}`,
1098
+ );
1099
+ }
1100
+ // The push also STORES: the entry r1, at its new top, which is the entry offset of `s`
1101
+ // because r1 has not moved yet. A value stored there before the push is overwritten, and
1102
+ // a load from it afterwards reads the caller's r1.
1103
+ refuseMixedSlot(ins, s, parseMem(s).off, 'save');
1104
+ saveSlots.set(parseMem(s).off, 'r1');
807
1105
  break;
808
1106
  }
809
1107
  throw new PpcUnsupportedError(
810
1108
  `cannot lift '${name}': stwu with update on ${parseMem(s).base} (store-with-base-update) not modelled`,
811
1109
  );
812
- case 'mflr':
1110
+ case 'mflr': {
1111
+ const lr = mkValue(T.unk(32));
1112
+ returnAddresses.set(lr, ins.addr);
1113
+ write(d, lr);
1114
+ break;
1115
+ }
813
1116
  case 'mtlr':
814
1117
  break;
815
1118
  // `mtctr rS` initialises the CTR loop counter — track it as the `ctr` pseudo-register so
@@ -821,21 +1124,20 @@ export function lift(
821
1124
  break;
822
1125
  case 'stmw':
823
1126
  if (parseMem(s).base === 'r1') {
824
- for (const slot of frameRange(d, parseMem(s).off)) {
825
- savedSlots.set(slot.off, slot.reg);
1127
+ for (const slot of frameRange(d, entryOffset(ins, s))) {
1128
+ refuseMixedSlot(ins, s, slot.off, 'save');
1129
+ saveSlots.set(slot.off, slot.reg);
826
1130
  }
827
1131
  break;
828
1132
  }
829
1133
  assertOrdinaryMem(s);
830
1134
  emitOpaqueDest(ins);
831
1135
  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.
1136
+ // The restore half: `frameLoad` judges each word it reads, exactly as for a single `lwz`.
835
1137
  case 'lmw':
836
1138
  if (parseMem(s).base === 'r1') {
837
1139
  for (const slot of frameRange(d, parseMem(s).off)) {
838
- frameLoad(slot.reg, `${slot.off}(r1)`);
1140
+ frameLoad(ins, slot.reg, `${slot.off}(r1)`);
839
1141
  }
840
1142
  break;
841
1143
  }
@@ -924,6 +1226,20 @@ export function lift(
924
1226
  }
925
1227
  break;
926
1228
  }
1229
+ // An rA field of 0 is the literal 0, not r0: `addi rD,0,SIMM` is `li`. A disassembler that
1230
+ // prints it as `addi rD,r0,SIMM` still means the constant.
1231
+ if (mnem === 'addi' && s === 'r0') {
1232
+ write(d, constVal(parseImm(t)));
1233
+ break;
1234
+ }
1235
+ // `addi rD,rS,0` is a register move: rS + 0 is rS, as Thumb's `add rD,rS,#0` is
1236
+ // (frontend/thumb.ts). mwcc 2.3.3 spells its moves this way. Lifted as an add, the `+ 0`
1237
+ // makes a pointer an integer sum, and C++ will not pass `(u32)&table + 0` as a
1238
+ // `const char *`. `addic` sets the carry and is not a move.
1239
+ if (mnem === 'addi' && parseImm(t) === 0) {
1240
+ write(d, read(s));
1241
+ break;
1242
+ }
927
1243
  emitBin('add', d, read(s), constVal(parseImm(t)));
928
1244
  break;
929
1245
  // add immediate SHIFTED — the register-based `%ha` anchor: mwcc derives an absolute base
@@ -932,6 +1248,10 @@ export function lift(
932
1248
  // arithmetic, and a reloc-carrying one never gets here — the choke point in `decode` takes
933
1249
  // it.
934
1250
  case 'addis':
1251
+ if (s === 'r0') {
1252
+ write(d, constVal((parseImm(t) << 16) >> 0)); // `addis rD,0,SIMM` is `lis`, as for `addi`
1253
+ break;
1254
+ }
935
1255
  emitBin('add', d, read(s), constVal((parseImm(t) << 16) >> 0));
936
1256
  break;
937
1257
  case 'subf':
@@ -1084,10 +1404,10 @@ export function lift(
1084
1404
  emitOpaqueDest(ins);
1085
1405
  break;
1086
1406
  }
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.
1407
+ // Word load/store: an r1-relative word is a frame slot (frameStore/frameLoad); otherwise it is
1408
+ // ordinary memory.
1089
1409
  case 'lwz':
1090
- if (frameLoad(d, s)) {
1410
+ if (frameLoad(ins, d, s)) {
1091
1411
  break;
1092
1412
  }
1093
1413
  emitLoad(ins, d, s, 4, true);
@@ -1102,7 +1422,7 @@ export function lift(
1102
1422
  emitLoad(ins, d, s, 1, false);
1103
1423
  break;
1104
1424
  case 'stw':
1105
- if (frameStore(d, s)) {
1425
+ if (frameStore(ins, d, s)) {
1106
1426
  break;
1107
1427
  }
1108
1428
  emitStore(ins, d, s, 4);
@@ -1162,6 +1482,41 @@ export function lift(
1162
1482
  recordCmp(c.cr, read(c.lhsTok), rhs, false);
1163
1483
  break;
1164
1484
  }
1485
+ case 'fadds':
1486
+ case 'fsubs':
1487
+ case 'fmuls':
1488
+ case 'fdivs':
1489
+ case 'fadd':
1490
+ case 'fsub':
1491
+ case 'fmul':
1492
+ case 'fdiv':
1493
+ case 'fneg':
1494
+ case 'fmr': {
1495
+ const { op } = FP_ARITH[mnem];
1496
+ if (rc || fpu === undefined || ins.ops.length === 0 || !ins.ops.every(isFpKey)) {
1497
+ emitOpaqueDest(ins);
1498
+ break;
1499
+ }
1500
+ if (callInsn) {
1501
+ throw new PpcUnsupportedError(
1502
+ `cannot lift '${name}': '${ins.mnemonic}' computes on a float and '${callInsn.mnemonic}' at ` +
1503
+ `0x${callInsn.addr.toString(16)} makes a call — the floating-point registers a call reads, returns ` +
1504
+ `in and destroys are not modelled`,
1505
+ );
1506
+ }
1507
+ if (fpType === null) {
1508
+ refuseBothPrecisions(name, ins.mnemonic);
1509
+ }
1510
+ const [dst, ...srcs] = ins.ops;
1511
+ if (op === 'copy') {
1512
+ writeVar(dst, bi, read(srcs[0]));
1513
+ break;
1514
+ }
1515
+ const v = mkValue(fpType);
1516
+ ops.push(mkOp(op, { operands: srcs.map(read), results: [v] }));
1517
+ writeVar(dst, bi, v);
1518
+ break;
1519
+ }
1165
1520
  default:
1166
1521
  emitOpaqueDest(ins);
1167
1522
  break; // unmodelled: an honest opaque, never a silent drop
@@ -1170,6 +1525,11 @@ export function lift(
1170
1525
  // next branch reading cr0 fuses (`andi. r0,r3,1; beq …` → `if ((a0 & 1) == 0)`).
1171
1526
  if (rc && lastDef) {
1172
1527
  cmpDef.set('cr0', { lhs: lastDef, rhs: constVal(0), signed: true });
1528
+ } else if (rc) {
1529
+ cmpDef.set(
1530
+ 'cr0',
1531
+ `'${ins.mnemonic}' at 0x${ins.addr.toString(16)} wrote it from no value this frontend models`,
1532
+ );
1173
1533
  }
1174
1534
  // THE CHOKE POINT. Everything above either took the relocation or threw; a relocation still
1175
1535
  // sitting here was dropped, and a dropped relocation is the printed placeholder standing in
@@ -1182,6 +1542,11 @@ export function lift(
1182
1542
  for (const ins of b.body) {
1183
1543
  decode(ins);
1184
1544
  }
1545
+ // No terminator writes a CR field (a `bc` reads one, `bdnz` writes CTR), so the state after the
1546
+ // body is the state at the block's exit. Recorded for EVERY block, including those whose edges
1547
+ // `inheritFlags` will refuse: the edge rule lives in one place, and a second copy of it here
1548
+ // could disagree with the first.
1549
+ exitCr.set(bi, { fields: cmpDef, none: noCmpWhy });
1185
1550
 
1186
1551
  const br = b.branch;
1187
1552
  // Recovered dense switch: the bounds block dispatches a `switch_br` over the scrutinee — N case
@@ -1248,11 +1613,14 @@ export function lift(
1248
1613
  // The branch names its cr field as the first operand when not cr0.
1249
1614
  const crName = br.ops[0]?.startsWith('cr') ? br.ops[0] : 'cr0';
1250
1615
  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.
1616
+ if (typeof cmp === 'string') {
1617
+ throw new PpcUnsupportedError(
1618
+ `cannot lift '${name}': conditional branch '${base}' tests ${crName}, but ${cmp}`,
1619
+ );
1620
+ }
1253
1621
  if (!cmp) {
1254
1622
  throw new PpcUnsupportedError(
1255
- `cannot lift '${name}': conditional branch '${base}' has no reaching compare (${crName}) in its block`,
1623
+ `cannot lift '${name}': conditional branch '${base}' has no reaching compare (${crName}): ${noCmpWhy}`,
1256
1624
  );
1257
1625
  }
1258
1626
  const table = !cmp.signed ? COND_UNSIGNED : COND_SIGNED;
@@ -1265,8 +1633,7 @@ export function lift(
1265
1633
  ops.push(mkOp('br', { attrs: { fallthrough: true }, successors: [succ(succIdx[bi][0])] }));
1266
1634
  return;
1267
1635
  }
1268
- const retOps = ssa.hasReachingDef(RET, bi) ? [readReg(RET, bi)] : [];
1269
- ops.push(mkOp('ret', { operands: retOps }));
1636
+ ops.push(mkOp('ret', { operands: returnOperands(bi) }));
1270
1637
  return;
1271
1638
  }
1272
1639
  // unconditional branch
@@ -1281,14 +1648,38 @@ export function lift(
1281
1648
  // float load's displacement) lands here, which is what keeps "not modelled" from becoming "not
1282
1649
  // emitted".
1283
1650
  highHalves.assertAllConsumed(name);
1651
+ // A value stored to the frame that nothing reloads is either a home the body never reads or an
1652
+ // OUTGOING stack argument (`stw r4,8(r1)` before a nine-argument `bl` passes the ninth in the
1653
+ // callee's parameter area, as ac-decomp's `aQMgr_order_decide_trade_*` do). The two are the same
1654
+ // store, and dropping the second loses an argument.
1655
+ for (const { addr, mem, read } of valueSlots.values()) {
1656
+ if (!read) {
1657
+ throw new PpcUnsupportedError(
1658
+ `cannot lift '${name}': local stack frames not supported — '${mem}' is stored at 0x${addr.toString(16)} ` +
1659
+ `and never read back, and an unread home and a call's argument passed on the stack are the same store`,
1660
+ );
1661
+ }
1662
+ }
1663
+ const argSlots = fpuArgSlots(name, fpu, ARG_REGS, isFpKey, fpType);
1664
+ mintArgSlotHoles(ssa, preds[0].length > 0, argSlots);
1284
1665
  ssa.finish();
1285
1666
  highHalves.assertNoneEscaped(name, irBlocks);
1667
+ // Checked on the finished function, after `finish` pruned the joins nothing reads, because a
1668
+ // return address reaches a use through a join's incoming arguments as well as directly.
1669
+ for (const b of irBlocks) {
1670
+ for (const op of b.ops) {
1671
+ const lr = [...op.operands, ...op.successors.flatMap((s) => s.args)].find((v) => returnAddresses.has(v));
1672
+ if (lr !== undefined) {
1673
+ throw new PpcUnsupportedError(
1674
+ `cannot lift '${name}': the return address the 'mflr' at 0x${returnAddresses.get(lr)!.toString(16)} ` +
1675
+ `copies out is used as a value — a function reading its own return address has no C spelling`,
1676
+ );
1677
+ }
1678
+ }
1679
+ }
1286
1680
 
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) ?? ''));
1681
+ abiSortEntryParams(irBlocks[0], preds[0].length > 0, paramReg, argSlots);
1682
+ ssa.fn.localObjects = statics.finish();
1292
1683
  return ssa.fn;
1293
1684
  }
1294
1685