@asmlift/core 0.7.0 → 0.8.1
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 +48 -24
- package/package.json +1 -1
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +126 -29
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +207 -230
- package/src/ir/core.ts +62 -3
- package/src/ir/opcodes.ts +9 -0
- package/src/ir/parse.ts +7 -1
- package/src/l3/advance.ts +2 -2
- package/src/l3/argbase.ts +2 -2
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +45 -1
- package/src/l3/basecse.ts +2 -2
- package/src/l3/coalesce.ts +109 -52
- package/src/l3/scopebase.ts +4 -4
- package/src/l3/tailret.ts +70 -0
- package/src/l3/unmerge.ts +2 -2
- package/src/l3/unreduce.ts +2 -1
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +128 -13
- package/src/pipeline.ts +22 -11
- package/src/raise/extscale.ts +5 -2
- package/src/raise/paramwidth.ts +111 -3
- package/src/raise/pre-recovery.ts +11 -1
- package/src/raise/retsink.ts +8 -4
- package/src/raise/tailsink.ts +17 -2
- package/src/rank-declare.ts +17 -9
- package/src/rank.ts +54 -20
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +12 -3
- package/src/structure/switch-recover.ts +1 -1
- package/src/target.ts +224 -14
- package/src/trace.ts +27 -18
- package/src/variation-definitions.ts +52 -2
- package/src/variation-gates.ts +3 -0
- package/src/variation-tokens.ts +1 -0
package/src/frontend/ppc.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// asmlift ISA frontend — PowerPC (GameCube/Wii, Metrowerks CodeWarrior `mwcceppc`). Input is
|
|
2
|
-
// disassembled text (`powerpc-eabi-objdump -d --no-show-raw-insn`), parsed by the
|
|
3
|
-
// `parseDisasm` with the reloc + branch-hint options enabled.
|
|
2
|
+
// disassembled text (`powerpc-eabi-objdump -d -r -M gekko --no-show-raw-insn`), parsed by the
|
|
3
|
+
// shared `parseDisasm` with the reloc + branch-hint options enabled. `-M gekko` is part of that
|
|
4
|
+
// contract, not decoration: the generic PowerPC dialect decodes Gekko's paired-single opcodes as
|
|
5
|
+
// POWER VSX, naming another register file at another offset. It is also the dialect that prints
|
|
6
|
+
// the branch-prediction hint suffixes (`beq-`, `bdnz+`) `hintSuffixes` strips below.
|
|
4
7
|
//
|
|
5
8
|
// ISA facts that shape this frontend:
|
|
6
9
|
// • NO DELAY SLOTS — a block simply ends at its terminator.
|
|
@@ -50,7 +53,9 @@ import { mkEmitKit, pushSwitchBr } from './emit';
|
|
|
50
53
|
import { FrontendUnsupportedError } from './errors';
|
|
51
54
|
import { assertInputFormat } from './format';
|
|
52
55
|
import type { Frontend } from './frontend';
|
|
56
|
+
import { makeHighHalves } from './high-half';
|
|
53
57
|
import { opaqueDest } from './opaque';
|
|
58
|
+
import { unspellableReason } from './reloc-symbol';
|
|
54
59
|
import { abiSortEntryParams } from './ssa';
|
|
55
60
|
import { makeSsaBuilder } from './ssa';
|
|
56
61
|
|
|
@@ -164,7 +169,11 @@ function recoverPpcJumpTables(instrs: Instr[], ad: AsmData): Map<number, PpcJT>
|
|
|
164
169
|
if (instrs[i].mnemonic !== 'bctr') {
|
|
165
170
|
continue;
|
|
166
171
|
}
|
|
167
|
-
const [lis,
|
|
172
|
+
const [lis, lwzx, mtctr] = [instrs[i - 5], instrs[i - 2], instrs[i - 1]];
|
|
173
|
+
// The index is scaled and the table's low half formed in either order: mwcc 2.4.2 emits the
|
|
174
|
+
// `slwi` first, GC/1.2.5n the `addi`. The two instructions are independent.
|
|
175
|
+
const [slwi, addi] =
|
|
176
|
+
instrs[i - 4].mnemonic === 'slwi' ? [instrs[i - 4], instrs[i - 3]] : [instrs[i - 3], instrs[i - 4]];
|
|
168
177
|
if (
|
|
169
178
|
lis.mnemonic !== 'lis' ||
|
|
170
179
|
slwi.mnemonic !== 'slwi' ||
|
|
@@ -187,8 +196,22 @@ function recoverPpcJumpTables(instrs: Instr[], ad: AsmData): Map<number, PpcJT>
|
|
|
187
196
|
continue;
|
|
188
197
|
} // rB = &table (lo) + 0
|
|
189
198
|
const rT = addi.ops[1];
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
// The table's base is the same `@ha`/`@l` pair the address fold consumes and must show the same
|
|
200
|
+
// evidence — both relocation types, one symbol, one addend — so a dispatch is never recovered
|
|
201
|
+
// from two instructions that never addressed the table together. It stays a separate pairing:
|
|
202
|
+
// it runs before the blocks exist, and it wants the table's NAME, for `readJumpTable`, not its
|
|
203
|
+
// address. It skips the naming policy deliberately — a table base is read here as data, never
|
|
204
|
+
// written into the candidate as an identifier — so the `@N` and `.`-prefixed symbols the
|
|
205
|
+
// address fold must refuse (292 of the corpus's `ADDR16` relocations) are harmless here.
|
|
206
|
+
const tableSym = lis.reloc?.sym;
|
|
207
|
+
if (
|
|
208
|
+
lis.ops[0] !== rT ||
|
|
209
|
+
!tableSym ||
|
|
210
|
+
lis.reloc?.type !== 'R_PPC_ADDR16_HA' ||
|
|
211
|
+
addi.reloc?.type !== 'R_PPC_ADDR16_LO' ||
|
|
212
|
+
addi.reloc.sym !== tableSym ||
|
|
213
|
+
addi.reloc.addend !== lis.reloc.addend
|
|
214
|
+
) {
|
|
192
215
|
continue;
|
|
193
216
|
}
|
|
194
217
|
// Bounds: the nearest preceding `cmplwi scrutReg,N-1 ; bgt DEF` guard. The `bgt` is itself a
|
|
@@ -353,15 +376,41 @@ export function lift(
|
|
|
353
376
|
// it is exempted from the loud-fail below; an UNrecovered `bctr` still fails loud.
|
|
354
377
|
const jts = asmData ? recoverPpcJumpTables(instrs, asmData) : new Map<number, PpcJT>();
|
|
355
378
|
const recoveredBctr = new Set([...jts.values()].map((j) => j.bctrAddr));
|
|
356
|
-
// A data reloc on an
|
|
379
|
+
// A data reloc on an instruction whose decode did not TAKE it means objdump printed a LINK-TIME
|
|
357
380
|
// placeholder (`lis r4,0` + R_PPC_ADDR16_HA sym): the real value is the symbol's half, and
|
|
358
381
|
// lifting the 0 silently reads the wrong address — plausible-but-wrong C, the forbidden class.
|
|
359
|
-
// The jump-table idiom's own @tbl pair never reaches
|
|
382
|
+
// The jump-table idiom's own @tbl pair never reaches this guard: a recovered dispatch block
|
|
360
383
|
// is the bounds branch's replaced fall-through, pruned as unreachable before decode.
|
|
361
|
-
|
|
384
|
+
//
|
|
385
|
+
// TWO refusals, because they are different problems and the reader acts on them differently. A
|
|
386
|
+
// symbol the naming policy calls unspellable (frontend/reloc-symbol.ts) is a DEAD END: recovering
|
|
387
|
+
// the address perfectly would still leave a name no C source can write, so no amount of lifting
|
|
388
|
+
// opens the row. A spellable symbol is a CAPABILITY GAP: the address is recoverable and the row
|
|
389
|
+
// waits on the fold. Saying which one a row hit is the whole value of the message.
|
|
390
|
+
const relocSite = (ins: Instr) => `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)}`;
|
|
391
|
+
/** Whether the decode of the instruction in hand has TAKEN its relocation. `decode` asks once, of
|
|
392
|
+
* every modelled instruction, so a relocation no case consumed refuses instead of being dropped.
|
|
393
|
+
* A per-case guard would only cover the cases that remember to carry one, and every other
|
|
394
|
+
* instruction would lift its printed placeholder as a value (`andi. r3,r3,0` under an
|
|
395
|
+
* `R_PPC_EMB_SDA21` is `a0 & 0`). An UNmodelled instruction refuses earlier and for a better
|
|
396
|
+
* reason: `lfs`/`lfd` under either relocation name the float gap, not this one. */
|
|
397
|
+
let relocTaken = false;
|
|
398
|
+
/** The symbol a relocation names, once the naming policy has passed it. Every recovery below goes
|
|
399
|
+
* 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. */
|
|
401
|
+
const spellableSym = (ins: Instr): string => {
|
|
402
|
+
relocTaken = true;
|
|
403
|
+
const sym = ins.reloc?.sym ?? '';
|
|
404
|
+
const why = unspellableReason(sym);
|
|
405
|
+
if (why) {
|
|
406
|
+
throw new PpcUnsupportedError(`${relocSite(ins)} ${why}`);
|
|
407
|
+
}
|
|
408
|
+
return sym;
|
|
409
|
+
};
|
|
410
|
+
const relocPlaceholder = (ins: Instr): never => {
|
|
362
411
|
throw new PpcUnsupportedError(
|
|
363
|
-
|
|
364
|
-
`
|
|
412
|
+
`${relocSite(ins)} carries a data relocation ('${spellableSym(ins)}') — the printed immediate ` +
|
|
413
|
+
`is a link-time placeholder, not the value`,
|
|
365
414
|
);
|
|
366
415
|
};
|
|
367
416
|
// TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
|
|
@@ -373,7 +422,9 @@ export function lift(
|
|
|
373
422
|
} // recovered switch dispatch
|
|
374
423
|
throw new PpcUnsupportedError(
|
|
375
424
|
`cannot lift '${name}': unmodelled control transfer '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
|
|
376
|
-
|
|
425
|
+
(ins.mnemonic === 'blrl' || ins.mnemonic === 'bctrl'
|
|
426
|
+
? `(an indirect call — a virtual dispatch or a call through a pointer — is not yet supported)`
|
|
427
|
+
: `(CTR-counted loop or indirect branch — mwcc -O4 loop unrolling is not yet supported)`),
|
|
377
428
|
);
|
|
378
429
|
}
|
|
379
430
|
}
|
|
@@ -388,27 +439,99 @@ export function lift(
|
|
|
388
439
|
|
|
389
440
|
const ssa = makeSsaBuilder(name, blocks.length, preds);
|
|
390
441
|
const { irBlocks, readVar, writeVar, paramReg } = ssa;
|
|
442
|
+
|
|
443
|
+
/** The HIGH half of a relocated address, per value standing for one. The invariant, and every
|
|
444
|
+
* way a placeholder could leak past it, lives in frontend/high-half.ts — shared with
|
|
445
|
+
* frontend/mips.ts, which folds the same two-instruction address under `%hi`/`%lo`.
|
|
446
|
+
*
|
|
447
|
+
* `lis rD,SYM@ha` produces no VALUE: `@ha` is `((SYM + 0x8000) >> 16) & 0xffff`, a number that
|
|
448
|
+
* means nothing until the sign-extended `@l` half completes it, and in a relocatable object both
|
|
449
|
+
* printed immediates are 0 anyway — the address lives entirely in the two relocation records.
|
|
450
|
+
* The `gaddr` is emitted where the `@l` lands, the same choice frontend/mips.ts makes for
|
|
451
|
+
* `lui %hi`.
|
|
452
|
+
*
|
|
453
|
+
* PowerPC is RELA, so the addend rides on the relocation record and is copied straight in; MIPS
|
|
454
|
+
* is REL and must read it out of the two instruction immediates. That is the one place the two
|
|
455
|
+
* folds diverge, and it is a relocation-format fact rather than a preference. */
|
|
456
|
+
const highHalves = makeHighHalves({
|
|
457
|
+
hi: '@ha',
|
|
458
|
+
hiArticle: 'an',
|
|
459
|
+
lo: '@l',
|
|
460
|
+
fail: (message) => {
|
|
461
|
+
throw new PpcUnsupportedError(message);
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
/** Reading a register AS A VALUE. A register holding a high half is not one, so this refuses loud
|
|
465
|
+
* rather than handing back a plausible number standing for an address. `foldLoHalf` is the only
|
|
466
|
+
* code entitled to look at a high half, which is why it reaches `readVar` directly. */
|
|
467
|
+
const readReg = (r: string, at: number): Value => highHalves.guardRead(name, r, readVar(r, at));
|
|
391
468
|
const RET = target.returnReg;
|
|
392
469
|
const ARG_REGS = target.argRegs;
|
|
393
470
|
|
|
394
471
|
// Best-effort call arity when a callee has no prototype: the count of contiguous argument
|
|
395
|
-
// registers (r3..) with a
|
|
396
|
-
//
|
|
397
|
-
|
|
472
|
+
// registers (r3..) with a VALUE reaching the call. A prototype's `params` is authoritative when
|
|
473
|
+
// supplied; this liveness heuristic covers the rest.
|
|
474
|
+
//
|
|
475
|
+
// A pending `@ha` high half is a def but not a value, and it must not raise the count: a `lis`
|
|
476
|
+
// hoisted into the prologue leaves its half in r4 across an intervening call, and counted, it
|
|
477
|
+
// makes `strlen(s)` into `strlen(s, <half>)` — which then refuses at the read. Measured on
|
|
478
|
+
// `pikmin:searchKanjiCode__FUs`, whose `lis r4` at 0x4 pairs only at 0x28, past the `bl strlen`.
|
|
479
|
+
//
|
|
480
|
+
// A GAP REFUSES. The count is contiguous, so an argument register with nothing reaching it ends
|
|
481
|
+
// it — and one is empty for two opposite reasons. Either the call really takes that few
|
|
482
|
+
// arguments, or the register still holds this function's own untouched incoming argument, a value
|
|
483
|
+
// the machine passes on that SSA has no definition for because nothing ever wrote it. When a
|
|
484
|
+
// LATER argument register does hold a value the second reading is the only one left, and taking
|
|
485
|
+
// the first drops that argument and every one after it: `ac-decomp:evw_anime_colreg_manual` sets
|
|
486
|
+
// up seven registers for `evw_color_set` and, with r4 at its incoming value, lifts to
|
|
487
|
+
// `evw_color_set(a0);` — its divide, its multiply and five arguments gone. Which reading it is
|
|
488
|
+
// cannot be decided here — the function's own arity is exactly what is missing — so this refuses
|
|
489
|
+
// and names the gap rather than guessing. A prototype answers it (`protoArity` is asked first).
|
|
490
|
+
const fallbackArgc = (bi: number, at: number): number => {
|
|
491
|
+
const holdsValue = (k: number) => ssa.hasReachingDef(ARG_REGS[k], bi, (v) => !highHalves.has(v));
|
|
398
492
|
let n = 0;
|
|
399
|
-
while (n < ARG_REGS.length &&
|
|
493
|
+
while (n < ARG_REGS.length && holdsValue(n)) {
|
|
400
494
|
n++;
|
|
401
495
|
}
|
|
496
|
+
for (let k = n + 1; k < ARG_REGS.length; k++) {
|
|
497
|
+
if (holdsValue(k)) {
|
|
498
|
+
throw new PpcUnsupportedError(
|
|
499
|
+
`cannot lift '${name}': the call at 0x${at.toString(16)} has no prototype, ${ARG_REGS[k]} holds a ` +
|
|
500
|
+
`value and ${ARG_REGS[n]} holds none — an argument register left at its incoming value and one ` +
|
|
501
|
+
`the call does not pass look the same here, so the argument count is not decidable`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
402
505
|
return n;
|
|
403
506
|
};
|
|
404
507
|
|
|
405
|
-
// Frame slots (r1-relative offsets) that hold a TRANSPARENT save
|
|
406
|
-
// entry value or the saved link register. A reload
|
|
407
|
-
// unchanged, so the in-register SSA value already carries it)
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
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
|
+
// `stmw rS,D(r1)` saves rS..r31 into consecutive words from D; `lmw rD,D(r1)` restores the same
|
|
526
|
+
// range.
|
|
527
|
+
const frameRange = (firstReg: string, off: number): Array<{ off: number; reg: string }> => {
|
|
528
|
+
const first = Number(firstReg.slice(1));
|
|
529
|
+
const slots: Array<{ off: number; reg: string }> = [];
|
|
530
|
+
for (let r = first; r <= 31; r++) {
|
|
531
|
+
slots.push({ off: off + (r - first) * 4, reg: `r${r}` });
|
|
532
|
+
}
|
|
533
|
+
return slots;
|
|
534
|
+
};
|
|
412
535
|
|
|
413
536
|
const fillBlock = (b: PpcBlock, bi: number) => {
|
|
414
537
|
const ops = irBlocks[bi].ops;
|
|
@@ -416,7 +539,7 @@ export function lift(
|
|
|
416
539
|
|
|
417
540
|
// A synthetic conditional-return block: just return the current return register.
|
|
418
541
|
if (b.synthReturn) {
|
|
419
|
-
const retOps = ssa.hasReachingDef(RET, bi) ? [
|
|
542
|
+
const retOps = ssa.hasReachingDef(RET, bi) ? [readReg(RET, bi)] : [];
|
|
420
543
|
ops.push(mkOp('ret', { operands: retOps }));
|
|
421
544
|
return;
|
|
422
545
|
}
|
|
@@ -434,7 +557,7 @@ export function lift(
|
|
|
434
557
|
`cannot lift '${name}': stack pointer r1 used as data (address-taken local / frame arithmetic) — not supported`,
|
|
435
558
|
);
|
|
436
559
|
}
|
|
437
|
-
return
|
|
560
|
+
return readReg(r, bi);
|
|
438
561
|
};
|
|
439
562
|
// Ordinary memory must be based on a real register that is not the frame pointer. A non-register
|
|
440
563
|
// base is an SDA/global-relative access (`stw r0,0(0)` — the base field is a 0 placeholder the
|
|
@@ -463,23 +586,28 @@ export function lift(
|
|
|
463
586
|
return false;
|
|
464
587
|
}
|
|
465
588
|
if (!ssa.hasReachingDef(srcReg, bi)) {
|
|
466
|
-
savedSlots.
|
|
589
|
+
savedSlots.set(off, srcReg);
|
|
467
590
|
return true;
|
|
468
591
|
}
|
|
469
592
|
throw new PpcUnsupportedError(
|
|
470
593
|
`cannot lift '${name}': spill of a live value to the stack ('${srcReg},${mem}') — local stack frames not supported`,
|
|
471
594
|
);
|
|
472
595
|
};
|
|
473
|
-
const frameLoad = (mem: string): boolean => {
|
|
596
|
+
const frameLoad = (dstReg: string, mem: string): boolean => {
|
|
474
597
|
const { base, off } = parseMem(mem);
|
|
475
598
|
if (base !== 'r1') {
|
|
476
599
|
return false;
|
|
477
600
|
}
|
|
478
|
-
|
|
601
|
+
const saved = savedSlots.get(off);
|
|
602
|
+
if (saved === dstReg) {
|
|
479
603
|
return true;
|
|
480
604
|
}
|
|
481
605
|
throw new PpcUnsupportedError(
|
|
482
|
-
|
|
606
|
+
saved === undefined
|
|
607
|
+
? `cannot lift '${name}': reload of a stack local ('${mem}') — local stack frames not supported`
|
|
608
|
+
: `cannot lift '${name}': reload of '${mem}' into ${dstReg}, a slot ${saved} was saved into — ` +
|
|
609
|
+
`a load that does not restore the register the slot holds is a value read back through the ` +
|
|
610
|
+
`stack, which is a local stack frame this frontend does not model`,
|
|
483
611
|
);
|
|
484
612
|
};
|
|
485
613
|
const write = (r: string, v: Value) => {
|
|
@@ -519,15 +647,83 @@ export function lift(
|
|
|
519
647
|
return v;
|
|
520
648
|
};
|
|
521
649
|
const emitShImm = kit.shImm;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
650
|
+
// Materialise the address of a named global — the same `gaddr` op the MIPS and Thumb frontends
|
|
651
|
+
// emit, which the structurer lowers to `SYM`, `((T *)&SYM)[i]` or `&SYM`.
|
|
652
|
+
const emitGaddr = (sym: string): Value => {
|
|
653
|
+
const g = mkValue(T.unk(32));
|
|
654
|
+
ops.push(mkOp('gaddr', { results: [g], attrs: { sym } }));
|
|
655
|
+
return g;
|
|
656
|
+
};
|
|
657
|
+
// `lwz rD,0(0)` under an `R_PPC_EMB_SDA21` relocation is a SMALL-DATA access. Both printed
|
|
658
|
+
// fields are link-time placeholders: the linker substitutes r13/r2 for the base register and a
|
|
659
|
+
// section-relative displacement for the offset, so the address is exactly `&SYM` plus the
|
|
660
|
+
// relocation's own addend. Because the printed operand is discarded rather than read, one that
|
|
661
|
+
// is NOT the expected placeholder refuses — a field this code ignores must be one that provably
|
|
662
|
+
// says nothing.
|
|
663
|
+
const sdaAccess = (ins: Instr, mem: string): { base: Value; off: number } | null => {
|
|
664
|
+
if (ins.reloc?.type !== 'R_PPC_EMB_SDA21') {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
if (mem !== '0(0)') {
|
|
668
|
+
throw new PpcUnsupportedError(
|
|
669
|
+
`${relocSite(ins)} carries a small-data relocation ('${ins.reloc.sym}') but its memory ` +
|
|
670
|
+
`operand is '${mem}', not the expected '0(0)' placeholder`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return { base: emitGaddr(spellableSym(ins)), off: ins.reloc.addend };
|
|
526
674
|
};
|
|
527
|
-
|
|
675
|
+
// The base value + byte offset of a displacement memory operand: a small-data global, or an
|
|
676
|
+
// ordinary register base. Read BEFORE a store's source register, because read order decides
|
|
677
|
+
// block-parameter layout.
|
|
678
|
+
const memOperand = (ins: Instr, mem: string): { base: Value; off: number } => {
|
|
679
|
+
const sda = sdaAccess(ins, mem);
|
|
680
|
+
if (sda) {
|
|
681
|
+
return sda;
|
|
682
|
+
}
|
|
528
683
|
assertOrdinaryMem(mem);
|
|
529
684
|
const { off, base } = parseMem(mem);
|
|
530
|
-
|
|
685
|
+
return { base: read(base), off };
|
|
686
|
+
};
|
|
687
|
+
// `addi rD,rHi,SYM@l` — the LOW half that completes a `lis rHi,SYM@ha`. Returns `&SYM`.
|
|
688
|
+
//
|
|
689
|
+
// The pairing is PROVEN, not guessed: the value rHi holds HERE must be the very placeholder a
|
|
690
|
+
// `lis` defined, and the two relocations must name the same symbol with the same addend.
|
|
691
|
+
// Asking SSA for that value is what makes it a proof — a redefinition on any path that reaches
|
|
692
|
+
// this read produces a different value (a block parameter, at a merge), which is not in
|
|
693
|
+
// `highHalves` and so refuses. Nothing consults adjacency or distance, which is why a pair eleven
|
|
694
|
+
// instructions apart folds — 26% of the corpus's pairs are not adjacent — while a register
|
|
695
|
+
// reused between the halves refuses.
|
|
696
|
+
//
|
|
697
|
+
// BLOCK IDENTITY IT DOES CONSULT, through SSA and not directly, and the refusal has to say so.
|
|
698
|
+
// `readVar` during block FILLING answers from what is sealed: a chain of single-predecessor
|
|
699
|
+
// blocks walks back to the `lis` and folds, but a read at a JOIN gets the block parameter that
|
|
700
|
+
// stands for the merge, and in an unsealed loop header an incomplete one — neither is in
|
|
701
|
+
// `highHalves`. So an `@ha` hoisted above a loop with its `@l` in the body refuses, and so does a
|
|
702
|
+
// pair split across a diamond even when BOTH paths carry the same half. Over a 29,850-function
|
|
703
|
+
// sweep this refusal has 0 inhabitants (`assertNoneEscaped` has 113), so the residual is
|
|
704
|
+
// written down rather than built.
|
|
705
|
+
const foldLoHalf = (ins: Instr, rHi: string, imm: string): Value => {
|
|
706
|
+
relocTaken = true;
|
|
707
|
+
const lo = ins.reloc!;
|
|
708
|
+
if (parseImm(imm) !== 0) {
|
|
709
|
+
throw new PpcUnsupportedError(
|
|
710
|
+
`${relocSite(ins)} carries the '@l' half of '${lo.sym}' but its immediate is '${imm}', ` +
|
|
711
|
+
`not the expected 0 placeholder`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
// The per-ISA rest of the match: PowerPC is RELA, so the two records must agree on the addend
|
|
715
|
+
// as well as the symbol — two `@ha`/`@l` pairs into the same array at different offsets are
|
|
716
|
+
// different addresses.
|
|
717
|
+
const hi = highHalves.pair(relocSite(ins), rHi, readVar(rHi, bi), lo.sym, (h) => h.addend === lo.addend);
|
|
718
|
+
return emitGaddr(hi.sym);
|
|
719
|
+
};
|
|
720
|
+
const emitLoad = (ins: Instr, d: string, mem: string, width: number, signed: boolean) => {
|
|
721
|
+
const { base, off } = memOperand(ins, mem);
|
|
722
|
+
emit('load', d, [base], { off, width, signed });
|
|
723
|
+
};
|
|
724
|
+
const emitStore = (ins: Instr, srcReg: string, mem: string, width: number) => {
|
|
725
|
+
const { base, off } = memOperand(ins, mem);
|
|
726
|
+
ops.push(mkOp('store', { operands: [base, read(srcReg)], attrs: { off, width } }));
|
|
531
727
|
};
|
|
532
728
|
// Register+register INDEXED addressing (`lwzx rD,rA,rB` = *(rA+rB), `stwx rS,rA,rB` = *(rA+rB)=rS).
|
|
533
729
|
// This is how mwcc emits EVERY variable-index array access (scalar and struct) — with rB the scaled
|
|
@@ -569,17 +765,19 @@ export function lift(
|
|
|
569
765
|
const rc = ins.mnemonic.length > 1 && ins.mnemonic.endsWith('.');
|
|
570
766
|
const mnem = rc ? ins.mnemonic.slice(0, -1) : ins.mnemonic;
|
|
571
767
|
lastDef = null;
|
|
768
|
+
relocTaken = false;
|
|
572
769
|
switch (mnem) {
|
|
573
770
|
case 'nop':
|
|
574
771
|
break;
|
|
575
772
|
// --- call + frame/link-register bookkeeping ---
|
|
576
773
|
// `bl <sym>`: read the argument registers (r3..), produce the return value in r3. The
|
|
577
|
-
// callee symbol comes from the relocation (ins.
|
|
774
|
+
// callee symbol comes from the relocation (ins.reloc); caller-saved clobbering is implicit
|
|
578
775
|
// (anything live across the call has already been moved to a callee-saved register).
|
|
579
776
|
case 'bl': {
|
|
580
|
-
|
|
777
|
+
relocTaken = ins.reloc?.type === 'R_PPC_REL24';
|
|
778
|
+
const sym = ins.reloc?.sym ?? 'func';
|
|
581
779
|
const declared = protoArity(prototypes[sym]);
|
|
582
|
-
const argc = declared ?? fallbackArgc(bi);
|
|
780
|
+
const argc = declared ?? fallbackArgc(bi, ins.addr);
|
|
583
781
|
const args: Value[] = [];
|
|
584
782
|
for (let k = 0; k < argc; k++) {
|
|
585
783
|
args.push(read(ARG_REGS[k]));
|
|
@@ -623,14 +821,22 @@ export function lift(
|
|
|
623
821
|
break;
|
|
624
822
|
case 'stmw':
|
|
625
823
|
if (parseMem(s).base === 'r1') {
|
|
626
|
-
|
|
824
|
+
for (const slot of frameRange(d, parseMem(s).off)) {
|
|
825
|
+
savedSlots.set(slot.off, slot.reg);
|
|
826
|
+
}
|
|
627
827
|
break;
|
|
628
828
|
}
|
|
629
829
|
assertOrdinaryMem(s);
|
|
630
830
|
emitOpaqueDest(ins);
|
|
631
831
|
break;
|
|
832
|
+
// The restore half. Every word it reads must be the slot the matching `stmw` wrote, for the
|
|
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.
|
|
632
835
|
case 'lmw':
|
|
633
836
|
if (parseMem(s).base === 'r1') {
|
|
837
|
+
for (const slot of frameRange(d, parseMem(s).off)) {
|
|
838
|
+
frameLoad(slot.reg, `${slot.off}(r1)`);
|
|
839
|
+
}
|
|
634
840
|
break;
|
|
635
841
|
}
|
|
636
842
|
assertOrdinaryMem(s);
|
|
@@ -640,15 +846,46 @@ export function lift(
|
|
|
640
846
|
write(d, read(s));
|
|
641
847
|
break; // move register (or rD,rS,rS)
|
|
642
848
|
case 'li':
|
|
643
|
-
// SDA21
|
|
644
|
-
|
|
645
|
-
|
|
849
|
+
// SDA21 ADDRESS formation encodes rA=0, so objdump prints `li rD,0` + R_PPC_EMB_SDA21:
|
|
850
|
+
// the linker rewrites it to `addi rD,r13,SYM@sdarx`, i.e. rD = &SYM. Same relocation and
|
|
851
|
+
// same recovery as the memory form above — only the field it lands in differs.
|
|
852
|
+
if (ins.reloc?.type === 'R_PPC_EMB_SDA21') {
|
|
853
|
+
if (parseImm(s) !== 0) {
|
|
854
|
+
throw new PpcUnsupportedError(
|
|
855
|
+
`${relocSite(ins)} carries a small-data relocation ('${ins.reloc.sym}') but its ` +
|
|
856
|
+
`immediate is '${s}', not the expected 0 placeholder`,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const g = emitGaddr(spellableSym(ins));
|
|
860
|
+
if (ins.reloc.addend === 0) {
|
|
861
|
+
write(d, g);
|
|
862
|
+
} else {
|
|
863
|
+
emitBin('add', d, g, constVal(ins.reloc.addend));
|
|
864
|
+
}
|
|
865
|
+
break;
|
|
646
866
|
}
|
|
647
867
|
write(d, constVal(parseImm(s)));
|
|
648
868
|
break; // load immediate (addi rD,0,imm)
|
|
649
869
|
case 'lis':
|
|
650
|
-
|
|
651
|
-
|
|
870
|
+
// `lis rD,SYM@ha` is the high half of an absolute address, and the ONLY instruction that
|
|
871
|
+
// carries `R_PPC_ADDR16_HA` in this corpus. rD is defined as a placeholder — see
|
|
872
|
+
// `highHalves`.
|
|
873
|
+
if (ins.reloc?.type === 'R_PPC_ADDR16_HA') {
|
|
874
|
+
if (parseImm(s) !== 0) {
|
|
875
|
+
throw new PpcUnsupportedError(
|
|
876
|
+
`${relocSite(ins)} carries the '@ha' half of '${ins.reloc.sym}' but its immediate ` +
|
|
877
|
+
`is '${s}', not the expected 0 placeholder`,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const hi = mkValue(T.unk(32));
|
|
881
|
+
highHalves.record(hi, {
|
|
882
|
+
sym: spellableSym(ins),
|
|
883
|
+
addend: ins.reloc.addend,
|
|
884
|
+
addr: ins.addr,
|
|
885
|
+
mnemonic: ins.mnemonic,
|
|
886
|
+
});
|
|
887
|
+
write(d, hi);
|
|
888
|
+
break;
|
|
652
889
|
}
|
|
653
890
|
write(d, constVal((parseImm(s) << 16) >> 0));
|
|
654
891
|
break; // load immediate shifted
|
|
@@ -659,11 +896,32 @@ export function lift(
|
|
|
659
896
|
// `addi r1,r1,N` is frame teardown (skip); any other addi is a real add-immediate.
|
|
660
897
|
case 'addi':
|
|
661
898
|
case 'addic':
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
899
|
+
// The STACK POINTER first, before any fold looks at the operands. `addi r1,r1,N` is frame
|
|
900
|
+
// teardown and is skipped; a relocation on it is no known compiler's output, and folding
|
|
901
|
+
// one would write an address into r1 and let the function lift as if the teardown had
|
|
902
|
+
// happened — silent, and about the frame rather than the value.
|
|
666
903
|
if (d === 'r1') {
|
|
904
|
+
if (ins.reloc) {
|
|
905
|
+
throw new PpcUnsupportedError(
|
|
906
|
+
`${relocSite(ins)} carries a relocation ('${ins.reloc.sym}') on a stack-pointer ` +
|
|
907
|
+
`adjust — an address built in r1 is not a shape this frontend models`,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
// `addi rD,rHi,SYM@l` completes the address a `lis` began: rD = &SYM (+ the relocation's
|
|
913
|
+
// addend). It is the only `@l` consumer modelled here. The others — `ori`, `addic`, and
|
|
914
|
+
// the `@l`-in-a-displacement form (1,935 integer load/store sites across the three
|
|
915
|
+
// checkouts, plus the float loads) — are left unbuilt per docs/level-tower.md's "earn the
|
|
916
|
+
// level": no row in this benchmark reaches them. Each refuses at its own guard, and a
|
|
917
|
+
// high half none of them consumed is caught by `readReg` or by the dangling-`@ha` check.
|
|
918
|
+
if (mnem === 'addi' && ins.reloc?.type === 'R_PPC_ADDR16_LO') {
|
|
919
|
+
const g = foldLoHalf(ins, s, t);
|
|
920
|
+
if (ins.reloc.addend === 0) {
|
|
921
|
+
write(d, g);
|
|
922
|
+
} else {
|
|
923
|
+
emitBin('add', d, g, constVal(ins.reloc.addend));
|
|
924
|
+
}
|
|
667
925
|
break;
|
|
668
926
|
}
|
|
669
927
|
emitBin('add', d, read(s), constVal(parseImm(t)));
|
|
@@ -671,11 +929,9 @@ export function lift(
|
|
|
671
929
|
// add immediate SHIFTED — the register-based `%ha` anchor: mwcc derives an absolute base
|
|
672
930
|
// from a scaled index (`addis r4,r3,-32736` = r3 + 0x80200000). The jump-table lis/addi
|
|
673
931
|
// pair recognizer is the only reloc-carrying consumer; an addis over a register is plain
|
|
674
|
-
// arithmetic, and a reloc-carrying one
|
|
932
|
+
// arithmetic, and a reloc-carrying one never gets here — the choke point in `decode` takes
|
|
933
|
+
// it.
|
|
675
934
|
case 'addis':
|
|
676
|
-
if (ins.sym) {
|
|
677
|
-
relocPlaceholder(ins);
|
|
678
|
-
}
|
|
679
935
|
emitBin('add', d, read(s), constVal((parseImm(t) << 16) >> 0));
|
|
680
936
|
break;
|
|
681
937
|
case 'subf':
|
|
@@ -729,10 +985,8 @@ export function lift(
|
|
|
729
985
|
emitBin('or', d, read(s), read(t));
|
|
730
986
|
break;
|
|
731
987
|
case 'ori':
|
|
732
|
-
// `ori rD,rA,sym@l` is the other
|
|
733
|
-
|
|
734
|
-
relocPlaceholder(ins);
|
|
735
|
-
}
|
|
988
|
+
// `ori rD,rA,sym@l` is the other `@l` half-former, unbuilt per "earn the level"; one that
|
|
989
|
+
// carries a relocation refuses at the choke point rather than becoming an `or` of 0.
|
|
736
990
|
emitBin('or', d, read(s), constVal(parseImm(t)));
|
|
737
991
|
break;
|
|
738
992
|
case 'xor':
|
|
@@ -833,31 +1087,31 @@ export function lift(
|
|
|
833
1087
|
// Word load/store: a transparent frame save/restore is skipped; a live-value spill or a
|
|
834
1088
|
// stack local fails loud (frameStore/frameLoad); otherwise it is ordinary memory.
|
|
835
1089
|
case 'lwz':
|
|
836
|
-
if (frameLoad(s)) {
|
|
1090
|
+
if (frameLoad(d, s)) {
|
|
837
1091
|
break;
|
|
838
1092
|
}
|
|
839
|
-
emitLoad(d, s, 4, true);
|
|
1093
|
+
emitLoad(ins, d, s, 4, true);
|
|
840
1094
|
break;
|
|
841
1095
|
case 'lha':
|
|
842
|
-
emitLoad(d, s, 2, true);
|
|
1096
|
+
emitLoad(ins, d, s, 2, true);
|
|
843
1097
|
break;
|
|
844
1098
|
case 'lhz':
|
|
845
|
-
emitLoad(d, s, 2, false);
|
|
1099
|
+
emitLoad(ins, d, s, 2, false);
|
|
846
1100
|
break;
|
|
847
1101
|
case 'lbz':
|
|
848
|
-
emitLoad(d, s, 1, false);
|
|
1102
|
+
emitLoad(ins, d, s, 1, false);
|
|
849
1103
|
break;
|
|
850
1104
|
case 'stw':
|
|
851
1105
|
if (frameStore(d, s)) {
|
|
852
1106
|
break;
|
|
853
1107
|
}
|
|
854
|
-
emitStore(d, s, 4);
|
|
1108
|
+
emitStore(ins, d, s, 4);
|
|
855
1109
|
break;
|
|
856
1110
|
case 'sth':
|
|
857
|
-
emitStore(d, s, 2);
|
|
1111
|
+
emitStore(ins, d, s, 2);
|
|
858
1112
|
break;
|
|
859
1113
|
case 'stb':
|
|
860
|
-
emitStore(d, s, 1);
|
|
1114
|
+
emitStore(ins, d, s, 1);
|
|
861
1115
|
break;
|
|
862
1116
|
// Register+register indexed forms (variable-index array access). Widths/signedness mirror the
|
|
863
1117
|
// displacement loads/stores above; `lhax` is the sign-extending halfword (algebraic).
|
|
@@ -917,6 +1171,12 @@ export function lift(
|
|
|
917
1171
|
if (rc && lastDef) {
|
|
918
1172
|
cmpDef.set('cr0', { lhs: lastDef, rhs: constVal(0), signed: true });
|
|
919
1173
|
}
|
|
1174
|
+
// THE CHOKE POINT. Everything above either took the relocation or threw; a relocation still
|
|
1175
|
+
// sitting here was dropped, and a dropped relocation is the printed placeholder standing in
|
|
1176
|
+
// for an address — the forbidden class.
|
|
1177
|
+
if (ins.reloc && !relocTaken) {
|
|
1178
|
+
relocPlaceholder(ins);
|
|
1179
|
+
}
|
|
920
1180
|
};
|
|
921
1181
|
|
|
922
1182
|
for (const ins of b.body) {
|
|
@@ -931,7 +1191,7 @@ export function lift(
|
|
|
931
1191
|
if (jt) {
|
|
932
1192
|
pushSwitchBr(
|
|
933
1193
|
ops,
|
|
934
|
-
|
|
1194
|
+
readReg(jt.scrutReg, bi),
|
|
935
1195
|
succIdx[bi].map((j) => succ(j)),
|
|
936
1196
|
);
|
|
937
1197
|
return;
|
|
@@ -977,7 +1237,7 @@ export function lift(
|
|
|
977
1237
|
}
|
|
978
1238
|
}
|
|
979
1239
|
const dec = mkValue(T.unk(32));
|
|
980
|
-
ops.push(mkOp('sub', { operands: [
|
|
1240
|
+
ops.push(mkOp('sub', { operands: [readReg('ctr', bi), constVal(1)], results: [dec] }));
|
|
981
1241
|
writeVar('ctr', bi, dec);
|
|
982
1242
|
const cond = mkCmp(ops, 'icmp_ne', dec, constVal(0));
|
|
983
1243
|
ops.push(mkOp('cond_br', { operands: [cond], successors: succIdx[bi].map((j) => succ(j)) }));
|
|
@@ -1002,10 +1262,10 @@ export function lift(
|
|
|
1002
1262
|
}
|
|
1003
1263
|
if (!br || isReturn(br)) {
|
|
1004
1264
|
if (!br && succIdx[bi].length) {
|
|
1005
|
-
ops.push(mkOp('br', { successors: [succ(succIdx[bi][0])] }));
|
|
1265
|
+
ops.push(mkOp('br', { attrs: { fallthrough: true }, successors: [succ(succIdx[bi][0])] }));
|
|
1006
1266
|
return;
|
|
1007
1267
|
}
|
|
1008
|
-
const retOps = ssa.hasReachingDef(RET, bi) ? [
|
|
1268
|
+
const retOps = ssa.hasReachingDef(RET, bi) ? [readReg(RET, bi)] : [];
|
|
1009
1269
|
ops.push(mkOp('ret', { operands: retOps }));
|
|
1010
1270
|
return;
|
|
1011
1271
|
}
|
|
@@ -1017,7 +1277,12 @@ export function lift(
|
|
|
1017
1277
|
fillBlock(b, bi);
|
|
1018
1278
|
ssa.markFilled(bi);
|
|
1019
1279
|
});
|
|
1280
|
+
// A high half no `@l` ever completed — every `@l` consumer this frontend does not model (`ori`, a
|
|
1281
|
+
// float load's displacement) lands here, which is what keeps "not modelled" from becoming "not
|
|
1282
|
+
// emitted".
|
|
1283
|
+
highHalves.assertAllConsumed(name);
|
|
1020
1284
|
ssa.finish();
|
|
1285
|
+
highHalves.assertNoneEscaped(name, irBlocks);
|
|
1021
1286
|
|
|
1022
1287
|
// ABI-ordered entry parameters (r3, r4, …) — a callee-saved copy can read a later argument
|
|
1023
1288
|
// register first, so sort the true entry's params by argument-register index.
|