@asmlift/core 0.4.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/package.json +1 -1
- package/src/backend/cfamily.ts +5 -2
- package/src/contracts.ts +166 -2
- package/src/frontend/mips.ts +13 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +249 -5
- package/src/frontend/thumb.ts +1082 -72
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +24 -14
- package/src/l3/argbase.ts +6 -1
- package/src/l3/ast.ts +9 -1
- package/src/l3/basecse.ts +57 -24
- package/src/l3/coalesce.ts +107 -38
- package/src/l3/dce.ts +31 -18
- package/src/l3/gates.ts +67 -0
- package/src/l3/scopebase.ts +11 -7
- package/src/l3/tailmerge.ts +8 -4
- package/src/pipeline.ts +60 -4
- package/src/raise/divpow2.ts +2 -1
- package/src/raise/gvn.ts +16 -6
- package/src/raise/pre-recovery.ts +4 -2
- package/src/raise/retsink.ts +5 -4
- package/src/raise/shortcircuit.ts +3 -5
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +29 -1
- package/src/rank.ts +26 -2
- package/src/structure/analysis.ts +168 -123
- package/src/structure/structure.ts +228 -63
- package/src/structure/switch-recover.ts +96 -27
package/package.json
CHANGED
package/src/backend/cfamily.ts
CHANGED
|
@@ -388,7 +388,10 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
|
|
|
388
388
|
out.push(`${bi}break;`);
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
|
-
|
|
391
|
+
// `?.length`, not just presence: a label with no statement under it is not valid C89, and an
|
|
392
|
+
// L3 pass (dce, reindex) may empty a default that arrived with statements — the structurer's
|
|
393
|
+
// own "don't attach an empty default" rule cannot see that.
|
|
394
|
+
if (s.default?.length) {
|
|
392
395
|
out.push(`${ci}default:`);
|
|
393
396
|
for (const t of s.default) {
|
|
394
397
|
out.push(...printStmt(t, bi, vt, leaf));
|
|
@@ -470,7 +473,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
|
470
473
|
const vt: VarTypes = declaredTypes(fn);
|
|
471
474
|
const lines: string[] = [];
|
|
472
475
|
for (const l of fn.locals) {
|
|
473
|
-
lines.push(` ${cType(l.type)} ${l.name};`);
|
|
476
|
+
lines.push(` ${l.volatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
|
|
474
477
|
}
|
|
475
478
|
for (const s of fn.body) {
|
|
476
479
|
lines.push(...printStmt(s, ' ', vt, leaf));
|
package/src/contracts.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// decompileRanked / decompileWithReport).
|
|
4
4
|
// A pass that regresses fails AT its boundary with a diagnostic, not three stages later as
|
|
5
5
|
// wrong C.
|
|
6
|
-
import type
|
|
6
|
+
import { type Block, type Fn, type Value, successorsOf } from './ir/core';
|
|
7
7
|
import { type IrType, typeToString } from './ir/types';
|
|
8
8
|
import type { BinOp, Expr, SFn, Stmt } from './l3/ast';
|
|
9
|
-
import { exprChildren, fieldSpellsDot, stmtChildren, stmtExprs } from './l3/ast';
|
|
9
|
+
import { exprChildren, fieldSpellsDot, gapReasonFor, stmtChildren, stmtExprs } from './l3/ast';
|
|
10
10
|
import { declaredTypes, exprCType } from './l3/typing';
|
|
11
11
|
|
|
12
12
|
export class ContractError extends Error {
|
|
@@ -58,6 +58,170 @@ export function assertResolved(sfn: SFn): void {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// ── effects: executed once, never dropped ──────────────────────────────────────────────────
|
|
62
|
+
//
|
|
63
|
+
// The three contracts around this one are about TYPING and SPELLABILITY. Nothing checked the
|
|
64
|
+
// property the structurer's materialization model exists to preserve: a call in the asm must run
|
|
65
|
+
// exactly as often in the emitted source. Its two failure modes are the two that hurt most —
|
|
66
|
+
// asmlift's first rule is that a loud failure beats a silently wrong answer, and both of these are
|
|
67
|
+
// silent:
|
|
68
|
+
//
|
|
69
|
+
// • DROPPED — a call the asm makes has no counterpart in the tree at all;
|
|
70
|
+
// • RE-RUN — inlining a call's value at more than one render position (or a structuring copy
|
|
71
|
+
// that duplicates a region onto a single path) makes one call execute twice. The round that
|
|
72
|
+
// recovered switch fall-through hit exactly this shape, and only an adversarial reviewer
|
|
73
|
+
// caught it.
|
|
74
|
+
//
|
|
75
|
+
// Deliberately narrow, so it never declines a function that is fine:
|
|
76
|
+
//
|
|
77
|
+
// • CALLS only. Loads legitimately re-render (that is the whole point of the inline-at-use
|
|
78
|
+
// model, and the alias gate governs it); stores are checked by neither direction here because
|
|
79
|
+
// the readability DCE pass is allowed to drop a provably dead one.
|
|
80
|
+
// • PER PATH, not per tree. Structuring may legitimately emit one block twice — two exclusive
|
|
81
|
+
// switch arms sharing a body, a duplicated return merge — and each path still executes it
|
|
82
|
+
// once. So the duplication rule compares the maximum over syntactic root-to-leaf paths (a
|
|
83
|
+
// branch takes the max of its arms, a loop body counts once, a fall-through arm chains into
|
|
84
|
+
// the next) against the IR's static count.
|
|
85
|
+
// • Names the IR does not have are ignored, and only calls carrying a target symbol are counted
|
|
86
|
+
// (every frontend that emits `call` today stamps one).
|
|
87
|
+
type CallCounts = Map<string, number>;
|
|
88
|
+
|
|
89
|
+
/** per-key combine of two count maps (`sum` for sequence, `max` for exclusive alternatives) */
|
|
90
|
+
function combine(a: CallCounts, b: CallCounts, f: (x: number, y: number) => number): CallCounts {
|
|
91
|
+
const out = new Map(a);
|
|
92
|
+
for (const [k, v] of b) {
|
|
93
|
+
out.set(k, f(out.get(k) ?? 0, v));
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** every `call` expression under `e`, counted by target name */
|
|
99
|
+
function callsInExpr(e: Expr, into: CallCounts): void {
|
|
100
|
+
if (e.k === 'call') {
|
|
101
|
+
into.set(e.fn, (into.get(e.fn) ?? 0) + 1);
|
|
102
|
+
}
|
|
103
|
+
exprChildren(e).forEach((c) => callsInExpr(c, into));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `total` = every occurrence in the tree; `path` = the most any single syntactic path executes */
|
|
107
|
+
function countCalls(stmts: Stmt[]): { total: CallCounts; path: CallCounts } {
|
|
108
|
+
let total: CallCounts = new Map();
|
|
109
|
+
let path: CallCounts = new Map();
|
|
110
|
+
const add = (r: { total: CallCounts; path: CallCounts }, pathF: (x: number, y: number) => number) => {
|
|
111
|
+
total = combine(total, r.total, (x, y) => x + y);
|
|
112
|
+
path = combine(path, r.path, pathF);
|
|
113
|
+
};
|
|
114
|
+
for (const s of stmts) {
|
|
115
|
+
const own: CallCounts = new Map();
|
|
116
|
+
stmtExprs(s).forEach((e) => callsInExpr(e, own));
|
|
117
|
+
add({ total: own, path: own }, (x, y) => x + y);
|
|
118
|
+
if (s.k === 'if') {
|
|
119
|
+
const t = countCalls(s.then);
|
|
120
|
+
const e = countCalls(s.else);
|
|
121
|
+
// exclusive arms: the path count is whichever arm runs, the total counts both
|
|
122
|
+
add(
|
|
123
|
+
{ total: combine(t.total, e.total, (x, y) => x + y), path: combine(t.path, e.path, Math.max) },
|
|
124
|
+
(x, y) => x + y,
|
|
125
|
+
);
|
|
126
|
+
} else if (s.k === 'switch') {
|
|
127
|
+
const arms = s.cases.map((c) => countCalls(c.body));
|
|
128
|
+
const dflt = countCalls(s.default ?? []);
|
|
129
|
+
// A fall-through arm continues into the NEXT one emitted (the last into `default`), so a
|
|
130
|
+
// path through arm i runs the chain starting at i — the shape the fall-through round's
|
|
131
|
+
// CRITICAL took. Built from the end; `chain[i]` is that arm's per-path count.
|
|
132
|
+
const chain: CallCounts[] = new Array(arms.length);
|
|
133
|
+
for (let i = arms.length - 1; i >= 0; i--) {
|
|
134
|
+
const next = i + 1 < arms.length ? chain[i + 1] : dflt.path;
|
|
135
|
+
chain[i] = s.cases[i].fallsThrough ? combine(arms[i].path, next, (x, y) => x + y) : arms[i].path;
|
|
136
|
+
}
|
|
137
|
+
const armTotal = arms.reduce((acc, a) => combine(acc, a.total, (x, y) => x + y), dflt.total);
|
|
138
|
+
const armPath = chain.reduce((acc, c) => combine(acc, c, Math.max), dflt.path);
|
|
139
|
+
add({ total: armTotal, path: armPath }, (x, y) => x + y);
|
|
140
|
+
} else {
|
|
141
|
+
// Sequenced children (a loop body, a `for`'s init/inc): counted ONCE — a loop's dynamic trip
|
|
142
|
+
// count is not a syntactic occurrence, and the IR side is static too.
|
|
143
|
+
for (const c of stmtChildren(s)) {
|
|
144
|
+
add(countCalls([c]), (x, y) => x + y);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { total, path };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Post structuring: every call the asm makes is emitted, and none is emitted more times than the
|
|
153
|
+
* asm makes it on any one path. See the note above for what this deliberately does not cover.
|
|
154
|
+
*/
|
|
155
|
+
export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
156
|
+
// Reachable blocks only: an unreachable block's call is legitimately never emitted.
|
|
157
|
+
const seen = new Set<Block>([fn.blocks[0]]);
|
|
158
|
+
for (const stack = [fn.blocks[0]]; stack.length;) {
|
|
159
|
+
for (const s of successorsOf(stack.pop()!)) {
|
|
160
|
+
if (!seen.has(s)) {
|
|
161
|
+
seen.add(s);
|
|
162
|
+
stack.push(s);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const irCalls: CallCounts = new Map();
|
|
167
|
+
// Unmodelled instructions, by the mnemonic the frontend stamped. Same "never dropped" property as
|
|
168
|
+
// a call, and it needs its own tally because an `opaque` carries no `target`.
|
|
169
|
+
const irOpaques = new Set<string>();
|
|
170
|
+
for (const b of seen) {
|
|
171
|
+
for (const op of b.ops) {
|
|
172
|
+
if (op.opcode === 'call' && typeof op.attrs.target === 'string') {
|
|
173
|
+
const t = op.attrs.target;
|
|
174
|
+
irCalls.set(t, (irCalls.get(t) ?? 0) + 1);
|
|
175
|
+
} else if (op.opcode === 'opaque') {
|
|
176
|
+
irOpaques.add(gapReasonFor(op.attrs.mnemonic));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// DROPPED only, not the RE-RUN half: a gap rendered twice is a diagnostic printed twice, which
|
|
181
|
+
// costs nothing because nothing recompiles it, and structuring legitimately duplicates a shared
|
|
182
|
+
// arm — so a per-path count here would fire on correct output.
|
|
183
|
+
//
|
|
184
|
+
// Bites only in ANNOTATE mode (under `strict` the gap is the `?` sentinel and structure() has
|
|
185
|
+
// already thrown), which is where it is needed: that is the CLI and benchmark default, and the
|
|
186
|
+
// only mode with no other backstop against a silently dropped opaque.
|
|
187
|
+
if (irOpaques.size) {
|
|
188
|
+
const emitted = new Set<string>();
|
|
189
|
+
const we = (e: Expr): void => {
|
|
190
|
+
if (e.k === 'marker') {
|
|
191
|
+
emitted.add(e.reason);
|
|
192
|
+
}
|
|
193
|
+
exprChildren(e).forEach(we);
|
|
194
|
+
};
|
|
195
|
+
const ws = (s: Stmt): void => {
|
|
196
|
+
stmtExprs(s).forEach(we);
|
|
197
|
+
stmtChildren(s).forEach(ws);
|
|
198
|
+
};
|
|
199
|
+
sfn.body.forEach(ws);
|
|
200
|
+
for (const reason of irOpaques) {
|
|
201
|
+
if (!emitted.has(reason)) {
|
|
202
|
+
throw new ContractError(
|
|
203
|
+
`structuring dropped the ${reason} in '${sfn.name}' — an instruction asmlift could not model left no trace`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!irCalls.size) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const { total, path } = countCalls(sfn.body);
|
|
212
|
+
for (const [name, n] of irCalls) {
|
|
213
|
+
if (!(total.get(name) ?? 0)) {
|
|
214
|
+
throw new ContractError(`structuring dropped the call to '${name}' in '${sfn.name}' — its effect is lost`);
|
|
215
|
+
}
|
|
216
|
+
const p = path.get(name) ?? 0;
|
|
217
|
+
if (p > n) {
|
|
218
|
+
throw new ContractError(
|
|
219
|
+
`structuring emitted ${p} calls to '${name}' on one path in '${sfn.name}', where the asm makes ${n}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
61
225
|
/** Post structuring: the AST's memory accesses and operators must be SPELLABLE — a `field`
|
|
62
226
|
* node's base a pointer-to-struct (`->`) or a struct value (`.`, an array element) carrying
|
|
63
227
|
* that field; no pointer operand under an operator C rejects; and every SCALAR `index` node's
|
package/src/frontend/mips.ts
CHANGED
|
@@ -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;
|
|
@@ -54,7 +54,7 @@ const isZero = (r: string) => r === 'zero' || r === '$0';
|
|
|
54
54
|
const isStackPtr = (r: string) => r === 'sp' || r === '$sp' || r === '$29';
|
|
55
55
|
// SSA-variable name for the stack slot at a constant `sp`-offset. Distinct namespace from the
|
|
56
56
|
// register names (which are alphabetic / `$N`), so it never collides with a real register var.
|
|
57
|
-
const stackSlot =
|
|
57
|
+
const stackSlot = stackSlotKey; // shared spelling: frontend/ssa.ts
|
|
58
58
|
// Sub-word memory mnemonics (widths 1 and 2). Used by the `spSlotSafe` guard in `lift`: a sub-word
|
|
59
59
|
// `sp`-relative access means the word stack-slot model is unsafe for that function.
|
|
60
60
|
const SUBWORD_MEM = new Set(['lb', 'lbu', 'lh', 'lhu', 'sb', 'sh']);
|
|
@@ -866,8 +866,8 @@ export function lift(
|
|
|
866
866
|
}
|
|
867
867
|
};
|
|
868
868
|
// TRUSTWORTHINESS GUARD (mirrors the PPC frontend): an unmodelled instruction must not silently
|
|
869
|
-
// drop its destination register — emit an honest `opaque
|
|
870
|
-
//
|
|
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).
|
|
871
871
|
const emitOpaqueDest = (ins: Instr) => {
|
|
872
872
|
// A `%hi`/`%lo` operand on an instruction NOT modelled as a global consumer — an FP load/store
|
|
873
873
|
// (`lwc1`/`ldc1`), or any unmodelled op — reaches here (the modelled consumers handle their own
|
|
@@ -889,8 +889,8 @@ export function lift(
|
|
|
889
889
|
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
890
890
|
isReg: isMipsReg,
|
|
891
891
|
isZero,
|
|
892
|
-
storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)
|
|
893
|
-
skipSafe: /^(nop|ssnop|break)
|
|
892
|
+
storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)$/i,
|
|
893
|
+
skipSafe: /^(nop|ssnop|break)$/i,
|
|
894
894
|
context: `${name} @0x${ins.addr.toString(16)}`,
|
|
895
895
|
});
|
|
896
896
|
if (!od) {
|
|
@@ -938,6 +938,13 @@ export function lift(
|
|
|
938
938
|
// incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local — neither modelled.
|
|
939
939
|
// Without this, readVar would FABRICATE a phantom entry parameter for the slot, silently emitting a
|
|
940
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.
|
|
941
948
|
if (!ssa.hasReachingDef(stackSlot(off), bi)) {
|
|
942
949
|
throw new FrontendUnsupportedError(
|
|
943
950
|
`cannot lift '${name}': load from stack slot sp@${off} that was never stored ` +
|
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),
|
|
@@ -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
|