@asmlift/core 0.3.0 → 0.5.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +130 -4
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +181 -4
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +37 -29
- package/src/frontend/opaque.ts +70 -20
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +279 -56
- package/src/frontend/thumb.ts +1372 -87
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +57 -3
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +221 -0
- package/src/l3/ast.ts +127 -5
- package/src/l3/basecse.ts +58 -62
- package/src/l3/coalesce.ts +215 -0
- package/src/l3/dce.ts +33 -41
- package/src/l3/gates.ts +67 -0
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +440 -0
- package/src/l3/tailmerge.ts +124 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +65 -6
- package/src/raise/divpow2.ts +227 -0
- package/src/raise/gvn.ts +151 -0
- package/src/raise/pre-recovery.ts +39 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +37 -7
- package/src/raise/shortcircuit.ts +262 -22
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +41 -3
- package/src/rank.ts +196 -20
- package/src/structure/analysis.ts +175 -89
- package/src/structure/structure.ts +588 -55
- package/src/structure/switch-recover.ts +117 -30
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
package/src/frontend/mips.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// compares (`sltu`/`sltiu`) lower to `icmp_ult`; recover types their operands u32 so the backend
|
|
20
20
|
// re-emits `sltu` (the operator is the same `<` — the signedness lives in the operand types).
|
|
21
21
|
import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
22
|
-
import type
|
|
22
|
+
import { NEGATED_ICMP, type Opcode } from '../ir/opcodes';
|
|
23
23
|
import { T } from '../ir/types';
|
|
24
24
|
import type { Prototypes } from '../proto';
|
|
25
25
|
import type { TargetDescription } from '../target';
|
|
@@ -31,7 +31,7 @@ import { assertInputFormat } from './format';
|
|
|
31
31
|
import type { Frontend } from './frontend';
|
|
32
32
|
import { opaqueDest } from './opaque';
|
|
33
33
|
import { isSplatMips, parseSplatMips } from './splat';
|
|
34
|
-
import { abiSortEntryParams } from './ssa';
|
|
34
|
+
import { abiSortEntryParams, stackSlotKey } from './ssa';
|
|
35
35
|
import { makeSsaBuilder } from './ssa';
|
|
36
36
|
|
|
37
37
|
type Instr = DisasmInstr;
|
|
@@ -47,19 +47,6 @@ const COND_Z: Record<string, Opcode> = {
|
|
|
47
47
|
bgez: 'icmp_sge',
|
|
48
48
|
};
|
|
49
49
|
const COND_RR: Record<string, Opcode> = { beq: 'icmp_eq', bne: 'icmp_ne' };
|
|
50
|
-
// Negated icmp opcode (for the `slt …; beqz` "branch when false" fold).
|
|
51
|
-
const NEG_ICMP: Record<string, Opcode> = {
|
|
52
|
-
icmp_slt: 'icmp_sge',
|
|
53
|
-
icmp_sge: 'icmp_slt',
|
|
54
|
-
icmp_sgt: 'icmp_sle',
|
|
55
|
-
icmp_sle: 'icmp_sgt',
|
|
56
|
-
icmp_ult: 'icmp_uge',
|
|
57
|
-
icmp_uge: 'icmp_ult',
|
|
58
|
-
icmp_ugt: 'icmp_ule',
|
|
59
|
-
icmp_ule: 'icmp_ugt',
|
|
60
|
-
icmp_eq: 'icmp_ne',
|
|
61
|
-
icmp_ne: 'icmp_eq',
|
|
62
|
-
};
|
|
63
50
|
|
|
64
51
|
const isZero = (r: string) => r === 'zero' || r === '$0';
|
|
65
52
|
// The stack pointer (`$29`). A `sw/lw` through it is not a store/load through a data pointer — it
|
|
@@ -67,7 +54,7 @@ const isZero = (r: string) => r === 'zero' || r === '$0';
|
|
|
67
54
|
const isStackPtr = (r: string) => r === 'sp' || r === '$sp' || r === '$29';
|
|
68
55
|
// SSA-variable name for the stack slot at a constant `sp`-offset. Distinct namespace from the
|
|
69
56
|
// register names (which are alphabetic / `$N`), so it never collides with a real register var.
|
|
70
|
-
const stackSlot =
|
|
57
|
+
const stackSlot = stackSlotKey; // shared spelling: frontend/ssa.ts
|
|
71
58
|
// Sub-word memory mnemonics (widths 1 and 2). Used by the `spSlotSafe` guard in `lift`: a sub-word
|
|
72
59
|
// `sp`-relative access means the word stack-slot model is unsafe for that function.
|
|
73
60
|
const SUBWORD_MEM = new Set(['lb', 'lbu', 'lh', 'lhu', 'sb', 'sh']);
|
|
@@ -604,6 +591,18 @@ export function lift(
|
|
|
604
591
|
}
|
|
605
592
|
return readVar(r, bi);
|
|
606
593
|
};
|
|
594
|
+
// `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
|
|
595
|
+
//
|
|
596
|
+
// Keyed by the SSA VALUE the compare produced, never by its register. Keying by register is the
|
|
597
|
+
// bug: `slt v0,a0,a1; xori v0,v0,1; beqz v0,L` (a materialised `a0 >= a1` that a branch then
|
|
598
|
+
// tests — IDO's spelling, and a live benchmark row) redefines v0, and a register-keyed record
|
|
599
|
+
// folds the branch against the DEAD `slt`, silently emitting the INVERTED condition. A value
|
|
600
|
+
// cannot go stale that way: `condValue` resolves the branch's register to whatever value reaches
|
|
601
|
+
// it and looks THAT up, so a redefinition simply misses and the honest `icmp_eq(rX, 0)` is
|
|
602
|
+
// emitted. It also needs no invalidation discipline to be maintained by every future writer —
|
|
603
|
+
// which matters, because `write` is NOT the only path that redefines a register (`lui
|
|
604
|
+
// rD,%hi(SYM)` deliberately reassigns rD's meaning without it).
|
|
605
|
+
const cmpDef = new Map<Value, { opcode: string; lhs: Value; rhs: Value }>();
|
|
607
606
|
const write = (r: string, v: Value) => {
|
|
608
607
|
// Writing a register clears any pending `%hi` it held — the high-half address is gone once the
|
|
609
608
|
// register is reassigned (e.g. `lw rHi, %lo(SYM)(rHi)` reuses the base as the load dest). A
|
|
@@ -626,13 +625,11 @@ export function lift(
|
|
|
626
625
|
// DELIBERATELY (unlike divState): a cross-block `mult`/`mflo` pair has no observed inhabitant,
|
|
627
626
|
// and the miss degrades to a LOUD opaque, never silence.
|
|
628
627
|
let mulState: { rs: Value; rt: Value; signed: boolean } | null = null;
|
|
629
|
-
// `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
|
|
630
|
-
const cmpDef = new Map<string, { value: Value; opcode: string; lhs: Value; rhs: Value }>();
|
|
631
628
|
const emitCmp = (opc: Opcode, d: string, lhs: Value, rhs: Value) => {
|
|
632
629
|
const v = mkValue(T.unk(32));
|
|
633
630
|
ops.push(mkOp(opc, { operands: [lhs, rhs], results: [v] }));
|
|
634
631
|
write(d, v);
|
|
635
|
-
cmpDef.set(
|
|
632
|
+
cmpDef.set(v, { opcode: opc, lhs, rhs });
|
|
636
633
|
};
|
|
637
634
|
|
|
638
635
|
const decode = (ins: Instr) => {
|
|
@@ -869,8 +866,8 @@ export function lift(
|
|
|
869
866
|
}
|
|
870
867
|
};
|
|
871
868
|
// TRUSTWORTHINESS GUARD (mirrors the PPC frontend): an unmodelled instruction must not silently
|
|
872
|
-
// drop its destination register — emit an honest `opaque
|
|
873
|
-
//
|
|
869
|
+
// drop its destination register — emit an honest `opaque`, which fails LOUD at assertResolved
|
|
870
|
+
// whether or not anything reads that register (see frontend/opaque.ts for the policy).
|
|
874
871
|
const emitOpaqueDest = (ins: Instr) => {
|
|
875
872
|
// A `%hi`/`%lo` operand on an instruction NOT modelled as a global consumer — an FP load/store
|
|
876
873
|
// (`lwc1`/`ldc1`), or any unmodelled op — reaches here (the modelled consumers handle their own
|
|
@@ -892,8 +889,8 @@ export function lift(
|
|
|
892
889
|
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
893
890
|
isReg: isMipsReg,
|
|
894
891
|
isZero,
|
|
895
|
-
storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)
|
|
896
|
-
skipSafe: /^(nop|ssnop|break)
|
|
892
|
+
storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)$/i,
|
|
893
|
+
skipSafe: /^(nop|ssnop|break)$/i,
|
|
897
894
|
context: `${name} @0x${ins.addr.toString(16)}`,
|
|
898
895
|
});
|
|
899
896
|
if (!od) {
|
|
@@ -941,6 +938,13 @@ export function lift(
|
|
|
941
938
|
// incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local — neither modelled.
|
|
942
939
|
// Without this, readVar would FABRICATE a phantom entry parameter for the slot, silently emitting a
|
|
943
940
|
// function of wrong arity that returns the wrong argument. Loud-fail instead of miscompiling.
|
|
941
|
+
//
|
|
942
|
+
// Those two cases are SEPARABLE, and the Thumb frontend now separates them (frontend/thumb.ts,
|
|
943
|
+
// incomingArgIndex): a slot at or above the callee's own frame cannot have been written by this
|
|
944
|
+
// function, so it is an incoming argument; below the frame top it is a local. Doing the same here
|
|
945
|
+
// needs O32's own frame rule — the 16-byte home area means a stack argument is NOT simply "above
|
|
946
|
+
// the frame", so the Thumb arithmetic does not carry over — plus ssa.ensureParam for the register
|
|
947
|
+
// half. Until then this stays one loud decline for both.
|
|
944
948
|
if (!ssa.hasReachingDef(stackSlot(off), bi)) {
|
|
945
949
|
throw new FrontendUnsupportedError(
|
|
946
950
|
`cannot lift '${name}': load from stack slot sp@${off} that was never stored ` +
|
|
@@ -1053,7 +1057,7 @@ function condValue(
|
|
|
1053
1057
|
ops: Op[],
|
|
1054
1058
|
read: (r: string) => Value,
|
|
1055
1059
|
constVal: (n: number) => Value,
|
|
1056
|
-
cmpDef: Map<
|
|
1060
|
+
cmpDef: Map<Value, { opcode: string; lhs: Value; rhs: Value }>,
|
|
1057
1061
|
): Value {
|
|
1058
1062
|
const mk = (opc: Opcode, l: Value, r: Value): Value => {
|
|
1059
1063
|
const v = mkValue(T.unk(32));
|
|
@@ -1064,15 +1068,19 @@ function condValue(
|
|
|
1064
1068
|
return mk(COND_RR[br.mnemonic], read(br.ops[0]), read(br.ops[1]));
|
|
1065
1069
|
}
|
|
1066
1070
|
// *z forms compare a register against zero — except beqz/bnez may fold a preceding `slt`.
|
|
1067
|
-
|
|
1068
|
-
|
|
1071
|
+
// Resolve the register to its reaching VALUE first: that is the fold's key (so a redefinition
|
|
1072
|
+
// between the compare and the branch misses instead of folding stale), and it is also what
|
|
1073
|
+
// routes the operand through `read`'s loud guards (sp / `%hi` / `gp` used as data) on every
|
|
1074
|
+
// path — the fold used to bypass them by never reading the register at all.
|
|
1075
|
+
const rsv = read(br.ops[0]);
|
|
1076
|
+
const folded = cmpDef.get(rsv);
|
|
1069
1077
|
if (folded && br.mnemonic === 'bnez') {
|
|
1070
|
-
return
|
|
1078
|
+
return rsv;
|
|
1071
1079
|
} // branch when slt is true
|
|
1072
1080
|
if (folded && br.mnemonic === 'beqz') {
|
|
1073
|
-
return mk(
|
|
1081
|
+
return mk(NEGATED_ICMP[folded.opcode], folded.lhs, folded.rhs);
|
|
1074
1082
|
} // …when false
|
|
1075
|
-
return mk(COND_Z[br.mnemonic],
|
|
1083
|
+
return mk(COND_Z[br.mnemonic], rsv, constVal(0));
|
|
1076
1084
|
}
|
|
1077
1085
|
|
|
1078
1086
|
/** The MIPS-II / IDO frontend, registered for the `mips` target. */
|
package/src/frontend/opaque.ts
CHANGED
|
@@ -7,6 +7,37 @@
|
|
|
7
7
|
// This module owns only the POLICY (which token is the destination, which are register sources).
|
|
8
8
|
// The frontend still owns the SSA plumbing (how to `read` a source and `write`/emit the result),
|
|
9
9
|
// because that is block-local state the policy must not touch.
|
|
10
|
+
//
|
|
11
|
+
// ─── A NOTE ON ALTERNATIVE MNEMONIC SPELLINGS ──────────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// Every ISA here accepts more than one spelling for some instructions, and the frontends handle
|
|
14
|
+
// that in two DIFFERENT ways on purpose. Which one is right is decided by the operands, not by
|
|
15
|
+
// taste:
|
|
16
|
+
//
|
|
17
|
+
// * PURE SYNONYM — same operands, same semantics, different name. Normalise it in a name→name
|
|
18
|
+
// table at the parse site, so every consumer of the mnemonic sees one name. Thumb does this
|
|
19
|
+
// for `ldsh`/`ldrsh`, `ldsb`/`ldrsb`, `ldm`/`ldmfd`/`ldmia`, `stm`/`stmea`/`stmia`, which
|
|
20
|
+
// ARM DDI 0029G Figure 1-6 gives a single encoding apiece.
|
|
21
|
+
//
|
|
22
|
+
// The table is the right shape THERE because the mnemonic is read by more than the decode
|
|
23
|
+
// switch — Thumb's `classifyXfer` matches it to tell a return from an indirect jump, the
|
|
24
|
+
// `storeClass` below matches it to decide whether an unmodelled op may be skipped, and the
|
|
25
|
+
// instruction-size walk tests it for `bl`. An alias arm on one `case` fixes one of those.
|
|
26
|
+
//
|
|
27
|
+
// * EXTENDED MNEMONIC / PSEUDO-INSTRUCTION — different operand GRAMMAR, so no rename can
|
|
28
|
+
// express it. Give it its own decode arm. MIPS `move rD,rS` (2 operands) is `addu rD,rS,zero`
|
|
29
|
+
// (3); PPC `mr rD,rS` is `or rD,rS,rS`; PPC `slwi rD,rS,n` (3) is `rlwinm rD,rS,n,mb,me` (5,
|
|
30
|
+
// with mask fields computed from n). Routing these through a table would be a category error.
|
|
31
|
+
//
|
|
32
|
+
// There is deliberately NO shared alias helper. As of writing, MIPS and PPC have no pure-synonym
|
|
33
|
+
// gap at all — every `unmodelled instruction` decline they produce across the whole benchmark is
|
|
34
|
+
// a genuinely unmodelled opcode (`lwc1`, `fmuls`, `fctiwz`, `subfe`, …), not a spelling — so such
|
|
35
|
+
// a helper would have exactly one caller. The bar for extracting one is the bar this module itself
|
|
36
|
+
// met: several frontends hand-copying the same policy AND observed drift between the copies.
|
|
37
|
+
//
|
|
38
|
+
// One thing that IS shared, and should stay shared: `display` below. A frontend that normalises
|
|
39
|
+
// spellings must still REPORT the one its input actually used, or a decline sends the reader
|
|
40
|
+
// looking for an instruction their disassembly does not contain.
|
|
10
41
|
import { FrontendUnsupportedError } from './errors';
|
|
11
42
|
|
|
12
43
|
export interface OpaquePolicy {
|
|
@@ -15,8 +46,9 @@ export interface OpaquePolicy {
|
|
|
15
46
|
/** token cleanup before classification — e.g. Thumb strips `[`/`]` off a memory operand.
|
|
16
47
|
* Default: identity. */
|
|
17
48
|
normalize?: (s: string) => string;
|
|
18
|
-
/** true iff the register is hardwired-zero (MIPS `$zero`/`$0`)
|
|
19
|
-
*
|
|
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. */
|
|
20
52
|
isZero?: (r: string) => boolean;
|
|
21
53
|
/** Mnemonics that WRITE MEMORY in this ISA: the "no register destination ⇒ safe to fall
|
|
22
54
|
* through" premise is FALSE for stores — skipping one silently deletes the write (Thumb
|
|
@@ -27,10 +59,15 @@ export interface OpaquePolicy {
|
|
|
27
59
|
storeClass?: RegExp;
|
|
28
60
|
/** attribution for thrown declines: the function being lifted (optionally "+ site"). */
|
|
29
61
|
context?: string;
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
62
|
+
/** How to SPELL the mnemonic in messages, when that differs from the name used to classify it.
|
|
63
|
+
* A frontend that normalises legacy spellings (Thumb `ldsh` -> `ldrsh`) classifies on the
|
|
64
|
+
* canonical name but must report the one the input file actually contains — otherwise a decline
|
|
65
|
+
* names an instruction the reader cannot find in their own .s. Defaults to `mnemonic`. */
|
|
66
|
+
display?: string;
|
|
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. */
|
|
34
71
|
skipSafe?: RegExp;
|
|
35
72
|
}
|
|
36
73
|
|
|
@@ -43,40 +80,53 @@ export interface OpaqueDest {
|
|
|
43
80
|
|
|
44
81
|
/** Decide the opaque destination + register source operands for an unmodelled instruction
|
|
45
82
|
* `mnemonic` with operand list `ops` (operands in objdump order — destination first). Returns
|
|
46
|
-
* `null` only
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* 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).
|
|
51
87
|
*
|
|
52
88
|
* When non-null, the caller MUST emit an `opaque` op that writes `dst` and consumes `srcRegs`
|
|
53
|
-
* (read through the frontend's own SSA)
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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. */
|
|
56
105
|
export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy): OpaqueDest | null {
|
|
106
|
+
const shown = policy.display ?? mnemonic;
|
|
57
107
|
if (policy.storeClass?.test(mnemonic)) {
|
|
58
108
|
// `context` names the function (and, where the ISA has addresses, the site) — this message
|
|
59
109
|
// lands verbatim in annotate-mode stub headers, where an un-attributed decline is
|
|
60
110
|
// unactionable in a multi-function run.
|
|
61
111
|
const where = policy.context ? `cannot lift '${policy.context}': ` : '';
|
|
62
112
|
throw new FrontendUnsupportedError(
|
|
63
|
-
`${where}unmodelled store-class instruction '${
|
|
113
|
+
`${where}unmodelled store-class instruction '${shown}' — a memory write cannot be skipped or degraded to a register opaque`,
|
|
64
114
|
);
|
|
65
115
|
}
|
|
66
116
|
const norm = policy.normalize ?? ((s) => s);
|
|
67
117
|
const dst = norm(ops[0] ?? '');
|
|
68
|
-
|
|
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)) {
|
|
69
122
|
if (policy.skipSafe?.test(mnemonic)) {
|
|
70
123
|
return null;
|
|
71
124
|
} // explicitly transparent for this ISA
|
|
72
125
|
const where = policy.context ? `cannot lift '${policy.context}': ` : '';
|
|
73
126
|
throw new FrontendUnsupportedError(
|
|
74
|
-
`${where}unmodelled effect instruction '${
|
|
127
|
+
`${where}unmodelled effect instruction '${shown}' — no register destination to degrade, and skipping it would silently delete its effect`,
|
|
75
128
|
);
|
|
76
129
|
}
|
|
77
|
-
if (policy.isZero?.(dst)) {
|
|
78
|
-
return null;
|
|
79
|
-
} // writes hardwired zero → a genuine no-op
|
|
80
130
|
const srcRegs = ops.slice(1).map(norm).filter(policy.isReg);
|
|
81
131
|
return { dst, srcRegs };
|
|
82
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),
|
|
@@ -480,15 +480,15 @@ export function lift(
|
|
|
480
480
|
const constVal = kit.cnst;
|
|
481
481
|
const emit = kit.emit;
|
|
482
482
|
// TRUSTWORTHINESS GUARD: an unmodelled instruction must not silently drop its destination —
|
|
483
|
-
// emit an honest `opaque` instead
|
|
484
|
-
// frontend/opaque.ts for the policy).
|
|
483
|
+
// emit an honest `opaque` instead, which fails LOUD at assertResolved whether or not anything
|
|
484
|
+
// reads that register (see frontend/opaque.ts for the policy).
|
|
485
485
|
const emitOpaqueDest = (ins: Instr) => {
|
|
486
486
|
// storeClass: every PPC store mnemonic is st* — an unmodelled one (`stwbrx`, `sthbrx`, …)
|
|
487
487
|
// must throw, never skip (its first token is the SOURCE register).
|
|
488
488
|
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
489
489
|
isReg,
|
|
490
|
-
storeClass: /^st
|
|
491
|
-
skipSafe: /^nop
|
|
490
|
+
storeClass: /^st/i,
|
|
491
|
+
skipSafe: /^nop$/i,
|
|
492
492
|
context: `${name} @0x${ins.addr.toString(16)}`,
|
|
493
493
|
});
|
|
494
494
|
if (!od) {
|
|
@@ -567,12 +567,23 @@ export function lift(
|
|
|
567
567
|
// (anything live across the call has already been moved to a callee-saved register).
|
|
568
568
|
case 'bl': {
|
|
569
569
|
const sym = ins.sym ?? 'func';
|
|
570
|
-
const
|
|
570
|
+
const declared = protoArity(prototypes[sym]);
|
|
571
|
+
const argc = declared ?? fallbackArgc(bi);
|
|
571
572
|
const args: Value[] = [];
|
|
572
573
|
for (let k = 0; k < argc; k++) {
|
|
573
574
|
args.push(read(ARG_REGS[k]));
|
|
574
575
|
}
|
|
575
|
-
emit
|
|
576
|
+
// Pushed with `tmp` rather than `emit` so the result register is written AFTER the clobber
|
|
577
|
+
// is recorded — the order matters: r3.. are volatile under the EABI, so a GUESSED arity
|
|
578
|
+
// that counted a register set up before an intervening call passes an argument the caller
|
|
579
|
+
// never set up (`finish()` cuts those back — frontend/ssa.ts), while the call's OWN result
|
|
580
|
+
// must stay fresh for the next call (`bar(foo())`).
|
|
581
|
+
const res = kit.tmp('call', args, { target: sym });
|
|
582
|
+
if (declared === undefined) {
|
|
583
|
+
ssa.recordGuessedCall(ops[ops.length - 1], bi, ARG_REGS);
|
|
584
|
+
}
|
|
585
|
+
ssa.noteCall(bi);
|
|
586
|
+
write(RET, res);
|
|
576
587
|
break;
|
|
577
588
|
}
|
|
578
589
|
// Stack-frame + link-register bookkeeping. `stwu r1,-N(r1)` / `addi r1,r1,N` adjust the frame
|