@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.
- package/README.md +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/frontend/mips.ts
CHANGED
|
@@ -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.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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`
|
|
586
|
-
*
|
|
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
|
-
|
|
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). `
|
|
659
|
-
// implicitly, so dropping
|
|
660
|
-
// (jump table / computed goto) is not a plain
|
|
661
|
-
//
|
|
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 === '
|
|
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
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
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
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
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
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|