@asmlift/core 0.4.0 → 0.6.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 (87) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -164
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +341 -22
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +24 -6
  10. package/src/frontend/opaque.ts +31 -18
  11. package/src/frontend/ppc.ts +54 -7
  12. package/src/frontend/ssa.ts +632 -13
  13. package/src/frontend/thumb.ts +2786 -286
  14. package/src/ir/alias.ts +129 -0
  15. package/src/ir/bits.ts +75 -0
  16. package/src/ir/core.ts +337 -2
  17. package/src/ir/opcodes.ts +156 -27
  18. package/src/ir/parse.ts +19 -2
  19. package/src/ir/print.ts +27 -2
  20. package/src/ir/simplify.ts +190 -3
  21. package/src/ir/struct-names.ts +42 -0
  22. package/src/ir/verify.ts +43 -49
  23. package/src/l3/address.ts +62 -0
  24. package/src/l3/argbase.ts +8 -2
  25. package/src/l3/ast.ts +464 -49
  26. package/src/l3/basecse.ts +709 -88
  27. package/src/l3/coalesce.ts +521 -66
  28. package/src/l3/dce.ts +54 -19
  29. package/src/l3/gates.ts +88 -0
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +113 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +110 -85
  42. package/src/l3/reindex.ts +715 -78
  43. package/src/l3/scopebase.ts +649 -219
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +23 -4
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +263 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +236 -13
  57. package/src/pipeline.ts +206 -49
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/divpow2.ts +4 -3
  61. package/src/raise/globalshape.ts +1038 -0
  62. package/src/raise/gvn.ts +44 -19
  63. package/src/raise/latch.ts +126 -0
  64. package/src/raise/memberarrays.ts +594 -0
  65. package/src/raise/narrow.ts +124 -0
  66. package/src/raise/narrowlocal.ts +556 -0
  67. package/src/raise/paramwidth.ts +179 -0
  68. package/src/raise/pre-recovery.ts +101 -16
  69. package/src/raise/recover.ts +56 -23
  70. package/src/raise/retsink.ts +215 -14
  71. package/src/raise/shortcircuit.ts +477 -79
  72. package/src/raise/struct-arrays.ts +21 -3
  73. package/src/raise/structs.ts +61 -3
  74. package/src/rank-axes.ts +630 -0
  75. package/src/rank-declare.ts +256 -0
  76. package/src/rank.ts +1726 -251
  77. package/src/structure/analysis.ts +1516 -220
  78. package/src/structure/bitfields.ts +332 -0
  79. package/src/structure/globalaccess.ts +274 -0
  80. package/src/structure/hazards.ts +411 -20
  81. package/src/structure/loops.ts +2 -49
  82. package/src/structure/namecoalesce.ts +435 -0
  83. package/src/structure/structure.ts +2850 -533
  84. package/src/structure/switch-recover.ts +688 -147
  85. package/src/symbols.ts +62 -1
  86. package/src/target.ts +367 -24
  87. package/src/trace.ts +111 -32
@@ -46,8 +46,9 @@ export interface OpaquePolicy {
46
46
  /** token cleanup before classification — e.g. Thumb strips `[`/`]` off a memory operand.
47
47
  * Default: identity. */
48
48
  normalize?: (s: string) => string;
49
- /** true iff the register is hardwired-zero (MIPS `$zero`/`$0`): writing it is a no-op, so it is
50
- * NOT a real destination and the instruction is safe to skip. Default: nothing is zero. */
49
+ /** true iff the register is hardwired-zero (MIPS `$zero`/`$0`). It is then not a real
50
+ * destination so there is nothing to degrade and the instruction is REFUSED, not skipped
51
+ * (`teq zero, zero` is a trap). Default: nothing is zero. */
51
52
  isZero?: (r: string) => boolean;
52
53
  /** Mnemonics that WRITE MEMORY in this ISA: the "no register destination ⇒ safe to fall
53
54
  * through" premise is FALSE for stores — skipping one silently deletes the write (Thumb
@@ -63,10 +64,10 @@ export interface OpaquePolicy {
63
64
  * canonical name but must report the one the input file actually contains — otherwise a decline
64
65
  * names an instruction the reader cannot find in their own .s. Defaults to `mnemonic`. */
65
66
  display?: string;
66
- /** Mnemonics PROVABLY effect-free — or deliberately transparent (Thumb push/pop frame ops) —
67
- * in this ISA: the ONLY unmodelled no-destination instructions that may be skipped. Any other
68
- * no-destination unmodelled instruction THROWS: a side-effect-only instruction (swi, syscall,
69
- * sync, cache…) skipped silently is a deleted effect — a silent miscompile. Default: none. */
67
+ /** Mnemonics PROVABLY effect-free — or deliberately transparent (Thumb push/pop frame ops) — in
68
+ * this ISA: the only unmodelled instructions that may be skipped at all. Anything else with no
69
+ * degradable destination THROWS, because a side-effect-only instruction (swi, syscall, sync,
70
+ * cache…) skipped silently is a deleted effect. Default: none. */
70
71
  skipSafe?: RegExp;
71
72
  }
72
73
 
@@ -79,16 +80,28 @@ export interface OpaqueDest {
79
80
 
80
81
  /** Decide the opaque destination + register source operands for an unmodelled instruction
81
82
  * `mnemonic` with operand list `ops` (operands in objdump order — destination first). Returns
82
- * `null` only when the instruction is provably skippable: a write to a hardwired-zero register,
83
- * or a policy.skipSafe mnemonic. An unmodelled STORE-CLASS instruction (policy.storeClass)
84
- * throws loud (a skipped memory write is a silent miscompile) and so does any OTHER
85
- * no-destination instruction not in skipSafe: with no register to degrade to a live-`?`
86
- * sentinel, skipping would silently delete a side effect (swi/syscall/sync/cache).
83
+ * `null` only for a policy.skipSafe mnemonic. An unmodelled STORE-CLASS instruction
84
+ * (policy.storeClass) throws loud (a skipped memory write is a silent miscompile), and so does any
85
+ * instruction with no degradable destination: with no register to carry the live-`?` sentinel,
86
+ * skipping would silently delete a side effect (swi/syscall/sync/cache).
87
87
  *
88
88
  * When non-null, the caller MUST emit an `opaque` op that writes `dst` and consumes `srcRegs`
89
- * (read through the frontend's own SSA): a DEAD opaque is DCE'd away harmlessly, while a LIVE one
90
- * reaches structuring as the sentinel `?` and trips `assertResolved`the loud failure the
91
- * contract requires, instead of a stale/absent value surfacing as confidently-wrong source. */
89
+ * (read through the frontend's own SSA). It reaches structuring as the sentinel `?` and trips
90
+ * `assertResolved` the loud failure the contract requiresWHETHER OR NOT anything reads `dst`;
91
+ * `opaque` carries `effects: true` (ir/opcodes.ts) for the same reason `call` does.
92
+ *
93
+ * Two properties of `ops[0]` read as permission to skip and are not. Both describe the
94
+ * DESTINATION, while the risk is everything else the instruction does:
95
+ * - nobody reads it — a dead register says nothing about a memory or system effect;
96
+ * - it is hardwired zero — MIPS `teq zero, zero` is a conditional TRAP.
97
+ * So `skipSafe`, a short per-ISA list of provably transparent mnemonics, is the only way an
98
+ * unmodelled instruction leaves no trace.
99
+ *
100
+ * `storeClass` is not load-bearing for soundness — a missed store degrades loudly like anything
101
+ * else — but it throws naming the memory write instead of an unresolvable value, and it catches
102
+ * the shape where `ops[0]` is a SOURCE (MIPS `swl rt, off(base)`) before a `dst` is fabricated
103
+ * from it. It and `skipSafe` match case-INSENSITIVELY: mnemonic case is a property of the
104
+ * disassembler, not of the instruction. */
92
105
  export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy): OpaqueDest | null {
93
106
  const shown = policy.display ?? mnemonic;
94
107
  if (policy.storeClass?.test(mnemonic)) {
@@ -102,7 +115,10 @@ export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy
102
115
  }
103
116
  const norm = policy.normalize ?? ((s) => s);
104
117
  const dst = norm(ops[0] ?? '');
105
- if (!policy.isReg(dst)) {
118
+ // No DEGRADABLE destination: `ops[0]` is not a register, or it is the hardwired zero. Same answer
119
+ // for both — a zero write is a genuine no-op only for a MODELLED instruction, and by construction
120
+ // nothing modelled reaches here.
121
+ if (!policy.isReg(dst) || policy.isZero?.(dst)) {
106
122
  if (policy.skipSafe?.test(mnemonic)) {
107
123
  return null;
108
124
  } // explicitly transparent for this ISA
@@ -111,9 +127,6 @@ export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy
111
127
  `${where}unmodelled effect instruction '${shown}' — no register destination to degrade, and skipping it would silently delete its effect`,
112
128
  );
113
129
  }
114
- if (policy.isZero?.(dst)) {
115
- return null;
116
- } // writes hardwired zero → a genuine no-op
117
130
  const srcRegs = ops.slice(1).map(norm).filter(policy.isReg);
118
131
  return { dst, srcRegs };
119
132
  }
@@ -24,7 +24,7 @@
24
24
  // the result against 0; that implicit compare is wired so a following `beq`/`bne` fuses.
25
25
  //
26
26
  // TRUSTWORTHINESS: an unmodelled instruction with a register destination emits an `opaque` value
27
- // (dead vanishes, live fails LOUD downstream); an unmodelled CONTROL TRANSFER throws
27
+ // that fails LOUD downstream read or not; an unmodelled CONTROL TRANSFER throws
28
28
  // PpcUnsupportedError in `lift`. Never plausible-but-wrong C.
29
29
  //
30
30
  // Scope: straight-line + `if`/diamond integer functions (incl. the conditional-return idiom),
@@ -353,6 +353,17 @@ export function lift(
353
353
  // it is exempted from the loud-fail below; an UNrecovered `bctr` still fails loud.
354
354
  const jts = asmData ? recoverPpcJumpTables(instrs, asmData) : new Map<number, PpcJT>();
355
355
  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
357
+ // placeholder (`lis r4,0` + R_PPC_ADDR16_HA sym): the real value is the symbol's half, and
358
+ // 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
360
+ // is the bounds branch's replaced fall-through, pruned as unreachable before decode.
361
+ const relocPlaceholder = (ins: Instr): void => {
362
+ 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`,
365
+ );
366
+ };
356
367
  // TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
357
368
  // would silently miscompile the control flow). CTR-counted loops and indirect branches land here.
358
369
  for (const ins of instrs) {
@@ -480,15 +491,15 @@ export function lift(
480
491
  const constVal = kit.cnst;
481
492
  const emit = kit.emit;
482
493
  // TRUSTWORTHINESS GUARD: an unmodelled instruction must not silently drop its destination —
483
- // emit an honest `opaque` instead: dead DCE'd; live assertResolved fails LOUD (see
484
- // frontend/opaque.ts for the policy).
494
+ // emit an honest `opaque` instead, which fails LOUD at assertResolved whether or not anything
495
+ // reads that register (see frontend/opaque.ts for the policy).
485
496
  const emitOpaqueDest = (ins: Instr) => {
486
497
  // storeClass: every PPC store mnemonic is st* — an unmodelled one (`stwbrx`, `sthbrx`, …)
487
498
  // must throw, never skip (its first token is the SOURCE register).
488
499
  const od = opaqueDest(ins.mnemonic, ins.ops, {
489
500
  isReg,
490
- storeClass: /^st/,
491
- skipSafe: /^nop$/,
501
+ storeClass: /^st/i,
502
+ skipSafe: /^nop$/i,
492
503
  context: `${name} @0x${ins.addr.toString(16)}`,
493
504
  });
494
505
  if (!od) {
@@ -567,12 +578,23 @@ export function lift(
567
578
  // (anything live across the call has already been moved to a callee-saved register).
568
579
  case 'bl': {
569
580
  const sym = ins.sym ?? 'func';
570
- const argc = protoArity(prototypes[sym]) ?? fallbackArgc(bi);
581
+ const declared = protoArity(prototypes[sym]);
582
+ const argc = declared ?? fallbackArgc(bi);
571
583
  const args: Value[] = [];
572
584
  for (let k = 0; k < argc; k++) {
573
585
  args.push(read(ARG_REGS[k]));
574
586
  }
575
- emit('call', RET, args, { target: sym });
587
+ // Pushed with `tmp` rather than `emit` so the result register is written separately from
588
+ // the op — r3.. are volatile under the EABI, so a GUESSED arity that counted a register
589
+ // set up before an intervening call passes an argument the caller never set up
590
+ // (`finish()` cuts those back — frontend/ssa.ts), and the call's OWN result is the
591
+ // CALLEE's write, so `noteCall` records the clobber after it rather than before.
592
+ const res = kit.tmp('call', args, { target: sym });
593
+ if (declared === undefined) {
594
+ ssa.recordGuessedCall(ops[ops.length - 1], bi, { argRegs: ARG_REGS, returnReg: RET });
595
+ }
596
+ write(RET, res);
597
+ ssa.noteCall(bi);
576
598
  break;
577
599
  }
578
600
  // Stack-frame + link-register bookkeeping. `stwu r1,-N(r1)` / `addi r1,r1,N` adjust the frame
@@ -618,9 +640,16 @@ export function lift(
618
640
  write(d, read(s));
619
641
  break; // move register (or rD,rS,rS)
620
642
  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);
646
+ }
621
647
  write(d, constVal(parseImm(s)));
622
648
  break; // load immediate (addi rD,0,imm)
623
649
  case 'lis':
650
+ if (ins.sym) {
651
+ relocPlaceholder(ins);
652
+ }
624
653
  write(d, constVal((parseImm(s) << 16) >> 0));
625
654
  break; // load immediate shifted
626
655
  case 'add':
@@ -630,11 +659,25 @@ export function lift(
630
659
  // `addi r1,r1,N` is frame teardown (skip); any other addi is a real add-immediate.
631
660
  case 'addi':
632
661
  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
+ }
633
666
  if (d === 'r1') {
634
667
  break;
635
668
  }
636
669
  emitBin('add', d, read(s), constVal(parseImm(t)));
637
670
  break;
671
+ // add immediate SHIFTED — the register-based `%ha` anchor: mwcc derives an absolute base
672
+ // from a scaled index (`addis r4,r3,-32736` = r3 + 0x80200000). The jump-table lis/addi
673
+ // 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).
675
+ case 'addis':
676
+ if (ins.sym) {
677
+ relocPlaceholder(ins);
678
+ }
679
+ emitBin('add', d, read(s), constVal((parseImm(t) << 16) >> 0));
680
+ break;
638
681
  case 'subf':
639
682
  case 'subfc':
640
683
  case 'subfo':
@@ -686,6 +729,10 @@ export function lift(
686
729
  emitBin('or', d, read(s), read(t));
687
730
  break;
688
731
  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
+ }
689
736
  emitBin('or', d, read(s), constVal(parseImm(t)));
690
737
  break;
691
738
  case 'xor':