@asmlift/core 0.6.0 → 0.8.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.
Files changed (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
@@ -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 shared
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, slwi, addi, lwzx, mtctr] = [instrs[i - 5], instrs[i - 4], instrs[i - 3], instrs[i - 2], instrs[i - 1]];
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
- const tableSym = lis.sym; // ADDR16_HA/LO @tbl (from inline -r reloc)
191
- if (lis.ops[0] !== rT || !tableSym || addi.sym !== tableSym) {
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 immediate-forming instruction means objdump printed a LINK-TIME
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 these guards: a recovered dispatch block
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
- const relocPlaceholder = (ins: Instr): void => {
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
- `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)} carries a data relocation ` +
364
- `('${ins.sym}') — the printed immediate is a link-time placeholder, not the value`,
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
- `(CTR-counted loop or indirect branch mwcc -O4 loop unrolling is not yet supported)`,
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 value reaching the call. A prototype's `params` is authoritative
396
- // when supplied; this liveness heuristic covers the rest.
397
- const fallbackArgc = (bi: number): number => {
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 && ssa.hasReachingDef(ARG_REGS[n], bi)) {
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 a callee-saved register's
406
- // entry value or the saved link register. A reload from one of these is dropped (the value is
407
- // unchanged, so the in-register SSA value already carries it). Function-scoped so a save in the
408
- // prologue block matches a restore in a different epilogue block. Any OTHER r1 access is a genuine
409
- // local spill / address-taken stack object this frontend cannot model those fail LOUD (below),
410
- // never silently drop, because a dropped local spill is a silent miscompile.
411
- const savedSlots = new Set<number>();
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) ? [readVar(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 readVar(r, bi);
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.add(off);
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
- if (savedSlots.has(off)) {
601
+ const saved = savedSlots.get(off);
602
+ if (saved === dstReg) {
479
603
  return true;
480
604
  }
481
605
  throw new PpcUnsupportedError(
482
- `cannot lift '${name}': reload of a stack local ('${mem}') — local stack frames not supported`,
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
- const emitLoad = (d: string, mem: string, width: number, signed: boolean) => {
523
- assertOrdinaryMem(mem);
524
- const { off, base } = parseMem(mem);
525
- emit('load', d, [read(base)], { off, width, signed });
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
- const emitStore = (srcReg: string, mem: string, width: number) => {
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
- ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
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.sym); caller-saved clobbering is implicit
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
- const sym = ins.sym ?? 'func';
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
- savedSlots.add(parseMem(s).off);
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 address formation encodes rA=0, so objdump prints `li rD,0` + R_PPC_EMB_SDA21
644
- if (ins.sym) {
645
- relocPlaceholder(ins);
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
- if (ins.sym) {
651
- relocPlaceholder(ins);
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
- // reloc first: a data reloc on a stack adjust is no known compiler's output — loud
663
- if (ins.sym) {
664
- relocPlaceholder(ins);
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 is a placeholder (guard above).
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 @l half-former same placeholder hazard as addi
733
- if (ins.sym) {
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
- readVar(jt.scrutReg, bi),
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: [readVar('ctr', bi), constVal(1)], results: [dec] }));
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) ? [readVar(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.