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