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