@asmlift/core 0.8.1 → 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 +267 -79
  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
@@ -7,9 +7,9 @@
7
7
  // EXCEPT that a conditional branch reads its comparison operands as of the branch, so those
8
8
  // SSA values are captured BEFORE the delay slot runs, then the delay slot executes, then the
9
9
  // `cond_br` is emitted. A `jr ra` return's delay slot (which computes the return value) runs
10
- // first, then the value is read. Branch-likely ops (`beql`/`bnel`…, which annul the delay slot
11
- // when not taken) and calls (`jal`) are out of scope — both loud-fail (the control-transfer
12
- // pre-scan in `lift`).
10
+ // first, then the value is read. A call's (`jal`) delay slot runs before the call, so it can set
11
+ // an argument up. A branch-likely (`beql`/`bnel`…) annuls its slot when not taken, so the slot is
12
+ // a block of its own on the taken edge (`toBlocks`).
13
13
  //
14
14
  // Comparison model: MIPS fuses compare-and-branch, so a branch lowers directly to an icmp
15
15
  // (the branch mnemonics are signed/equality only). `bltz/bgez/blez/bgtz` compare against zero; `beq/bne`
@@ -24,6 +24,7 @@ import { T } from '../ir/types';
24
24
  import type { Prototypes } from '../proto';
25
25
  import type { TargetDescription } from '../target';
26
26
  import { type AsmData, readJumpTable, textRelocAt } from './asmdata';
27
+ import { callDeclarations } from './call-plan';
27
28
  import {
28
29
  type DisasmInstr,
29
30
  parseImm,
@@ -33,13 +34,14 @@ import {
33
34
  symbolStart,
34
35
  } from './disasm';
35
36
  import { mkEmitKit, pushSwitchBr } from './emit';
36
- import { FrontendUnsupportedError } from './errors';
37
+ import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
37
38
  import { assertInputFormat } from './format';
39
+ import { fpPrecision, fpuArgSlots, refuseBothPrecisions, writesFloatReturn } from './fpu';
38
40
  import type { Frontend } from './frontend';
39
41
  import { makeHighHalves } from './high-half';
40
42
  import { opaqueDest } from './opaque';
41
- import { isSplatMips, parseSplatMips } from './splat';
42
- import { abiSortEntryParams, stackSlotKey } from './ssa';
43
+ import { MIPS_FP_REG, isSplatMips, mipsEvenFpKey, parseSplatMips } from './splat';
44
+ import { abiSortEntryParams, mintArgSlotHoles, stackSlotKey } from './ssa';
43
45
  import { makeSsaBuilder } from './ssa';
44
46
 
45
47
  type Instr = DisasmInstr;
@@ -73,10 +75,39 @@ const LIKELY_BASE: Record<string, string> = {
73
75
  // code nor the compare is modelled, so these refuse whether or not they are also likely.
74
76
  const isFpCondBranch = (m: string) => m.startsWith('bc1');
75
77
 
78
+ /** The FPU arithmetic this frontend lifts, onto the float opcodes (`mov.s`/`mov.d` are copies and
79
+ * have none), with the precision each computes in: `.s` single, `.d` double, one per function
80
+ * (`frontend/fpu.ts` `fpPrecision`). Every other FPU instruction — the conversions, the moves to and
81
+ * from the integer file and memory, the compares — keeps the register-file refusal `opaqueDest`
82
+ * gives it, and so does one of these naming an odd register (a double's second half, or a single
83
+ * this frontend does not key) or a target with no `fpu` homes. `docs/floating-point.md` says what
84
+ * the next layer is. */
85
+ const FP_ARITH: Readonly<Record<string, { op: Opcode | 'copy'; width: 32 | 64 }>> = {
86
+ 'add.s': { op: 'fadd', width: 32 },
87
+ 'sub.s': { op: 'fsub', width: 32 },
88
+ 'mul.s': { op: 'fmul', width: 32 },
89
+ 'div.s': { op: 'fdiv', width: 32 },
90
+ 'neg.s': { op: 'fneg', width: 32 },
91
+ 'mov.s': { op: 'copy', width: 32 },
92
+ 'add.d': { op: 'fadd', width: 64 },
93
+ 'sub.d': { op: 'fsub', width: 64 },
94
+ 'mul.d': { op: 'fmul', width: 64 },
95
+ 'div.d': { op: 'fdiv', width: 64 },
96
+ 'neg.d': { op: 'fneg', width: 64 },
97
+ 'mov.d': { op: 'copy', width: 64 },
98
+ };
99
+ const FP_ARITH_MNEMONICS: ReadonlySet<string> = new Set(Object.keys(FP_ARITH));
100
+ /** A key the SSA builder holds for the FPU's file: `mipsEvenFpKey`'s canonical spelling. */
101
+ const isFpKey = (k: string) => /^\$f\d+$/.test(k);
102
+
76
103
  const isZero = (r: string) => r === 'zero' || r === '$0';
77
104
  // The stack pointer (`$29`). A `sw/lw` through it is not a store/load through a data pointer — it
78
105
  // is a spill/reload of a stack SLOT (an argument home slot or a local). See emitStore/emitLoad.
79
106
  const isStackPtr = (r: string) => r === 'sp' || r === '$sp' || r === '$29';
107
+ // Where O32 puts a call's argument k ≥ 4: the caller's outgoing area keeps `[0,16)` as the home of
108
+ // a0..a3, and argument k is the word at `16 + 4(k − 4)` above the `sp` the call is made at.
109
+ // Compiled on ido7.1, gcc2.7.2kmc and gcc2.7.2: `g(a, b, 1, 2, 3)` stores the 3 to `16(sp)`.
110
+ const STACK_ARGS_AT = 16;
80
111
  // SSA-variable name for the stack slot at a constant `sp`-offset. Distinct namespace from the
81
112
  // register names (which are alphabetic / `$N`), so it never collides with a real register var.
82
113
  const stackSlot = stackSlotKey; // shared spelling: frontend/ssa.ts
@@ -135,6 +166,10 @@ const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { zer
135
166
  // A recovered JUMP TABLE's dispatch is untouched: Regime B reads the table through `asmdata.ts` and
136
167
  // prunes the dispatch block, so its `lui %hi(.rodata)` never reaches `decode`.
137
168
  //
169
+ // `R_MIPS_26` is a `jal`'s callee, and a `j`'s target where the jump stays in this section. objdump
170
+ // prints the field, which in a relocatable object is the addend and not the address, so the callee
171
+ // is read off this record alone.
172
+ //
138
173
  // `R_MIPS_GOT16`/`R_MIPS_CALL16`/`R_MIPS_GPREL16` are PIC/small-data access, already refused by the
139
174
  // `gp`-as-data guard; widening this carrier to them would trade those messages for worse ones
140
175
  // without recovering an address.
@@ -145,7 +180,7 @@ const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { zer
145
180
  function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData): void {
146
181
  const byAddr = new Map(instrs.map((ins) => [ins.addr, ins]));
147
182
  for (const r of ad.relocs) {
148
- if (r.section !== '.text' || (r.type !== 'R_MIPS_HI16' && r.type !== 'R_MIPS_LO16')) {
183
+ if (r.section !== '.text' || (r.type !== 'R_MIPS_HI16' && r.type !== 'R_MIPS_LO16' && r.type !== 'R_MIPS_26')) {
149
184
  continue;
150
185
  }
151
186
  const ins = byAddr.get(r.offset);
@@ -582,13 +617,13 @@ function toBlocks(
582
617
  return { blocks: blocks.filter((b) => reachable.has(b)), succAddrs };
583
618
  }
584
619
 
585
- /** Lift disassembled MIPS text → an L1 Fn with block-argument SSA. `prototypes` reserved for
586
- * call arity (calls are a later milestone). */
620
+ /** Lift disassembled MIPS text → an L1 Fn with block-argument SSA. `prototypes` declares each
621
+ * callee's arity (`frontend/call-plan.ts`). */
587
622
  export function lift(
588
623
  name: string,
589
624
  asm: string,
590
625
  target: TargetDescription,
591
- _prototypes: Prototypes = {},
626
+ prototypes: Prototypes = {},
592
627
  asmData?: AsmData,
593
628
  ): Fn {
594
629
  // Two input dialects reach this frontend: `objdump -d` text (IDO/KMC — no compiler-emitted asm),
@@ -655,16 +690,24 @@ export function lift(
655
690
  attachMipsRelocs(name, instrs, asmData);
656
691
  }
657
692
  // TRUSTWORTHINESS: fail LOUD on a control transfer this frontend cannot model — the `opaque`
658
- // path cannot catch these (implicit or no register destination). `jal`/`jalr` clobber `v0`
659
- // implicitly, so dropping a call fabricates `v0` from a stale value; a `jr` to anything but `ra`
660
- // (jump table / computed goto) is not a plain return. Calls are a later milestone; until then
661
- // they are a catchable "out of scope" signal, mirroring the PPC frontend.
693
+ // path cannot catch these (implicit or no register destination). A `jalr` clobbers `v0`
694
+ // implicitly, so dropping it fabricates `v0` from a stale value, and the callee it reaches is a
695
+ // register's value; a `jr` to anything but `ra` (jump table / computed goto) is not a plain
696
+ // return. `jal` is lowered where its block is filled (`lowerJal`).
662
697
  for (const ins of instrs) {
663
- if (ins.mnemonic === 'jal' || ins.mnemonic === 'jalr') {
698
+ if (ins.mnemonic === 'jalr') {
664
699
  throw new FrontendUnsupportedError(
665
700
  `cannot lift '${name}': function call '${ins.mnemonic}' at 0x${ins.addr.toString(16)} — MIPS calls not yet modelled`,
666
701
  );
667
702
  }
703
+ // A `j` relocated against a symbol leaves this section's code for another function's, which is
704
+ // a tail call; the target objdump prints is the field, not the address.
705
+ if (ins.mnemonic === 'j' && ins.reloc !== undefined && !ins.reloc.sym.startsWith('.')) {
706
+ throw new FrontendUnsupportedError(
707
+ `cannot lift '${name}': 'j' at 0x${ins.addr.toString(16)} jumps to '${ins.reloc.sym}' — a tail call into ` +
708
+ 'another function is not modelled',
709
+ );
710
+ }
668
711
  if (ins.mnemonic === 'jr' && ins.ops[0] !== 'ra' && !recoveredJr.has(ins.addr)) {
669
712
  throw new FrontendUnsupportedError(
670
713
  `cannot lift '${name}': indirect jump 'jr ${ins.ops[0] ?? ''}' at 0x${ins.addr.toString(16)} — jump tables / tail calls not supported`,
@@ -682,6 +725,20 @@ export function lift(
682
725
  }
683
726
  checkDelaySlots(name, instrs);
684
727
  const { blocks, succAddrs } = toBlocks(instrs, jts, likelyAddrs);
728
+ // PREHEADER, as in frontend/ppc.ts: an entry that is a branch target gets an empty block ahead of
729
+ // it, whose live-ins are the function's parameters. One word below the function, so it falls
730
+ // through to it.
731
+ if (blocks.some((b) => succAddrs.get(b)!.includes(blocks[0].startAddr))) {
732
+ const ph: MipsBlock = {
733
+ startAddr: blocks[0].startAddr - 4,
734
+ body: [],
735
+ branch: null,
736
+ delay: null,
737
+ delayAnnulled: false,
738
+ };
739
+ succAddrs.set(ph, [blocks[0].startAddr]);
740
+ blocks.unshift(ph);
741
+ }
685
742
  const idxOf = new Map(blocks.map((b, i) => [b.startAddr, i]));
686
743
 
687
744
  // CFG predecessors by block index.
@@ -706,24 +763,48 @@ export function lift(
706
763
  // `[0,16)` as the caller-owned home area (in NEITHER range — caller-owned, but not an argument)
707
764
  // with stack arguments from 16 up, which is what `mips32be.cspec`'s `<localrange>` and stack
708
765
  // `<pentry offset="16">` encode.
709
- const ssa = makeSsaBuilder(name, blocks.length, preds);
766
+ const fpType = fpPrecision(instrs, (m) => FP_ARITH[m]?.width);
767
+ const ssa = makeSsaBuilder(name, blocks.length, preds, undefined, (k) =>
768
+ isFpKey(k) ? (fpType ?? T.f32()) : undefined,
769
+ );
710
770
  const { irBlocks, readVar, writeVar, paramReg } = ssa;
711
771
  const RET = target.returnReg;
712
772
  const ARG_REGS = target.argRegs;
773
+ const fpu = target.fpu;
774
+ const floatReturn = writesFloatReturn(
775
+ blocks.flatMap((b) => (b.delay ? [...b.body, b.delay] : b.body)),
776
+ FP_ARITH_MNEMONICS,
777
+ mipsEvenFpKey,
778
+ fpu,
779
+ );
713
780
 
714
781
  // The pending `%hi` halves of this function's global addresses, keyed by the SSA VALUE each `lui`
715
782
  // defines (frontend/high-half.ts holds the invariant and why a register-keyed map cannot answer
716
783
  // the question; frontend/ppc.ts folds the same one). FUNCTION-scoped, because a value is: a pair
717
784
  // split across blocks folds when SSA says the half reaches, and refuses when what arrives is the
718
785
  // block parameter standing for a merge.
719
- const highHalves = makeHighHalves({
720
- hi: '%hi',
721
- hiArticle: 'a',
722
- lo: '%lo',
723
- fail: (message) => {
724
- throw new FrontendUnsupportedError(message);
725
- },
726
- });
786
+ /** This frontend's refusal, for the shared code that refuses on its behalf. */
787
+ const fail: FrontendRefusal = (message) => {
788
+ throw new FrontendUnsupportedError(message);
789
+ };
790
+ const highHalves = makeHighHalves({ hi: '%hi', hiArticle: 'a', lo: '%lo', fail });
791
+ // O32 passes the words past a0..a3 on the stack and returns a word in v0. This lowering builds no
792
+ // register pair and receives no struct through a hidden pointer, so the plan refuses both. A call
793
+ // through an argument register bounds no arity here: a leading float leaves a0 free.
794
+ const calls = callDeclarations(
795
+ name,
796
+ target,
797
+ prototypes,
798
+ { pairs: false, memoryReturn: false, stackArgs: true, voidReturn: true, argRegisterBoundsArity: false },
799
+ fail,
800
+ );
801
+ // The first instruction naming the FPU's file, a data register or the control register. A call
802
+ // in a function that has one refuses: which floating-point registers a callee reads, returns in
803
+ // and destroys is not modelled. A float that crosses the call untouched in $f12 or $f0 names no
804
+ // register here, so only the callee's declaration shows it (`plan`).
805
+ const fpuInstr = instrs.find(
806
+ (ins) => /^(cfc1|ctc1)$/i.test(ins.mnemonic) || ins.ops.some((o) => MIPS_FP_REG.test(o)),
807
+ );
727
808
 
728
809
  // SOUNDNESS GUARD. The word stack-slot model (emitLoad/emitStore) is safe ONLY when every
729
810
  // sp-relative access in the function is word-width. If a SUB-WORD sp access aliases a word slot
@@ -737,6 +818,42 @@ export function lift(
737
818
  SUBWORD_MEM.has(ins.mnemonic) && ins.ops.length > 0 && isStackPtr(parseMem(ins.ops[ins.ops.length - 1]).base),
738
819
  );
739
820
 
821
+ // THE FRAME A CALL'S STACK ARGUMENTS ARE STAGED IN: the bytes one `addiu sp,sp,-N` pushed. Its
822
+ // outgoing area is `[16, N)`; a word at or above N is this function's own incoming home area, which
823
+ // IDO stores a0..a3 to, so it is never an argument of a call made here. Undefined where the
824
+ // function pushes no frame or more than one.
825
+ const pushes = instrs.filter(
826
+ (ins) =>
827
+ (ins.mnemonic === 'addiu' || ins.mnemonic === 'addi') &&
828
+ isStackPtr(ins.ops[0]) &&
829
+ isStackPtr(ins.ops[1] ?? '') &&
830
+ parseImm(ins.ops[2] ?? '') < 0,
831
+ );
832
+ const frameSize = pushes.length === 1 ? -parseImm(pushes[0].ops[2]) : undefined;
833
+
834
+ // THE FRAME'S SAVES. A word store of `ra`, or of a register O32 does not let a callee destroy,
835
+ // made while the register still holds what the caller left in it, is a SAVE: the word holds the
836
+ // caller's value, which no C value names, so the store mints no parameter. The reload into the
837
+ // same register is the restore, and what it writes refuses any use, as a `%hi` half does: a read
838
+ // at once, a use reached through a merge once the function is built. A reload into another
839
+ // register refuses. Every other word store is a VALUE slot, and a slot is one kind for the whole
840
+ // function. Keyed by the raw `sp` offset, as every slot here is, so a prologue save matches an
841
+ // epilogue restore in any block.
842
+ const saveSlots = new Map<number, string>();
843
+ const valueSlots = new Set<number>();
844
+ const restores = new Map<Value, { reg: string; addr: number }>();
845
+ // A `$`-spelt operand is an FPU register or a bare number, and the target's list names neither.
846
+ const savedByCallee = (r: string) =>
847
+ r === 'ra' || (!r.startsWith('$') && !target.callerSaved.includes(r) && !['zero', 'sp', 'gp'].includes(r));
848
+ const refuseMixedSlot = (ins: Instr, mem: string, off: number, kind: 'save' | 'value') => {
849
+ if (kind === 'save' ? valueSlots.has(off) : saveSlots.has(off)) {
850
+ throw new FrontendUnsupportedError(
851
+ `cannot lift '${name}': local stack frames not supported — '${mem}' at 0x${ins.addr.toString(16)} ` +
852
+ `is a word this function both saves a register in and stores a value to`,
853
+ );
854
+ }
855
+ };
856
+
740
857
  // Hardware divide state (capabilities.hwDivide). MIPS `div`/`divu rs,rt` set the hi/lo pair
741
858
  // implicitly; a later `mflo`/`mfhi` reads the quotient/remainder. FUNCTION-scoped: GCC schedules
742
859
  // the `mflo` into a SEPARATE block after the trap-check branch (`bnez rt; break 7`), so a
@@ -780,7 +897,15 @@ export function lift(
780
897
  }
781
898
  // A register holding the high half of an address is NOT a value. The legitimate consumers
782
899
  // (`addiu %lo`, a `%lo` load/store base) reach the half through `foldLoHalf` instead.
783
- return highHalves.guardRead(name, r, readVar(r, bi));
900
+ const v = readVar(r, bi);
901
+ const restore = restores.get(v);
902
+ if (restore) {
903
+ throw new FrontendUnsupportedError(
904
+ `cannot lift '${name}': ${r} is read after the 'lw' at 0x${restore.addr.toString(16)} restores the ` +
905
+ `caller's ${restore.reg} into it — no C value names it`,
906
+ );
907
+ }
908
+ return highHalves.guardRead(name, r, v);
784
909
  };
785
910
  // `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
786
911
  //
@@ -1076,6 +1201,24 @@ export function lift(
1076
1201
  case 'sb':
1077
1202
  emitStore(ins, d, s, 1);
1078
1203
  break;
1204
+ // A call reaches here only from a delay slot (the fill loop lowers every other one), and a
1205
+ // transfer in a delay slot is one the ISA leaves undefined.
1206
+ case 'jal':
1207
+ throw new FrontendUnsupportedError(`${site(ins)} sits in the delay slot of a transfer`);
1208
+ case 'add.s':
1209
+ case 'sub.s':
1210
+ case 'mul.s':
1211
+ case 'div.s':
1212
+ case 'neg.s':
1213
+ case 'mov.s':
1214
+ case 'add.d':
1215
+ case 'sub.d':
1216
+ case 'mul.d':
1217
+ case 'div.d':
1218
+ case 'neg.d':
1219
+ case 'mov.d':
1220
+ emitFpArith(ins);
1221
+ break;
1079
1222
  default:
1080
1223
  emitOpaqueDest(ins);
1081
1224
  break; // unmodelled: an honest opaque, never a silent drop
@@ -1108,9 +1251,16 @@ export function lift(
1108
1251
  if (ins.reloc) {
1109
1252
  relocPlaceholder(ins);
1110
1253
  }
1111
- // storeClass: unmodelled MIPS stores — incl. the unaligned pair swl/swr and the FPU stores,
1112
- // whose FIRST token is a register (a SOURCE, not a dest) that would otherwise fabricate an
1113
- // opaque write to it while dropping the real memory write.
1254
+ // storeClass: unmodelled MIPS stores, whose FIRST token is a register (a SOURCE, not a dest)
1255
+ // that would otherwise fabricate an opaque write to it while dropping the real memory write.
1256
+ // `swc1`/`sdc1` are in the list and are NOT reached by any spelling `fpReg` covers — an FPU
1257
+ // store's `rt` is an FP register by ISA, so the arm above claims it first — but that is a
1258
+ // claim about the PREDICATE, not about the ISA, and it is false for every register spelling
1259
+ // the predicate misses: the store then arrives here and is named by the memory it writes
1260
+ // instead of by the file it moves. So they are kept as a live backstop, and
1261
+ // `test/fp-refusal.test.ts` asserts the FP arm wins on each spelling rather than assuming it.
1262
+ // Everything else here is genuinely reachable: `swl`/`swr` are the unaligned pair, `sc`/`sd*`
1263
+ // the rest.
1114
1264
  // skipSafe `break`: the compiler-emitted divide-by-zero guard trap inside the hw-divide
1115
1265
  // idiom (KMC GCC `break 0x7`); recompiling the recovered `/` regenerates it, so it is
1116
1266
  // transparent by the same modelling as the divide itself (byte-exactness proven by the
@@ -1118,6 +1268,22 @@ export function lift(
1118
1268
  const od = opaqueDest(ins.mnemonic, ins.ops, {
1119
1269
  isReg: isMipsReg,
1120
1270
  isZero,
1271
+ // The coprocessor-1 register file. `isMipsReg` rejects `$f12` — the `$`-prefixed forms it
1272
+ // takes are the numeric GPRs (`$4`) — so without this arm an `add.s` is refused for the
1273
+ // lesser reason, "no register destination", while `mfc1 v0,$f12` has a destination it
1274
+ // ACCEPTS and would build an opaque whose source list quietly omits the register the
1275
+ // instruction actually read. The file is what is missing; say so.
1276
+ //
1277
+ // BOTH DIALECTS AND EVERY SPELLING, which is why this is `frontend/splat.ts`'s exported
1278
+ // constant and not a literal: the reader decides which tokens still carry a sigil when they
1279
+ // get here, so a predicate written twice is a predicate that can disagree with itself, and
1280
+ // each half of that disagreement is silent. Its docblock lists the spellings and says why
1281
+ // the sigil is required.
1282
+ fpReg: MIPS_FP_REG,
1283
+ // The FPU CONTROL register, which both moves spell `$31` — a GPR-shaped token no register
1284
+ // predicate can tell from an integer one. `ctc1`'s first operand is its SOURCE, so this
1285
+ // also stops an opaque being fabricated on a register the instruction only reads.
1286
+ fpControl: /^(cfc1|ctc1)$/i,
1121
1287
  storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)$/i,
1122
1288
  skipSafe: /^(nop|ssnop|break)$/i,
1123
1289
  context: `${name} @0x${ins.addr.toString(16)}`,
@@ -1131,6 +1297,28 @@ export function lift(
1131
1297
  write(od.dst, res);
1132
1298
  };
1133
1299
  const emitUn = kit.un;
1300
+ // Arithmetic on the FPU's file, keyed by `mipsEvenFpKey` so both dialects' spellings of one
1301
+ // register are one SSA variable. Anything it cannot key — an odd half, a target with no float
1302
+ // homes — keeps the register-file refusal.
1303
+ const emitFpArith = (ins: Instr) => {
1304
+ const keys = ins.ops.map(mipsEvenFpKey);
1305
+ if (fpu === undefined || keys.length === 0 || keys.some((k) => k === null)) {
1306
+ emitOpaqueDest(ins);
1307
+ return;
1308
+ }
1309
+ if (fpType === null) {
1310
+ refuseBothPrecisions(name, ins.mnemonic);
1311
+ }
1312
+ const [dst, ...srcs] = keys as string[];
1313
+ const { op } = FP_ARITH[ins.mnemonic];
1314
+ if (op === 'copy') {
1315
+ write(dst, read(srcs[0]));
1316
+ return;
1317
+ }
1318
+ const v = mkValue(fpType);
1319
+ ops.push(mkOp(op, { operands: srcs.map(read), results: [v] }));
1320
+ write(dst, v);
1321
+ };
1134
1322
  // `%lo` completes the address a `lui %hi` began. The pairing asks SSA — through `readVar`, not
1135
1323
  // `read`, which refuses a half — what `rHi` holds HERE, so a pair separated by unrelated
1136
1324
  // instructions folds while a register reused between the halves does not, and a half that
@@ -1174,6 +1362,19 @@ export function lift(
1174
1362
  // spurious pointer parameter). Compiler spills/reloads are always word-width; sub-word `sp`
1175
1363
  // access is not spill output, so it stays on the memory path.
1176
1364
  if (spSlotSafe && isStackPtr(base) && width === 4) {
1365
+ const saved = saveSlots.get(off);
1366
+ if (saved !== undefined) {
1367
+ if (saved !== d) {
1368
+ throw new FrontendUnsupportedError(
1369
+ `cannot lift '${name}': reload of '${mem}' into ${d}, a slot ${saved} was saved into — the ` +
1370
+ `word holds the caller's ${saved}, which no C value names`,
1371
+ );
1372
+ }
1373
+ const caller = mkValue(T.unk(32));
1374
+ restores.set(caller, { reg: d, addr: ins.addr });
1375
+ write(d, caller);
1376
+ return;
1377
+ }
1177
1378
  // SOUNDNESS GUARD (mirrors PPC frameLoad): only route through the slot SSA var if that slot was
1178
1379
  // actually STORED (has a reaching def). A word `lw` from an sp offset that was NEVER spilled is an
1179
1380
  // incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local — neither modelled.
@@ -1210,14 +1411,118 @@ export function lift(
1210
1411
  // slot's value in SSA, do NOT emit a `store` through `sp`. A never-reloaded spill (the ABI
1211
1412
  // home-slot store) then has no uses and simply drops. See isStackPtr / spSlotSafe.
1212
1413
  if (spSlotSafe && isStackPtr(base) && width === 4) {
1414
+ if (savedByCallee(srcReg) && ssa.holdsEntryValue(srcReg, bi)) {
1415
+ refuseMixedSlot(ins, mem, off, 'save');
1416
+ saveSlots.set(off, srcReg);
1417
+ return;
1418
+ }
1419
+ refuseMixedSlot(ins, mem, off, 'value');
1420
+ valueSlots.add(off);
1213
1421
  writeVar(stackSlot(off), bi, read(srcReg));
1214
1422
  return;
1215
1423
  }
1216
1424
  ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
1217
1425
  };
1218
1426
 
1219
- for (const ins of b.body) {
1220
- decode(ins);
1427
+ // A CALL, WITH ITS DELAY SLOT RUN FIRST: the word at `jal + 4` executes before the callee is
1428
+ // entered, so it can set an argument up (KMC stores argument 5 there). The plan answers what
1429
+ // the callee's declaration says (`frontend/call-plan.ts`); what is O32's alone is here: where
1430
+ // argument k ≥ 4 lives, and the guess that cannot see past a3.
1431
+ const lowerJal = (ins: Instr) => {
1432
+ const at = `0x${ins.addr.toString(16)}`;
1433
+ const callee = splat ? ins.ops[0] : ins.reloc?.type === 'R_MIPS_26' ? ins.reloc.sym : undefined;
1434
+ if (callee?.startsWith('.')) {
1435
+ throw new FrontendUnsupportedError(
1436
+ `${site(ins)} is relocated against the section '${callee}' — a callee known only by its offset ` +
1437
+ 'in a section has no name to call it by',
1438
+ );
1439
+ }
1440
+ if (callee === undefined || !/^[A-Za-z_]\w*$/.test(callee)) {
1441
+ throw new FrontendUnsupportedError(
1442
+ `${site(ins)} has no callee symbol — no R_MIPS_26 relocation names it, and its operand ` +
1443
+ `'${ins.ops[0] ?? ''}' is not a name`,
1444
+ );
1445
+ }
1446
+ if (fpuInstr !== undefined) {
1447
+ throw new FrontendUnsupportedError(
1448
+ `cannot lift '${name}': '${fpuInstr.mnemonic}' at 0x${fpuInstr.addr.toString(16)} uses the FPU and ` +
1449
+ `'jal' at ${at} makes a call — the floating-point registers a call reads, returns in and destroys ` +
1450
+ 'are not modelled',
1451
+ );
1452
+ }
1453
+ if (frameSize === undefined) {
1454
+ throw new FrontendUnsupportedError(
1455
+ `cannot lift '${name}': the call at ${at} is made in a frame that is not one 'addiu sp,sp,-N', so ` +
1456
+ 'where its outgoing stack arguments end is not known',
1457
+ );
1458
+ }
1459
+ calls.lower({
1460
+ callee,
1461
+ ssa,
1462
+ bi,
1463
+ read,
1464
+ write,
1465
+ // ARGUMENT WORD k ≥ 4 travels in `16+4(k-4)(sp)`, inside the one frame this function pushed
1466
+ stackWord: (k) => {
1467
+ const off = STACK_ARGS_AT + 4 * (k - ARG_REGS.length);
1468
+ if (off >= frameSize) {
1469
+ throw new FrontendUnsupportedError(
1470
+ `cannot lift '${name}': outgoing stack argument ${k + 1} of '${callee}' at ${at} travels in ` +
1471
+ `${off}(sp), past the ${frameSize}-byte frame this function pushed`,
1472
+ );
1473
+ }
1474
+ if (!valueSlots.has(off) || !ssa.hasReachingDef(stackSlot(off), bi)) {
1475
+ throw new FrontendUnsupportedError(
1476
+ `cannot lift '${name}': outgoing stack argument ${k + 1} of '${callee}' at ${at} travels in ` +
1477
+ `${off}(sp), and no value stored there reaches the call`,
1478
+ );
1479
+ }
1480
+ return readVar(stackSlot(off), bi);
1481
+ },
1482
+ // a pending `%hi` half is not an argument
1483
+ highHalves,
1484
+ guess: {
1485
+ // a gap in the argument registers refuses, naming this address (`fallbackArgc`)
1486
+ at: ins.addr,
1487
+ // A GUESS THAT FILLS a0..a3 CANNOT SAY WHERE THE LIST ENDS: a word of the outgoing area that
1488
+ // reaches the call is argument 5 onward, or a local the compiler put there.
1489
+ refuse: (argc) => {
1490
+ if (argc < ARG_REGS.length) {
1491
+ return;
1492
+ }
1493
+ const passed = [...valueSlots]
1494
+ .filter((off) => off >= STACK_ARGS_AT && off < frameSize && ssa.hasReachingDef(stackSlot(off), bi))
1495
+ .sort((x, y) => x - y)[0];
1496
+ if (passed !== undefined) {
1497
+ throw new FrontendUnsupportedError(
1498
+ `cannot lift '${name}': outgoing stack arguments not modelled — the undeclared call to '${callee}' ` +
1499
+ `at ${at} fills a0..a3, and the word stored to ${passed}(sp) reaches it where an argument past ` +
1500
+ 'the registers travels',
1501
+ );
1502
+ }
1503
+ },
1504
+ },
1505
+ });
1506
+ // hi and lo are the callee's to overwrite
1507
+ divState = null;
1508
+ mulState = null;
1509
+ };
1510
+ for (let i = 0; i < b.body.length; i++) {
1511
+ const ins = b.body[i];
1512
+ if (ins.mnemonic !== 'jal') {
1513
+ decode(ins);
1514
+ continue;
1515
+ }
1516
+ const slot = b.body[i + 1];
1517
+ if (slot?.addr !== ins.addr + 4) {
1518
+ throw new FrontendUnsupportedError(
1519
+ `${site(ins)} — its delay slot at 0x${(ins.addr + 4).toString(16)} is not the next instruction of ` +
1520
+ 'its block (a branch target, a control transfer, or missing), so it does not run before this call alone',
1521
+ );
1522
+ }
1523
+ decode(slot);
1524
+ i++;
1525
+ lowerJal(ins);
1221
1526
  }
1222
1527
 
1223
1528
  // Terminator. For a conditional branch, capture the comparison operands from the register
@@ -1277,17 +1582,25 @@ export function lift(
1277
1582
  // loud, but for a merge that never happened. Rejecting it gives the honest void return.
1278
1583
  // PowerPC reads its return register through the guard, so the refusal it would get is already
1279
1584
  // the right one; it passes this same predicate to its call-arity count.
1280
- const retOps = ssa.hasReachingDef(RET, bi, (v) => !highHalves.has(v)) ? [readVar(RET, bi)] : [];
1585
+ //
1586
+ // NOR IS WHAT A VOID CALLEE LEFT IN v0. The call writes no v0 and destroys it, and `holdsValue`
1587
+ // ends a path there, so a function that ends on that call returns nothing.
1281
1588
  if (!br) {
1282
1589
  // A nullified slot's block leaves by its branch's TARGET, which need not be the next
1283
1590
  // address, so `fallthrough` is claimed only where control really does fall through.
1284
1591
  const to = succAddrs.get(b)![0];
1285
1592
  const next = (b.body[b.body.length - 1]?.addr ?? b.startAddr) + 4;
1286
1593
  ops.push(mkOp('br', { attrs: to === next ? { fallthrough: true } : {}, successors: [succ(to)] }));
1287
- } // fall-through
1288
- else {
1289
- ops.push(mkOp('ret', { operands: retOps }));
1594
+ return;
1290
1595
  }
1596
+ // A FLOAT RETURN is decided for the whole function (frontend/fpu.ts `writesFloatReturn`), and
1597
+ // then `v0` is scratch like any other integer register.
1598
+ const retOps = floatReturn
1599
+ ? [read(fpu!.returnReg)]
1600
+ : ssa.holdsValue(RET, bi, (v) => !highHalves.has(v))
1601
+ ? [readVar(RET, bi)]
1602
+ : [];
1603
+ ops.push(mkOp('ret', { operands: retOps }));
1291
1604
  return;
1292
1605
  }
1293
1606
  // unconditional branch
@@ -1299,15 +1612,25 @@ export function lift(
1299
1612
  ssa.markFilled(bi);
1300
1613
  });
1301
1614
  highHalves.assertAllConsumed(name);
1615
+ const argSlots = fpuArgSlots(name, fpu, ARG_REGS, isFpKey, fpType);
1616
+ mintArgSlotHoles(ssa, preds[0].length > 0, argSlots);
1302
1617
  ssa.finish();
1303
1618
  highHalves.assertNoneEscaped(name, irBlocks);
1619
+ for (const blk of irBlocks) {
1620
+ const used = [
1621
+ ...blk.params,
1622
+ ...blk.ops.flatMap((op) => [...op.operands, ...op.results, ...op.successors.flatMap((s) => s.args)]),
1623
+ ];
1624
+ const restore = used.map((v) => restores.get(v)).find((r) => r !== undefined);
1625
+ if (restore) {
1626
+ throw new FrontendUnsupportedError(
1627
+ `cannot lift '${name}': the caller's ${restore.reg}, which the 'lw' at 0x${restore.addr.toString(16)} ` +
1628
+ `restores, reaches a value this function computes with — no C value names it`,
1629
+ );
1630
+ }
1631
+ }
1304
1632
 
1305
- // ABI-ordered entry parameters (a0, a1, …) — a callee-saved copy can read a later argument
1306
- // register first. Only the true entry (no predecessors) is sorted; a loop header's phis are
1307
- // index-aligned with predecessor args and must not be reordered.
1308
- const entry = irBlocks[0];
1309
- // non-ABI live-in ranks FIRST (indexOf's -1) — deliberate MIPS/PPC tie-break; Thumb's is 99/last
1310
- abiSortEntryParams(entry, preds[0].length > 0, (v) => ARG_REGS.indexOf(paramReg.get(v) ?? ''));
1633
+ abiSortEntryParams(irBlocks[0], preds[0].length > 0, paramReg, argSlots);
1311
1634
  return ssa.fn;
1312
1635
  }
1313
1636