@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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/frontend/opaque.ts
CHANGED
|
@@ -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`)
|
|
50
|
-
*
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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)
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* (read through the frontend's own SSA). It reaches structuring as the sentinel `?` and trips
|
|
90
|
+
* `assertResolved` — the loud failure the contract requires — WHETHER 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
|
-
|
|
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
|
}
|
package/src/frontend/ppc.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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':
|