@asmlift/core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- 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 +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/frontend/mips.ts
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
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`) — unless it is a copy of the word before the
|
|
13
|
+
// target, when the branch is retargeted there with an empty slot (`normaliseBranchLikely`, which
|
|
14
|
+
// does the same for a `b`/`j` whose slot is that copy).
|
|
13
15
|
//
|
|
14
16
|
// Comparison model: MIPS fuses compare-and-branch, so a branch lowers directly to an icmp
|
|
15
17
|
// (the branch mnemonics are signed/equality only). `bltz/bgez/blez/bgtz` compare against zero; `beq/bne`
|
|
@@ -18,14 +20,21 @@
|
|
|
18
20
|
// with no consuming branch (e.g. `return a>b` → `slt v0,a1,a0`) stays an icmp value. Unsigned
|
|
19
21
|
// compares (`sltu`/`sltiu`) lower to `icmp_ult`; recover types their operands u32 so the backend
|
|
20
22
|
// re-emits `sltu` (the operator is the same `<` — the signedness lives in the operand types).
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
+
import { ownArgumentWordsOn } from '../aggregate';
|
|
24
|
+
import { type Block, Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
25
|
+
import { NEGATED_FCMP, NEGATED_ICMP, type Opcode } from '../ir/opcodes';
|
|
23
26
|
import { T } from '../ir/types';
|
|
24
|
-
import type
|
|
25
|
-
import type {
|
|
27
|
+
import { type Prototypes, declaresVoidReturn, ownReturnWidth, wordsOf } from '../proto';
|
|
28
|
+
import type { ContextGlobals } from '../proto-context';
|
|
29
|
+
import type { SymbolMap } from '../symbols';
|
|
30
|
+
import { O32_REGISTER_ALIASES, type TargetDescription } from '../target';
|
|
26
31
|
import { type AsmData, readJumpTable, textRelocAt } from './asmdata';
|
|
32
|
+
import { callDeclarations, foldByValueArgs } from './call-plan';
|
|
33
|
+
import { pinDeadReads } from './dead-reads';
|
|
34
|
+
import { pinDeviceAccesses } from './device-pins';
|
|
27
35
|
import {
|
|
28
36
|
type DisasmInstr,
|
|
37
|
+
type DisasmReloc,
|
|
29
38
|
parseImm,
|
|
30
39
|
parseMem,
|
|
31
40
|
parseDisasm as parseSharedDisasm,
|
|
@@ -33,14 +42,59 @@ import {
|
|
|
33
42
|
symbolStart,
|
|
34
43
|
} from './disasm';
|
|
35
44
|
import { mkEmitKit, pushSwitchBr } from './emit';
|
|
36
|
-
import { FrontendUnsupportedError } from './errors';
|
|
45
|
+
import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
|
|
37
46
|
import { assertInputFormat } from './format';
|
|
38
|
-
import
|
|
47
|
+
import {
|
|
48
|
+
type ReturnHomeRule,
|
|
49
|
+
carriesFloatHome,
|
|
50
|
+
floatHomeAt,
|
|
51
|
+
fpPrecision,
|
|
52
|
+
fpuArgSlots,
|
|
53
|
+
refuseBothPrecisions,
|
|
54
|
+
refuseFpWordCrossings,
|
|
55
|
+
returnHomeRule,
|
|
56
|
+
settleReturnHome,
|
|
57
|
+
writesFloatReturn,
|
|
58
|
+
} from './fpu';
|
|
59
|
+
import {
|
|
60
|
+
FRAME_OBJECT_AUDIT,
|
|
61
|
+
type FrameObjectRelift,
|
|
62
|
+
type FrameRange,
|
|
63
|
+
areaAboveOutgoing,
|
|
64
|
+
landsOnArea,
|
|
65
|
+
liftWithRelift,
|
|
66
|
+
nameMergedSretObjects,
|
|
67
|
+
refuseSretOutsideFrame,
|
|
68
|
+
} from './frame-objects';
|
|
69
|
+
import { type Frontend, type LiftOptions, ownDeclarationOf } from './frontend';
|
|
39
70
|
import { makeHighHalves } from './high-half';
|
|
71
|
+
import {
|
|
72
|
+
type HoistedDispatch,
|
|
73
|
+
type IdiomLeft,
|
|
74
|
+
type StaleIdiomRead,
|
|
75
|
+
type TableKey,
|
|
76
|
+
agreedTable,
|
|
77
|
+
caseEntries,
|
|
78
|
+
enterCaseTarget,
|
|
79
|
+
entersBetween,
|
|
80
|
+
idiomLeaves,
|
|
81
|
+
makeTableBases,
|
|
82
|
+
parkedIn,
|
|
83
|
+
refuseReachableDispatch,
|
|
84
|
+
refuseStaleIdiomReads,
|
|
85
|
+
scaledIndexIn,
|
|
86
|
+
} from './jump-table';
|
|
40
87
|
import { opaqueDest } from './opaque';
|
|
41
|
-
import { isSplatMips, parseSplatMips } from './splat';
|
|
42
|
-
import {
|
|
43
|
-
|
|
88
|
+
import { MIPS_FP_REG, isSplatMips, mipsEvenFpKey, parseSplatMips } from './splat';
|
|
89
|
+
import {
|
|
90
|
+
type ArgSlots,
|
|
91
|
+
abiSortEntryParams,
|
|
92
|
+
incomingStackArgKey,
|
|
93
|
+
mintArgSlotHoles,
|
|
94
|
+
stackArgSlots,
|
|
95
|
+
stackSlotKey,
|
|
96
|
+
} from './ssa';
|
|
97
|
+
import { makeSsaBuilder, refuseSavedRegisterLeaks, registerLiveIns, unifyRegisterSpellings } from './ssa';
|
|
44
98
|
|
|
45
99
|
type Instr = DisasmInstr;
|
|
46
100
|
|
|
@@ -68,15 +122,85 @@ const LIKELY_BASE: Record<string, string> = {
|
|
|
68
122
|
bgtzl: 'bgtz',
|
|
69
123
|
bltzl: 'bltz',
|
|
70
124
|
bgezl: 'bgez',
|
|
125
|
+
bc1tl: 'bc1t',
|
|
126
|
+
bc1fl: 'bc1f',
|
|
71
127
|
};
|
|
72
|
-
// A coprocessor-1 branch tests
|
|
73
|
-
//
|
|
128
|
+
// A coprocessor-1 branch tests the FP condition code (`fcc`) that a `c.cond.s/d` sets: `bc1t` is
|
|
129
|
+
// taken when it is set, `bc1f` when it is clear. MIPS I-III have one code and the branch names it
|
|
130
|
+
// by its target alone; a MIPS IV `bc1t $fccN, L` names one of eight, which is not modelled.
|
|
74
131
|
const isFpCondBranch = (m: string) => m.startsWith('bc1');
|
|
132
|
+
const FP_COND_BRANCH: Readonly<Record<string, boolean>> = { bc1t: true, bc1f: false };
|
|
133
|
+
|
|
134
|
+
/** The FPU arithmetic this frontend lifts, onto the float opcodes (`mov.s`/`mov.d` are copies and
|
|
135
|
+
* have none), with the precision each computes in: `.s` single, `.d` double, one per function
|
|
136
|
+
* (`frontend/fpu.ts` `fpPrecision`). With the conversions and compares below, and `mfc1`/`mtc1`,
|
|
137
|
+
* that is the decode; every other FPU instruction — the moves to and from memory, the conversions
|
|
138
|
+
* between the precisions — keeps the register-file refusal `opaqueDest` gives it, and so does one
|
|
139
|
+
* of these naming an odd register (a double's second half, or a single this frontend does not
|
|
140
|
+
* key) or a target with no `fpu` homes. `docs/floating-point.md` says what the next layer is. */
|
|
141
|
+
const FP_ARITH: Readonly<Record<string, { op: Opcode | 'copy'; width: 32 | 64 }>> = {
|
|
142
|
+
'add.s': { op: 'fadd', width: 32 },
|
|
143
|
+
'sub.s': { op: 'fsub', width: 32 },
|
|
144
|
+
'mul.s': { op: 'fmul', width: 32 },
|
|
145
|
+
'div.s': { op: 'fdiv', width: 32 },
|
|
146
|
+
'neg.s': { op: 'fneg', width: 32 },
|
|
147
|
+
'mov.s': { op: 'copy', width: 32 },
|
|
148
|
+
'add.d': { op: 'fadd', width: 64 },
|
|
149
|
+
'sub.d': { op: 'fsub', width: 64 },
|
|
150
|
+
'mul.d': { op: 'fmul', width: 64 },
|
|
151
|
+
'div.d': { op: 'fdiv', width: 64 },
|
|
152
|
+
'neg.d': { op: 'fneg', width: 64 },
|
|
153
|
+
'mov.d': { op: 'copy', width: 64 },
|
|
154
|
+
};
|
|
155
|
+
/** The moves between the FPU's file and the integer file's VALUES, onto the conversions: `trunc.w`
|
|
156
|
+
* truncates a float to the integer word it leaves in an FPU register (`mfc1` moves it out), and
|
|
157
|
+
* `cvt.s.w`/`cvt.d.w` convert the integer word `mtc1` moved in. Each is one precision, like the
|
|
158
|
+
* arithmetic. The `cvt.w` and `cfc1`/`ctc1` rounding-mode sequence IDO converts an UNSIGNED word
|
|
159
|
+
* with, `cvt.s.d`/`cvt.d.s` between the precisions, and the other roundings stay refused. */
|
|
160
|
+
const FP_CONVERT: Readonly<Record<string, { op: Opcode; width: 32 | 64 }>> = {
|
|
161
|
+
'trunc.w.s': { op: 'fptosi', width: 32 },
|
|
162
|
+
'trunc.w.d': { op: 'fptosi', width: 64 },
|
|
163
|
+
'cvt.s.w': { op: 'sitofp', width: 32 },
|
|
164
|
+
'cvt.d.w': { op: 'sitofp', width: 64 },
|
|
165
|
+
};
|
|
166
|
+
/** The compares that set the condition code, onto the float compares. Only the ORDERED three are
|
|
167
|
+
* here, because only they are what IDO, KMC and GCC 2.7.2 emit for C's relations (compiled, every
|
|
168
|
+
* relation in both precisions): `a > b` is `c.lt.s b, a`, `a != b` is `c.eq.s` with the branch
|
|
169
|
+
* sense flipped, and a NaN answers false to all three. */
|
|
170
|
+
const FP_COMPARE: Readonly<Record<string, { op: Opcode; width: 32 | 64 }>> = {
|
|
171
|
+
'c.eq.s': { op: 'fcmp_oeq', width: 32 },
|
|
172
|
+
'c.lt.s': { op: 'fcmp_olt', width: 32 },
|
|
173
|
+
'c.le.s': { op: 'fcmp_ole', width: 32 },
|
|
174
|
+
'c.eq.d': { op: 'fcmp_oeq', width: 64 },
|
|
175
|
+
'c.lt.d': { op: 'fcmp_olt', width: 64 },
|
|
176
|
+
'c.le.d': { op: 'fcmp_ole', width: 64 },
|
|
177
|
+
};
|
|
178
|
+
const FP_ARITH_MNEMONICS: ReadonlySet<string> = new Set(Object.keys(FP_ARITH));
|
|
179
|
+
/** The decoded instructions that leave a FLOAT in an FPU register, which is what puts one in the
|
|
180
|
+
* float return home: `trunc.w` leaves an integer word there, and `mtc1` the integer it moved. */
|
|
181
|
+
const FP_FLOAT_WRITERS: ReadonlySet<string> = new Set([
|
|
182
|
+
...FP_ARITH_MNEMONICS,
|
|
183
|
+
...Object.keys(FP_CONVERT).filter((m) => FP_CONVERT[m].op === 'sitofp'),
|
|
184
|
+
]);
|
|
185
|
+
/** The decoded instructions that carry a float's information into the INTEGER file: a `mfc1` of a
|
|
186
|
+
* truncated word, and a condition-code branch. Where one is present a value in `v0` may be the
|
|
187
|
+
* function's result, and a write to `$f0` a temporary, so neither decides the return home
|
|
188
|
+
* (frontend/fpu.ts `returnHomeRule`). */
|
|
189
|
+
const crossesToIntegers = (m: string) => m === 'mfc1' || (LIKELY_BASE[m] ?? m) in FP_COND_BRANCH;
|
|
190
|
+
/** A key the SSA builder holds for the FPU's file: `mipsEvenFpKey`'s canonical spelling. */
|
|
191
|
+
const isFpKey = (k: string) => /^\$f\d+$/.test(k);
|
|
75
192
|
|
|
76
193
|
const isZero = (r: string) => r === 'zero' || r === '$0';
|
|
77
194
|
// The stack pointer (`$29`). A `sw/lw` through it is not a store/load through a data pointer — it
|
|
78
195
|
// is a spill/reload of a stack SLOT (an argument home slot or a local). See emitStore/emitLoad.
|
|
79
196
|
const isStackPtr = (r: string) => r === 'sp' || r === '$sp' || r === '$29';
|
|
197
|
+
// Where O32 puts a call's argument k ≥ 4: the caller's outgoing area keeps `[0,16)` as the home of
|
|
198
|
+
// a0..a3, and argument k is the word at `16 + 4(k − 4)` above the `sp` the call is made at.
|
|
199
|
+
// Compiled on ido7.1, gcc2.7.2kmc and gcc2.7.2: `g(a, b, 1, 2, 3)` stores the 3 to `16(sp)`.
|
|
200
|
+
const STACK_ARGS_AT = 16;
|
|
201
|
+
/** Every load whose last operand is `off(base)`: integer, unaligned and FPU
|
|
202
|
+
* (`outgoing-area-probes.test.ts` classifies every memory mnemonic of the committed probes against it). */
|
|
203
|
+
export const MIPS_FRAME_LOAD = /^(lb|lbu|lh|lhu|lw|lwl|lwr|lwu|ll|ld|ldl|ldr|lwc1|ldc1)$/;
|
|
80
204
|
// SSA-variable name for the stack slot at a constant `sp`-offset. Distinct namespace from the
|
|
81
205
|
// register names (which are alphabetic / `$N`), so it never collides with a real register var.
|
|
82
206
|
const stackSlot = stackSlotKey; // shared spelling: frontend/ssa.ts
|
|
@@ -89,10 +213,11 @@ const SUBWORD_MEM = new Set(['lb', 'lbu', 'lh', 'lhu', 'sb', 'sh']);
|
|
|
89
213
|
const isMipsReg = (s: string | undefined): s is string =>
|
|
90
214
|
/^(\$\d+|[a-z][a-z0-9]*)$/i.test(s ?? '') && !/^0x/i.test(s ?? '');
|
|
91
215
|
// `jr ra`. A non-ra `jr` also lands here, but by block-fill time it is either a recovered
|
|
92
|
-
// switch dispatch (its block is pruned as unreachable
|
|
216
|
+
// switch dispatch (its block is pruned as unreachable, which `refuseReachableDispatch` checks) or
|
|
217
|
+
// has already loud-failed in `lift`.
|
|
93
218
|
const isReturn = (ins: Instr) => ins.mnemonic === 'jr';
|
|
94
219
|
const isUncond = (ins: Instr) => ins.mnemonic === 'b' || ins.mnemonic === 'j';
|
|
95
|
-
const isCond = (ins: Instr) => ins.mnemonic in COND_Z || ins.mnemonic in COND_RR;
|
|
220
|
+
const isCond = (ins: Instr) => ins.mnemonic in COND_Z || ins.mnemonic in COND_RR || ins.mnemonic in FP_COND_BRANCH;
|
|
96
221
|
const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins);
|
|
97
222
|
// ANY instruction that redirects control, modelled or not: on MIPS that is the whole `b*` space
|
|
98
223
|
// (minus the `break` trap) plus `j*`. `isXfer` is the subset this frontend models; this is the
|
|
@@ -135,6 +260,10 @@ const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { zer
|
|
|
135
260
|
// A recovered JUMP TABLE's dispatch is untouched: Regime B reads the table through `asmdata.ts` and
|
|
136
261
|
// prunes the dispatch block, so its `lui %hi(.rodata)` never reaches `decode`.
|
|
137
262
|
//
|
|
263
|
+
// `R_MIPS_26` is a `jal`'s callee, and a `j`'s target where the jump stays in this section. objdump
|
|
264
|
+
// prints the field, which in a relocatable object is the addend and not the address, so the callee
|
|
265
|
+
// is read off this record alone.
|
|
266
|
+
//
|
|
138
267
|
// `R_MIPS_GOT16`/`R_MIPS_CALL16`/`R_MIPS_GPREL16` are PIC/small-data access, already refused by the
|
|
139
268
|
// `gp`-as-data guard; widening this carrier to them would trade those messages for worse ones
|
|
140
269
|
// without recovering an address.
|
|
@@ -142,10 +271,15 @@ const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { zer
|
|
|
142
271
|
// THE ADDEND IS NOT ON THE RECORD. MIPS objects are REL: the relocation has no addend field and the
|
|
143
272
|
// address's low bits live in the two instruction immediates, so a record that DOES carry an addend
|
|
144
273
|
// is not the format assumed here, and refuses.
|
|
145
|
-
function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData): void {
|
|
274
|
+
function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData, freed: ReadonlySet<number>): void {
|
|
146
275
|
const byAddr = new Map(instrs.map((ins) => [ins.addr, ins]));
|
|
147
276
|
for (const r of ad.relocs) {
|
|
148
|
-
if (r.section !== '.text' || (r.type !== 'R_MIPS_HI16' && r.type !== 'R_MIPS_LO16')) {
|
|
277
|
+
if (r.section !== '.text' || (r.type !== 'R_MIPS_HI16' && r.type !== 'R_MIPS_LO16' && r.type !== 'R_MIPS_26')) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// A slot `normaliseBranchLikely` emptied holds a `nop` now; its old word's relocation
|
|
281
|
+
// describes the copy at the target, which carries its own.
|
|
282
|
+
if (freed.has(r.offset)) {
|
|
149
283
|
continue;
|
|
150
284
|
}
|
|
151
285
|
const ins = byAddr.get(r.offset);
|
|
@@ -195,61 +329,165 @@ interface MipsBlock {
|
|
|
195
329
|
delayAnnulled: boolean;
|
|
196
330
|
}
|
|
197
331
|
|
|
198
|
-
// Mnemonics whose
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
'
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
'move'
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
'mul',
|
|
236
|
-
]);
|
|
237
|
-
const destReg = (ins: Instr): string | null => (WRITES_D.has(ins.mnemonic) ? ins.ops[0] : null);
|
|
332
|
+
// Mnemonics whose FIRST operand is a source, never a destination: the stores, the branches that
|
|
333
|
+
// compare it, and the moves and products that read it.
|
|
334
|
+
const SOURCE_FIRST = /^(s[bhwd]|sw[lr]|sc|s[wd]c1|b\w*|j|jr|mult|multu|div|divu|mt\w+|ctc1|t\w+|cache|sync)$/;
|
|
335
|
+
|
|
336
|
+
/** THE INTEGER REGISTER AN INSTRUCTION WRITES, or null — the one classifier every reader here asks
|
|
337
|
+
* (the jump-table trace, the `gp` skip, the frame-depth walk, the save scan, the 64-bit return
|
|
338
|
+
* check), so no two of them can disagree on a mnemonic.
|
|
339
|
+
*
|
|
340
|
+
* A DENY-LIST: every instruction writes its first operand unless it is one of `SOURCE_FIRST`'s.
|
|
341
|
+
* Every reader asks "is this the write of r?", and an unknown write answered no would be stepped
|
|
342
|
+
* past as though r still held what came before, so the default is the answer that stops them. A
|
|
343
|
+
* call writes `ra`, whatever it names first (`jal f`, and `jalr rs` with the implicit `ra`). */
|
|
344
|
+
const writtenReg = (ins: Instr): string | null => {
|
|
345
|
+
if (ins.mnemonic === 'jalr') {
|
|
346
|
+
return ins.ops.length > 1 ? ins.ops[0] : 'ra';
|
|
347
|
+
}
|
|
348
|
+
if (isCallInstr(ins)) {
|
|
349
|
+
return 'ra';
|
|
350
|
+
}
|
|
351
|
+
return SOURCE_FIRST.test(ins.mnemonic) || !isMipsReg(ins.ops[0]) ? null : ins.ops[0];
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/** A call: every instruction that links into `ra` — `jal`, `jalr`, and the linking branches. */
|
|
355
|
+
const isCallInstr = (ins: Instr) => /^(jal|jalr|bal|bgezall?|bltzall?)$/.test(ins.mnemonic);
|
|
356
|
+
|
|
357
|
+
/** The register a register COPY reads — `move rD,rS`, or `addu`/`add`/`or` with `zero` beside it,
|
|
358
|
+
* which is how Splat and IDO spell the same move — or undefined for any other instruction. */
|
|
359
|
+
const copiedReg = (ins: Instr): string | undefined => {
|
|
360
|
+
const [, s, t] = ins.ops;
|
|
361
|
+
if (ins.mnemonic === 'move') {
|
|
362
|
+
return s;
|
|
363
|
+
}
|
|
364
|
+
if (!/^(addu|add|or)$/.test(ins.mnemonic) || s === undefined || t === undefined) {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
return isZero(t) ? s : isZero(s) ? t : undefined;
|
|
368
|
+
};
|
|
238
369
|
|
|
239
370
|
// A recovered dense-switch jump table (Regime B), keyed
|
|
240
|
-
// by the BOUNDS branch (`beqz tmp, DEF`) whose block emits the `switch_br`.
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// `
|
|
371
|
+
// by the BOUNDS branch (`beqz tmp, DEF`) whose block emits the `switch_br`. The table base is one of:
|
|
372
|
+
// - absolute, formed in the dispatch's block: `lui at,%hi(T) ; addu at,at,rI ; lw rV,%lo(T)(at)`,
|
|
373
|
+
// the table's `%lo` folded into the load's displacement (IDO, KMC and gcc 2.7.2 non-PIC, whenever
|
|
374
|
+
// the block forms the base itself);
|
|
375
|
+
// - absolute, hoisted: `lui rH,%hi(T) ; addiu rB,rH,%lo(T)` in a loop's preheader, the load at
|
|
376
|
+
// displacement 0 (KMC and gcc 2.7.2; `HoistedDispatch`);
|
|
377
|
+
// - PIC, IDO `-KPIC`: `lw at,%got(T)(gp)` GOT16 (its displacement the `%hi`), the load carrying
|
|
378
|
+
// the `%lo` LO16, and `addu rV,rV,gp` after it (the entries are gp-relative) (`inBlockTable`).
|
|
379
|
+
// The table must sit in a read-only section (`tableBaseOf`).
|
|
380
|
+
// Delay slots are honoured — the `beqz` delay slot (which on IDO carries the DEFAULT return value
|
|
381
|
+
// `li v0,-1`) still executes before the `switch_br`.
|
|
245
382
|
interface MipsJT {
|
|
246
383
|
scrutReg: string;
|
|
247
384
|
caseAddrs: number[];
|
|
248
385
|
defaultAddr: number;
|
|
249
386
|
jrAddr: number;
|
|
387
|
+
/** The start of the bounds block when its `sltiu` runs on the edges into it, not inside it
|
|
388
|
+
* (`mipsBounds`); null otherwise. */
|
|
389
|
+
entry: number | null;
|
|
390
|
+
/** The base register and table of a dispatch whose block does not form its base (KMC and gcc
|
|
391
|
+
* 2.7.2 hoist the `lui`/`addiu` pair into a loop's preheader): `lift` proves the base reaches it
|
|
392
|
+
* (frontend/jump-table.ts). Null when the pruned block forms the base itself. */
|
|
393
|
+
hoisted: HoistedDispatch | null;
|
|
394
|
+
/** What the pruned dispatch leaves in each register it writes (`idiomLeaves`). */
|
|
395
|
+
leaves: ReadonlyMap<string, IdiomLeft>;
|
|
250
396
|
}
|
|
251
397
|
|
|
252
|
-
|
|
398
|
+
/** A MIPS jump table's bounds guard for the dispatch whose `jr` is at `i`: `sltiu tmp,scrutReg,N`
|
|
399
|
+
* then `beqz tmp,DEF`, where the beqz is the nearest transfer before the `jr`. `at` is the beqz's
|
|
400
|
+
* index.
|
|
401
|
+
*
|
|
402
|
+
* The `sltiu` is the last write of `tmp` on EVERY path into the beqz, so it is found per entry of
|
|
403
|
+
* the beqz's block, and every entry must run the same one: inside the block, after nothing that
|
|
404
|
+
* writes `tmp` or the scrutinee (IDO schedules `move s0,a1` between the two); or, where the block
|
|
405
|
+
* holds none, as the last word each edge into it runs. IDO rotates a `while` loop so that both of
|
|
406
|
+
* its entries do: the bounds `sltiu` sits in the delay slot of the loop-entry `beqz` (which runs it
|
|
407
|
+
* on the fall-through) and in the LIKELY slot of the latch `bnezl` (which runs it on the taken edge
|
|
408
|
+
* alone). An edge whose last word is not that `sltiu` — the not-taken edge of a likely branch, a
|
|
409
|
+
* call's return, the function's entry — leaves `tmp` unknown, and declines. `likely` holds the
|
|
410
|
+
* branch-likely addresses `normaliseBranchLikely` rewrote. */
|
|
411
|
+
function mipsBounds(
|
|
412
|
+
instrs: Instr[],
|
|
413
|
+
i: number,
|
|
414
|
+
scrutReg: string,
|
|
415
|
+
likely: ReadonlySet<number>,
|
|
416
|
+
): { at: number; addr: number; n: number; def: number; entry: number | null } | null {
|
|
417
|
+
let k = i - 1;
|
|
418
|
+
while (k >= 0 && !isControlTransfer(instrs[k])) {
|
|
419
|
+
k--;
|
|
420
|
+
}
|
|
421
|
+
const beqz = instrs[k];
|
|
422
|
+
if (!beqz || beqz.mnemonic !== 'beqz' || beqz.target === undefined) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const tmp = beqz.ops[0];
|
|
426
|
+
const isBound = (c: Instr | undefined) => c?.mnemonic === 'sltiu' && c.ops[0] === tmp && c.ops[1] === scrutReg;
|
|
427
|
+
const bound = (c: Instr, entry: number | null) => {
|
|
428
|
+
// `sltiu tmp, scrut, N` bounds `scrut < N`: the immediate IS the case count (PPC's `cmplwi`
|
|
429
|
+
// states the max index).
|
|
430
|
+
const n = parseImm(c.ops[2]);
|
|
431
|
+
return n < 2 ? null : { at: k, addr: beqz.addr, n, def: beqz.target!, entry };
|
|
432
|
+
};
|
|
433
|
+
const isTarget = (addr: number) => instrs.some((x) => x.target === addr);
|
|
434
|
+
// The beqz's block: back to a branch target, or to the word after a transfer's delay slot.
|
|
435
|
+
let s = k;
|
|
436
|
+
while (
|
|
437
|
+
s > 0 &&
|
|
438
|
+
!isTarget(instrs[s].addr) &&
|
|
439
|
+
!isControlTransfer(instrs[s - 1]) &&
|
|
440
|
+
!(s > 1 && isControlTransfer(instrs[s - 2]))
|
|
441
|
+
) {
|
|
442
|
+
s--;
|
|
443
|
+
}
|
|
444
|
+
for (let j = k - 1; j >= s; j--) {
|
|
445
|
+
const c = instrs[j];
|
|
446
|
+
if (isBound(c)) {
|
|
447
|
+
return bound(c, null);
|
|
448
|
+
}
|
|
449
|
+
if (writtenReg(c) === tmp || writtenReg(c) === scrutReg) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// No `sltiu` in the block: the last word each edge into it runs.
|
|
454
|
+
const last: Array<Instr | undefined> = [];
|
|
455
|
+
const p = instrs[s - 1];
|
|
456
|
+
const t = instrs[s - 2];
|
|
457
|
+
if (p === undefined || isControlTransfer(p)) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
if (t !== undefined && isControlTransfer(t)) {
|
|
461
|
+
// `p` is a delay slot: the fall-through edge runs it after a branch that is neither likely (its
|
|
462
|
+
// not-taken edge annuls the slot) nor a call (which returns here from the callee); a jump has no
|
|
463
|
+
// fall-through edge at all.
|
|
464
|
+
if (likely.has(t.addr) || isCallInstr(t)) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
if (!isUncond(t) && !isReturn(t)) {
|
|
468
|
+
last.push(p);
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
last.push(p);
|
|
472
|
+
}
|
|
473
|
+
// A taken edge runs the branch's delay slot, a likely one's included.
|
|
474
|
+
instrs.forEach((x, o) => {
|
|
475
|
+
if (x.target === instrs[s].addr) {
|
|
476
|
+
last.push(isCallInstr(x) ? undefined : instrs[o + 1]);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
const first = last[0];
|
|
480
|
+
if (
|
|
481
|
+
first === undefined ||
|
|
482
|
+
!isBound(first) ||
|
|
483
|
+
!last.every((c) => c !== undefined && c.ops.join() === first.ops.join() && c.mnemonic === 'sltiu')
|
|
484
|
+
) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
return bound(first, instrs[s].addr);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function recoverMipsJumpTables(instrs: Instr[], ad: AsmData, likely: ReadonlySet<number>): Map<number, MipsJT> {
|
|
253
491
|
// Nearest def of `reg` strictly before index `p`, not crossing a control transfer (so the trace
|
|
254
492
|
// stays inside the dispatch's extended block). Returns the instruction index, or -1.
|
|
255
493
|
const defBefore = (reg: string, p: number): number => {
|
|
@@ -257,14 +495,18 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
|
|
|
257
495
|
if (isXfer(instrs[j])) {
|
|
258
496
|
return -1;
|
|
259
497
|
}
|
|
260
|
-
if (
|
|
498
|
+
if (writtenReg(instrs[j]) === reg) {
|
|
261
499
|
return j;
|
|
262
500
|
}
|
|
263
501
|
}
|
|
264
502
|
return -1;
|
|
265
503
|
};
|
|
266
|
-
// Is `ins` a table-base materialisation (`lui rB,hi` HI16, or `lw rB,
|
|
267
|
-
// relocation into a data section? Returns {sym, addend} locating the table, or null.
|
|
504
|
+
// Is `ins` a table-base materialisation (`lui rB,hi` HI16, or `lw rB,hi(gp)` GOT16) with a `.text`
|
|
505
|
+
// relocation into a READ-ONLY data section? Returns {sym, addend} locating the table, or null.
|
|
506
|
+
// A writable table is refused: the table is read once, at its initial contents, and a switch
|
|
507
|
+
// lifted from it would ignore every store that retargets an entry. The compilers put a switch's
|
|
508
|
+
// table in `.rodata`/`.rdata`; what lands in `.data` is a non-const computed-goto label table
|
|
509
|
+
// (`static void *t[] = {&&a, ...}`), which its own function may write (`t[1] = &&c`).
|
|
268
510
|
const tableBaseOf = (ins: Instr): { sym: string; addend: number } | null => {
|
|
269
511
|
const isLui = ins.mnemonic === 'lui';
|
|
270
512
|
const isGpLw = ins.mnemonic === 'lw' && ins.ops[1] !== undefined && parseMem(ins.ops[1]).base === 'gp';
|
|
@@ -276,12 +518,76 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
|
|
|
276
518
|
return null;
|
|
277
519
|
}
|
|
278
520
|
const sec = ad.symbols.get(r.sym)?.section ?? r.sym; // ".rodata" section symbol → ".rodata"
|
|
279
|
-
if (!/^\.(rodata|rdata
|
|
521
|
+
if (!/^\.(rodata|rdata)$/.test(sec)) {
|
|
280
522
|
return null;
|
|
281
523
|
}
|
|
282
524
|
return { sym: r.sym, addend: r.addend };
|
|
283
525
|
};
|
|
284
526
|
|
|
527
|
+
/** The table a base formed OUTSIDE the dispatch block addresses, as far as the instructions say:
|
|
528
|
+
* every `addiu rB,rH,%lo` in the function, with the `lui rH,%hi` nearest before it, must name
|
|
529
|
+
* one table, or the guess is left undecided. KMC and gcc 2.7.2 form the base in a loop's
|
|
530
|
+
* preheader. Only a guess — `lift` pairs the halves and proves the base reaches the dispatch
|
|
531
|
+
* (frontend/jump-table.ts). The table's offset is the pair's own `%hi`/`%lo` halves, as REL
|
|
532
|
+
* relocations carry it. */
|
|
533
|
+
const hoistedTable = (rB: string): TableKey | null =>
|
|
534
|
+
agreedTable(
|
|
535
|
+
instrs.flatMap((x, q) => {
|
|
536
|
+
const lo = x.mnemonic === 'addiu' && x.ops[0] === rB ? textRelocAt(ad, x.addr) : null;
|
|
537
|
+
if (lo?.type !== 'R_MIPS_LO16') {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
let p = q - 1;
|
|
541
|
+
while (p >= 0 && writtenReg(instrs[p]) !== x.ops[1]) {
|
|
542
|
+
p--;
|
|
543
|
+
}
|
|
544
|
+
const hi = p >= 0 && instrs[p].mnemonic === 'lui' ? tableBaseOf(instrs[p]) : null;
|
|
545
|
+
if (!hi || hi.sym !== lo.sym) {
|
|
546
|
+
return [{ sym: '', addend: NaN }];
|
|
547
|
+
}
|
|
548
|
+
return [paired(hi, parseImm(instrs[p].ops[1]), parseImm(x.ops[2]))];
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
/** The table a `%hi` / `%lo` pair addresses: the high record's symbol and addend plus both
|
|
552
|
+
* halves' immediates, the `%lo` sign-extended — REL keeps the offset in the immediates, so a
|
|
553
|
+
* table 64 KiB into its section has a high half of 1 and one past 32 KiB has a negative `%lo`.
|
|
554
|
+
* The high half is a `lui`'s immediate (HI16) or a `lw rB,hi(gp)`'s displacement (GOT16 on the
|
|
555
|
+
* section symbol, whose GOT entry holds the 64 KiB page the LO16 completes). */
|
|
556
|
+
const paired = (hi: TableKey, hiImm: number, loImm: number): TableKey => ({
|
|
557
|
+
sym: hi.sym,
|
|
558
|
+
addend: hi.addend + (((hiImm & 0xffff) << 16) + (((loImm & 0xffff) << 16) >> 16)),
|
|
559
|
+
});
|
|
560
|
+
/** The table a base the dispatch's block forms addresses, read with the load's displacement `off`.
|
|
561
|
+
* The base pairs with the load: IDO, KMC and gcc 2.7.2 fold the table's `%lo` into the `lw`
|
|
562
|
+
* whenever the block forms the base, so `lw rV,%lo(T+K)(at)`'s displacement is the low half of
|
|
563
|
+
* the TABLE's address, not an index offset. One rule (`paired`) for both bases: a `lui`'s HI16,
|
|
564
|
+
* and IDO `-KPIC`'s GOT16 (`lw at,%got(T)(gp)`), whose displacement is the high half — `lw
|
|
565
|
+
* at,1(gp)` for a table 64 KiB in, then `lw rV,-24576(at)` for one at 40 KiB, or `lw rV,8(at)`
|
|
566
|
+
* for a later table of the unit. Refuses (null):
|
|
567
|
+
* - a non-zero displacement with no R_MIPS_LO16 on the load — `table[i + off/4]`, the base
|
|
568
|
+
* left unpaired. No compiler emits an unpaired HI16 or GOT16 for a table; displacement 0
|
|
569
|
+
* with no LO16 is still read at the base's own half, the address the pair gives with a
|
|
570
|
+
* zero `%lo`, so the hand fixtures that carry no LO16 keep lifting. An unpaired GOT16 is
|
|
571
|
+
* read only at immediate 0: against a global symbol it names that symbol's own GOT slot,
|
|
572
|
+
* not a page, so a non-zero immediate is no high half;
|
|
573
|
+
* - a LO16 on another symbol, or any other relocation on the load. */
|
|
574
|
+
const inBlockTable = (base: Instr, ld: Instr, off: number): TableKey | null => {
|
|
575
|
+
const hi = tableBaseOf(base);
|
|
576
|
+
if (!hi) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const isLui = base.mnemonic === 'lui';
|
|
580
|
+
const hiImm = isLui ? parseImm(base.ops[1]) : parseMem(base.ops[1]).off;
|
|
581
|
+
const lo = textRelocAt(ad, ld.addr);
|
|
582
|
+
if (lo === null) {
|
|
583
|
+
return off === 0 && (isLui || hiImm === 0) ? paired(hi, hiImm, 0) : null;
|
|
584
|
+
}
|
|
585
|
+
if (lo.type !== 'R_MIPS_LO16' || lo.sym !== hi.sym) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
return paired(hi, hiImm, off);
|
|
589
|
+
};
|
|
590
|
+
|
|
285
591
|
const out = new Map<number, MipsJT>();
|
|
286
592
|
for (let i = 0; i < instrs.length; i++) {
|
|
287
593
|
const jr = instrs[i];
|
|
@@ -291,10 +597,12 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
|
|
|
291
597
|
let rV = jr.ops[0];
|
|
292
598
|
// Optional IDO `+gp`: `addu rV, rL, gp` before the load — the loaded value is the other operand.
|
|
293
599
|
let ldIdx = defBefore(rV, i);
|
|
600
|
+
let gpIdx = -1;
|
|
294
601
|
if (ldIdx >= 0 && instrs[ldIdx].mnemonic === 'addu') {
|
|
295
602
|
const [a, b] = [instrs[ldIdx].ops[1], instrs[ldIdx].ops[2]];
|
|
296
603
|
if (a === 'gp' || b === 'gp') {
|
|
297
604
|
rV = a === 'gp' ? b : a;
|
|
605
|
+
gpIdx = ldIdx;
|
|
298
606
|
ldIdx = defBefore(rV, ldIdx);
|
|
299
607
|
}
|
|
300
608
|
}
|
|
@@ -302,9 +610,6 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
|
|
|
302
610
|
continue;
|
|
303
611
|
} // rV = *(rAddr)
|
|
304
612
|
const mem = parseMem(instrs[ldIdx].ops[1]);
|
|
305
|
-
if (mem.off !== 0) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
613
|
const aIdx = defBefore(mem.base, ldIdx); // rAddr = base + index
|
|
309
614
|
if (aIdx < 0 || instrs[aIdx].mnemonic !== 'addu') {
|
|
310
615
|
continue;
|
|
@@ -312,74 +617,137 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
|
|
|
312
617
|
const [oa, ob] = [instrs[aIdx].ops[1], instrs[aIdx].ops[2]];
|
|
313
618
|
// One operand is the shifted index (`sll rIdx, scrut, 2` — identity guard), the other the table base.
|
|
314
619
|
let scrutReg: string | null = null,
|
|
315
|
-
table: { sym: string; addend: number } | null = null
|
|
620
|
+
table: { sym: string; addend: number } | null = null,
|
|
621
|
+
sllIdx = -1,
|
|
622
|
+
baseIdx = -1,
|
|
623
|
+
hoisted: HoistedDispatch | null = null;
|
|
316
624
|
for (const [idxCand, baseCand] of [
|
|
317
625
|
[oa, ob],
|
|
318
626
|
[ob, oa],
|
|
319
627
|
] as const) {
|
|
320
628
|
const si = defBefore(idxCand, aIdx);
|
|
321
629
|
const bi2 = defBefore(baseCand, aIdx);
|
|
322
|
-
|
|
630
|
+
// A base the dispatch's block does not form is a HOISTED one (`hoistedTable`), whose `%lo`
|
|
631
|
+
// its own `addiu` carries: the load reads displacement 0 off it.
|
|
632
|
+
const out = bi2 < 0 && si >= 0 && mem.off === 0 ? hoistedTable(baseCand) : null;
|
|
633
|
+
if (si < 0 || (bi2 < 0 && !out)) {
|
|
323
634
|
continue;
|
|
324
635
|
}
|
|
325
636
|
const sll = instrs[si];
|
|
326
637
|
if (sll.mnemonic !== 'sll' || (sll.ops[2] !== '0x2' && sll.ops[2] !== '2')) {
|
|
327
638
|
continue;
|
|
328
639
|
} // index*4
|
|
329
|
-
const tb =
|
|
640
|
+
const tb = out ?? inBlockTable(instrs[bi2], instrs[ldIdx], mem.off);
|
|
330
641
|
if (!tb) {
|
|
331
642
|
continue;
|
|
332
643
|
}
|
|
333
644
|
scrutReg = sll.ops[1];
|
|
334
645
|
table = tb;
|
|
646
|
+
[sllIdx, baseIdx] = [si, bi2];
|
|
647
|
+
hoisted = out ? { reg: baseCand, table: out } : null;
|
|
335
648
|
break;
|
|
336
649
|
}
|
|
337
650
|
if (!scrutReg || !table) {
|
|
338
651
|
continue;
|
|
339
652
|
}
|
|
340
|
-
// Bounds: `sltiu tmp, scrutReg, N ; beqz tmp, DEF
|
|
341
|
-
// block
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (c.mnemonic === 'sltiu' && c.ops[1] === scrutReg) {
|
|
346
|
-
const tmp = c.ops[0];
|
|
347
|
-
// the beqz reading tmp (the block terminator); it may follow the sltiu directly.
|
|
348
|
-
for (let k = j + 1; k < instrs.length && k <= j + 3; k++) {
|
|
349
|
-
if (instrs[k].mnemonic === 'beqz' && instrs[k].ops[0] === tmp && instrs[k].target !== undefined) {
|
|
350
|
-
// `sltiu tmp, scrut, N` bounds `scrut < N` — the immediate IS the case count (unlike PPC's
|
|
351
|
-
// `cmplwi rS, N-1` which uses the max index). So N = the immediate, not immediate+1.
|
|
352
|
-
bounds = { addr: instrs[k].addr, n: parseImm(c.ops[2]), def: instrs[k].target! };
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
653
|
+
// Bounds: `sltiu tmp, scrutReg, N ; beqz tmp, DEF`, the beqz the nearest transfer before the
|
|
654
|
+
// dispatch. Its block emits the `switch_br`, reading the scrutinee where the beqz reads `tmp`.
|
|
655
|
+
const bounds = mipsBounds(instrs, i, scrutReg, likely);
|
|
656
|
+
if (!bounds) {
|
|
657
|
+
continue;
|
|
358
658
|
}
|
|
359
|
-
|
|
659
|
+
// THE DISPATCH BLOCK — after the bounds branch's delay slot, through the `jr` and its own slot —
|
|
660
|
+
// is pruned unread once the table is recovered, so it may hold the idiom and nothing else:
|
|
661
|
+
// anything more would be dropped. Nothing may branch into it past the bounds check, the `jr`'s
|
|
662
|
+
// slot must be empty, and nothing from the bounds slot on may rewrite the scrutinee before the
|
|
663
|
+
// `sll` scales it (IDO's in-place `sll` IN the slot is that `sll`).
|
|
664
|
+
const k = bounds.at;
|
|
665
|
+
const idiom = new Set([ldIdx, aIdx, gpIdx, sllIdx, baseIdx]);
|
|
666
|
+
if (
|
|
667
|
+
instrs[i + 1]?.mnemonic !== 'nop' ||
|
|
668
|
+
entersBetween(instrs, instrs[k].addr, instrs[i + 1].addr) ||
|
|
669
|
+
instrs.slice(k + 2, i).some((_, o) => !idiom.has(k + 2 + o)) ||
|
|
670
|
+
instrs.slice(k + 1, sllIdx).some((x) => writtenReg(x) === scrutReg)
|
|
671
|
+
) {
|
|
360
672
|
continue;
|
|
361
673
|
}
|
|
362
674
|
const caseAddrs = readJumpTable(ad, table.sym, table.addend, bounds.n);
|
|
363
675
|
if (!caseAddrs) {
|
|
364
676
|
continue;
|
|
365
677
|
}
|
|
366
|
-
|
|
678
|
+
// Only the words past the bounds branch's delay slot are pruned: the slot runs on both paths and
|
|
679
|
+
// is lifted (KMC's `sll`, IDO's in-place one, a `lui` gcc puts there).
|
|
680
|
+
const leaves = idiomLeaves(
|
|
681
|
+
[...idiom]
|
|
682
|
+
.filter((x) => x > k + 1)
|
|
683
|
+
.sort((a, b) => a - b)
|
|
684
|
+
.flatMap((x) => {
|
|
685
|
+
const r = writtenReg(instrs[x]);
|
|
686
|
+
return r === null ? [] : [{ reg: r, scaled: x === sllIdx }];
|
|
687
|
+
}),
|
|
688
|
+
);
|
|
689
|
+
out.set(bounds.addr, {
|
|
690
|
+
scrutReg,
|
|
691
|
+
caseAddrs,
|
|
692
|
+
defaultAddr: bounds.def,
|
|
693
|
+
jrAddr: jr.addr,
|
|
694
|
+
entry: bounds.entry,
|
|
695
|
+
hoisted,
|
|
696
|
+
leaves,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
// A bound read off its block's ENTRY EDGES holds only for the edges `mipsBounds` saw. A table's
|
|
700
|
+
// arm is an edge too, and no table existed while it looked: one landing there declines.
|
|
701
|
+
const arms = new Set([...out.values()].flatMap((jt) => [...jt.caseAddrs, jt.defaultAddr]));
|
|
702
|
+
for (const [addr, jt] of out) {
|
|
703
|
+
if (jt.entry !== null && arms.has(jt.entry)) {
|
|
704
|
+
out.delete(addr);
|
|
705
|
+
}
|
|
367
706
|
}
|
|
368
707
|
return out;
|
|
369
708
|
}
|
|
370
709
|
|
|
371
|
-
// BRANCH-LIKELY → the ordinary branch that tests the same way, in place
|
|
372
|
-
//
|
|
373
|
-
//
|
|
710
|
+
// BRANCH-LIKELY → the ordinary branch that tests the same way, in place. The mnemonic is all that
|
|
711
|
+
// differs in the COMPARISON; what differs in the FLOW is the delay slot, which `toBlocks` then
|
|
712
|
+
// places on the taken edge alone. `likely` holds the branches so placed.
|
|
713
|
+
//
|
|
714
|
+
// UNLESS THE SLOT IS THE TARGET'S OWN FIRST WORD, STOLEN — which is how IDO, KMC and GCC 2.7.2 fill
|
|
715
|
+
// a likely slot: `bnezl r,T; S` where the word at T-4 is also S (ido 4932 of the 4984 likely
|
|
716
|
+
// branches in the af splat corpus). Then the branch is retargeted to T-4 with an empty slot,
|
|
717
|
+
// `bnez r,T-4; nop`, and `freed` holds the slot addresses so emptied. That is exact on both paths
|
|
718
|
+
// WHATEVER ELSE REACHES T-4: taken runs S then T either way, and not taken annuls S either way. It
|
|
719
|
+
// matters because the copy otherwise becomes a block of its own on the taken edge, which reaches T
|
|
720
|
+
// past every join T-4 is part of — a switch arm whose `break` it skips, so the arms stop
|
|
721
|
+
// linearizing (ido), or a recovered switch's bounds branch that cannot annul its slot (KMC). The
|
|
722
|
+
// rewrite holds only while every word means what its bytes say, so it is made only when:
|
|
723
|
+
// - the two words are the same instruction with the same relocation — text alone cannot tell
|
|
724
|
+
// `lui at,0x0` against one symbol from the same text against another;
|
|
725
|
+
// - T-4 is not the slot itself (T = branch+8, a slot that skips only itself: nothing to share);
|
|
726
|
+
// - T-4 and T are both in the function, and T-4 is no transfer's delay slot — a slot runs only
|
|
727
|
+
// after its own transfer, so falling into one from T-8's side is not what its bytes say;
|
|
728
|
+
// - the slot completes no `%lo`. Its `%hi` is a register each path loads for itself (`lui v1;
|
|
729
|
+
// b T; lw v1,%lo(g)(v1)` against `lui v1; …; lw v1,%lo(g)(v1); T:`), and retargeted, the one
|
|
730
|
+
// copy at T-4 reads that register at a join, where it is the block parameter and not the half:
|
|
731
|
+
// the high/low fold would refuse a function the kept slot lifts. Exact either way, so the
|
|
732
|
+
// shape keeps its slot rather than trade a lift for a decline.
|
|
733
|
+
// A branch landing on the slot refuses below, rewrite or not; a recovered jump-table arm landing on
|
|
734
|
+
// a freed slot refuses in `lift`, where the tables exist.
|
|
374
735
|
//
|
|
375
736
|
// Refuses, by name, every shape that placement cannot model. A nullified slot read as an ordinary
|
|
376
737
|
// always-executed one is not a cosmetic error: `absi` would return `-x` for every `x >= 0`, and a
|
|
377
738
|
// store in such a slot would be performed on a path that never performs it. That is C which
|
|
378
739
|
// compiles and is wrong — strictly worse than the decline it would replace.
|
|
379
|
-
function normaliseBranchLikely(
|
|
740
|
+
function normaliseBranchLikely(
|
|
741
|
+
name: string,
|
|
742
|
+
instrs: Instr[],
|
|
743
|
+
startAddr: number,
|
|
744
|
+
relocOf: (ins: Instr) => DisasmReloc | null,
|
|
745
|
+
options: LiftOptions = {},
|
|
746
|
+
): { likely: Set<number>; freed: Set<number> } {
|
|
380
747
|
const likely = instrs.filter((ins) => ins.mnemonic in LIKELY_BASE);
|
|
381
|
-
|
|
382
|
-
|
|
748
|
+
const uncond = instrs.filter(isUncond);
|
|
749
|
+
if (likely.length === 0 && uncond.length === 0) {
|
|
750
|
+
return { likely: new Set(), freed: new Set() };
|
|
383
751
|
}
|
|
384
752
|
const refusal = (ins: Instr, why: string) =>
|
|
385
753
|
new FrontendUnsupportedError(
|
|
@@ -397,6 +765,32 @@ function normaliseBranchLikely(name: string, instrs: Instr[], startAddr: number)
|
|
|
397
765
|
const at = new Map(instrs.map((ins) => [ins.addr, ins]));
|
|
398
766
|
const hex = (a: number) => `0x${a.toString(16)}`;
|
|
399
767
|
const rewritten = new Set<number>();
|
|
768
|
+
const freed = new Set<number>();
|
|
769
|
+
// The words as listed, before any slot below is emptied.
|
|
770
|
+
const listed = new Map(at);
|
|
771
|
+
// `br T; S` → `br T-4; nop` when S is the word at T-4, stolen; true when it did.
|
|
772
|
+
const unsteal = (br: Instr, slot: Instr): boolean => {
|
|
773
|
+
const target = br.target!;
|
|
774
|
+
const stolen = at.get(target - 4);
|
|
775
|
+
const beforeStolen = at.get(target - 8);
|
|
776
|
+
if (
|
|
777
|
+
target === br.addr + 8 ||
|
|
778
|
+
stolen === undefined ||
|
|
779
|
+
!at.has(target) ||
|
|
780
|
+
(beforeStolen !== undefined && isControlTransfer(beforeStolen)) ||
|
|
781
|
+
!sameWord(slot, stolen, relocOf) ||
|
|
782
|
+
relocOf(slot)?.type === 'R_MIPS_LO16'
|
|
783
|
+
) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
br.target = target - 4;
|
|
787
|
+
// The emptied slot is a fresh `nop`: the old word's relocation must not ride along with it.
|
|
788
|
+
const nop: Instr = { addr: slot.addr, mnemonic: 'nop', ops: [] };
|
|
789
|
+
instrs[instrs.indexOf(slot)] = nop;
|
|
790
|
+
at.set(slot.addr, nop);
|
|
791
|
+
freed.add(slot.addr);
|
|
792
|
+
return true;
|
|
793
|
+
};
|
|
400
794
|
for (const br of likely) {
|
|
401
795
|
const slot = at.get(br.addr + 4);
|
|
402
796
|
const fallThrough = at.get(br.addr + 8);
|
|
@@ -433,9 +827,68 @@ function normaliseBranchLikely(name: string, instrs: Instr[], startAddr: number)
|
|
|
433
827
|
throw refusal(br, 'another branch targets the delay slot, which would run it unconditioned');
|
|
434
828
|
}
|
|
435
829
|
br.mnemonic = LIKELY_BASE[br.mnemonic];
|
|
436
|
-
|
|
830
|
+
if (!unsteal(br, slot)) {
|
|
831
|
+
rewritten.add(br.addr);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// AN UNCONDITIONAL `b`/`j` RUNS ITS SLOT ON ITS ONE EDGE ALONE, which is all the proof above asks
|
|
835
|
+
// of the slot, and the same fill steals the target's first word into it (`b T; S` with S at T-4:
|
|
836
|
+
// ido 1234, gcc 2.7.2 1075, KMC 51 in the splat corpus). Unretargeted, the copy is the `b`'s own
|
|
837
|
+
// body and T-4 a separate block reaching T, so a join that the two share is split into one arm
|
|
838
|
+
// per copy. Asked here only where nothing else depends on the slot: not a slot already empty, not
|
|
839
|
+
// a `b` in some transfer's own delay slot, not a slot another branch lands on (entered there, the
|
|
840
|
+
// word runs and the flow falls on to the `b`'s next word, not to T), and not a `j` relocated
|
|
841
|
+
// against a symbol, whose printed target is a field rather than an address (it refuses below).
|
|
842
|
+
for (const br of uncond) {
|
|
843
|
+
const slot = at.get(br.addr + 4);
|
|
844
|
+
const prev = at.get(br.addr - 4);
|
|
845
|
+
const reloc = br.mnemonic === 'j' ? relocOf(br) : null;
|
|
846
|
+
if (
|
|
847
|
+
br.target === undefined ||
|
|
848
|
+
slot === undefined ||
|
|
849
|
+
slot.mnemonic === 'nop' ||
|
|
850
|
+
isControlTransfer(slot) ||
|
|
851
|
+
targets.has(slot.addr) ||
|
|
852
|
+
(prev === undefined ? br.addr !== startAddr : isControlTransfer(prev)) ||
|
|
853
|
+
(reloc !== null && !reloc.sym.startsWith('.'))
|
|
854
|
+
) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
// A `b` THAT JUMPS OVER EXACTLY THE ONE COPY (`b T; S; S; T:`, KMC `sw_void`, ido `mergeu16`)
|
|
858
|
+
// would, retargeted, jump to its own next word: the two arms are each written out, and nothing
|
|
859
|
+
// was stolen from T. That is a fact of the jump, so it is answered here.
|
|
860
|
+
if (br.target - 4 === br.addr + 8) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
// A COMMON TAIL OF TWO OR MORE WORDS is the question the asm leaves open: `sll; b T; sra`
|
|
864
|
+
// against `sll; sra; T:` (ido `mergenarrow`) is one sign extension per arm, written out twice,
|
|
865
|
+
// or a stolen `sra` whose arms the source merged — the same object either way. Retargeted, the
|
|
866
|
+
// `<< 16` stays in each arm and the `>> 16` moves past the join; kept, each arm reads as the
|
|
867
|
+
// one `(s16)` it is. Neither reading wins on the corpus (over the af/mp3/sbk2 splat functions
|
|
868
|
+
// with this shape, retargeting shortens 19 lifts and lengthens 3, `func_80026D34` the
|
|
869
|
+
// `mergenarrow` case), so the default retargets and the kept slot is the `/kept-slot` lift
|
|
870
|
+
// variation the differ referees. A likely slot emptied above the `b` is no common tail; one
|
|
871
|
+
// emptied before T still counts, as the word it was. A `nop` is no tail at all.
|
|
872
|
+
const before = listed.get(br.target - 8);
|
|
873
|
+
const tail =
|
|
874
|
+
prev !== undefined && prev.mnemonic !== 'nop' && before !== undefined && sameWord(prev, before, relocOf);
|
|
875
|
+
if (tail && options.keepTailSlots === true) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (unsteal(br, slot) && tail) {
|
|
879
|
+
options.onTailSlot?.();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return { likely: rewritten, freed };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Two words are the same instruction: one spelling and one relocation (or none on either). */
|
|
886
|
+
function sameWord(a: Instr, b: Instr, relocOf: (ins: Instr) => DisasmReloc | null): boolean {
|
|
887
|
+
if (a.mnemonic !== b.mnemonic || a.ops.join() !== b.ops.join() || a.target !== b.target) {
|
|
888
|
+
return false;
|
|
437
889
|
}
|
|
438
|
-
|
|
890
|
+
const [ra, rb] = [relocOf(a), relocOf(b)];
|
|
891
|
+
return ra === null || rb === null ? ra === rb : ra.type === rb.type && ra.sym === rb.sym && ra.addend === rb.addend;
|
|
439
892
|
}
|
|
440
893
|
|
|
441
894
|
// EVERY control transfer this frontend models has a delay slot — the word at `branch + 4`, which
|
|
@@ -466,6 +919,368 @@ function checkDelaySlots(name: string, instrs: Instr[]): void {
|
|
|
466
919
|
}
|
|
467
920
|
}
|
|
468
921
|
|
|
922
|
+
/** WHETHER THE RETURN ENDING BLOCK `bi` MAY HAND BACK A 64-BIT PAIR, read off the asm: O32 returns
|
|
923
|
+
* one in v0:v1, the low word in v1, and v1 is also ordinary scratch, so the question is what each
|
|
924
|
+
* write of v1 that reaches the return was made for. Two answers say "the low word":
|
|
925
|
+
*
|
|
926
|
+
* - NOTHING IN THE FUNCTION READS IT, on any path from it. An optimising compiler computes no
|
|
927
|
+
* dead value, so a write no instruction reads is one the caller reads (`lw v0,16(sp);
|
|
928
|
+
* lw v1,20(sp)` for `return e;`, `mflo v1` for a product). A loop's induction update is read
|
|
929
|
+
* round the back edge, and a value one arm of a branch reads is read, so neither counts.
|
|
930
|
+
* - IT IS AN `addu`/`addiu` WHOSE SUM IS COMPARED, unsigned, against what it added: the carry out
|
|
931
|
+
* of a 64-bit add's low word into its high one (`addu v1,a3,a0; sltu t,v1,a0`, `addiu v1,a1,5;
|
|
932
|
+
* sltiu t,v1,5`). A pointer bound check compares the sum against something else (`addu v1,a1,a0;
|
|
933
|
+
* sltu v0,v0,v1`). A function that computes a carry and returns only the high word is declined
|
|
934
|
+
* with the pairs — loud, and the price of one rule.
|
|
935
|
+
* - IT IS A COPY OF A REGISTER THAT STILL HOLDS THE SAME VALUE wherever v1 is read: the reads
|
|
936
|
+
* could have named the source, so the copy is made for the caller (`move v1,a3; jr ra;
|
|
937
|
+
* sw v1,4(a0)` for `*p = x; return x;`). A copy made because its source is about to be
|
|
938
|
+
* overwritten (a swap, a pointer bumped after its old value is kept), or read past a join, is
|
|
939
|
+
* scratch.
|
|
940
|
+
*
|
|
941
|
+
* A call ends a path either way: it destroys v1 (`clobberedByCall`), and its delay slot runs
|
|
942
|
+
* before it. The entry ends the backward walk, since what v1 held there is no value this
|
|
943
|
+
* function computed. */
|
|
944
|
+
function v1MayHoldLowWord(blocks: readonly MipsBlock[], preds: readonly (readonly number[])[], bi: number): boolean {
|
|
945
|
+
const readsV1 = (ins: Instr) => {
|
|
946
|
+
const dest = writtenReg(ins) !== null && writtenReg(ins) === ins.ops[0];
|
|
947
|
+
return ins.ops.some((o, k) => !(dest && k === 0) && (o === 'v1' || /\(v1\)$/.test(o)));
|
|
948
|
+
};
|
|
949
|
+
// Each block in execution order: a call's delay slot runs before the call.
|
|
950
|
+
const runs = blocks.map((b) => {
|
|
951
|
+
const seq = [...b.body, ...(b.branch ? [b.branch] : []), ...(b.delay ? [b.delay] : [])];
|
|
952
|
+
for (let k = 0; k + 1 < seq.length; k++) {
|
|
953
|
+
if (isCallInstr(seq[k])) {
|
|
954
|
+
[seq[k], seq[k + 1]] = [seq[k + 1], seq[k]];
|
|
955
|
+
k++;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return seq;
|
|
959
|
+
});
|
|
960
|
+
const succs = blocks.map((): number[] => []);
|
|
961
|
+
preds.forEach((ps, to) => ps.forEach((from) => succs[from].push(to)));
|
|
962
|
+
// What follows the write at `runs[at][k]` on every path, up to a call or the next write of v1:
|
|
963
|
+
// the instructions that read it, and whether register `src` is written meanwhile.
|
|
964
|
+
const after = (at: number, k: number, src: string | undefined) => {
|
|
965
|
+
const readers: Instr[] = [];
|
|
966
|
+
let srcWritten = false;
|
|
967
|
+
const seen = new Set<number>();
|
|
968
|
+
const from = (blk: number, start: number) => {
|
|
969
|
+
for (const ins of runs[blk].slice(start)) {
|
|
970
|
+
if (readsV1(ins)) {
|
|
971
|
+
readers.push(ins);
|
|
972
|
+
}
|
|
973
|
+
if (isCallInstr(ins) || writtenReg(ins) === 'v1') {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
srcWritten ||= src !== undefined && writtenReg(ins) === src;
|
|
977
|
+
}
|
|
978
|
+
for (const n of succs[blk]) {
|
|
979
|
+
if (!seen.has(n)) {
|
|
980
|
+
seen.add(n);
|
|
981
|
+
from(n, 0);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
from(at, k + 1);
|
|
986
|
+
return { readers, srcWritten };
|
|
987
|
+
};
|
|
988
|
+
const lowWord = (at: number, k: number): boolean => {
|
|
989
|
+
const ins = runs[at][k];
|
|
990
|
+
const [, x, y] = ins.ops;
|
|
991
|
+
// A register copy's source, unless that is zero (a constant).
|
|
992
|
+
const copied = copiedReg(ins);
|
|
993
|
+
const src = copied === undefined || isZero(copied) ? undefined : copied;
|
|
994
|
+
const { readers, srcWritten } = after(at, k, src);
|
|
995
|
+
// The carry compares the sum against what was added: `sltu t,v1,X` for `addu v1,X,Y` (either
|
|
996
|
+
// addend other than v1 itself), `sltiu t,v1,k` for `addiu v1,X,k`.
|
|
997
|
+
const carries = (r: Instr) =>
|
|
998
|
+
r.ops[1] === 'v1' &&
|
|
999
|
+
((ins.mnemonic === 'addu' && r.mnemonic === 'sltu' && r.ops[2] !== 'v1' && [x, y].includes(r.ops[2])) ||
|
|
1000
|
+
(ins.mnemonic === 'addiu' && r.mnemonic === 'sltiu' && parseImm(r.ops[2] ?? '') === parseImm(y ?? '')));
|
|
1001
|
+
// A copy is judged only where its every read follows it in its own block: a read past a join
|
|
1002
|
+
// is where a compiler coalesces each arm's value into one register (`addu v1,a1,zero` on one
|
|
1003
|
+
// arm, `ori v1,a1,0xe000` on the other), which is scratch.
|
|
1004
|
+
const local = readers.every((r) => runs[at].indexOf(r) > k);
|
|
1005
|
+
return readers.length === 0 || readers.some(carries) || (src !== undefined && !srcWritten && local);
|
|
1006
|
+
};
|
|
1007
|
+
// Every write of v1 that reaches the end of `bi`, walking back.
|
|
1008
|
+
const seen = new Set<number>();
|
|
1009
|
+
const back = (at: number, end: number): boolean => {
|
|
1010
|
+
for (let k = end - 1; k >= 0; k--) {
|
|
1011
|
+
const ins = runs[at][k];
|
|
1012
|
+
if (isCallInstr(ins)) {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
if (writtenReg(ins) === 'v1') {
|
|
1016
|
+
return lowWord(at, k);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return preds[at].some((p) => !seen.has(p) && (seen.add(p), back(p, runs[p].length)));
|
|
1020
|
+
};
|
|
1021
|
+
return back(bi, runs[bi].length);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/** THE FRAME DEPTH AT EACH INSTRUCTION: how many bytes below the `sp` the function was entered
|
|
1025
|
+
* with it runs, so an `off(sp)` it makes names entry-relative byte `off - depth`. Exact or absent —
|
|
1026
|
+
* null wherever one number per instruction is not proven, and every reader then keeps its refusal.
|
|
1027
|
+
*
|
|
1028
|
+
* Proven for the frame every compiler here builds: at most one `addiu sp,sp,-N`, in the entry
|
|
1029
|
+
* block (anywhere in it, IDO's leaves load before they push), and `addiu sp,sp,N` only in a block
|
|
1030
|
+
* that returns, after which the block runs at depth 0. The entry runs from depth 0 and every other
|
|
1031
|
+
* block from N, which holds because the entry runs once and every other block is reached through
|
|
1032
|
+
* it. Refused: any other write to `sp`, a second push, a push outside the entry or in an entry a
|
|
1033
|
+
* branch re-enters (the preheader's case), a pop of another size or in a block that does not
|
|
1034
|
+
* return. */
|
|
1035
|
+
function frameDepths(blocks: readonly MipsBlock[], entryReentered: boolean): ReadonlyMap<Instr, number> | null {
|
|
1036
|
+
const depths = new Map<Instr, number>();
|
|
1037
|
+
const adjust = (ins: Instr): number | null | undefined => {
|
|
1038
|
+
if (!isStackPtr(writtenReg(ins) ?? '')) {
|
|
1039
|
+
return undefined; // leaves sp alone
|
|
1040
|
+
}
|
|
1041
|
+
const by = parseImm(ins.ops[2] ?? '');
|
|
1042
|
+
return (ins.mnemonic === 'addiu' || ins.mnemonic === 'addi') && isStackPtr(ins.ops[1] ?? '') && Number.isFinite(by)
|
|
1043
|
+
? by
|
|
1044
|
+
: null;
|
|
1045
|
+
};
|
|
1046
|
+
const pushes = blocks.flatMap((b) => [...b.body, ...(b.delay ? [b.delay] : [])]).filter((i) => (adjust(i) ?? 0) < 0);
|
|
1047
|
+
if (pushes.length > 1) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
const size = pushes.length === 1 ? -adjust(pushes[0])! : 0;
|
|
1051
|
+
for (const [bi, b] of blocks.entries()) {
|
|
1052
|
+
let depth = bi === 0 ? 0 : size;
|
|
1053
|
+
for (const ins of [...b.body, ...(b.delay ? [b.delay] : [])]) {
|
|
1054
|
+
depths.set(ins, depth);
|
|
1055
|
+
const by = adjust(ins);
|
|
1056
|
+
if (by === undefined) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (by === null) {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
if (by < 0 && (bi !== 0 || entryReentered || depth !== 0)) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
if (by > 0 && (by !== depth || b.branch === null || !isReturn(b.branch))) {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
depth -= by;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return depths;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** What the return proof knows at one point: which registers hold the caller's return address on
|
|
1075
|
+
* every path (`regs`, `ra` among them while it is untouched), the instruction that last wrote
|
|
1076
|
+
* something else into `ra` on some path (`lost`), the entry-`sp` offset of the word it was saved
|
|
1077
|
+
* in (`slot`), and `sp`'s signed offset from its entry value (`depth`, -24 after `addiu sp,sp,-24`;
|
|
1078
|
+
* the opposite sign to `frameDepths`' positive depth), each null where unknown. */
|
|
1079
|
+
interface RaState {
|
|
1080
|
+
readonly regs: ReadonlySet<string>;
|
|
1081
|
+
readonly lost: Instr | null;
|
|
1082
|
+
readonly slot: number | null;
|
|
1083
|
+
readonly depth: number | null;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/** Bytes a store mnemonic writes. Only the stores the lowering models (`emitStore`) are named; the
|
|
1087
|
+
* proof treats any other instruction that writes no register and addresses memory through `sp`
|
|
1088
|
+
* (`swl`/`swr`, `sc`, `sd*`, `swc1`/`sdc1`, all refused earlier by `opaqueDest`'s `storeClass` or
|
|
1089
|
+
* the FPU arm today) as a store of unknown bytes, which overwrites the save word. */
|
|
1090
|
+
const STORE_BYTES: Readonly<Record<string, [before: number, width: number]>> = {
|
|
1091
|
+
sb: [0, 1],
|
|
1092
|
+
sh: [0, 2],
|
|
1093
|
+
sw: [0, 4],
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
/** Whether `r` is one of the target's `kernelRegs` (O32's `k0`/`k1`), in any spelling. */
|
|
1097
|
+
const isKernelReg = (kernelRegs: readonly string[], r: string) =>
|
|
1098
|
+
kernelRegs.includes(r === '$26' ? 'k0' : r === '$27' ? 'k1' : r.replace(/^\$/, ''));
|
|
1099
|
+
|
|
1100
|
+
/** EVERY `jr ra` OF THIS FUNCTION JUMPS TO THE CALLER'S RETURN ADDRESS, or the lift declines — the
|
|
1101
|
+
* MIPS half of what `refuseReturnsNotThroughCallersLr` asks of Thumb (frontend/thumb.ts).
|
|
1102
|
+
*
|
|
1103
|
+
* `jr ra` reads as a return from its operand alone, and it is one only where `ra` holds the address
|
|
1104
|
+
* this function was called with. It reads `ra` when it runs, before its delay slot. A forward
|
|
1105
|
+
* dataflow over the blocks tracks where that address is:
|
|
1106
|
+
* • `ra` holds it at entry; a call (`jal`) writes `ra`, runs its delay slot, and only then
|
|
1107
|
+
* destroys every register O32 lets a callee destroy (and its home area), so a slot's write is
|
|
1108
|
+
* exposed to the callee; a slot that writes `ra` refuses, since the callee then returns
|
|
1109
|
+
* somewhere other than the code after the call; a copy (`move`, `addu`/`or` with `zero`)
|
|
1110
|
+
* carries it on; any other write to a register — `lw ra,0(a0)`, `addiu ra,ra,8`, `lui ra` —
|
|
1111
|
+
* loses it;
|
|
1112
|
+
* • a `sw` of a register holding it through `sp` puts it in that word, named by its offset from
|
|
1113
|
+
* the entry `sp` (this walk follows `addiu sp,sp,N`; any other write to `sp` makes the depth
|
|
1114
|
+
* unknown), and a `lw` from that word copies it back out. Any other store reaching those bytes,
|
|
1115
|
+
* a store through `sp` at an unknown depth or of unknown width (`STORE_BYTES`), and a call whose
|
|
1116
|
+
* home area (`[sp, sp+16)` at the call) holds the word overwrite it.
|
|
1117
|
+
* Paths meet by agreement: a register or the word holds the address only if it does on every path
|
|
1118
|
+
* in, and a depth the paths disagree on is unknown.
|
|
1119
|
+
*
|
|
1120
|
+
* So `move ra,a1; jr ra`, `lw ra,0(a0)`, `addiu ra,ra,8`, `lui ra,0x8000`, a `move ra` on one path
|
|
1121
|
+
* only, and a `jal` whose `ra` nothing reloaded all decline, where every compiler's `sw ra,N(sp)` …
|
|
1122
|
+
* `lw ra,N(sp)` and a leaf that never names `ra` are returns. A reload from a word another
|
|
1123
|
+
* register was saved in already refuses (`emitLoad`). No compiler emits a violation; hand-written
|
|
1124
|
+
* asm can, and it would otherwise lift as a C `return`. A tail call (`j` to another function) and an indirect
|
|
1125
|
+
* `jr` already refuse in `lift`, so `jr ra` is the only exit asked.
|
|
1126
|
+
*
|
|
1127
|
+
* A copy into a kernel register (`kernelRegs`, O32's `k0`/`k1`) holds nothing: an exception handler
|
|
1128
|
+
* may overwrite it at any instruction, so `move k0,ra` … `move ra,k0` declines.
|
|
1129
|
+
*
|
|
1130
|
+
* NOT ASKED: a store through a register holding a frame address is not traced to the save word.
|
|
1131
|
+
* A compiler addresses that word only through `sp`. */
|
|
1132
|
+
function refuseReturnsNotThroughCallersRa(
|
|
1133
|
+
name: string,
|
|
1134
|
+
blocks: readonly MipsBlock[],
|
|
1135
|
+
succIdx: readonly number[][],
|
|
1136
|
+
callerSaved: readonly string[],
|
|
1137
|
+
kernelRegs: readonly string[],
|
|
1138
|
+
): void {
|
|
1139
|
+
const canon = (r: string) => (r === '$ra' || r === '$31' ? 'ra' : r);
|
|
1140
|
+
const join = (a: RaState, b: RaState): RaState => {
|
|
1141
|
+
const regs = new Set([...a.regs].filter((r) => b.regs.has(r)));
|
|
1142
|
+
const depth = a.depth !== null && a.depth === b.depth ? a.depth : null;
|
|
1143
|
+
return {
|
|
1144
|
+
regs,
|
|
1145
|
+
lost: regs.has('ra') ? null : ((a.regs.has('ra') ? null : a.lost) ?? (b.regs.has('ra') ? null : b.lost)),
|
|
1146
|
+
slot: depth !== null && a.slot !== null && a.slot === b.slot ? a.slot : null,
|
|
1147
|
+
depth,
|
|
1148
|
+
};
|
|
1149
|
+
};
|
|
1150
|
+
const same = (a: RaState, b: RaState) =>
|
|
1151
|
+
a.lost === b.lost &&
|
|
1152
|
+
a.slot === b.slot &&
|
|
1153
|
+
a.depth === b.depth &&
|
|
1154
|
+
a.regs.size === b.regs.size &&
|
|
1155
|
+
[...a.regs].every((r) => b.regs.has(r));
|
|
1156
|
+
|
|
1157
|
+
const run = (bi: number, inState: RaState): RaState => {
|
|
1158
|
+
const regs = new Set(inState.regs);
|
|
1159
|
+
let { lost, slot, depth } = inState;
|
|
1160
|
+
const b = blocks[bi];
|
|
1161
|
+
const step = (ins: Instr): void => {
|
|
1162
|
+
const mn = ins.mnemonic;
|
|
1163
|
+
if (mn === 'jr' && canon(ins.ops[0] ?? '') === 'ra' && !regs.has('ra')) {
|
|
1164
|
+
throw new FrontendUnsupportedError(
|
|
1165
|
+
`cannot lift '${name}': the 'jr ra' at 0x${ins.addr.toString(16)} may not return through the caller's ` +
|
|
1166
|
+
`return address — ` +
|
|
1167
|
+
(lost === null
|
|
1168
|
+
? `ra does not hold it on some path`
|
|
1169
|
+
: `on some path the '${lost.mnemonic}' at 0x${lost.addr.toString(16)} wrote ra, and no reload from ` +
|
|
1170
|
+
`the word it was saved in put it back`),
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
const store = STORE_BYTES[mn];
|
|
1174
|
+
if (store !== undefined) {
|
|
1175
|
+
const mem = ins.ops[1] ?? '';
|
|
1176
|
+
if (mem.includes('(') && isStackPtr(parseMem(mem).base)) {
|
|
1177
|
+
const at = depth === null ? null : parseMem(mem).off + depth;
|
|
1178
|
+
if (mn === 'sw' && at !== null && regs.has(canon(ins.ops[0]))) {
|
|
1179
|
+
slot = at;
|
|
1180
|
+
} else if (at === null || (slot !== null && at - store[0] < slot + 4 && at - store[0] + store[1] > slot)) {
|
|
1181
|
+
slot = null;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const w = writtenReg(ins);
|
|
1187
|
+
if (w === null) {
|
|
1188
|
+
if (ins.ops.some((op) => op.includes('(') && isStackPtr(parseMem(op).base))) {
|
|
1189
|
+
slot = null;
|
|
1190
|
+
}
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const dest = canon(w);
|
|
1194
|
+
const mem = ins.ops[1] ?? '';
|
|
1195
|
+
const reload =
|
|
1196
|
+
mn === 'lw' &&
|
|
1197
|
+
slot !== null &&
|
|
1198
|
+
depth !== null &&
|
|
1199
|
+
mem.includes('(') &&
|
|
1200
|
+
isStackPtr(parseMem(mem).base) &&
|
|
1201
|
+
parseMem(mem).off + depth === slot;
|
|
1202
|
+
const copy = copiedReg(ins);
|
|
1203
|
+
const holds = !isKernelReg(kernelRegs, dest) && (reload || (copy !== undefined && regs.has(canon(copy))));
|
|
1204
|
+
if (isStackPtr(dest)) {
|
|
1205
|
+
depth =
|
|
1206
|
+
depth !== null &&
|
|
1207
|
+
/^addiu?$/.test(mn) &&
|
|
1208
|
+
isStackPtr(ins.ops[1] ?? '') &&
|
|
1209
|
+
/^-?(0x)?[0-9a-f]+$/i.test(ins.ops[2] ?? '')
|
|
1210
|
+
? depth + parseImm(ins.ops[2])
|
|
1211
|
+
: null;
|
|
1212
|
+
}
|
|
1213
|
+
if (holds) {
|
|
1214
|
+
regs.add(dest);
|
|
1215
|
+
} else {
|
|
1216
|
+
regs.delete(dest);
|
|
1217
|
+
if (dest === 'ra') {
|
|
1218
|
+
lost = ins;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (dest === 'ra' && holds) {
|
|
1222
|
+
lost = null;
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
// A call links `ra`, runs its delay slot, then enters the callee (the doc above). The lowering
|
|
1226
|
+
// refuses a `jal` whose slot is not the next word of its block, and every other linking transfer
|
|
1227
|
+
// before `toBlocks`, so this walk only asserts it.
|
|
1228
|
+
const seq = [...b.body, ...(b.branch ? [b.branch] : []), ...(b.delay ? [b.delay] : [])];
|
|
1229
|
+
for (let k = 0; k < seq.length; k++) {
|
|
1230
|
+
const call = seq[k];
|
|
1231
|
+
if (!isCallInstr(call)) {
|
|
1232
|
+
step(call);
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
regs.delete('ra');
|
|
1236
|
+
lost = call;
|
|
1237
|
+
const delay = seq[k + 1];
|
|
1238
|
+
if (delay?.addr !== call.addr + 4) {
|
|
1239
|
+
throw new Error(
|
|
1240
|
+
`internal: the '${call.mnemonic}' at 0x${call.addr.toString(16)} in '${name}' reached the return proof ` +
|
|
1241
|
+
`without its delay slot as the next instruction of its block, which the lowering's fill loop refuses`,
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
const w = writtenReg(delay);
|
|
1245
|
+
if (w !== null && canon(w) === 'ra') {
|
|
1246
|
+
throw new FrontendUnsupportedError(
|
|
1247
|
+
`cannot lift '${name}': the '${delay.mnemonic}' at 0x${delay.addr.toString(16)} in the delay slot of the ` +
|
|
1248
|
+
`'${call.mnemonic}' at 0x${call.addr.toString(16)} writes ra, so the callee may not return through the ` +
|
|
1249
|
+
`caller's return address to the code after the call`,
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
step(delay);
|
|
1253
|
+
k++;
|
|
1254
|
+
for (const r of callerSaved) {
|
|
1255
|
+
regs.delete(r);
|
|
1256
|
+
}
|
|
1257
|
+
if (slot !== null && (depth === null || (slot >= depth && slot < depth + STACK_ARGS_AT))) {
|
|
1258
|
+
slot = null;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return { regs, lost, slot, depth };
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
if (blocks.length === 0) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const into: Array<RaState | undefined> = blocks.map(() => undefined);
|
|
1268
|
+
into[0] = { regs: new Set(['ra']), lost: null, slot: null, depth: 0 };
|
|
1269
|
+
const work = [0];
|
|
1270
|
+
while (work.length) {
|
|
1271
|
+
const bi = work.pop()!;
|
|
1272
|
+
const out = run(bi, into[bi]!);
|
|
1273
|
+
for (const s of succIdx[bi]) {
|
|
1274
|
+
const prev = into[s];
|
|
1275
|
+
const next = prev === undefined ? out : join(prev, out);
|
|
1276
|
+
if (prev === undefined || !same(prev, next)) {
|
|
1277
|
+
into[s] = next;
|
|
1278
|
+
work.push(s);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
469
1284
|
// Split the instruction stream into basic blocks. Delay slots are consumed into their
|
|
470
1285
|
// branching block; leaders are the entry, every branch target, and each conditional branch's
|
|
471
1286
|
// fall-through. A branch-LIKELY's slot is not consumed — it becomes its own block (see
|
|
@@ -582,15 +1397,39 @@ function toBlocks(
|
|
|
582
1397
|
return { blocks: blocks.filter((b) => reachable.has(b)), succAddrs };
|
|
583
1398
|
}
|
|
584
1399
|
|
|
585
|
-
/** Lift disassembled MIPS text → an L1 Fn with block-argument SSA. `prototypes`
|
|
586
|
-
*
|
|
1400
|
+
/** Lift disassembled MIPS text → an L1 Fn with block-argument SSA. `prototypes` declares each
|
|
1401
|
+
* callee's arity (`frontend/call-plan.ts`).
|
|
1402
|
+
*
|
|
1403
|
+
* AT MOST TWICE, as on Thumb (`frontend/frame-objects.ts` `liftWithRelift`): where the frame-object audit answers
|
|
1404
|
+
* that a callee may write the local area without bound, or that nothing escapes and the per-object
|
|
1405
|
+
* model does not describe it, it asks for the area kept as one object, and the second lift's audit
|
|
1406
|
+
* judges it so and refuses rather than asking again. */
|
|
587
1407
|
export function lift(
|
|
588
1408
|
name: string,
|
|
589
1409
|
asm: string,
|
|
590
1410
|
target: TargetDescription,
|
|
591
|
-
|
|
1411
|
+
prototypes: Prototypes = {},
|
|
592
1412
|
asmData?: AsmData,
|
|
1413
|
+
symbols?: SymbolMap,
|
|
1414
|
+
context?: ContextGlobals,
|
|
1415
|
+
options: LiftOptions = {},
|
|
593
1416
|
): Fn {
|
|
1417
|
+
return liftWithRelift(name, (oneObject) =>
|
|
1418
|
+
liftOnce(name, asm, target, prototypes, asmData, symbols, context, oneObject, options),
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function liftOnce(
|
|
1423
|
+
name: string,
|
|
1424
|
+
asm: string,
|
|
1425
|
+
target: TargetDescription,
|
|
1426
|
+
prototypes: Prototypes,
|
|
1427
|
+
asmData: AsmData | undefined,
|
|
1428
|
+
symbols: SymbolMap | undefined,
|
|
1429
|
+
context: ContextGlobals | undefined,
|
|
1430
|
+
oneObject: FrameRange | undefined,
|
|
1431
|
+
options: LiftOptions,
|
|
1432
|
+
): Fn | FrameObjectRelift {
|
|
594
1433
|
// Two input dialects reach this frontend: `objdump -d` text (IDO/KMC — no compiler-emitted asm),
|
|
595
1434
|
// and Splat-disassembled `.s` (pmret-style N64 projects). Splat is normalised to the SAME
|
|
596
1435
|
// DisasmInstr[] shape (frontend/splat.ts) so everything below is dialect-agnostic.
|
|
@@ -604,38 +1443,56 @@ export function lift(
|
|
|
604
1443
|
if (instrs.length === 0) {
|
|
605
1444
|
throw new FrontendUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
|
|
606
1445
|
}
|
|
1446
|
+
// $30 has two names, and everything below keys a register by its spelling.
|
|
1447
|
+
unifyRegisterSpellings(instrs, O32_REGISTER_ALIASES);
|
|
607
1448
|
// Headerless input — a raw fragment, and every Splat listing — says where the function starts
|
|
608
1449
|
// only by its first line.
|
|
609
1450
|
const startAddr = (splat ? undefined : symbolStart(sliced)) ?? instrs[0].addr;
|
|
610
|
-
// An FP condition-code branch
|
|
611
|
-
//
|
|
612
|
-
// FIRST, before the likely rewrite, so that a function
|
|
613
|
-
// than whatever the likely placement
|
|
1451
|
+
// An FP condition-code branch this frontend cannot read — one naming a MIPS IV condition code, or
|
|
1452
|
+
// any on a target with no FPU homes — is a DIFFERENT gap from a branch-likely, and saying so is
|
|
1453
|
+
// what lets each be worked on alone. Asked FIRST, before the likely rewrite, so that a function
|
|
1454
|
+
// carrying both reports the FP gap rather than whatever the likely placement says about it.
|
|
614
1455
|
for (const ins of instrs) {
|
|
615
|
-
|
|
1456
|
+
const plain = LIKELY_BASE[ins.mnemonic] ?? ins.mnemonic;
|
|
1457
|
+
if (isFpCondBranch(ins.mnemonic) && !(target.fpu && plain in FP_COND_BRANCH && ins.ops.length === 1)) {
|
|
616
1458
|
throw new FrontendUnsupportedError(
|
|
617
1459
|
`cannot lift '${name}': floating-point condition-code branch '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
|
|
618
|
-
|
|
1460
|
+
'— only the one condition code a `c.cond` sets, named by the target alone, is modelled',
|
|
619
1461
|
);
|
|
620
1462
|
}
|
|
621
1463
|
}
|
|
622
1464
|
// BRANCH-LIKELY is rewritten to its ordinary branch FIRST, so everything downstream — jump-table
|
|
623
1465
|
// recovery's block-boundary walk included — sees one branch vocabulary. What stays special is the
|
|
624
1466
|
// slot, and `toBlocks` is where that is placed.
|
|
625
|
-
|
|
1467
|
+
// A word's relocation is on the word in Splat text, and in the object's table for objdump.
|
|
1468
|
+
const relocOf = (ins: Instr): DisasmReloc | null =>
|
|
1469
|
+
splat ? (ins.reloc ?? null) : asmData ? textRelocAt(asmData, ins.addr) : null;
|
|
1470
|
+
const { likely: likelyAddrs, freed: freedSlots } = normaliseBranchLikely(name, instrs, startAddr, relocOf, options);
|
|
626
1471
|
// Regime B: recover jump tables from the `jr`-dispatch idiom + the AsmData table. A recovered
|
|
627
1472
|
// dispatch's `jr` is subsumed into a `switch_br` (emitted from its bounds block), so it is
|
|
628
1473
|
// exempted from the loud-fail below; an UNrecovered `jr <non-ra>` still fails loud.
|
|
629
|
-
const jts = asmData ? recoverMipsJumpTables(instrs, asmData) : new Map<number, MipsJT>();
|
|
1474
|
+
const jts = asmData ? recoverMipsJumpTables(instrs, asmData, likelyAddrs) : new Map<number, MipsJT>();
|
|
630
1475
|
const recoveredJr = new Set([...jts.values()].map((j) => j.jrAddr));
|
|
631
|
-
// THE
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
1476
|
+
// THE WAYS A RECOVERED TABLE MEETS A LIKELY SLOT, none of which `normaliseBranchLikely` can see:
|
|
1477
|
+
// the table does not exist while it runs. (1) The dispatch's bounds branch emits a `switch_br`,
|
|
1478
|
+
// which runs its delay slot unconditionally — a nullified slot is not that. KMC writes this
|
|
1479
|
+
// bounds branch as `beqzl at,DEF` with DEF's first word in the slot (`sw_h3`..`sw_h5`), and that
|
|
1480
|
+
// one was retargeted to an ordinary branch before it got here; a slot that is NOT the default's
|
|
1481
|
+
// stolen word still refuses. (2) An arm of the table lands ON a nullified slot, which would run
|
|
1482
|
+
// the slot with no branch conditioning it and then jump to that branch's target: `case 1:` would
|
|
1483
|
+
// take the taken arm's value where the hardware runs the slot and falls on through. (3) An arm
|
|
1484
|
+
// lands on a slot the rewrite EMPTIED, a likely branch's or a `b`/`j`'s, which would run the
|
|
1485
|
+
// `nop` where the hardware runs the word it replaced. (2) and (3) have never been seen in the
|
|
1486
|
+
// corpus; refuse rather than answer any of them wrong.
|
|
638
1487
|
const jtArms = new Set([...jts.values()].flatMap((jt) => [...jt.caseAddrs, jt.defaultAddr]));
|
|
1488
|
+
for (const addr of freedSlots) {
|
|
1489
|
+
if (jtArms.has(addr)) {
|
|
1490
|
+
throw new FrontendUnsupportedError(
|
|
1491
|
+
`cannot lift '${name}': a recovered switch arm lands on the delay slot at 0x${addr.toString(16)}, ` +
|
|
1492
|
+
'which the stolen-slot rewrite of a branch-likely or `b`/`j` emptied',
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
639
1496
|
for (const addr of likelyAddrs) {
|
|
640
1497
|
if (jts.has(addr)) {
|
|
641
1498
|
throw new FrontendUnsupportedError(
|
|
@@ -652,19 +1509,27 @@ export function lift(
|
|
|
652
1509
|
// text spells the halves and writes its own records). Runs AFTER jump-table recovery, so that
|
|
653
1510
|
// reads `.text` relocs pristine and a recovered table base is decided before any of this.
|
|
654
1511
|
if (!splat && asmData) {
|
|
655
|
-
attachMipsRelocs(name, instrs, asmData);
|
|
1512
|
+
attachMipsRelocs(name, instrs, asmData, freedSlots);
|
|
656
1513
|
}
|
|
657
1514
|
// 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
|
-
//
|
|
1515
|
+
// path cannot catch these (implicit or no register destination). A `jalr` clobbers `v0`
|
|
1516
|
+
// implicitly, so dropping it fabricates `v0` from a stale value, and the callee it reaches is a
|
|
1517
|
+
// register's value; a `jr` to anything but `ra` (jump table / computed goto) is not a plain
|
|
1518
|
+
// return. `jal` is lowered where its block is filled (`lowerJal`).
|
|
662
1519
|
for (const ins of instrs) {
|
|
663
|
-
if (ins.mnemonic === '
|
|
1520
|
+
if (ins.mnemonic === 'jalr') {
|
|
664
1521
|
throw new FrontendUnsupportedError(
|
|
665
1522
|
`cannot lift '${name}': function call '${ins.mnemonic}' at 0x${ins.addr.toString(16)} — MIPS calls not yet modelled`,
|
|
666
1523
|
);
|
|
667
1524
|
}
|
|
1525
|
+
// A `j` relocated against a symbol leaves this section's code for another function's, which is
|
|
1526
|
+
// a tail call; the target objdump prints is the field, not the address.
|
|
1527
|
+
if (ins.mnemonic === 'j' && ins.reloc !== undefined && !ins.reloc.sym.startsWith('.')) {
|
|
1528
|
+
throw new FrontendUnsupportedError(
|
|
1529
|
+
`cannot lift '${name}': 'j' at 0x${ins.addr.toString(16)} jumps to '${ins.reloc.sym}' — a tail call into ` +
|
|
1530
|
+
'another function is not modelled',
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
668
1533
|
if (ins.mnemonic === 'jr' && ins.ops[0] !== 'ra' && !recoveredJr.has(ins.addr)) {
|
|
669
1534
|
throw new FrontendUnsupportedError(
|
|
670
1535
|
`cannot lift '${name}': indirect jump 'jr ${ins.ops[0] ?? ''}' at 0x${ins.addr.toString(16)} — jump tables / tail calls not supported`,
|
|
@@ -682,6 +1547,26 @@ export function lift(
|
|
|
682
1547
|
}
|
|
683
1548
|
checkDelaySlots(name, instrs);
|
|
684
1549
|
const { blocks, succAddrs } = toBlocks(instrs, jts, likelyAddrs);
|
|
1550
|
+
// `isReturn` reads every surviving non-`ra` `jr` as a return, which holds only while each
|
|
1551
|
+
// recovered dispatch's block is pruned (frontend/jump-table.ts).
|
|
1552
|
+
refuseReachableDispatch(name, blocks, recoveredJr, (m) => {
|
|
1553
|
+
throw new FrontendUnsupportedError(m);
|
|
1554
|
+
});
|
|
1555
|
+
// PREHEADER, as in frontend/ppc.ts: an entry that is a branch target gets an empty block ahead of
|
|
1556
|
+
// it, whose live-ins are the function's parameters. One word below the function, so it falls
|
|
1557
|
+
// through to it.
|
|
1558
|
+
const entryReentered = blocks.some((b) => succAddrs.get(b)!.includes(blocks[0].startAddr));
|
|
1559
|
+
if (entryReentered) {
|
|
1560
|
+
const ph: MipsBlock = {
|
|
1561
|
+
startAddr: blocks[0].startAddr - 4,
|
|
1562
|
+
body: [],
|
|
1563
|
+
branch: null,
|
|
1564
|
+
delay: null,
|
|
1565
|
+
delayAnnulled: false,
|
|
1566
|
+
};
|
|
1567
|
+
succAddrs.set(ph, [blocks[0].startAddr]);
|
|
1568
|
+
blocks.unshift(ph);
|
|
1569
|
+
}
|
|
685
1570
|
const idxOf = new Map(blocks.map((b, i) => [b.startAddr, i]));
|
|
686
1571
|
|
|
687
1572
|
// CFG predecessors by block index.
|
|
@@ -695,47 +1580,364 @@ export function lift(
|
|
|
695
1580
|
}
|
|
696
1581
|
});
|
|
697
1582
|
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
// is
|
|
1583
|
+
// THE SLOT MODEL CLAIMS ONE PARTITION, `ownedLocals` (`ownedFrame`, below): a def-less read of a
|
|
1584
|
+
// word this function provably owns is an uninitialised local, and any other def-less slot read
|
|
1585
|
+
// refuses (frontend/ssa.ts, LiveInModel) unless `emitLoad` places it. It declares no
|
|
1586
|
+
// `declaredLocals`, so no slot is stamped a declaration rank. A slot key is the RAW `sp@<off>`;
|
|
1587
|
+
// what the offset names is read through the frame depth (`frameDepths`, below): a word at or past
|
|
1588
|
+
// entry + 16 is incoming stack argument k, read and written as `@sarg<k>` (`incomingStackKey`;
|
|
1589
|
+
// O32's `<pentry offset="16">` in `mips32be.cspec`), and `[0,16)` at entry is the caller-owned
|
|
1590
|
+
// home of a0..a3, which is in neither range. A function that takes a frame address gets a local
|
|
1591
|
+
// area as well (`measureO32Frame`, `compilerBehaviors.o32LocalArea`), whose accesses are memory
|
|
1592
|
+
// through an `laddr` the shared frame-object audit proves, as on Thumb.
|
|
703
1593
|
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
// `
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
|
|
1594
|
+
// THE REGISTER HALF of the live-in model IS claimed: a def-less read of a register O32 passes no
|
|
1595
|
+
// argument in, and that is scratch or the prologue saved (`prologueSavedRegs`), is an
|
|
1596
|
+
// uninitialised local (`registerLiveIns`, frontend/ssa.ts, as on Thumb and PPC). The saves are
|
|
1597
|
+
// measured HERE, before any block is lowered, because the builder evaluates the model once, at
|
|
1598
|
+
// its first use, while `saveSlots` below fills progressively. The function's own declaration
|
|
1599
|
+
// bounds the argument registers (`ownArgumentWordsOn`): KMC and gcc 2.7.2 home a local in a2/a3
|
|
1600
|
+
// past a two-parameter one.
|
|
1601
|
+
const fpType = fpPrecision(instrs, (m) => FP_ARITH[m]?.width ?? FP_CONVERT[m]?.width ?? FP_COMPARE[m]?.width);
|
|
1602
|
+
const savedRegs = prologueSavedRegs(
|
|
1603
|
+
[...blocks[0].body, ...(blocks[0].delay ? [blocks[0].delay] : [])],
|
|
1604
|
+
target,
|
|
1605
|
+
instrs,
|
|
1606
|
+
);
|
|
1607
|
+
// The frame half is measured further down (`ownedFrame`), once the frame depths are; a model the
|
|
1608
|
+
// builder asks for before that claims no frame, which only refuses more.
|
|
1609
|
+
let ownedFrameOf: (() => { from: number; to: number } | undefined) | undefined;
|
|
1610
|
+
const liveIns = registerLiveIns(
|
|
1611
|
+
target,
|
|
1612
|
+
savedRegs,
|
|
1613
|
+
options.contextOwnWords ?? ownArgumentWordsOn(name, prototypes[name], target),
|
|
1614
|
+
);
|
|
1615
|
+
const ssa = makeSsaBuilder(
|
|
1616
|
+
name,
|
|
1617
|
+
blocks.length,
|
|
1618
|
+
preds,
|
|
1619
|
+
() => {
|
|
1620
|
+
const owned = ownedFrameOf?.();
|
|
1621
|
+
return {
|
|
1622
|
+
...liveIns,
|
|
1623
|
+
saveSite: 'store in its entry block',
|
|
1624
|
+
...(owned === undefined ? {} : { ownedLocals: owned }),
|
|
1625
|
+
};
|
|
1626
|
+
},
|
|
1627
|
+
(k) => (isFpKey(k) ? (fpType ?? T.f32()) : undefined),
|
|
1628
|
+
liveIns.globalRegs,
|
|
1629
|
+
);
|
|
710
1630
|
const { irBlocks, readVar, writeVar, paramReg } = ssa;
|
|
711
1631
|
const RET = target.returnReg;
|
|
1632
|
+
// Whether this function's own declaration says its return fits one word (`v1MayHoldLowWord`):
|
|
1633
|
+
// void, or a scalar or pointer no wider than 32 bits. An aggregate says nothing here. A declared
|
|
1634
|
+
// WIDER return is refused for every frontend after the lift (pipeline.ts
|
|
1635
|
+
// `refuseNarrowReturnsOfWideDeclaration`), so this only clears the asm rule, never requires it.
|
|
1636
|
+
const own = prototypes[name];
|
|
1637
|
+
const ownWidth = ownReturnWidth(own);
|
|
1638
|
+
const ownReturnFitsAWord = declaresVoidReturn(own) || (ownWidth !== undefined && ownWidth <= 32);
|
|
712
1639
|
const ARG_REGS = target.argRegs;
|
|
1640
|
+
const fpu = target.fpu;
|
|
1641
|
+
const bodyInstrs = blocks.flatMap((b) => (b.delay ? [...b.body, b.delay] : b.body));
|
|
1642
|
+
// Which home the function returns in (frontend/fpu.ts `returnHomeRule`). The branches count: a
|
|
1643
|
+
// condition-code branch is how a compare's answer reaches the integer file.
|
|
1644
|
+
const homeRule: ReturnHomeRule = !fpu
|
|
1645
|
+
? { home: 'integer', declared: false }
|
|
1646
|
+
: returnHomeRule(
|
|
1647
|
+
ownDeclarationOf(name, prototypes, options.contextOwnProto),
|
|
1648
|
+
[...bodyInstrs, ...blocks.flatMap((b) => (b.branch ? [b.branch] : []))].some((ins) =>
|
|
1649
|
+
crossesToIntegers(ins.mnemonic),
|
|
1650
|
+
),
|
|
1651
|
+
() => writesFloatReturn(bodyInstrs, FP_FLOAT_WRITERS, mipsEvenFpKey, fpu),
|
|
1652
|
+
);
|
|
1653
|
+
const floatReturn = homeRule.home === 'float';
|
|
713
1654
|
|
|
714
1655
|
// The pending `%hi` halves of this function's global addresses, keyed by the SSA VALUE each `lui`
|
|
715
1656
|
// defines (frontend/high-half.ts holds the invariant and why a register-keyed map cannot answer
|
|
716
1657
|
// the question; frontend/ppc.ts folds the same one). FUNCTION-scoped, because a value is: a pair
|
|
717
1658
|
// split across blocks folds when SSA says the half reaches, and refuses when what arrives is the
|
|
718
1659
|
// block parameter standing for a merge.
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1660
|
+
/** This frontend's refusal, for the shared code that refuses on its behalf. */
|
|
1661
|
+
const fail: FrontendRefusal = (message) => {
|
|
1662
|
+
throw new FrontendUnsupportedError(message);
|
|
1663
|
+
};
|
|
1664
|
+
const highHalves = makeHighHalves({ hi: '%hi', hiArticle: 'a', lo: '%lo', fail });
|
|
1665
|
+
/** The jump-table bases a recovered dispatch reads from outside its own block — the `%lo` that
|
|
1666
|
+
* completes one defines no value but the base (frontend/jump-table.ts). */
|
|
1667
|
+
const tableBases = makeTableBases(
|
|
1668
|
+
[...jts.values()].flatMap((j) => (j.hoisted ? [j.hoisted] : [])),
|
|
1669
|
+
fail,
|
|
1670
|
+
);
|
|
1671
|
+
/** What this frontend defines registers with that is no argument: a pending `%hi` half and a
|
|
1672
|
+
* table base. */
|
|
1673
|
+
const placeholders = {
|
|
1674
|
+
has: (v: Value) => highHalves.has(v) || tableBases.has(v),
|
|
1675
|
+
guardRead: (n: string, r: string, v: Value) => tableBases.guardRead(n, r, highHalves.guardRead(n, r, v)),
|
|
1676
|
+
};
|
|
1677
|
+
// A table entry naming an address no block of this function starts at (another function's
|
|
1678
|
+
// code, read off a table that is not this dispatch's) declines here, before any arm is built.
|
|
1679
|
+
const armBlock = (addr: number): number => {
|
|
1680
|
+
const j = idxOf.get(addr);
|
|
1681
|
+
if (j === undefined) {
|
|
1682
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': jump-table target is not a block boundary`);
|
|
1683
|
+
}
|
|
1684
|
+
return j;
|
|
1685
|
+
};
|
|
1686
|
+
// What an elided dispatch left at each case target's entry (jump-table.ts `caseEntries`), and the
|
|
1687
|
+
// markers written there, by value. Not a placeholder: a case may copy one (`move v0,at`), and a
|
|
1688
|
+
// return or a call that skipped it would drop what the case handed on.
|
|
1689
|
+
const entries = caseEntries(
|
|
1690
|
+
blocks.flatMap((b, at) => {
|
|
1691
|
+
const jt = b.branch ? jts.get(b.branch.addr) : undefined;
|
|
1692
|
+
return jt === undefined
|
|
1693
|
+
? []
|
|
1694
|
+
: [
|
|
1695
|
+
{
|
|
1696
|
+
at,
|
|
1697
|
+
scrutReg: jt.scrutReg,
|
|
1698
|
+
cases: jt.caseAddrs.map(armBlock),
|
|
1699
|
+
defaultBlock: armBlock(jt.defaultAddr),
|
|
1700
|
+
leaves: jt.leaves,
|
|
1701
|
+
},
|
|
1702
|
+
];
|
|
1703
|
+
}),
|
|
1704
|
+
preds,
|
|
1705
|
+
);
|
|
1706
|
+
const staleIdiom = new Map<Value, StaleIdiomRead>();
|
|
1707
|
+
const parked = parkedIn(staleIdiom);
|
|
1708
|
+
const scaledIndex = scaledIndexIn(staleIdiom);
|
|
1709
|
+
// O32 passes the words past a0..a3 on the stack, returns a word in v0, and returns every struct
|
|
1710
|
+
// and union through a hidden pointer in a0 (`aggregateReturn: 'memory'`). This lowering builds no
|
|
1711
|
+
// register pair, so the plan refuses one. A call through an argument register bounds no arity
|
|
1712
|
+
// here: a leading float leaves a0 free.
|
|
1713
|
+
const calls = callDeclarations(
|
|
1714
|
+
name,
|
|
1715
|
+
target,
|
|
1716
|
+
prototypes,
|
|
1717
|
+
{ pairs: false, memoryReturn: true, stackArgs: true, voidReturn: true, argRegisterBoundsArity: false },
|
|
1718
|
+
fail,
|
|
1719
|
+
() => ownDeclarationOf(name, prototypes, options.contextOwnProto),
|
|
1720
|
+
);
|
|
1721
|
+
// The first instruction naming the FPU's file, a data register or the control register. A call
|
|
1722
|
+
// in a function that has one refuses: which floating-point registers a callee reads, returns in
|
|
1723
|
+
// and destroys is not modelled. A float that crosses the call untouched in $f12 or $f0 names no
|
|
1724
|
+
// register here, so only the callee's declaration shows it (`plan`).
|
|
1725
|
+
const fpuInstr = instrs.find(
|
|
1726
|
+
(ins) => /^(cfc1|ctc1)$/i.test(ins.mnemonic) || ins.ops.some((o) => MIPS_FP_REG.test(o)),
|
|
1727
|
+
);
|
|
1728
|
+
|
|
1729
|
+
// THE FRAME A CALL'S STACK ARGUMENTS ARE STAGED IN: the bytes one `addiu sp,sp,-N` pushed. Its
|
|
1730
|
+
// outgoing area is `[16, N)`; a word at or above N is this function's own incoming home area, which
|
|
1731
|
+
// IDO stores a0..a3 to, so it is never an argument of a call made here. Undefined where the
|
|
1732
|
+
// function pushes no frame or more than one.
|
|
1733
|
+
const pushes = instrs.filter(
|
|
1734
|
+
(ins) =>
|
|
1735
|
+
(ins.mnemonic === 'addiu' || ins.mnemonic === 'addi') &&
|
|
1736
|
+
isStackPtr(ins.ops[0]) &&
|
|
1737
|
+
isStackPtr(ins.ops[1] ?? '') &&
|
|
1738
|
+
parseImm(ins.ops[2] ?? '') < 0,
|
|
1739
|
+
);
|
|
1740
|
+
const frameSize = pushes.length === 1 ? -parseImm(pushes[0].ops[2]) : undefined;
|
|
1741
|
+
const depthAt = frameDepths(blocks, entryReentered);
|
|
1742
|
+
// AN INCOMING STACK WORD HAS ONE NAME, `@sarg<k>`, for its loads AND its stores: O32 passes
|
|
1743
|
+
// argument word k >= 4 at entry sp + 16 + 4(k-4), above the caller's 16-byte home of a0..a3, and
|
|
1744
|
+
// a word slot there is keyed by that entry-relative position (`frameDepths`), never by the raw
|
|
1745
|
+
// `sp@<off>` of the depth it was reached at. A function that assigns to its stack parameter (IDO
|
|
1746
|
+
// stores it back in place: `if (a) e = b;` is `beqz a0; sw a1,40(sp)`) is then the same SSA
|
|
1747
|
+
// variable as one that assigns to a register parameter: a read no store reaches is the caller's
|
|
1748
|
+
// argument through the ordinary live-in path, and one a store reaches on some paths is the merge
|
|
1749
|
+
// of the two. The store keeps no memory effect, which is sound because nothing else reads the
|
|
1750
|
+
// word: the caller never reads its outgoing area back (`compilerBehaviors` on all three O32
|
|
1751
|
+
// compilers, measured), a sub-word or FPU access to it turns the slot model off or refuses
|
|
1752
|
+
// (`spSlotSafe`, the FP arm), and taking its address refuses (it is outside the local area).
|
|
1753
|
+
// Undefined for every other word, and wherever the depth walk could not measure the frame.
|
|
1754
|
+
const incomingStackKey = (ins: Instr, off: number): string | undefined => {
|
|
1755
|
+
const depth = depthAt?.get(ins);
|
|
1756
|
+
const entryOff = depth === undefined ? undefined : off - depth;
|
|
1757
|
+
return entryOff === undefined ? undefined : incomingStackArgKey(entryOff, STACK_ARGS_AT, ARG_REGS.length);
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
// THE FRAME'S SAVES. A word store of `ra`, or of a register O32 does not let a callee destroy,
|
|
1761
|
+
// made while the register still holds what the caller left in it, is a SAVE: the word holds the
|
|
1762
|
+
// caller's value, which no C value names, so the store mints no parameter. The reload into the
|
|
1763
|
+
// same register is the restore, and what it writes refuses any use, as a `%hi` half does: a read
|
|
1764
|
+
// at once, a use reached through a merge once the function is built. A reload into another
|
|
1765
|
+
// register refuses. Every other word store is a VALUE slot, and a slot is one kind for the whole
|
|
1766
|
+
// function. Keyed by the raw `sp` offset, as every slot here is, so a prologue save matches an
|
|
1767
|
+
// epilogue restore in any block.
|
|
1768
|
+
const saveSlots = new Map<number, string>();
|
|
1769
|
+
const valueSlots = new Set<number>();
|
|
1770
|
+
const restores = new Map<Value, { reg: string; addr: number }>();
|
|
1771
|
+
// Each `lw` that restores a save, and the register it restores: a save holds only while every path
|
|
1772
|
+
// restores it after its last write (`refuseSavedRegisterLeaks`).
|
|
1773
|
+
const restoredBy = new Map<Instr, ReadonlySet<string>>();
|
|
1774
|
+
// Every load into the register the compiler discards an unused read into at a level that keeps a
|
|
1775
|
+
// plain one (`compilerBehaviors.discardsUnusedReadsInto`, frontend/dead-reads.ts `discarded`):
|
|
1776
|
+
// IDO's `lw zero`, which `write` drops, so the load's value is recorded here or nowhere.
|
|
1777
|
+
const discarded = new Set<Value>();
|
|
1778
|
+
const discardReg = target.compilerBehaviors.discardsUnusedReadsInto;
|
|
1779
|
+
const discards = (r: string) => discardReg !== undefined && (r === discardReg || (isZero(r) && isZero(discardReg)));
|
|
1780
|
+
// A `$`-spelt operand is an FPU register or a bare number, and the target's list names neither.
|
|
1781
|
+
const savedByCallee = (r: string) =>
|
|
1782
|
+
r === 'ra' || (!r.startsWith('$') && !target.callerSaved.includes(r) && !['zero', 'sp', 'gp'].includes(r));
|
|
1783
|
+
const refuseMixedSlot = (ins: Instr, mem: string, off: number, kind: 'save' | 'value') => {
|
|
1784
|
+
if (kind === 'save' ? valueSlots.has(off) : saveSlots.has(off)) {
|
|
1785
|
+
throw new FrontendUnsupportedError(
|
|
1786
|
+
`cannot lift '${name}': local stack frames not supported — '${mem}' at 0x${ins.addr.toString(16)} ` +
|
|
1787
|
+
`is a word this function both saves a register in and stores a value to`,
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
const code = blocks.flatMap((b) => [...b.body, ...(b.delay ? [b.delay] : [])]);
|
|
1793
|
+
// THE LOADED-WORD BOUND (`compilerBehaviors.outgoingAreaNeverLoadedFrom`, 16 on all three O32
|
|
1794
|
+
// toolchains): the lowest frame word this function LOADS at the frame's own depth from that
|
|
1795
|
+
// offset up, or `Infinity`. No word at or above it is any call's stack argument (the field says
|
|
1796
|
+
// why); below 16 a load proves nothing, since the compilers load back the a1..a3 home words they
|
|
1797
|
+
// stage a by-value aggregate in. Every block here is entry-reachable, so every load counted can
|
|
1798
|
+
// run; a load at another depth names other storage under the same raw offset and is not counted.
|
|
1799
|
+
// Read once, before the frame is partitioned: it caps where an unsized call's outgoing area can
|
|
1800
|
+
// end (`o32OutgoingArea`'s `widest`, which the local area's partition and the owned range of an
|
|
1801
|
+
// uninitialised read take), the a0..a3 guess below weighs only the words beneath it, and the
|
|
1802
|
+
// frame-object audit takes it too (`loadedWords`), counting what the IR does not show the audit:
|
|
1803
|
+
// the slot reloads and the epilogue's `lw ra`.
|
|
1804
|
+
const loadedFrom = target.compilerBehaviors.outgoingAreaNeverLoadedFrom;
|
|
1805
|
+
const loadedWordBound = ((): number => {
|
|
1806
|
+
if (loadedFrom === undefined || frameSize === undefined || depthAt === null) {
|
|
1807
|
+
return Infinity;
|
|
1808
|
+
}
|
|
1809
|
+
let bound = Infinity;
|
|
1810
|
+
for (const ins of code) {
|
|
1811
|
+
const last = ins.ops[ins.ops.length - 1] ?? '';
|
|
1812
|
+
if (!MIPS_FRAME_LOAD.test(ins.mnemonic) || !last.includes('(') || depthAt.get(ins) !== frameSize) {
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
const { base, off } = parseMem(last);
|
|
1816
|
+
const word = off - (((off % 4) + 4) % 4);
|
|
1817
|
+
if (isStackPtr(base) && word >= loadedFrom) {
|
|
1818
|
+
bound = Math.min(bound, word);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return bound;
|
|
1822
|
+
})();
|
|
1823
|
+
|
|
1824
|
+
// THE LOCAL AREA, where a function takes a frame address (`measureO32Frame`): every `off(sp)`
|
|
1825
|
+
// access inside it is MEMORY, read and written through an `laddr` the frame-object audit then
|
|
1826
|
+
// proves, and every one outside it keeps the slot model below. Null where no frame address is
|
|
1827
|
+
// taken, or the frame is one this cannot partition, and then every capture keeps the
|
|
1828
|
+
// sp-as-data refusal.
|
|
1829
|
+
/** The stack words a `jal`'s callee takes, by its declaration; null where nothing declares it. */
|
|
1830
|
+
const stackWordsOf = (callee: string | undefined): number | null => {
|
|
1831
|
+
const declared = callee === undefined ? null : calls.declaredCall(callee);
|
|
1832
|
+
return declared === null ? null : Math.max(0, wordsOf(declared.widths, target) - ARG_REGS.length);
|
|
1833
|
+
};
|
|
1834
|
+
const frame = measureO32Frame({
|
|
1835
|
+
name,
|
|
1836
|
+
code,
|
|
1837
|
+
entry: [...blocks[0].body, ...(blocks[0].delay ? [blocks[0].delay] : [])],
|
|
1838
|
+
depthAt,
|
|
1839
|
+
frameSize,
|
|
1840
|
+
rule: target.compilerBehaviors.o32LocalArea,
|
|
1841
|
+
stackWordsOf,
|
|
1842
|
+
savedByCallee,
|
|
1843
|
+
splat,
|
|
1844
|
+
loadedWordBound,
|
|
726
1845
|
});
|
|
1846
|
+
/** Whether `off(sp)` at `ins` is an access inside the local area, refusing one that straddles its
|
|
1847
|
+
* edge: half memory and half slot is neither. */
|
|
1848
|
+
const inLocalArea = (ins: Instr, off: number, width: number): boolean => {
|
|
1849
|
+
// The area is in post-push offsets, and `off` is the access's own: before the push (IDO's
|
|
1850
|
+
// leaves load first) or after the pop it names the byte `size - depth` higher, so an incoming
|
|
1851
|
+
// stack word read at entry as `lw t6,16(sp)` is not the local a later `0(sp)` stores, whatever
|
|
1852
|
+
// the raw numbers say. `measureO32Frame` refuses an access at another depth whose TRUE position
|
|
1853
|
+
// lies in the area, so one at another depth is never in it.
|
|
1854
|
+
if (frame === null || depthAt?.get(ins) !== frame.size) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
if (off >= frame.area.to || off + width <= frame.area.from) {
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
if (off < frame.area.from || off + width > frame.area.to) {
|
|
1861
|
+
throw new FrontendUnsupportedError(
|
|
1862
|
+
`cannot lift '${name}': address-taken stack local — '${ins.mnemonic}' at 0x${ins.addr.toString(16)} reaches across an edge of the ` +
|
|
1863
|
+
`local area, sp+${frame.area.from} up to sp+${frame.area.to} — part memory, part slot`,
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
return true;
|
|
1867
|
+
};
|
|
727
1868
|
|
|
728
1869
|
// SOUNDNESS GUARD. The word stack-slot model (emitLoad/emitStore) is safe ONLY when every
|
|
729
|
-
// sp-relative access
|
|
1870
|
+
// sp-relative access it keys is word-width. If a SUB-WORD sp access aliases a word slot
|
|
730
1871
|
// (`sw a0,4(sp)` then `lbu v0,4(sp)`), routing the word store to an SSA slot while the sub-word
|
|
731
1872
|
// reload stays on the memory path DROPS the store and reads uninitialised memory — a silent
|
|
732
1873
|
// miscompile that ALSO masks the struct-overlap loud-fail (raise/structs.ts). So if ANY sub-word
|
|
733
|
-
// sp access exists, disable slot-modelling for the whole function:
|
|
734
|
-
// back to the memory path, which loud-fails on a genuine overlap
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1874
|
+
// sp access exists OUTSIDE the local area, disable slot-modelling for the whole function:
|
|
1875
|
+
// everything sp-relative falls back to the memory path, which loud-fails on a genuine overlap
|
|
1876
|
+
// instead of miscompiling. One inside it is memory like every access there (`inLocalArea`).
|
|
1877
|
+
const spSlotSafe = !code.some((ins) => {
|
|
1878
|
+
if (!SUBWORD_MEM.has(ins.mnemonic) || ins.ops.length === 0) {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
const { base, off } = parseMem(ins.ops[ins.ops.length - 1]);
|
|
1882
|
+
return isStackPtr(base) && !inLocalArea(ins, off, ins.mnemonic.includes('b') ? 1 : 2);
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
// THE FRAME HALF of the live-in model (`LiveInModel.ownedLocals`), in the raw `sp@<off>` keys
|
|
1886
|
+
// the slot model uses: the words of the one frame this function pushed that it OWNS, so that a
|
|
1887
|
+
// def-less read of one is an uninitialised local — IDO homes `int r; if (a > 0) r = a*2;` in
|
|
1888
|
+
// `4(sp)` of an 8-byte leaf frame and reads it back on the path that never stored it. A word no
|
|
1889
|
+
// store reaches on ANY path is the same local (`uninit_spill` reads `36(sp)` and `0(sp)`, which
|
|
1890
|
+
// no instruction stores), as on Thumb (`lowerLoad`).
|
|
1891
|
+
//
|
|
1892
|
+
// WHAT ELSE COULD HAVE WRITTEN THE WORD is the whole soundness of it, and the claim is made only
|
|
1893
|
+
// where the answer is "nothing":
|
|
1894
|
+
// • another function holding a frame address — none is taken (`takesFrameAddress`), so the
|
|
1895
|
+
// frame-object audit has nothing to retract;
|
|
1896
|
+
// • a CALLEE, which O32 lets write the bottom of this frame: it stores its a0..a3 into the
|
|
1897
|
+
// home words `[0,16)` (IDO: `sw a2,40(sp)` in its own frame) and assigns a stack parameter in
|
|
1898
|
+
// place (`compilerBehaviors.localsAboveOutgoingArea`'s probe). So a function that calls owns
|
|
1899
|
+
// only the words past its widest call's outgoing area, `[16 + 4·words, frameSize)`. One whose
|
|
1900
|
+
// callee no declaration sizes owns the words from the loaded-word bound up, which no call's
|
|
1901
|
+
// stack argument reaches (`o32OutgoingArea`'s `widest`), and claims nothing where it loads
|
|
1902
|
+
// no frame word from 16 up. A leaf owns the whole frame;
|
|
1903
|
+
// • the same raw offset at ANOTHER depth — the key is the raw offset, so a word reached before
|
|
1904
|
+
// the push or after the pop names other storage under the same spelling. Every `sp` access
|
|
1905
|
+
// must run at the frame's own depth, save the incoming stack words that are keyed `@sarg<k>`.
|
|
1906
|
+
// The caller's home `[frameSize, frameSize + 16)` is in no range, so a def-less read there still
|
|
1907
|
+
// refuses. Sub-word frame traffic turns the slot model off (`spSlotSafe`) and claims nothing.
|
|
1908
|
+
let ownedMemo: { range: { from: number; to: number } | undefined } | undefined;
|
|
1909
|
+
const ownedFrame = (): { from: number; to: number } | undefined =>
|
|
1910
|
+
(ownedMemo ??= { range: measureOwnedFrame() }).range;
|
|
1911
|
+
const measureOwnedFrame = (): { from: number; to: number } | undefined => {
|
|
1912
|
+
if (frameSize === undefined || depthAt === null || !spSlotSafe || code.some(takesFrameAddress)) {
|
|
1913
|
+
return undefined;
|
|
1914
|
+
}
|
|
1915
|
+
for (const ins of code) {
|
|
1916
|
+
const last = ins.ops[ins.ops.length - 1] ?? '';
|
|
1917
|
+
if (!last.includes('(') || !isStackPtr(parseMem(last).base) || depthAt.get(ins) === frameSize) {
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (ins.mnemonic === 'lw' || ins.mnemonic === 'sw') {
|
|
1921
|
+
if (incomingStackKey(ins, parseMem(last).off) !== undefined) {
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
return undefined;
|
|
1926
|
+
}
|
|
1927
|
+
// A declaration the call lowering cannot carry out refuses THERE, in its own words; measured
|
|
1928
|
+
// here first, it claims nothing rather than pre-empting that refusal with another.
|
|
1929
|
+
let outgoing: ReturnType<typeof o32OutgoingArea>;
|
|
1930
|
+
try {
|
|
1931
|
+
outgoing = o32OutgoingArea(code, stackWordsOf, splat, loadedWordBound);
|
|
1932
|
+
} catch (e) {
|
|
1933
|
+
if (e instanceof FrontendUnsupportedError) {
|
|
1934
|
+
return undefined;
|
|
1935
|
+
}
|
|
1936
|
+
throw e;
|
|
1937
|
+
}
|
|
1938
|
+
return outgoing.widest < frameSize ? { from: outgoing.widest, to: frameSize } : undefined;
|
|
1939
|
+
};
|
|
1940
|
+
ownedFrameOf = ownedFrame;
|
|
739
1941
|
|
|
740
1942
|
// Hardware divide state (capabilities.hwDivide). MIPS `div`/`divu rs,rt` set the hi/lo pair
|
|
741
1943
|
// implicitly; a later `mflo`/`mfhi` reads the quotient/remainder. FUNCTION-scoped: GCC schedules
|
|
@@ -745,9 +1947,37 @@ export function lift(
|
|
|
745
1947
|
// overwrites the same hi/lo pair, so it clears this (below) — whichever divide/multiply ran most
|
|
746
1948
|
// recently owns the next `mf*`, the true hardware semantics.
|
|
747
1949
|
let divState: { rs: Value; rt: Value; signed: boolean } | null = null;
|
|
1950
|
+
// The frame offset each `laddr` this lift emitted names, and the deferred `sp + index` sums
|
|
1951
|
+
// (`emitLaddr`, `materialize`) — function-scoped, since a register holding one crosses blocks.
|
|
1952
|
+
const laddrAt = new Map<Value, number>();
|
|
1953
|
+
const frameSums = new Map<Value, FrameSum>();
|
|
1954
|
+
// Every capture this lift moved into another (`addiu rD,rCapture,k` re-minted as the `laddr` of
|
|
1955
|
+
// the sum, or `addu` deferring one into a `FrameSum`), handed to the audit as Thumb hands its
|
|
1956
|
+
// own: a base only the move read names no object, so the audit drops it rather than judging a
|
|
1957
|
+
// dead `laddr` as a capture (`foldMovedCaptures`).
|
|
1958
|
+
const movedCaptures = new Set<Value>();
|
|
748
1959
|
|
|
1960
|
+
/** Every return, as it is emitted: `settleReturnHome` may hand it the float home instead. */
|
|
1961
|
+
const returns: { op: Op; bi: number }[] = [];
|
|
1962
|
+
/** The words an `mtc1` moved into an FPU register (`emitFpMove`). */
|
|
1963
|
+
const movedIn = new Set<Value>();
|
|
749
1964
|
const fillBlock = (b: MipsBlock, bi: number) => {
|
|
750
1965
|
const ops = irBlocks[bi].ops;
|
|
1966
|
+
// A case target of an elided dispatch is entered with what the idiom left in its registers.
|
|
1967
|
+
const entry = entries.get(bi);
|
|
1968
|
+
if (entry !== undefined) {
|
|
1969
|
+
enterCaseTarget(
|
|
1970
|
+
entry,
|
|
1971
|
+
ops,
|
|
1972
|
+
{
|
|
1973
|
+
key: (r) => r,
|
|
1974
|
+
scrutinee: (r) => readVar(r, bi),
|
|
1975
|
+
dispatchScrutinee: (r) => ssa.readAtEnd(r, entry.at),
|
|
1976
|
+
write: (r, v) => writeVar(r, bi, v),
|
|
1977
|
+
},
|
|
1978
|
+
staleIdiom,
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
751
1981
|
// Materialise the address of a named global — the same `gaddr` op the Thumb frontend emits; the
|
|
752
1982
|
// structurer lowers a load/store through it to `SYM` (scalar) or `((T *)&SYM)[i]` (aggregate).
|
|
753
1983
|
const emitGaddr = (sym: string): Value => {
|
|
@@ -760,15 +1990,23 @@ export function lift(
|
|
|
760
1990
|
return constVal(0);
|
|
761
1991
|
}
|
|
762
1992
|
// Reading `sp` as a DATA operand means frame-pointer arithmetic or an address-taken local
|
|
763
|
-
// (`addiu a0,sp,8` = `&local`)
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
1993
|
+
// (`addiu a0,sp,8` = `&local`). Fabricating a value for `sp` invents a PHANTOM leading
|
|
1994
|
+
// parameter that shifts every real argument — a silent miscompile. Fail LOUD instead,
|
|
1995
|
+
// mirroring the PPC frontend's r1. Frame setup/teardown (`addiu sp,sp,±N`), word spill/reload
|
|
1996
|
+
// slots and, where `measureO32Frame` drew a local area, frame captures are handled before `read`.
|
|
767
1997
|
if (isStackPtr(r)) {
|
|
768
1998
|
throw new FrontendUnsupportedError(
|
|
769
1999
|
`cannot lift '${name}': stack pointer used as data (address-taken local / frame arithmetic) — local stack frames not supported`,
|
|
770
2000
|
);
|
|
771
2001
|
}
|
|
2002
|
+
// A KERNEL REGISTER (`target.kernelRegs`) holds nothing this function wrote: an exception
|
|
2003
|
+
// handler may overwrite it between any two instructions.
|
|
2004
|
+
if (isKernelReg(target.kernelRegs ?? [], r)) {
|
|
2005
|
+
throw new FrontendUnsupportedError(
|
|
2006
|
+
`cannot lift '${name}': ${r} read as a value — the ABI reserves it to the kernel, and an exception ` +
|
|
2007
|
+
'handler may overwrite it at any instruction',
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
772
2010
|
// `gp` writes (the PIC prologue) are skipped as transparent; a `gp` READ that survives here is a
|
|
773
2011
|
// PIC/small-data global access (`lw x,off(gp)`) this frontend does not model — the recovered
|
|
774
2012
|
// jump-table dispatch's own `lw …,0(gp)` is elided, so reaching this is a genuine decline, not a
|
|
@@ -778,10 +2016,55 @@ export function lift(
|
|
|
778
2016
|
`cannot lift '${name}': gp used as data (PIC / small-data global access) — not supported`,
|
|
779
2017
|
);
|
|
780
2018
|
}
|
|
781
|
-
// A register holding the high half of an address is NOT a value.
|
|
782
|
-
// (`addiu %lo`, a `%lo` load/store base
|
|
783
|
-
|
|
2019
|
+
// A register holding the high half of an address is NOT a value. Its legitimate consumers
|
|
2020
|
+
// (`addiu %lo`, a `%lo` load/store base, and `addu` adding a runtime index between the
|
|
2021
|
+
// halves) reach the half without this read.
|
|
2022
|
+
const v = readVar(r, bi);
|
|
2023
|
+
const sum = frameSums.get(v);
|
|
2024
|
+
if (sum !== undefined) {
|
|
2025
|
+
return materialize(sum, 0);
|
|
2026
|
+
}
|
|
2027
|
+
const restore = restores.get(v);
|
|
2028
|
+
if (restore) {
|
|
2029
|
+
throw new FrontendUnsupportedError(
|
|
2030
|
+
`cannot lift '${name}': ${r} is read after the 'lw' at 0x${restore.addr.toString(16)} restores the ` +
|
|
2031
|
+
`caller's ${restore.reg} into it — no C value names it`,
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
return placeholders.guardRead(name, r, v);
|
|
2035
|
+
};
|
|
2036
|
+
// A FRAME ADDRESS AS DATA. `addiu rD,sp,k` and `move rD,sp` name frame offset k, and lower to
|
|
2037
|
+
// an `laddr` the frame-object audit proves. `addu rD,sp,rX` — in either operand order — names
|
|
2038
|
+
// `sp + rX`, a runtime index from the frame base, and that sum is DEFERRED: the access through
|
|
2039
|
+
// it says which object it indexes, since `lw v0,24(v0)` after `addu v0,sp,t6` is `a[i]` on an
|
|
2040
|
+
// array at sp+24 (IDO), not at sp+0. So the register holds a `FrameSum`, and each use builds
|
|
2041
|
+
// `laddr(base + displacement) + index` there — an access at its displacement, any other use at
|
|
2042
|
+
// 0. A capture or sum moved by a constant is the same capture at the sum. Where a frame address
|
|
2043
|
+
// lies against the local area is the shared audit's to judge (`FRAME_OBJECT_AUDIT`,
|
|
2044
|
+
// frontend/frame-objects.ts `frameObjects`), as on Thumb: below it or above it refuses.
|
|
2045
|
+
const emitLaddr = (k: number): Value => {
|
|
2046
|
+
const v = mkValue(T.unk(32));
|
|
2047
|
+
ops.push(mkOp('laddr', { results: [v], attrs: { off: k } }));
|
|
2048
|
+
laddrAt.set(v, k);
|
|
2049
|
+
return v;
|
|
2050
|
+
};
|
|
2051
|
+
const materialize = (sum: FrameSum, displacement: number): Value => {
|
|
2052
|
+
const v = mkValue(T.unk(32));
|
|
2053
|
+
ops.push(mkOp('add', { operands: [emitLaddr(sum.base + displacement), sum.index], results: [v] }));
|
|
2054
|
+
return v;
|
|
784
2055
|
};
|
|
2056
|
+
const deferSum = (base: number, index: Value): Value => {
|
|
2057
|
+
const v = mkValue(T.unk(32));
|
|
2058
|
+
frameSums.set(v, { base, index });
|
|
2059
|
+
return v;
|
|
2060
|
+
};
|
|
2061
|
+
// What a register holds when it may be a frame address, unguarded — `read` then guards and
|
|
2062
|
+
// materializes everything else.
|
|
2063
|
+
const rawFrame = (r: string | undefined): Value | undefined =>
|
|
2064
|
+
frame === null || r === undefined || isZero(r) || isStackPtr(r) || r === 'gp' ? undefined : readVar(r, bi);
|
|
2065
|
+
/** Whether `r` holds a pending `%hi` half here (indexed or not), unguarded. */
|
|
2066
|
+
const halfIn = (r: string | undefined): boolean =>
|
|
2067
|
+
r !== undefined && !isZero(r) && !isStackPtr(r) && r !== 'gp' && highHalves.has(readVar(r, bi));
|
|
785
2068
|
// `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
|
|
786
2069
|
//
|
|
787
2070
|
// Keyed by the SSA VALUE the compare produced, never by its register. Keying by register is the
|
|
@@ -812,6 +2095,10 @@ export function lift(
|
|
|
812
2095
|
// DELIBERATELY (unlike divState): a cross-block `mult`/`mflo` pair has no observed inhabitant,
|
|
813
2096
|
// and the miss degrades to a LOUD opaque, never silence.
|
|
814
2097
|
let mulState: { rs: Value; rt: Value; signed: boolean } | null = null;
|
|
2098
|
+
// The FP condition code a `c.cond` set in THIS block, as the compare it holds. Block-local like
|
|
2099
|
+
// `mulState`: every compare and branch the compilers emit sit in one block, and a branch with
|
|
2100
|
+
// none set here refuses rather than read a code across an edge.
|
|
2101
|
+
let fcc: { opcode: Opcode; lhs: Value; rhs: Value; value: Value } | null = null;
|
|
815
2102
|
const emitCmp = (opc: Opcode, d: string, lhs: Value, rhs: Value) => {
|
|
816
2103
|
const v = mkValue(T.unk(32));
|
|
817
2104
|
ops.push(mkOp(opc, { operands: [lhs, rhs], results: [v] }));
|
|
@@ -825,6 +2112,14 @@ export function lift(
|
|
|
825
2112
|
// Set by every case entitled to fold a relocation, cleared per instruction, and answered by
|
|
826
2113
|
// `relocPlaceholder`.
|
|
827
2114
|
let relocTaken = false;
|
|
2115
|
+
// The words loads into the discard register read whose pair half has not come yet (`emitLoad`):
|
|
2116
|
+
// each one's base register, or the symbol a relocation names, and its offset. IDO's -O1
|
|
2117
|
+
// scheduler puts frame reloads, argument loads and stores between the halves, a store through
|
|
2118
|
+
// the pair's own base among them (`s->a; s->b = x; g(x);` is `lw zero,0(t6); sw t7,8(t6);
|
|
2119
|
+
// lw a0,28(sp); jal g; lw at,4(t6)`). No access makes the `at` word anything but the pair half
|
|
2120
|
+
// — IDO never gives `at` a C value — so a word stays pending across every one of them. What ends
|
|
2121
|
+
// one: a write to its base register, which moves the address the half would read, and a call.
|
|
2122
|
+
let firstWords: { reg: string | undefined; sym: string | undefined; off: number }[] = [];
|
|
828
2123
|
// EVERY immediate this frontend turns into a value goes through here. objdump prints them as
|
|
829
2124
|
// numbers, so a non-numeric one means the operand is not an immediate at all — a relocation
|
|
830
2125
|
// spelling the reader failed to resolve, a label — and bare `parseImm` answers NaN, which
|
|
@@ -843,20 +2138,33 @@ export function lift(
|
|
|
843
2138
|
const decode = (ins: Instr) => {
|
|
844
2139
|
const [d, s, t] = ins.ops;
|
|
845
2140
|
relocTaken = false;
|
|
2141
|
+
if (ins.mnemonic.startsWith('jal')) {
|
|
2142
|
+
firstWords = [];
|
|
2143
|
+
} else if (ins.mnemonic !== 'lw') {
|
|
2144
|
+
const w = writtenReg(ins);
|
|
2145
|
+
firstWords = firstWords.filter((f) => f.reg === undefined || f.reg !== w);
|
|
2146
|
+
}
|
|
846
2147
|
// The GOT/small-data base register `gp` is set up by IDO's PIC prologue (`lui gp; addiu gp,gp,lo;
|
|
847
2148
|
// addu gp,gp,t9`, an `_gp_disp` HI16/LO16 pair). It is a RELOCATION base, never program data — and
|
|
848
2149
|
// reading `t9` for the `addu` would fabricate a phantom leading parameter (like the sp/r1 guards).
|
|
849
2150
|
// Its only real use, the GOT table-base `lw ...,0(gp)` in a jump-table dispatch, is subsumed by the
|
|
850
2151
|
// recovered `switch_br` (that block is elided). So a write to `gp` is transparent: skip it.
|
|
851
|
-
if (
|
|
2152
|
+
if (writtenReg(ins) === 'gp') {
|
|
852
2153
|
return;
|
|
853
2154
|
}
|
|
854
2155
|
switch (ins.mnemonic) {
|
|
855
2156
|
case 'nop':
|
|
856
2157
|
break;
|
|
857
|
-
case 'move':
|
|
858
|
-
|
|
859
|
-
|
|
2158
|
+
case 'move': {
|
|
2159
|
+
// pseudo: addu/or rD,rS,zero
|
|
2160
|
+
if (frame !== null && isStackPtr(s)) {
|
|
2161
|
+
write(d, emitLaddr(0));
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
const raw = rawFrame(s);
|
|
2165
|
+
write(d, raw !== undefined && frameSums.has(raw) ? raw : read(s));
|
|
2166
|
+
break;
|
|
2167
|
+
}
|
|
860
2168
|
case 'li':
|
|
861
2169
|
write(d, constVal(imm(ins, s)));
|
|
862
2170
|
break; // pseudo: load immediate
|
|
@@ -905,10 +2213,39 @@ export function lift(
|
|
|
905
2213
|
if (isStackPtr(d)) {
|
|
906
2214
|
break;
|
|
907
2215
|
}
|
|
2216
|
+
if (frame !== null && ins.reloc === undefined) {
|
|
2217
|
+
if (isStackPtr(s)) {
|
|
2218
|
+
write(d, emitLaddr(imm(ins, t)));
|
|
2219
|
+
break;
|
|
2220
|
+
}
|
|
2221
|
+
const raw = rawFrame(s)!;
|
|
2222
|
+
const at = raw === undefined ? undefined : laddrAt.get(raw);
|
|
2223
|
+
if (at !== undefined) {
|
|
2224
|
+
movedCaptures.add(raw!);
|
|
2225
|
+
write(d, emitLaddr(at + imm(ins, t)));
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
const sum = raw === undefined ? undefined : frameSums.get(raw);
|
|
2229
|
+
if (sum !== undefined) {
|
|
2230
|
+
write(d, deferSum(sum.base + imm(ins, t), sum.index));
|
|
2231
|
+
break;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
908
2234
|
// `addiu rD, rHi, %lo(SYM)` completes a global's address begun by a `lui %hi(SYM)`:
|
|
909
2235
|
// rD = &SYM (+ a byte offset into it). Emits the shared `gaddr` op.
|
|
910
2236
|
if (ins.reloc?.type === 'R_MIPS_LO16') {
|
|
911
|
-
const
|
|
2237
|
+
const at = pairLoHalf(ins, s, imm(ins, t));
|
|
2238
|
+
// The base of a jump table a dispatch reads from outside its block is no value but the
|
|
2239
|
+
// table base (frontend/jump-table.ts), which only that dispatch may take — and a table
|
|
2240
|
+
// base is never indexed.
|
|
2241
|
+
const table = { sym: ins.reloc.sym, addend: at.off };
|
|
2242
|
+
if (at.index === undefined && tableBases.wants(table)) {
|
|
2243
|
+
const base = mkValue(T.unk(32));
|
|
2244
|
+
tableBases.record(base, table, ins.addr);
|
|
2245
|
+
write(d, base);
|
|
2246
|
+
break;
|
|
2247
|
+
}
|
|
2248
|
+
const { base: g, off } = addressAt(ins, at);
|
|
912
2249
|
// `&SYM` (offset 0), or `&SYM + N` for a byte offset into the global. The `add` tree is
|
|
913
2250
|
// folded byte-correctly by memAccess when this address is a load/store base; if it
|
|
914
2251
|
// instead ESCAPES as a value, `assertDerefsTyped` declines it (the byte offset would
|
|
@@ -923,9 +2260,47 @@ export function lift(
|
|
|
923
2260
|
emitBin('add', d, read(s), constVal(imm(ins, t)));
|
|
924
2261
|
break;
|
|
925
2262
|
case 'addu':
|
|
926
|
-
case 'add':
|
|
2263
|
+
case 'add': {
|
|
2264
|
+
// `addu rD,sp,zero` is Splat's spelling of objdump's `move rD,sp`: the frame base.
|
|
2265
|
+
if (frame !== null && isStackPtr(copiedReg(ins) ?? '')) {
|
|
2266
|
+
write(d, emitLaddr(0));
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
if (frame !== null && t !== undefined && isStackPtr(s) !== isStackPtr(t) && !isZero(isStackPtr(s) ? t : s)) {
|
|
2270
|
+
write(d, deferSum(0, read(isStackPtr(s) ? t : s)));
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
const [rs, rt] = [rawFrame(s), rawFrame(t)];
|
|
2274
|
+
const capturedAt = (v: Value | undefined) => (v === undefined ? undefined : laddrAt.get(v));
|
|
2275
|
+
const [cs, ct] = [capturedAt(rs), capturedAt(rt)];
|
|
2276
|
+
if ((cs === undefined) !== (ct === undefined)) {
|
|
2277
|
+
movedCaptures.add((cs === undefined ? rt : rs)!);
|
|
2278
|
+
write(d, deferSum((cs ?? ct)!, read(cs === undefined ? s : t)));
|
|
2279
|
+
break;
|
|
2280
|
+
}
|
|
2281
|
+
// `lui rH,%hi(S); addu rH,rH,idx; l*/s* x,%lo(S)(rH)` is ido's, KMC's and GCC 2.7.2's
|
|
2282
|
+
// `S[i]`: a runtime byte index added BETWEEN the halves. rD is still no value — it is an
|
|
2283
|
+
// indexed half (frontend/high-half.ts `index`) that only its `%lo` completes, to
|
|
2284
|
+
// `gaddr(S) + idx` at the halves' combined offset. Either operand order is accepted, as
|
|
2285
|
+
// `addu` commutes (every compiler measured puts rH first). Two halves added together
|
|
2286
|
+
// refuse: two addresses added is not an address. A COPY (`addu rD,rH,zero`) is no index:
|
|
2287
|
+
// it reads the half as a value and refuses, as `move` and `or rD,rH,zero` do.
|
|
2288
|
+
const [hs, ht] = copiedReg(ins) === undefined ? [halfIn(s), halfIn(t)] : [false, false];
|
|
2289
|
+
if (hs && ht) {
|
|
2290
|
+
throw new FrontendUnsupportedError(
|
|
2291
|
+
`${site(ins)} adds a high half to another high half — the sum of two addresses is not an address this frontend can write down`,
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
if (hs || ht) {
|
|
2295
|
+
const idx = read(hs ? t! : s);
|
|
2296
|
+
const v = mkValue(T.unk(32));
|
|
2297
|
+
highHalves.index(v, readVar(hs ? s : t!, bi), idx, { addr: ins.addr, mnemonic: ins.mnemonic });
|
|
2298
|
+
write(d, v);
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
927
2301
|
emitBin('add', d, read(s), read(t));
|
|
928
2302
|
break;
|
|
2303
|
+
}
|
|
929
2304
|
case 'subu':
|
|
930
2305
|
case 'sub':
|
|
931
2306
|
emitBin('sub', d, read(s), read(t));
|
|
@@ -983,6 +2358,10 @@ export function lift(
|
|
|
983
2358
|
emitBin('and', d, read(s), constVal(imm(ins, t)));
|
|
984
2359
|
break;
|
|
985
2360
|
case 'or':
|
|
2361
|
+
if (frame !== null && isStackPtr(copiedReg(ins) ?? '')) {
|
|
2362
|
+
write(d, emitLaddr(0)); // `or rD,sp,zero`, IDO's spelling of the same move
|
|
2363
|
+
break;
|
|
2364
|
+
}
|
|
986
2365
|
isZero(t) ? write(d, read(s)) : emitBin('or', d, read(s), read(t));
|
|
987
2366
|
break;
|
|
988
2367
|
case 'ori':
|
|
@@ -1076,6 +2455,42 @@ export function lift(
|
|
|
1076
2455
|
case 'sb':
|
|
1077
2456
|
emitStore(ins, d, s, 1);
|
|
1078
2457
|
break;
|
|
2458
|
+
// A call reaches here only from a delay slot (the fill loop lowers every other one), and a
|
|
2459
|
+
// transfer in a delay slot is one the ISA leaves undefined.
|
|
2460
|
+
case 'jal':
|
|
2461
|
+
throw new FrontendUnsupportedError(`${site(ins)} sits in the delay slot of a transfer`);
|
|
2462
|
+
case 'add.s':
|
|
2463
|
+
case 'sub.s':
|
|
2464
|
+
case 'mul.s':
|
|
2465
|
+
case 'div.s':
|
|
2466
|
+
case 'neg.s':
|
|
2467
|
+
case 'mov.s':
|
|
2468
|
+
case 'add.d':
|
|
2469
|
+
case 'sub.d':
|
|
2470
|
+
case 'mul.d':
|
|
2471
|
+
case 'div.d':
|
|
2472
|
+
case 'neg.d':
|
|
2473
|
+
case 'mov.d':
|
|
2474
|
+
emitFpArith(ins);
|
|
2475
|
+
break;
|
|
2476
|
+
case 'mfc1':
|
|
2477
|
+
case 'mtc1':
|
|
2478
|
+
emitFpMove(ins);
|
|
2479
|
+
break;
|
|
2480
|
+
case 'trunc.w.s':
|
|
2481
|
+
case 'trunc.w.d':
|
|
2482
|
+
case 'cvt.s.w':
|
|
2483
|
+
case 'cvt.d.w':
|
|
2484
|
+
emitFpConvert(ins);
|
|
2485
|
+
break;
|
|
2486
|
+
case 'c.eq.s':
|
|
2487
|
+
case 'c.lt.s':
|
|
2488
|
+
case 'c.le.s':
|
|
2489
|
+
case 'c.eq.d':
|
|
2490
|
+
case 'c.lt.d':
|
|
2491
|
+
case 'c.le.d':
|
|
2492
|
+
emitFpCompare(ins);
|
|
2493
|
+
break;
|
|
1079
2494
|
default:
|
|
1080
2495
|
emitOpaqueDest(ins);
|
|
1081
2496
|
break; // unmodelled: an honest opaque, never a silent drop
|
|
@@ -1108,9 +2523,16 @@ export function lift(
|
|
|
1108
2523
|
if (ins.reloc) {
|
|
1109
2524
|
relocPlaceholder(ins);
|
|
1110
2525
|
}
|
|
1111
|
-
// storeClass: unmodelled MIPS stores
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
2526
|
+
// storeClass: unmodelled MIPS stores, whose FIRST token is a register (a SOURCE, not a dest)
|
|
2527
|
+
// that would otherwise fabricate an opaque write to it while dropping the real memory write.
|
|
2528
|
+
// `swc1`/`sdc1` are in the list and are NOT reached by any spelling `fpReg` covers — an FPU
|
|
2529
|
+
// store's `rt` is an FP register by ISA, so the arm above claims it first — but that is a
|
|
2530
|
+
// claim about the PREDICATE, not about the ISA, and it is false for every register spelling
|
|
2531
|
+
// the predicate misses: the store then arrives here and is named by the memory it writes
|
|
2532
|
+
// instead of by the file it moves. So they are kept as a live backstop, and
|
|
2533
|
+
// `test/fp-refusal.test.ts` asserts the FP arm wins on each spelling rather than assuming it.
|
|
2534
|
+
// Everything else here is genuinely reachable: `swl`/`swr` are the unaligned pair, `sc`/`sd*`
|
|
2535
|
+
// the rest.
|
|
1114
2536
|
// skipSafe `break`: the compiler-emitted divide-by-zero guard trap inside the hw-divide
|
|
1115
2537
|
// idiom (KMC GCC `break 0x7`); recompiling the recovered `/` regenerates it, so it is
|
|
1116
2538
|
// transparent by the same modelling as the divide itself (byte-exactness proven by the
|
|
@@ -1118,6 +2540,22 @@ export function lift(
|
|
|
1118
2540
|
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
1119
2541
|
isReg: isMipsReg,
|
|
1120
2542
|
isZero,
|
|
2543
|
+
// The coprocessor-1 register file. `isMipsReg` rejects `$f12` — the `$`-prefixed forms it
|
|
2544
|
+
// takes are the numeric GPRs (`$4`) — so without this arm an `add.s` is refused for the
|
|
2545
|
+
// lesser reason, "no register destination", while `mfc1 v0,$f12` has a destination it
|
|
2546
|
+
// ACCEPTS and would build an opaque whose source list quietly omits the register the
|
|
2547
|
+
// instruction actually read. The file is what is missing; say so.
|
|
2548
|
+
//
|
|
2549
|
+
// BOTH DIALECTS AND EVERY SPELLING, which is why this is `frontend/splat.ts`'s exported
|
|
2550
|
+
// constant and not a literal: the reader decides which tokens still carry a sigil when they
|
|
2551
|
+
// get here, so a predicate written twice is a predicate that can disagree with itself, and
|
|
2552
|
+
// each half of that disagreement is silent. Its docblock lists the spellings and says why
|
|
2553
|
+
// the sigil is required.
|
|
2554
|
+
fpReg: MIPS_FP_REG,
|
|
2555
|
+
// The FPU CONTROL register, which both moves spell `$31` — a GPR-shaped token no register
|
|
2556
|
+
// predicate can tell from an integer one. `ctc1`'s first operand is its SOURCE, so this
|
|
2557
|
+
// also stops an opaque being fabricated on a register the instruction only reads.
|
|
2558
|
+
fpControl: /^(cfc1|ctc1)$/i,
|
|
1121
2559
|
storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)$/i,
|
|
1122
2560
|
skipSafe: /^(nop|ssnop|break)$/i,
|
|
1123
2561
|
context: `${name} @0x${ins.addr.toString(16)}`,
|
|
@@ -1131,24 +2569,127 @@ export function lift(
|
|
|
1131
2569
|
write(od.dst, res);
|
|
1132
2570
|
};
|
|
1133
2571
|
const emitUn = kit.un;
|
|
2572
|
+
// Arithmetic on the FPU's file, keyed by `mipsEvenFpKey` so both dialects' spellings of one
|
|
2573
|
+
// register are one SSA variable. Anything it cannot key — an odd half, a target with no float
|
|
2574
|
+
// homes — keeps the register-file refusal.
|
|
2575
|
+
const emitFpArith = (ins: Instr) => {
|
|
2576
|
+
const keys = ins.ops.map(mipsEvenFpKey);
|
|
2577
|
+
if (fpu === undefined || keys.length === 0 || keys.some((k) => k === null)) {
|
|
2578
|
+
emitOpaqueDest(ins);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (fpType === null) {
|
|
2582
|
+
refuseBothPrecisions(name, ins.mnemonic);
|
|
2583
|
+
}
|
|
2584
|
+
const [dst, ...srcs] = keys as string[];
|
|
2585
|
+
const { op } = FP_ARITH[ins.mnemonic];
|
|
2586
|
+
if (op === 'copy') {
|
|
2587
|
+
write(dst, readFloat(ins, srcs[0]));
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
const v = mkValue(fpType);
|
|
2591
|
+
ops.push(mkOp(op, { operands: srcs.map((k) => readFloat(ins, k)), results: [v] }));
|
|
2592
|
+
write(dst, v);
|
|
2593
|
+
};
|
|
2594
|
+
// AN FPU REGISTER HOLDS A FLOAT OR AN INTEGER WORD, and which is a fact about the instruction
|
|
2595
|
+
// that last wrote it: `trunc.w` and `mtc1` leave a word, everything else a float. Each read says
|
|
2596
|
+
// which it expects and refuses the other, so a word is never computed on as a float, nor a
|
|
2597
|
+
// float's bits moved out as an integer.
|
|
2598
|
+
const readFloat = (ins: Instr, key: string): Value => {
|
|
2599
|
+
const v = read(key);
|
|
2600
|
+
if (v.type.kind !== 'float') {
|
|
2601
|
+
throw new FrontendUnsupportedError(`${site(ins)} reads ${key} as a float, and it holds an integer word`);
|
|
2602
|
+
}
|
|
2603
|
+
return v;
|
|
2604
|
+
};
|
|
2605
|
+
const readWord = (ins: Instr, key: string): Value => {
|
|
2606
|
+
const v = read(key);
|
|
2607
|
+
if (v.type.kind === 'float') {
|
|
2608
|
+
throw new FrontendUnsupportedError(
|
|
2609
|
+
`${site(ins)} reads ${key} as an integer word, and it holds a float — moving a float's bits is not modelled`,
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
return v;
|
|
2613
|
+
};
|
|
2614
|
+
// The keys of an instruction's FPU operands, or the register-file refusal where it names an odd
|
|
2615
|
+
// half or the target has no float homes.
|
|
2616
|
+
const fpOperands = (ins: Instr, at: readonly number[]): string[] | null => {
|
|
2617
|
+
const keys = at.map((i) => mipsEvenFpKey(ins.ops[i] ?? ''));
|
|
2618
|
+
if (fpu === undefined || keys.some((k) => k === null)) {
|
|
2619
|
+
emitOpaqueDest(ins);
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
if (fpType === null) {
|
|
2623
|
+
refuseBothPrecisions(name, ins.mnemonic);
|
|
2624
|
+
}
|
|
2625
|
+
return keys as string[];
|
|
2626
|
+
};
|
|
2627
|
+
// `mfc1 rt, fs` / `mtc1 rt, fs`: an integer word between the files, unchanged. Which words came
|
|
2628
|
+
// IN is kept for `settleReturnHome`: the IR holds both homes as one value.
|
|
2629
|
+
const emitFpMove = (ins: Instr) => {
|
|
2630
|
+
const keys = fpOperands(ins, [1]);
|
|
2631
|
+
if (keys === null) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
if (ins.mnemonic === 'mfc1') {
|
|
2635
|
+
write(ins.ops[0], readWord(ins, keys[0]));
|
|
2636
|
+
} else {
|
|
2637
|
+
const v = read(ins.ops[0]);
|
|
2638
|
+
movedIn.add(v);
|
|
2639
|
+
write(keys[0], v);
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
const emitFpConvert = (ins: Instr) => {
|
|
2643
|
+
const keys = fpOperands(ins, [0, 1]);
|
|
2644
|
+
if (keys === null) {
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
const { op } = FP_CONVERT[ins.mnemonic];
|
|
2648
|
+
const toInt = op === 'fptosi';
|
|
2649
|
+
const v = mkValue(toInt ? T.unk(32) : fpType!);
|
|
2650
|
+
ops.push(mkOp(op, { operands: [toInt ? readFloat(ins, keys[1]) : readWord(ins, keys[1])], results: [v] }));
|
|
2651
|
+
write(keys[0], v);
|
|
2652
|
+
};
|
|
2653
|
+
const emitFpCompare = (ins: Instr) => {
|
|
2654
|
+
const keys = fpOperands(ins, [0, 1]);
|
|
2655
|
+
if (keys === null) {
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
const opcode = FP_COMPARE[ins.mnemonic].op;
|
|
2659
|
+
const [lhs, rhs] = keys.map((k) => readFloat(ins, k));
|
|
2660
|
+
const value = mkValue(T.unk(32));
|
|
2661
|
+
ops.push(mkOp(opcode, { operands: [lhs, rhs], results: [value] }));
|
|
2662
|
+
fcc = { opcode, lhs, rhs, value };
|
|
2663
|
+
};
|
|
1134
2664
|
// `%lo` completes the address a `lui %hi` began. The pairing asks SSA — through `readVar`, not
|
|
1135
2665
|
// `read`, which refuses a half — what `rHi` holds HERE, so a pair separated by unrelated
|
|
1136
2666
|
// instructions folds while a register reused between the halves does not, and a half that
|
|
1137
2667
|
// arrives only through a merge or a loop header comes back as the block parameter standing for
|
|
1138
2668
|
// the join, which is not a half and refuses.
|
|
1139
|
-
const foldLoHalf = (ins: Instr, rHi: string, loImm: number): { base: Value; off: number } =>
|
|
2669
|
+
const foldLoHalf = (ins: Instr, rHi: string, loImm: number): { base: Value; off: number } =>
|
|
2670
|
+
addressAt(ins, pairLoHalf(ins, rHi, loImm));
|
|
2671
|
+
/** `&SYM + off` (`+ index` for an indexed half), for the `%lo` at `ins` that completed it. `off`
|
|
2672
|
+
* may be negative: `S[i - k]` puts the constant element in the `%lo`, whether the index is added
|
|
2673
|
+
* between the halves (`lw v0,-4(v0)`, ido, KMC and GCC 2.7.2) or after a base completed first
|
|
2674
|
+
* (`addiu v0,v0,%lo(S)-4; addu v0,a0,v0`, KMC and GCC 2.7.2), and the address is exactly
|
|
2675
|
+
* `S + off` either way. */
|
|
2676
|
+
const addressAt = (ins: Instr, { off, index }: { off: number; index?: Value }): { base: Value; off: number } => {
|
|
2677
|
+
const g = emitGaddr(ins.reloc!.sym);
|
|
2678
|
+
if (index === undefined) {
|
|
2679
|
+
return { base: g, off };
|
|
2680
|
+
}
|
|
2681
|
+
const v = mkValue(T.unk(32));
|
|
2682
|
+
ops.push(mkOp('add', { operands: [g, index], results: [v] }));
|
|
2683
|
+
return { base: v, off };
|
|
2684
|
+
};
|
|
2685
|
+
/** The byte offset into its symbol of the address the `%lo` at `ins` completes, the pair proven,
|
|
2686
|
+
* and the runtime byte index an indexed half added between the halves. */
|
|
2687
|
+
const pairLoHalf = (ins: Instr, rHi: string, loImm: number): { off: number; index?: Value } => {
|
|
1140
2688
|
relocTaken = true;
|
|
1141
|
-
|
|
1142
|
-
const hi = highHalves.pair(site(ins), rHi, readVar(rHi, bi),
|
|
2689
|
+
// `addressAt` adds the index back, so this consumer takes an indexed half.
|
|
2690
|
+
const hi = highHalves.pair(site(ins), rHi, readVar(rHi, bi), ins.reloc!.sym, undefined, true);
|
|
1143
2691
|
// The addend is split across the two instruction immediates because MIPS is REL.
|
|
1144
|
-
|
|
1145
|
-
if (off < 0) {
|
|
1146
|
-
throw new FrontendUnsupportedError(
|
|
1147
|
-
`${site(ins)} completes '${lo.sym}' at ` +
|
|
1148
|
-
`byte offset ${off} — an address BELOW the symbol (an index-biased array base) is not yet modelled`,
|
|
1149
|
-
);
|
|
1150
|
-
}
|
|
1151
|
-
return { base: emitGaddr(lo.sym), off };
|
|
2692
|
+
return { off: hi.addend + loImm, index: hi.index };
|
|
1152
2693
|
};
|
|
1153
2694
|
// A displacement memory operand whose base is a `%hi` half: the global's address plus the
|
|
1154
2695
|
// access offset. A non-`%lo` instruction returns null (the caller falls through to the ordinary
|
|
@@ -1160,33 +2701,110 @@ export function lift(
|
|
|
1160
2701
|
const { off, base } = parseMem(mem);
|
|
1161
2702
|
return foldLoHalf(ins, base, off);
|
|
1162
2703
|
};
|
|
2704
|
+
// The address an access in the local area, or through a frame sum, is made at — undefined for
|
|
2705
|
+
// every other access. A register the prologue SAVES may not be stored there: the save area is
|
|
2706
|
+
// outside it by construction, so a save inside it is a frame this does not describe.
|
|
2707
|
+
const frameAccess = (ins: Instr, base: string, off: number, width: number, stored?: string) => {
|
|
2708
|
+
if (isStackPtr(base)) {
|
|
2709
|
+
if (!inLocalArea(ins, off, width)) {
|
|
2710
|
+
return undefined;
|
|
2711
|
+
}
|
|
2712
|
+
if (stored !== undefined && savedByCallee(stored) && ssa.holdsEntryValue(stored, bi)) {
|
|
2713
|
+
throw new FrontendUnsupportedError(
|
|
2714
|
+
`cannot lift '${name}': address-taken stack local — '${ins.mnemonic}' at 0x${ins.addr.toString(16)} saves the caller's ${stored} inside the local area, a frame this does not describe`,
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
return emitLaddr(off);
|
|
2718
|
+
}
|
|
2719
|
+
const raw = rawFrame(base);
|
|
2720
|
+
const sum = raw === undefined ? undefined : frameSums.get(raw);
|
|
2721
|
+
return sum === undefined ? undefined : materialize(sum, off);
|
|
2722
|
+
};
|
|
2723
|
+
// IDO reads a `volatile` doubleword nothing uses into the register PAIR its discard register
|
|
2724
|
+
// heads: `lw zero,0(a0)` then `lw at,4(a0)` (O32 pairs a register with the next one, and `at`
|
|
2725
|
+
// follows `zero`), measured at -O1 and -O0 for `volatile long long` and `volatile double`
|
|
2726
|
+
// through a pointer, an index and a global (`lui at` re-formed between the words), the `at`
|
|
2727
|
+
// word in the `jr`'s delay slot at -O1. So a word load into `at` is discarded too where it reads
|
|
2728
|
+
// the word above one the discard register took, at the same address, with no write to that
|
|
2729
|
+
// address's base register and no call between (`firstWords`); anywhere else `at` is the
|
|
2730
|
+
// assembler's own temporary.
|
|
2731
|
+
const loaded = (d: string, res: Value, discard: boolean) => {
|
|
2732
|
+
if (discard) {
|
|
2733
|
+
discarded.add(res);
|
|
2734
|
+
}
|
|
2735
|
+
write(d, res);
|
|
2736
|
+
};
|
|
1163
2737
|
const emitLoad = (ins: Instr, d: string, mem: string, width: number, signed: boolean) => {
|
|
2738
|
+
const sym = ins.reloc?.type === 'R_MIPS_LO16' ? ins.reloc.sym : undefined;
|
|
2739
|
+
const { off: wordOff, base: wordBase } = parseMem(mem);
|
|
2740
|
+
const word = { reg: sym === undefined ? wordBase : undefined, sym, off: wordOff };
|
|
2741
|
+
const first =
|
|
2742
|
+
d === 'at' && width === 4
|
|
2743
|
+
? firstWords.find((f) => word.reg === f.reg && word.sym === f.sym && word.off === f.off + 4)
|
|
2744
|
+
: undefined;
|
|
2745
|
+
const discard = discards(d) || first !== undefined;
|
|
2746
|
+
firstWords = firstWords.filter((f) => f !== first && (f.reg === undefined || f.reg !== d));
|
|
2747
|
+
if (discards(d) && width === 4) {
|
|
2748
|
+
firstWords.push(word);
|
|
2749
|
+
}
|
|
1164
2750
|
const g = globalBase(ins, mem);
|
|
1165
2751
|
if (g) {
|
|
1166
2752
|
const res = mkValue(T.unk(32));
|
|
1167
2753
|
ops.push(mkOp('load', { operands: [g.base], results: [res], attrs: { off: g.off, width, signed } }));
|
|
1168
|
-
|
|
2754
|
+
loaded(d, res, discard);
|
|
1169
2755
|
return;
|
|
1170
2756
|
}
|
|
1171
2757
|
const { off, base } = parseMem(mem);
|
|
2758
|
+
// An access inside the local area, or through a frame sum, is MEMORY (`inLocalArea`).
|
|
2759
|
+
const at = frameAccess(ins, base, off, width);
|
|
2760
|
+
if (at !== undefined) {
|
|
2761
|
+
const res = mkValue(T.unk(32));
|
|
2762
|
+
ops.push(mkOp('load', { operands: [at], results: [res], attrs: { off: 0, width, signed } }));
|
|
2763
|
+
loaded(d, res, discard);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
1172
2766
|
// A word reload from a stack slot is transparent to dataflow — the SAME value spilled — so
|
|
1173
2767
|
// route it through the slot SSA variable, not a `load` through `sp` (which would make `sp` a
|
|
1174
2768
|
// spurious pointer parameter). Compiler spills/reloads are always word-width; sub-word `sp`
|
|
1175
2769
|
// access is not spill output, so it stays on the memory path.
|
|
1176
2770
|
if (spSlotSafe && isStackPtr(base) && width === 4) {
|
|
2771
|
+
const saved = saveSlots.get(off);
|
|
2772
|
+
if (saved !== undefined) {
|
|
2773
|
+
if (saved !== d) {
|
|
2774
|
+
throw new FrontendUnsupportedError(
|
|
2775
|
+
`cannot lift '${name}': reload of '${mem}' into ${d}, a slot ${saved} was saved into — the ` +
|
|
2776
|
+
`word holds the caller's ${saved}, which no C value names`,
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
const caller = mkValue(T.unk(32));
|
|
2780
|
+
restores.set(caller, { reg: d, addr: ins.addr });
|
|
2781
|
+
restoredBy.set(ins, new Set([d]));
|
|
2782
|
+
write(d, caller);
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
1177
2785
|
// SOUNDNESS GUARD (mirrors PPC frameLoad): only route through the slot SSA var if that slot was
|
|
1178
2786
|
// actually STORED (has a reaching def). A word `lw` from an sp offset that was NEVER spilled is an
|
|
1179
|
-
// incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
2787
|
+
// incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local. Without this,
|
|
2788
|
+
// readVar would FABRICATE a phantom entry parameter for the slot, silently emitting a function
|
|
2789
|
+
// of wrong arity that returns the wrong argument.
|
|
1182
2790
|
//
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
//
|
|
1187
|
-
//
|
|
1188
|
-
//
|
|
1189
|
-
|
|
2791
|
+
// The two are told apart by where the word is (`incomingStackKey`): a word at or past entry +
|
|
2792
|
+
// 16 is that argument, `@sarg<k>`, a parameter through the ordinary live-in path, as on
|
|
2793
|
+
// Thumb, merged with whatever this function stored there. Everything else keeps the
|
|
2794
|
+
// decline: a word of this frame, of the home area, or at an unproven depth. A sub-word or
|
|
2795
|
+
// floating-point incoming argument never reaches here (the `spSlotSafe` guard and the FP
|
|
2796
|
+
// arm).
|
|
2797
|
+
const incoming = incomingStackKey(ins, off);
|
|
2798
|
+
if (incoming !== undefined) {
|
|
2799
|
+
write(d, readVar(incoming, bi));
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
// A word of the frame this function OWNS (`ownedFrame`) that no store reaches is an
|
|
2803
|
+
// uninitialised local, which `readVar` answers with `undef` through the live-in model.
|
|
2804
|
+
const owned = ownedFrame();
|
|
2805
|
+
const ownsWord =
|
|
2806
|
+
owned !== undefined && depthAt?.get(ins) === frameSize && off >= owned.from && off + 4 <= owned.to;
|
|
2807
|
+
if (!ownsWord && !ssa.hasReachingDef(stackSlot(off), bi)) {
|
|
1190
2808
|
throw new FrontendUnsupportedError(
|
|
1191
2809
|
`cannot lift '${name}': load from stack slot sp@${off} that was never stored ` +
|
|
1192
2810
|
`(stack-passed argument beyond the 4 register args, or an address-taken/uninitialised local) — not modelled`,
|
|
@@ -1197,7 +2815,7 @@ export function lift(
|
|
|
1197
2815
|
}
|
|
1198
2816
|
const res = mkValue(T.unk(32));
|
|
1199
2817
|
ops.push(mkOp('load', { operands: [read(base)], results: [res], attrs: { off, width, signed } }));
|
|
1200
|
-
|
|
2818
|
+
loaded(d, res, discard);
|
|
1201
2819
|
};
|
|
1202
2820
|
const emitStore = (ins: Instr, srcReg: string, mem: string, width: number) => {
|
|
1203
2821
|
const g = globalBase(ins, mem);
|
|
@@ -1206,18 +2824,172 @@ export function lift(
|
|
|
1206
2824
|
return;
|
|
1207
2825
|
}
|
|
1208
2826
|
const { off, base } = parseMem(mem);
|
|
2827
|
+
const at = frameAccess(ins, base, off, width, srcReg);
|
|
2828
|
+
if (at !== undefined) {
|
|
2829
|
+
ops.push(mkOp('store', { operands: [at, read(srcReg)], attrs: { off: 0, width } }));
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
1209
2832
|
// A word spill to a stack slot (an argument home slot `sw a0,0(sp)`, or a local): record the
|
|
1210
2833
|
// slot's value in SSA, do NOT emit a `store` through `sp`. A never-reloaded spill (the ABI
|
|
1211
2834
|
// home-slot store) then has no uses and simply drops. See isStackPtr / spSlotSafe.
|
|
1212
2835
|
if (spSlotSafe && isStackPtr(base) && width === 4) {
|
|
1213
|
-
|
|
2836
|
+
// A global register variable's register is never saved — compiled code stores it only as a
|
|
2837
|
+
// value, the outgoing stack argument `k(1, 2, 3, 4, g)` is (`sw s0,16(sp)`) — and the builder
|
|
2838
|
+
// records no definition for one (its writes are stores to the global), so "still holds the
|
|
2839
|
+
// entry value" is no evidence of a save there.
|
|
2840
|
+
if (savedByCallee(srcReg) && !liveIns.globalRegs?.has(srcReg) && ssa.holdsEntryValue(srcReg, bi)) {
|
|
2841
|
+
refuseMixedSlot(ins, mem, off, 'save');
|
|
2842
|
+
saveSlots.set(off, srcReg);
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
refuseMixedSlot(ins, mem, off, 'value');
|
|
2846
|
+
valueSlots.add(off);
|
|
2847
|
+
// an assignment to an incoming stack parameter is that parameter's SSA variable
|
|
2848
|
+
writeVar(incomingStackKey(ins, off) ?? stackSlot(off), bi, read(srcReg));
|
|
1214
2849
|
return;
|
|
1215
2850
|
}
|
|
1216
2851
|
ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
|
|
1217
2852
|
};
|
|
1218
2853
|
|
|
1219
|
-
|
|
1220
|
-
|
|
2854
|
+
// A CALL, WITH ITS DELAY SLOT RUN FIRST: the word at `jal + 4` executes before the callee is
|
|
2855
|
+
// entered, so it can set an argument up (KMC stores argument 5 there). The plan answers what
|
|
2856
|
+
// the callee's declaration says (`frontend/call-plan.ts`); what is O32's alone is here: where
|
|
2857
|
+
// argument k ≥ 4 lives, and the guess that cannot see past a3.
|
|
2858
|
+
const lowerJal = (ins: Instr) => {
|
|
2859
|
+
const at = `0x${ins.addr.toString(16)}`;
|
|
2860
|
+
const callee = jalCallee(ins, splat);
|
|
2861
|
+
if (callee?.startsWith('.')) {
|
|
2862
|
+
throw new FrontendUnsupportedError(
|
|
2863
|
+
`${site(ins)} is relocated against the section '${callee}' — a callee known only by its offset ` +
|
|
2864
|
+
'in a section has no name to call it by',
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
if (callee === undefined || !/^[A-Za-z_]\w*$/.test(callee)) {
|
|
2868
|
+
throw new FrontendUnsupportedError(
|
|
2869
|
+
`${site(ins)} has no callee symbol — no R_MIPS_26 relocation names it, and its operand ` +
|
|
2870
|
+
`'${ins.ops[0] ?? ''}' is not a name`,
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
if (fpuInstr !== undefined) {
|
|
2874
|
+
throw new FrontendUnsupportedError(
|
|
2875
|
+
`cannot lift '${name}': '${fpuInstr.mnemonic}' at 0x${fpuInstr.addr.toString(16)} uses the FPU and ` +
|
|
2876
|
+
`'jal' at ${at} makes a call — the floating-point registers a call reads, returns in and destroys ` +
|
|
2877
|
+
'are not modelled',
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
if (frameSize === undefined) {
|
|
2881
|
+
throw new FrontendUnsupportedError(
|
|
2882
|
+
`cannot lift '${name}': the call at ${at} is made in a frame that is not one 'addiu sp,sp,-N', so ` +
|
|
2883
|
+
'where its outgoing stack arguments end is not known',
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
calls.lower({
|
|
2887
|
+
callee,
|
|
2888
|
+
ssa,
|
|
2889
|
+
bi,
|
|
2890
|
+
read,
|
|
2891
|
+
write,
|
|
2892
|
+
// ARGUMENT WORD k ≥ 4 travels in `16+4(k-4)(sp)`, inside the one frame this function pushed
|
|
2893
|
+
stackWord: (k) => {
|
|
2894
|
+
const off = STACK_ARGS_AT + 4 * (k - ARG_REGS.length);
|
|
2895
|
+
if (off >= frameSize) {
|
|
2896
|
+
throw new FrontendUnsupportedError(
|
|
2897
|
+
`cannot lift '${name}': outgoing stack argument ${k + 1} of '${callee}' at ${at} travels in ` +
|
|
2898
|
+
`${off}(sp), past the ${frameSize}-byte frame this function pushed`,
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
if (!valueSlots.has(off) || !ssa.hasReachingDef(stackSlot(off), bi)) {
|
|
2902
|
+
throw new FrontendUnsupportedError(
|
|
2903
|
+
`cannot lift '${name}': outgoing stack argument ${k + 1} of '${callee}' at ${at} travels in ` +
|
|
2904
|
+
`${off}(sp), and no value stored there reaches the call`,
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
return readVar(stackSlot(off), bi);
|
|
2908
|
+
},
|
|
2909
|
+
// a pending `%hi` half and a table base are no argument
|
|
2910
|
+
placeholders,
|
|
2911
|
+
parked,
|
|
2912
|
+
scaledIndex,
|
|
2913
|
+
guess: {
|
|
2914
|
+
// a gap in the argument registers refuses, naming this address (`fallbackArgc`)
|
|
2915
|
+
at: ins.addr,
|
|
2916
|
+
// A GUESS THAT FILLS a0..a3 CANNOT SAY WHERE THE LIST ENDS: a word of the outgoing area that
|
|
2917
|
+
// reaches the call is argument 5 onward, or a local the compiler put there.
|
|
2918
|
+
refuse: (argc) => {
|
|
2919
|
+
// …and with a local area a guess says less still. A word staged for the call above the
|
|
2920
|
+
// home area may be MEMORY there, which the slot test below cannot see; and a word
|
|
2921
|
+
// staged below it reaches the call whether or not a3 is set, since an 8-aligned
|
|
2922
|
+
// argument (`long long`, a struct holding one) skips a3 for the stack. Where the
|
|
2923
|
+
// loaded-word bound reaches the local area, no word of it is a stack argument, and the
|
|
2924
|
+
// slot test below sees every word that can be one.
|
|
2925
|
+
if (frame !== null) {
|
|
2926
|
+
if (argc >= ARG_REGS.length && loadedWordBound > frame.area.from) {
|
|
2927
|
+
throw new FrontendUnsupportedError(
|
|
2928
|
+
`cannot lift '${name}': outgoing stack arguments not modelled — the undeclared call to '${callee}' ` +
|
|
2929
|
+
`at ${at} fills a0..a3 in a frame whose local area holds a captured address, so where its ` +
|
|
2930
|
+
'stack words end is not known',
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
const staged = [...valueSlots]
|
|
2934
|
+
.filter(
|
|
2935
|
+
(off) =>
|
|
2936
|
+
off >= STACK_ARGS_AT &&
|
|
2937
|
+
off < frame.area.from &&
|
|
2938
|
+
off < loadedWordBound &&
|
|
2939
|
+
ssa.hasReachingDef(stackSlot(off), bi),
|
|
2940
|
+
)
|
|
2941
|
+
.sort((x, y) => x - y)[0];
|
|
2942
|
+
if (staged !== undefined) {
|
|
2943
|
+
throw new FrontendUnsupportedError(
|
|
2944
|
+
`cannot lift '${name}': outgoing stack arguments not modelled — the word stored to ${staged}(sp), ` +
|
|
2945
|
+
`below the local area, reaches the undeclared call to '${callee}' at ${at}, where a stack ` +
|
|
2946
|
+
'argument travels',
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
if (argc < ARG_REGS.length) {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
// …save a word at or above the loaded-word bound, which is a local's
|
|
2954
|
+
const passed = [...valueSlots]
|
|
2955
|
+
.filter(
|
|
2956
|
+
(off) =>
|
|
2957
|
+
off >= STACK_ARGS_AT &&
|
|
2958
|
+
off < frameSize &&
|
|
2959
|
+
off < loadedWordBound &&
|
|
2960
|
+
ssa.hasReachingDef(stackSlot(off), bi),
|
|
2961
|
+
)
|
|
2962
|
+
.sort((x, y) => x - y)[0];
|
|
2963
|
+
if (passed !== undefined) {
|
|
2964
|
+
throw new FrontendUnsupportedError(
|
|
2965
|
+
`cannot lift '${name}': outgoing stack arguments not modelled — the undeclared call to '${callee}' ` +
|
|
2966
|
+
`at ${at} fills a0..a3, and the word stored to ${passed}(sp) reaches it where an argument past ` +
|
|
2967
|
+
'the registers travels',
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
},
|
|
2971
|
+
},
|
|
2972
|
+
});
|
|
2973
|
+
// hi and lo are the callee's to overwrite
|
|
2974
|
+
divState = null;
|
|
2975
|
+
mulState = null;
|
|
2976
|
+
};
|
|
2977
|
+
for (let i = 0; i < b.body.length; i++) {
|
|
2978
|
+
const ins = b.body[i];
|
|
2979
|
+
if (ins.mnemonic !== 'jal') {
|
|
2980
|
+
decode(ins);
|
|
2981
|
+
continue;
|
|
2982
|
+
}
|
|
2983
|
+
const slot = b.body[i + 1];
|
|
2984
|
+
if (slot?.addr !== ins.addr + 4) {
|
|
2985
|
+
throw new FrontendUnsupportedError(
|
|
2986
|
+
`${site(ins)} — its delay slot at 0x${(ins.addr + 4).toString(16)} is not the next instruction of ` +
|
|
2987
|
+
'its block (a branch target, a control transfer, or missing), so it does not run before this call alone',
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
decode(slot);
|
|
2991
|
+
i++;
|
|
2992
|
+
lowerJal(ins);
|
|
1221
2993
|
}
|
|
1222
2994
|
|
|
1223
2995
|
// Terminator. For a conditional branch, capture the comparison operands from the register
|
|
@@ -1238,20 +3010,45 @@ export function lift(
|
|
|
1238
3010
|
return { block: irBlocks[j], args: [] };
|
|
1239
3011
|
};
|
|
1240
3012
|
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
3013
|
+
// `bc1t` is taken on the code a compare in this block set, `bc1f` on its logical NOT, which is
|
|
3014
|
+
// the UNORDERED compare (`NEGATED_FCMP`): `!(a < b)` is true on a NaN, and `a >= b` is not. A
|
|
3015
|
+
// compare in the delay slot would set the code after the branch read it.
|
|
3016
|
+
const fpCondValue = (ins: Instr): Value => {
|
|
3017
|
+
if (fcc === null) {
|
|
3018
|
+
throw new FrontendUnsupportedError(`${site(ins)} tests a condition code no compare in its block set`);
|
|
3019
|
+
}
|
|
3020
|
+
if (b.delay && b.delay.mnemonic in FP_COMPARE) {
|
|
3021
|
+
throw new FrontendUnsupportedError(`${site(ins)} has a compare in its delay slot`);
|
|
3022
|
+
}
|
|
3023
|
+
if (FP_COND_BRANCH[ins.mnemonic]) {
|
|
3024
|
+
return fcc.value;
|
|
3025
|
+
}
|
|
3026
|
+
const v = mkValue(T.unk(32));
|
|
3027
|
+
ops.push(mkOp(NEGATED_FCMP[fcc.opcode], { operands: [fcc.lhs, fcc.rhs], results: [v] }));
|
|
3028
|
+
return v;
|
|
3029
|
+
};
|
|
3030
|
+
|
|
3031
|
+
// Recovered dense switch: dispatch a `switch_br` over the scrutinee (N case blocks, dense
|
|
3032
|
+
// 0..N-1, + default). The scrutinee is the value the `sltiu` bounded, so it is read BEFORE the
|
|
3033
|
+
// `beqz` delay slot runs, as a conditional branch reads its operands: IDO can scale the index
|
|
3034
|
+
// IN PLACE there (`sll t6,t6,2`), and reading after the slot would switch on `k << 2` over the
|
|
3035
|
+
// unscaled labels 0..N-1. The slot still runs, because it can also compute the DEFAULT value
|
|
3036
|
+
// (IDO's `li v0,-1`, read by the default block).
|
|
1244
3037
|
const jt = br ? jts.get(br.addr) : undefined;
|
|
1245
3038
|
if (jt) {
|
|
3039
|
+
const scrut = readVar(jt.scrutReg, bi);
|
|
1246
3040
|
if (b.delay) {
|
|
1247
3041
|
decode(b.delay);
|
|
1248
3042
|
}
|
|
1249
|
-
|
|
3043
|
+
if (jt.hoisted) {
|
|
3044
|
+
tableBases.demand(jt.hoisted, bi, jt.jrAddr);
|
|
3045
|
+
}
|
|
3046
|
+
pushSwitchBr(ops, scrut, [...jt.caseAddrs, jt.defaultAddr].map(succ));
|
|
1250
3047
|
return;
|
|
1251
3048
|
}
|
|
1252
3049
|
|
|
1253
3050
|
if (br && isCond(br)) {
|
|
1254
|
-
const cond = condValue(br, ops, read, constVal, cmpDef);
|
|
3051
|
+
const cond = br.mnemonic in FP_COND_BRANCH ? fpCondValue(br) : condValue(br, ops, read, constVal, cmpDef);
|
|
1255
3052
|
// A NULLIFIED slot is not decoded here: it is its own block on the taken edge, and the
|
|
1256
3053
|
// not-taken edge skips past it (toBlocks). Every pass after this one sees ordinary
|
|
1257
3054
|
// conditional execution.
|
|
@@ -1277,17 +3074,48 @@ export function lift(
|
|
|
1277
3074
|
// loud, but for a merge that never happened. Rejecting it gives the honest void return.
|
|
1278
3075
|
// PowerPC reads its return register through the guard, so the refusal it would get is already
|
|
1279
3076
|
// the right one; it passes this same predicate to its call-arity count.
|
|
1280
|
-
|
|
3077
|
+
//
|
|
3078
|
+
// NOR IS WHAT A VOID CALLEE LEFT IN v0. The call writes no v0 and destroys it, and `holdsValue`
|
|
3079
|
+
// ends a path there, so a function that ends on that call returns nothing.
|
|
1281
3080
|
if (!br) {
|
|
1282
3081
|
// A nullified slot's block leaves by its branch's TARGET, which need not be the next
|
|
1283
3082
|
// address, so `fallthrough` is claimed only where control really does fall through.
|
|
1284
3083
|
const to = succAddrs.get(b)![0];
|
|
1285
3084
|
const next = (b.body[b.body.length - 1]?.addr ?? b.startAddr) + 4;
|
|
1286
3085
|
ops.push(mkOp('br', { attrs: to === next ? { fallthrough: true } : {}, successors: [succ(to)] }));
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
// A FLOAT RETURN is decided for the whole function (frontend/fpu.ts `returnHomeRule`), and
|
|
3089
|
+
// then `v0` is scratch like any other integer register. An undecided one returns `v0` here,
|
|
3090
|
+
// and `settleReturnHome` may hand the return the float home instead.
|
|
3091
|
+
//
|
|
3092
|
+
// NOR ANYTHING AT ALL WHERE THE FUNCTION'S OWN DECLARATION IS VOID. Then `v0` is scratch on every
|
|
3093
|
+
// path: a loop can leave a value in it on one path while a void callee destroyed it on another
|
|
3094
|
+
// (`for (..) if (t[i] == 7) { Ping(i); return; }`), and reading it would join the callee's
|
|
3095
|
+
// leftover into a return the declaration says carries nothing — refused as a stale read
|
|
3096
|
+
// (ssa.ts `refuseStaleCallerSavedReads`), and dropped unread after the lift anyway (pipeline.ts
|
|
3097
|
+
// `dropVoidReturnOperands`). The same declaration `ownReturnFitsAWord` reads.
|
|
3098
|
+
const returnsWord =
|
|
3099
|
+
!floatReturn &&
|
|
3100
|
+
!declaresVoidReturn(own) &&
|
|
3101
|
+
ssa.holdsValue(RET, bi, (v) => !placeholders.has(v) && !parked(v, RET));
|
|
3102
|
+
// A 64-BIT INTEGER RETURN COMES BACK IN v0:v1, the high word in v0, and this lowering returns
|
|
3103
|
+
// one word: returning v0 alone hands back the high word with the low one dropped, a function
|
|
3104
|
+
// that compiles and returns the wrong value (`long long f(int a, int b, int c, int d,
|
|
3105
|
+
// long long e) { return e; }` read as `return a4;`). Where v1 was last written for the caller
|
|
3106
|
+
// (`v1MayHoldLowWord`) this refuses, unless the function's own declaration says the return
|
|
3107
|
+
// fits a word. Modelling the pair is the capability this stands in for.
|
|
3108
|
+
if (returnsWord && !ownReturnFitsAWord && v1MayHoldLowWord(blocks, preds, bi)) {
|
|
3109
|
+
throw new FrontendUnsupportedError(
|
|
3110
|
+
`cannot lift '${name}': the return at 0x${br.addr.toString(16)} may hand back the halves of a 64-bit pair ` +
|
|
3111
|
+
'in v0:v1 (v1 is written for the caller, and nothing declares a return no wider than a word), and this ' +
|
|
3112
|
+
'lowering returns one word',
|
|
3113
|
+
);
|
|
1290
3114
|
}
|
|
3115
|
+
const retOps = floatReturn ? [read(fpu!.returnReg)] : returnsWord ? [readVar(RET, bi)] : [];
|
|
3116
|
+
const ret = mkOp('ret', { operands: retOps });
|
|
3117
|
+
ops.push(ret);
|
|
3118
|
+
returns.push({ op: ret, bi });
|
|
1291
3119
|
return;
|
|
1292
3120
|
}
|
|
1293
3121
|
// unconditional branch
|
|
@@ -1298,19 +3126,371 @@ export function lift(
|
|
|
1298
3126
|
fillBlock(b, bi);
|
|
1299
3127
|
ssa.markFilled(bi);
|
|
1300
3128
|
});
|
|
3129
|
+
refuseSavedRegisterLeaks(
|
|
3130
|
+
name,
|
|
3131
|
+
blocks.map((b) => ({
|
|
3132
|
+
instrs: [...b.body, ...(b.delay ? [b.delay] : [])],
|
|
3133
|
+
returns: b.branch !== null && isReturn(b.branch) && b.branch.ops[0] === 'ra',
|
|
3134
|
+
})),
|
|
3135
|
+
preds,
|
|
3136
|
+
new Set([...saveSlots.values()].filter((r) => r !== 'ra')),
|
|
3137
|
+
(ins, r) => writtenReg(ins) === r,
|
|
3138
|
+
restoredBy,
|
|
3139
|
+
);
|
|
3140
|
+
// What the float home holds at each return, and, where the home is undecided and holds a float on
|
|
3141
|
+
// every path into every return, that float — read now, before the SSA is finished.
|
|
3142
|
+
const homes = fpu ? returns.map(({ bi }) => floatHomeAt(ssa, fpu.returnReg, bi)) : [];
|
|
3143
|
+
const carriesFloat = carriesFloatHome(homeRule, homes);
|
|
3144
|
+
if (carriesFloat) {
|
|
3145
|
+
returns.forEach(({ op, bi }) => op.operands.push(readVar(fpu!.returnReg, bi)));
|
|
3146
|
+
}
|
|
1301
3147
|
highHalves.assertAllConsumed(name);
|
|
3148
|
+
tableBases.prove(name, ssa);
|
|
3149
|
+
refuseStaleIdiomReads(
|
|
3150
|
+
name,
|
|
3151
|
+
irBlocks,
|
|
3152
|
+
staleIdiom,
|
|
3153
|
+
ssa,
|
|
3154
|
+
prototypes[name] ?? options.contextOwnProto?.proto,
|
|
3155
|
+
fail,
|
|
3156
|
+
calls,
|
|
3157
|
+
);
|
|
3158
|
+
const argSlots = o32ArgSlots(name, fpuArgSlots(name, fpu, ARG_REGS, isFpKey, fpType), ARG_REGS);
|
|
3159
|
+
mintArgSlotHoles(ssa, preds[0].length > 0, argSlots);
|
|
1302
3160
|
ssa.finish();
|
|
3161
|
+
refuseFpWordCrossings(name, ssa.fn);
|
|
3162
|
+
// Asked once the lift is done, so a refusal the stream meets first keeps its own reason.
|
|
3163
|
+
if (fpu) {
|
|
3164
|
+
settleReturnHome(name, ssa.fn, homeRule, returns, homes, carriesFloat, fpu, RET, movedIn);
|
|
3165
|
+
}
|
|
1303
3166
|
highHalves.assertNoneEscaped(name, irBlocks);
|
|
3167
|
+
tableBases.assertNoneEscaped(name, irBlocks);
|
|
3168
|
+
for (const blk of irBlocks) {
|
|
3169
|
+
const used = [
|
|
3170
|
+
...blk.params,
|
|
3171
|
+
...blk.ops.flatMap((op) => [...op.operands, ...op.results, ...op.successors.flatMap((s) => s.args)]),
|
|
3172
|
+
];
|
|
3173
|
+
const restore = used.map((v) => restores.get(v)).find((r) => r !== undefined);
|
|
3174
|
+
if (restore) {
|
|
3175
|
+
throw new FrontendUnsupportedError(
|
|
3176
|
+
`cannot lift '${name}': the caller's ${restore.reg}, which the 'lw' at 0x${restore.addr.toString(16)} ` +
|
|
3177
|
+
`restores, reaches a value this function computes with — no C value names it`,
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// A struct passed by value becomes the one object its words hold (call-plan.ts `foldByValueArgs`)
|
|
3183
|
+
// before the audit judges the frame objects, as on Thumb.
|
|
3184
|
+
foldByValueArgs(name, irBlocks, calls, { symbols, context });
|
|
3185
|
+
nameMergedSretObjects(irBlocks);
|
|
1304
3186
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
3187
|
+
if (frame !== null) {
|
|
3188
|
+
refuseEscapedFrameSums(name, irBlocks, frameSums);
|
|
3189
|
+
// Prove every `laddr` names storage of this frame at a shape the machine states, or decline
|
|
3190
|
+
// (`auditFrameObjects`); an accepted function then gets the device pin the audit answers.
|
|
3191
|
+
const verdict = FRAME_OBJECT_AUDIT.run({
|
|
3192
|
+
name,
|
|
3193
|
+
irBlocks,
|
|
3194
|
+
ownedLocals: frame.area,
|
|
3195
|
+
declaredLocals: frame.area,
|
|
3196
|
+
usedSlotOffsets: new Set([...valueSlots].filter((o) => o < frame.size)),
|
|
3197
|
+
loadedWords: loadedFrom === undefined ? undefined : { from: loadedFrom, bound: loadedWordBound },
|
|
3198
|
+
capturedObjectIsTheWholeFrame: false,
|
|
3199
|
+
movedCaptures,
|
|
3200
|
+
returnsWithoutHiddenPointer: calls.returnsWithoutHiddenPointer,
|
|
3201
|
+
symbols,
|
|
3202
|
+
target,
|
|
3203
|
+
oneObject,
|
|
3204
|
+
});
|
|
3205
|
+
if (verdict !== undefined && 'oneObject' in verdict) {
|
|
3206
|
+
return verdict;
|
|
3207
|
+
}
|
|
3208
|
+
if (verdict !== undefined) {
|
|
3209
|
+
pinDeviceAccesses(irBlocks, verdict, target, symbols);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
refuseSretOutsideFrame(name, irBlocks, target);
|
|
3213
|
+
pinDeadReads(irBlocks, target, {
|
|
3214
|
+
name,
|
|
3215
|
+
own: prototypes[name],
|
|
3216
|
+
atUndeclaredCall: calls.heldAtUndeclaredCall,
|
|
3217
|
+
spilled: ssa.frameWrites,
|
|
3218
|
+
discarded,
|
|
3219
|
+
declarations: { symbols, context },
|
|
3220
|
+
});
|
|
3221
|
+
|
|
3222
|
+
// Asked once everything else lifted, so a function that declines for another reason keeps that message.
|
|
3223
|
+
refuseReturnsNotThroughCallersRa(
|
|
3224
|
+
name,
|
|
3225
|
+
blocks,
|
|
3226
|
+
blocks.map((b) => succAddrs.get(b)!.flatMap((a) => (idxOf.has(a) ? [idxOf.get(a)!] : []))),
|
|
3227
|
+
target.callerSaved,
|
|
3228
|
+
target.kernelRegs ?? [],
|
|
3229
|
+
);
|
|
3230
|
+
abiSortEntryParams(irBlocks[0], preds[0].length > 0, paramReg, argSlots);
|
|
1311
3231
|
return ssa.fn;
|
|
1312
3232
|
}
|
|
1313
3233
|
|
|
3234
|
+
/** `sp + index`, deferred to its use (`materialize`): the frame offset it starts from and the index. */
|
|
3235
|
+
interface FrameSum {
|
|
3236
|
+
readonly base: number;
|
|
3237
|
+
readonly index: Value;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
/** A frame sum is a VALUE no op defines — every use builds its own `laddr` and `add` — so one that
|
|
3241
|
+
* reached a phi, or anything else the fill did not materialize, refuses. */
|
|
3242
|
+
function refuseEscapedFrameSums(name: string, irBlocks: readonly Block[], sums: ReadonlyMap<Value, FrameSum>): void {
|
|
3243
|
+
for (const blk of irBlocks) {
|
|
3244
|
+
for (const op of blk.ops) {
|
|
3245
|
+
if ([...op.operands, ...op.successors.flatMap((x) => x.args)].some((v) => sums.has(v))) {
|
|
3246
|
+
throw new FrontendUnsupportedError(
|
|
3247
|
+
`cannot lift '${name}': address-taken stack local — a frame address plus a runtime index reaches \`${op.opcode}\` across a join, ` +
|
|
3248
|
+
'which object it indexes is not known there',
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
/** An instruction that takes a frame address as data: `addiu rD,sp,k`, `move rD,sp`, or `addu
|
|
3256
|
+
* rD,sp,rX` in either operand order. */
|
|
3257
|
+
function takesFrameAddress(ins: Instr): boolean {
|
|
3258
|
+
const [d, s, t] = ins.ops;
|
|
3259
|
+
if (d === undefined || isStackPtr(d)) {
|
|
3260
|
+
return false;
|
|
3261
|
+
}
|
|
3262
|
+
if (ins.mnemonic === 'addiu' || ins.mnemonic === 'addi' || ins.mnemonic === 'move') {
|
|
3263
|
+
return isStackPtr(s ?? '');
|
|
3264
|
+
}
|
|
3265
|
+
if (ins.mnemonic === 'or') {
|
|
3266
|
+
return isStackPtr(copiedReg(ins) ?? '');
|
|
3267
|
+
}
|
|
3268
|
+
return (
|
|
3269
|
+
(ins.mnemonic === 'addu' || ins.mnemonic === 'add') && t !== undefined && isStackPtr(s ?? '') !== isStackPtr(t)
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
/** THE REGISTERS THE PROLOGUE SAVES: a word store to `sp` of a register O32 makes a callee
|
|
3274
|
+
* preserve, in the entry block (its delay slot included — the slot of a branch-likely is its own
|
|
3275
|
+
* block, so never this one) before any instruction NAMES that register, read or written. What it
|
|
3276
|
+
* stores is then what the caller left there, which is the half of `LiveInModel.uninitRegs` the
|
|
3277
|
+
* frontend owes: a compiler homes a local in a callee-saved register only after saving it, so a
|
|
3278
|
+
* def-less read of a saved one is that local, uninitialised, and of an unsaved one is a global
|
|
3279
|
+
* register variable's or refuses (`registerLiveIns`). "Nothing named it first" is PPC's condition
|
|
3280
|
+
* (`prologueSavedRegs` there): a read before the store would be a use of the caller's value. A
|
|
3281
|
+
* re-entered entry gets an empty preheader as `blocks[0]`, so it measures no save at all.
|
|
3282
|
+
*
|
|
3283
|
+
* A SAVE IS RELOADED. In a function that returns, a store no `lw` of the same register from `sp`
|
|
3284
|
+
* ever answers saves nothing: the caller gets the register back as the body left it, so the word
|
|
3285
|
+
* is a value. gcc 2.7.2 and KMC put exactly such a store in the entry block for a file-scope
|
|
3286
|
+
* global register variable passed on the stack — `register int g __asm__("$16"); return k(1, 2,
|
|
3287
|
+
* 3, 4, g) + g;` is `sw s0,16(sp); jal k` and no reload of s0 — and counting it as a save would
|
|
3288
|
+
* turn the read of `g` after the call into `uninit_s0` and drop the fifth argument. Any `lw` of the
|
|
3289
|
+
* register from `sp` will do, whatever its word: which word a restore reads is judged where it is
|
|
3290
|
+
* lowered. A function with no return keeps the entry-block reading: nothing reaches a caller. */
|
|
3291
|
+
function prologueSavedRegs(
|
|
3292
|
+
entry: readonly Instr[],
|
|
3293
|
+
target: TargetDescription,
|
|
3294
|
+
code: readonly Instr[],
|
|
3295
|
+
): ReadonlySet<string> {
|
|
3296
|
+
const saved = new Set<string>();
|
|
3297
|
+
const named = new Set<string>();
|
|
3298
|
+
const calleeSaved = (r: string) => isMipsReg(r) && !r.startsWith('$') && !target.callerSaved.includes(r);
|
|
3299
|
+
const returns = code.some((ins) => ins.mnemonic === 'jr' && ins.ops[0] === 'ra');
|
|
3300
|
+
const reloaded = (r: string) =>
|
|
3301
|
+
code.some((ins) => ins.mnemonic === 'lw' && ins.ops[0] === r && isStackPtr(parseMem(ins.ops[1] ?? '').base));
|
|
3302
|
+
for (const ins of entry) {
|
|
3303
|
+
const [src, mem] = ins.ops;
|
|
3304
|
+
if (ins.mnemonic === 'sw' && src !== undefined && mem !== undefined && isStackPtr(parseMem(mem).base)) {
|
|
3305
|
+
if (calleeSaved(src) && !named.has(src) && (!returns || reloaded(src))) {
|
|
3306
|
+
saved.add(src);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
for (const op of ins.ops) {
|
|
3310
|
+
const reg = op.includes('(') ? parseMem(op).base : op;
|
|
3311
|
+
if (isMipsReg(reg)) {
|
|
3312
|
+
named.add(reg);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
const w = writtenReg(ins);
|
|
3316
|
+
if (w !== null) {
|
|
3317
|
+
named.add(w);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
return saved;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
/** The local area of a function that takes a frame address (`o32LocalArea`), and the depth every
|
|
3324
|
+
* access it routes runs at — or null, which leaves every capture its sp-as-data refusal. Null
|
|
3325
|
+
* where nothing takes a frame address; where the depth walk is unproven or the frame is not one
|
|
3326
|
+
* push; where a capture runs at another depth; where the target states no layout or leaves no
|
|
3327
|
+
* area; and where an `sp` access at another depth lands on the area's bytes, which the routing by
|
|
3328
|
+
* raw offset would then misname.
|
|
3329
|
+
*
|
|
3330
|
+
* The outgoing area ends past the widest call's stack words, read off each callee's declaration
|
|
3331
|
+
* (`stackWordsOf`). A CALL NO DECLARATION SIZES refuses here, where the number is computed, rather
|
|
3332
|
+
* than relying on its own lowering to refuse it: gcc stages `fp(1, 2, 3, 4, x)`'s fifth word at
|
|
3333
|
+
* sp+16 and puts the local at sp+24, so an extent read without that call would draw the area over
|
|
3334
|
+
* the staged word. A call through a register (`jalr`) or a linking branch always refuses. A `jal`
|
|
3335
|
+
* to an undeclared callee refuses where the area depends on its extent — where drawing the
|
|
3336
|
+
* outgoing area as far as it can reach would move it: up to the lowest save, or to the
|
|
3337
|
+
* loaded-word bound (`o32OutgoingArea`'s `widest`) where that is lower. That is gcc's layout,
|
|
3338
|
+
* whose locals start where the outgoing area ends: `tl(1, 2, 3, (long long)x)` to an
|
|
3339
|
+
* unprototyped `tl` skips a3 and stages x at sp+16..24, which no guess of a0..a3 sees. IDO's area
|
|
3340
|
+
* starts above the saves, which the outgoing area lies below whatever its extent, so there the
|
|
3341
|
+
* undeclared call costs nothing.
|
|
3342
|
+
* The saves are the prologue's: a store of `ra` or a callee-saved register in the entry block
|
|
3343
|
+
* before anything writes it. */
|
|
3344
|
+
/** THE OUTGOING ARGUMENT AREA OF AN O32 FRAME, the part of it a callee may write: the a0..a3 home
|
|
3345
|
+
* words `[0,16)` and the widest call's stack words past them, by each callee's declaration. `end`
|
|
3346
|
+
* is where it stops, 0 in a leaf. `unsized` is the first call no declaration sizes (a `jal` nothing
|
|
3347
|
+
* declares, or a call through a register), counted in `end` as taking no stack words. `widest`
|
|
3348
|
+
* is the most the area can reach: `end` where every call is sized, else no further than the
|
|
3349
|
+
* loaded-word bound (`compilerBehaviors.outgoingAreaNeverLoadedFrom`: the compiler loads no word
|
|
3350
|
+
* of its outgoing area from that offset up, so the lowest word there this function loads is a
|
|
3351
|
+
* local's, and so is every word above it), and `Infinity` where no load bounds it. Each caller
|
|
3352
|
+
* reads the one number: `measureO32Frame` refuses where it could move the local area, and
|
|
3353
|
+
* `ownedFrame` claims the frame only from it up. */
|
|
3354
|
+
function o32OutgoingArea(
|
|
3355
|
+
code: readonly Instr[],
|
|
3356
|
+
stackWordsOf: (callee: string | undefined) => number | null,
|
|
3357
|
+
splat: boolean,
|
|
3358
|
+
loadedWordBound: number,
|
|
3359
|
+
): { end: number; unsized: Instr | undefined; widest: number } {
|
|
3360
|
+
const calls = code.filter(isCallInstr);
|
|
3361
|
+
const words = calls.map((ins) => (ins.mnemonic === 'jal' ? stackWordsOf(jalCallee(ins, splat)) : null));
|
|
3362
|
+
const end = calls.length === 0 ? 0 : STACK_ARGS_AT + 4 * Math.max(0, ...words.map((w) => w ?? 0));
|
|
3363
|
+
const unsized = calls.find((_, k) => words[k] === null);
|
|
3364
|
+
return { end, unsized, widest: unsized === undefined ? end : Math.max(end, loadedWordBound) };
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
export function measureO32Frame({
|
|
3368
|
+
name,
|
|
3369
|
+
code,
|
|
3370
|
+
entry,
|
|
3371
|
+
depthAt,
|
|
3372
|
+
frameSize,
|
|
3373
|
+
rule,
|
|
3374
|
+
stackWordsOf,
|
|
3375
|
+
savedByCallee,
|
|
3376
|
+
splat,
|
|
3377
|
+
loadedWordBound = Infinity,
|
|
3378
|
+
}: {
|
|
3379
|
+
name: string;
|
|
3380
|
+
code: readonly Instr[];
|
|
3381
|
+
entry: readonly Instr[];
|
|
3382
|
+
depthAt: ReadonlyMap<Instr, number> | null;
|
|
3383
|
+
frameSize: number | undefined;
|
|
3384
|
+
rule: TargetDescription['compilerBehaviors']['o32LocalArea'];
|
|
3385
|
+
/** The stack words a `jal`'s callee takes, by its declaration; null where nothing declares it. */
|
|
3386
|
+
stackWordsOf: (callee: string | undefined) => number | null;
|
|
3387
|
+
savedByCallee: (r: string) => boolean;
|
|
3388
|
+
splat: boolean;
|
|
3389
|
+
/** The lowest frame word from 16 up this function loads (`lift`'s loaded-word bound), which caps
|
|
3390
|
+
* how far an unsized call's stack words can reach; `Infinity` where nothing bounds it. */
|
|
3391
|
+
loadedWordBound?: number;
|
|
3392
|
+
}): { area: FrameRange; size: number } | null {
|
|
3393
|
+
const captures = code.filter(takesFrameAddress);
|
|
3394
|
+
if (captures.length === 0 || depthAt === null || frameSize === undefined) {
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3397
|
+
if (captures.some((ins) => depthAt.get(ins) !== frameSize)) {
|
|
3398
|
+
return null;
|
|
3399
|
+
}
|
|
3400
|
+
const unsized = code.find((ins) => isCallInstr(ins) && ins.mnemonic !== 'jal');
|
|
3401
|
+
if (unsized !== undefined) {
|
|
3402
|
+
throw new FrontendUnsupportedError(
|
|
3403
|
+
`cannot lift '${name}': outgoing stack arguments not modelled — the call '${unsized.mnemonic}' at ` +
|
|
3404
|
+
`0x${unsized.addr.toString(16)} has no declaration to size its stack words by, in a frame whose local ` +
|
|
3405
|
+
'area holds a captured address',
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
const written = new Set<string>();
|
|
3409
|
+
const saves: number[] = [];
|
|
3410
|
+
for (const ins of entry) {
|
|
3411
|
+
const mem = ins.mnemonic === 'sw' ? parseMem(ins.ops[1] ?? '') : undefined;
|
|
3412
|
+
if (mem !== undefined && isStackPtr(mem.base) && savedByCallee(ins.ops[0]) && !written.has(ins.ops[0])) {
|
|
3413
|
+
saves.push(mem.off);
|
|
3414
|
+
} else if (writtenReg(ins) !== null) {
|
|
3415
|
+
written.add(writtenReg(ins)!);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
const {
|
|
3419
|
+
end: outgoingEnd,
|
|
3420
|
+
unsized: undeclared,
|
|
3421
|
+
widest: widestEnd,
|
|
3422
|
+
} = o32OutgoingArea(code, stackWordsOf, splat, loadedWordBound);
|
|
3423
|
+
const area = o32LocalArea(rule, { frameSize, outgoingEnd, saves });
|
|
3424
|
+
if (area === null) {
|
|
3425
|
+
return null;
|
|
3426
|
+
}
|
|
3427
|
+
if (undeclared !== undefined) {
|
|
3428
|
+
const widest = o32LocalArea(rule, {
|
|
3429
|
+
frameSize,
|
|
3430
|
+
outgoingEnd: Math.max(outgoingEnd, Math.min(frameSize, widestEnd, ...saves)),
|
|
3431
|
+
saves,
|
|
3432
|
+
});
|
|
3433
|
+
if (widest?.from !== area.from || widest.to !== area.to) {
|
|
3434
|
+
throw new FrontendUnsupportedError(
|
|
3435
|
+
`cannot lift '${name}': outgoing stack arguments not modelled — nothing declares the callee of the \`jal\` at ` +
|
|
3436
|
+
`0x${undeclared.addr.toString(16)}, so how many stack words it takes is unknown, and this compiler's local ` +
|
|
3437
|
+
'area starts past them',
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
for (const ins of code) {
|
|
3442
|
+
const last = ins.ops[ins.ops.length - 1] ?? '';
|
|
3443
|
+
const depth = depthAt.get(ins);
|
|
3444
|
+
if (!/\(/.test(last) || !isStackPtr(parseMem(last).base) || depth === frameSize) {
|
|
3445
|
+
continue;
|
|
3446
|
+
}
|
|
3447
|
+
if (landsOnArea(area, parseMem(last).off - depth! + frameSize, 4)) {
|
|
3448
|
+
return null;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
return { area, size: frameSize };
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
/** A `jal`'s callee: the Splat operand, or the R_MIPS_26 record's symbol. */
|
|
3455
|
+
function jalCallee(ins: Instr, splat: boolean): string | undefined {
|
|
3456
|
+
return splat ? ins.ops[0] : ins.reloc?.type === 'R_MIPS_26' ? ins.reloc.sym : undefined;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
/** THE LOCAL AREA OF AN O32 FRAME, `[from, to)` in post-push `sp` offsets: where this compiler
|
|
3460
|
+
* keeps the locals and spill temps (`compilerBehaviors.o32LocalArea`), so where a captured frame
|
|
3461
|
+
* address may name a local and nothing else does. Below it is the outgoing area, which ends at
|
|
3462
|
+
* `outgoingEnd` — 0 in a function that makes no call, else the 16-byte home of a0..a3 and then
|
|
3463
|
+
* every stack word a call takes; above it, or below it on IDO, the registers the prologue saved at
|
|
3464
|
+
* `saves`. Null where the target states no layout, or the area is empty. */
|
|
3465
|
+
function o32LocalArea(
|
|
3466
|
+
rule: TargetDescription['compilerBehaviors']['o32LocalArea'],
|
|
3467
|
+
{ frameSize, outgoingEnd, saves }: { frameSize: number; outgoingEnd: number; saves: readonly number[] },
|
|
3468
|
+
): FrameRange | null {
|
|
3469
|
+
if (rule === 'above-outgoing-area') {
|
|
3470
|
+
return areaAboveOutgoing({ frameSize, outgoingEnd, saves, align: 8 });
|
|
3471
|
+
}
|
|
3472
|
+
if (rule === 'above-saved-registers') {
|
|
3473
|
+
const area = { from: Math.max(outgoingEnd, ...saves.map((o) => o + 4)), to: frameSize };
|
|
3474
|
+
return area.from < area.to ? area : null;
|
|
3475
|
+
}
|
|
3476
|
+
return null;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
/** O32's slots: the shared register-then-stack-word rule (`stackArgSlots`), with a floating-point
|
|
3480
|
+
* register read beside a stack word refused — where a float shadows the integer slots ahead of
|
|
3481
|
+
* the stack words is not modelled here. */
|
|
3482
|
+
function o32ArgSlots(name: string, regs: ArgSlots, argRegs: readonly string[]): ArgSlots {
|
|
3483
|
+
return stackArgSlots(regs, argRegs, (readKeys, top) => {
|
|
3484
|
+
const float = readKeys.find(isFpKey);
|
|
3485
|
+
if (float !== undefined) {
|
|
3486
|
+
throw new FrontendUnsupportedError(
|
|
3487
|
+
`cannot lift '${name}': ${float} carries a floating-point argument beside the incoming stack argument ` +
|
|
3488
|
+
`${top + 1} — which integer slots the float takes ahead of the stack words is not modelled`,
|
|
3489
|
+
);
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
|
|
1314
3494
|
// Build the "branch taken" condition value for a conditional branch (emitting the icmp op).
|
|
1315
3495
|
function condValue(
|
|
1316
3496
|
br: Instr,
|
|
@@ -1345,3 +3525,7 @@ function condValue(
|
|
|
1345
3525
|
|
|
1346
3526
|
/** The MIPS-II / IDO frontend, registered for the `mips` target. */
|
|
1347
3527
|
export const mipsFrontend: Frontend = { id: 'mips', inputFormat: 'objdump', lift };
|
|
3528
|
+
|
|
3529
|
+
/** Internal surface for this module's own tests, and for nothing else (`frontend/frame-objects.ts`
|
|
3530
|
+
* says why it is not a plain export). */
|
|
3531
|
+
export const __testing = { o32LocalArea, refuseReturnsNotThroughCallersRa };
|