@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/backend/cpp.ts
CHANGED
|
@@ -38,6 +38,7 @@ export function cppSymbol(spec: CppFnSpec): string {
|
|
|
38
38
|
export function cppBackend(spec: CppFnSpec): LanguageBackend {
|
|
39
39
|
return {
|
|
40
40
|
id: 'cpp',
|
|
41
|
+
spellsSwitchFallthrough: true,
|
|
41
42
|
emit(fn: SFn): string {
|
|
42
43
|
// Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
|
|
43
44
|
// (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.
|
package/src/backend/pascal.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// Turbo/Delphi/FreePascal.
|
|
11
11
|
import { IrType, typeToString } from '../ir/types';
|
|
12
12
|
import { BinOp, Expr, LanguageBackend, SFn, Stmt } from '../l3/ast';
|
|
13
|
-
import {
|
|
13
|
+
import { orderSlotLocals } from '../l3/slotorder';
|
|
14
|
+
import { type VarTypes, declaredTypes, derefStrideOk, exprCType, writesNonPointerIntoPointer } from '../l3/typing';
|
|
14
15
|
|
|
15
16
|
// Infix operators IDO Pascal spells directly.
|
|
16
17
|
const OP: Partial<Record<BinOp, string>> = {
|
|
@@ -18,6 +19,10 @@ const OP: Partial<Record<BinOp, string>> = {
|
|
|
18
19
|
// match C's truncated `%` (sign of the DIVIDEND) — verified: `a mod 3` mis-scores against the
|
|
19
20
|
// IDO C `a % 3` codegen. There is no faithful IDO-Pascal spelling of a signed C remainder, so the
|
|
20
21
|
// backend fails LOUD on `%` (below) rather than emit a silently-wrong `mod`. `/`→`div` DOES match.
|
|
22
|
+
//
|
|
23
|
+
// And no `/u`/`%u` either, for the same reason `>>>` is absent from BIT_FN below: `div` over this
|
|
24
|
+
// backend's signed `Integer` is the SIGNED division, so lending it to the unsigned twin would
|
|
25
|
+
// emit `div` where the machine did `divu`. They reach the loud decline instead.
|
|
21
26
|
'+': '+',
|
|
22
27
|
'-': '-',
|
|
23
28
|
'*': '*',
|
|
@@ -95,7 +100,7 @@ function makePrinter(vt: VarTypes) {
|
|
|
95
100
|
throw new Error(`pascal backend: a multidimensional array access has no IDO Pascal spelling yet`);
|
|
96
101
|
}
|
|
97
102
|
const bt = exprCType(e.base, vt);
|
|
98
|
-
if ((bt !== undefined && !derefStrideOk(bt, e.width)) || (bt === undefined && e.width !== 4)) {
|
|
103
|
+
if ((bt !== undefined && !derefStrideOk(bt, e.width, e.signed)) || (bt === undefined && e.width !== 4)) {
|
|
99
104
|
throw new Error(
|
|
100
105
|
`pascal backend: a ${e.width}-byte access through a base of type '${bt ? typeToString(bt) : '<unknowable>'}' has no faithful spelling (no reinterpret cast)`,
|
|
101
106
|
);
|
|
@@ -111,7 +116,10 @@ function makePrinter(vt: VarTypes) {
|
|
|
111
116
|
// Casts have no faithful IDO-Pascal spelling yet — fail LOUD rather than emit silently-wrong
|
|
112
117
|
// source. Tree-level producers reaching here: the width-narrowing idiom casts (agbcc-gated,
|
|
113
118
|
// so never on this path today), structure.ts's STRUCT-pointer casts (unreachable too — the
|
|
114
|
-
// `field` case above throws first),
|
|
119
|
+
// `field` case above throws first), intify's `(s32)ptr` legalization (any target), and the
|
|
120
|
+
// byte-pointer walk of a pointer offset by a runtime value (any target, and reachable with no
|
|
121
|
+
// `field` in the tree — it declines two functions that used to emit here, whose Pascal was
|
|
122
|
+
// silently walking ELEMENTS where the asm walked bytes).
|
|
115
123
|
// Scalar deref casts never appear in the tree — the index case above owns that judgment.
|
|
116
124
|
case 'cast':
|
|
117
125
|
throw new Error(`pascal backend: cast has no IDO Pascal spelling yet`);
|
|
@@ -148,15 +156,13 @@ function makePrinter(vt: VarTypes) {
|
|
|
148
156
|
stmts.flatMap((x, i) => ps(fnName, x, ind, tl && i === stmts.length - 1));
|
|
149
157
|
switch (s.k) {
|
|
150
158
|
case 'assign': {
|
|
151
|
-
// The write-side sibling of the index case's deref discipline
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const ct = exprCType(s.value, vt);
|
|
157
|
-
if (dt?.kind === 'ptr' && ct && ct.kind !== 'ptr' && ct.kind !== 'array') {
|
|
159
|
+
// The write-side sibling of the index case's deref discipline. The C family answers the
|
|
160
|
+
// same question (l3/typing.ts writesNonPointerIntoPointer) with a reinterpret cast;
|
|
161
|
+
// Pascal has none, so it declines LOUD here instead of failing three stages later in upas.
|
|
162
|
+
if (writesNonPointerIntoPointer(vt(s.name), s.value, vt)) {
|
|
163
|
+
const ct = exprCType(s.value, vt);
|
|
158
164
|
throw new Error(
|
|
159
|
-
`pascal backend: assigning a ${typeToString(ct)} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
|
|
165
|
+
`pascal backend: assigning a ${ct ? typeToString(ct) : '<unknowable>'} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
|
|
160
166
|
);
|
|
161
167
|
}
|
|
162
168
|
return [`${indent}${s.name} := ${pe(s.value)};`];
|
|
@@ -263,7 +269,15 @@ function makePrinter(vt: VarTypes) {
|
|
|
263
269
|
|
|
264
270
|
export const pascalBackend: LanguageBackend = {
|
|
265
271
|
id: 'pascal',
|
|
266
|
-
|
|
272
|
+
// `case-of` has no fall-through, and this file's `switch` printing loud-fails a `fallsThrough`
|
|
273
|
+
// arm. Declared so RECOVERY never mints one for this backend: a comparison-tree switch also
|
|
274
|
+
// spells as plain if-nesting, which Pascal prints, so the choice is between a decompiled
|
|
275
|
+
// function and a stub.
|
|
276
|
+
spellsSwitchFallthrough: false,
|
|
277
|
+
emit(fn0: SFn): string {
|
|
278
|
+
// The declaration list is put into the target's own frame order HERE, as the C family does it
|
|
279
|
+
// in its shared assembler — owned by `emit`, never by a `.emit(` call site (l3/slotorder.ts).
|
|
280
|
+
const fn = orderSlotLocals(fn0);
|
|
267
281
|
// Same env discipline as the C family (cfamily.ts cFamilyBody): the printer judges derefs
|
|
268
282
|
// against the exact declarations it emits.
|
|
269
283
|
const ps = makePrinter(declaredTypes(fn));
|
package/src/contracts.ts
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
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 Fn, type Value, reachableBlocks } 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 {
|
|
9
|
+
import {
|
|
10
|
+
exprChildren,
|
|
11
|
+
fieldSpellsDot,
|
|
12
|
+
gapReasonFor,
|
|
13
|
+
mapExprChildren,
|
|
14
|
+
stmtChildren,
|
|
15
|
+
stmtExprs,
|
|
16
|
+
stmtLists,
|
|
17
|
+
walkExprs,
|
|
18
|
+
} from './l3/ast';
|
|
10
19
|
import { declaredTypes, exprCType } from './l3/typing';
|
|
11
20
|
|
|
12
21
|
export class ContractError extends Error {
|
|
@@ -40,24 +49,338 @@ export function assertTypesRecovered(fn: Fn): void {
|
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
/** Post structuring: the AST must reference no unresolved
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
52
|
+
/** Post structuring: the AST must reference no unresolved name. The structurer emits the sentinel
|
|
53
|
+
* var `"?"` when it cannot resolve a value (a dropped def, or an opcode it has no lowering for),
|
|
54
|
+
* which would print as uncompilable source. Fail at the boundary instead of emitting garbage.
|
|
55
|
+
*
|
|
56
|
+
* `undefined` is the same failure from the other side — not a spelling the structurer chooses
|
|
57
|
+
* (`Expr` declares `name: string`) but a `varName.get(v)!` whose value was never adopted, printing
|
|
58
|
+
* as the token `undefined`. Both are checked on every ROUTE a name takes into the AST, and those
|
|
59
|
+
* are not all expressions: `var` / `addr` / `field` / `call` carry one, and so does an `assign`'s
|
|
60
|
+
* DESTINATION — a bare string field the expression walk never reaches. */
|
|
47
61
|
export function assertResolved(sfn: SFn): void {
|
|
48
62
|
// Derived from the shared exprChildren/stmtExprs/stmtChildren traversal so no statement kind
|
|
49
63
|
// can be missed. A gap `marker` is annotate-mode's DESIGNED spelling of an unresolved value
|
|
50
|
-
// ("resolved" by construction); only its args could still hide a stray
|
|
64
|
+
// ("resolved" by construction); only its args could still hide a stray name — and args are
|
|
51
65
|
// exactly its children.
|
|
52
|
-
const
|
|
53
|
-
|
|
66
|
+
const badName = (n: string | undefined): boolean => n === '?' || n === undefined;
|
|
67
|
+
// Every Expr kind that CARRIES a name, not just `var` — each is read through the same
|
|
68
|
+
// `map.get(d)!` / `attrs.x as string` and prints straight into the source. `carriesName` is asked
|
|
69
|
+
// separately because an ABSENT name is the case being caught: keying off `nameOf` alone refuses nothing.
|
|
70
|
+
const carriesName = (e: Expr): boolean => e.k === 'var' || e.k === 'addr' || e.k === 'field' || e.k === 'call';
|
|
71
|
+
const nameOf = (e: Expr): string | undefined =>
|
|
72
|
+
e.k === 'call' ? e.fn : e.k === 'marker' ? undefined : (e as { name?: string }).name;
|
|
73
|
+
const badExpr = (e: Expr): boolean => (carriesName(e) && badName(nameOf(e))) || exprChildren(e).some(badExpr);
|
|
74
|
+
// An `assign`'s DESTINATION is a bare string field, so the expression walk never reaches it.
|
|
75
|
+
const badStmt = (s: Stmt): boolean =>
|
|
76
|
+
(s.k === 'assign' && badName(s.name)) || stmtExprs(s).some(badExpr) || stmtChildren(s).some(badStmt);
|
|
54
77
|
if (sfn.body.some(badStmt)) {
|
|
55
78
|
throw new ContractError(
|
|
56
|
-
`structuring left an unresolved
|
|
79
|
+
`structuring left an unresolved name ('?' or one never adopted) in '${sfn.name}' — ` +
|
|
80
|
+
`a dropped def, an unlowered opcode, or a name the structurer assumed the naming pipeline gave it`,
|
|
57
81
|
);
|
|
58
82
|
}
|
|
59
83
|
}
|
|
60
84
|
|
|
85
|
+
// ── effects: executed once, never dropped ──────────────────────────────────────────────────
|
|
86
|
+
//
|
|
87
|
+
// The three contracts around this one are about TYPING and SPELLABILITY. Nothing checked the
|
|
88
|
+
// property the structurer's materialization model exists to preserve: a call in the asm must run
|
|
89
|
+
// exactly as often in the emitted source. Its two failure modes are the two that hurt most —
|
|
90
|
+
// asmlift's first rule is that a loud failure beats a silently wrong answer, and both of these are
|
|
91
|
+
// silent:
|
|
92
|
+
//
|
|
93
|
+
// • DROPPED — a call the asm makes has no counterpart in the tree at all;
|
|
94
|
+
// • RE-RUN — inlining a call's value at more than one render position (or a structuring copy
|
|
95
|
+
// that duplicates a region onto a single path) makes one call execute twice. The round that
|
|
96
|
+
// recovered switch fall-through hit exactly this shape, and only an adversarial reviewer
|
|
97
|
+
// caught it.
|
|
98
|
+
//
|
|
99
|
+
// Deliberately narrow, so it never declines a function that is fine:
|
|
100
|
+
//
|
|
101
|
+
// • CALLS only. Loads legitimately re-render (that is the whole point of the inline-at-use
|
|
102
|
+
// model, and the alias gate governs it); stores are checked by neither direction here because
|
|
103
|
+
// the readability DCE pass is allowed to drop a provably dead one.
|
|
104
|
+
// • PER PATH, not per tree. Structuring may legitimately emit one block twice — two exclusive
|
|
105
|
+
// switch arms sharing a body, a duplicated return merge — and each path still executes it
|
|
106
|
+
// once. So the duplication rule compares the maximum over syntactic root-to-leaf paths (a
|
|
107
|
+
// branch takes the max of its arms, a loop body counts once, a fall-through arm chains into
|
|
108
|
+
// the next) against the IR's static count.
|
|
109
|
+
// • Names the IR does not have are ignored, and only calls carrying a target symbol are counted
|
|
110
|
+
// (every frontend that emits `call` today stamps one).
|
|
111
|
+
type CallCounts = Map<string, number>;
|
|
112
|
+
|
|
113
|
+
/** per-key combine of two count maps (`sum` for sequence, `max` for exclusive alternatives) */
|
|
114
|
+
function combine(a: CallCounts, b: CallCounts, f: (x: number, y: number) => number): CallCounts {
|
|
115
|
+
const out = new Map(a);
|
|
116
|
+
for (const [k, v] of b) {
|
|
117
|
+
out.set(k, f(out.get(k) ?? 0, v));
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** every `call` expression under `e`, counted by target name */
|
|
123
|
+
function callsInExpr(e: Expr, into: CallCounts): void {
|
|
124
|
+
if (e.k === 'call') {
|
|
125
|
+
into.set(e.fn, (into.get(e.fn) ?? 0) + 1);
|
|
126
|
+
}
|
|
127
|
+
exprChildren(e).forEach((c) => callsInExpr(c, into));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `total` = every occurrence in the tree; `path` = the most any single syntactic path executes */
|
|
131
|
+
function countCalls(stmts: Stmt[]): { total: CallCounts; path: CallCounts } {
|
|
132
|
+
let total: CallCounts = new Map();
|
|
133
|
+
let path: CallCounts = new Map();
|
|
134
|
+
const add = (r: { total: CallCounts; path: CallCounts }, pathF: (x: number, y: number) => number) => {
|
|
135
|
+
total = combine(total, r.total, (x, y) => x + y);
|
|
136
|
+
path = combine(path, r.path, pathF);
|
|
137
|
+
};
|
|
138
|
+
for (const s of stmts) {
|
|
139
|
+
const own: CallCounts = new Map();
|
|
140
|
+
stmtExprs(s).forEach((e) => callsInExpr(e, own));
|
|
141
|
+
add({ total: own, path: own }, (x, y) => x + y);
|
|
142
|
+
if (s.k === 'if') {
|
|
143
|
+
const t = countCalls(s.then);
|
|
144
|
+
const e = countCalls(s.else);
|
|
145
|
+
// exclusive arms: the path count is whichever arm runs, the total counts both
|
|
146
|
+
add(
|
|
147
|
+
{ total: combine(t.total, e.total, (x, y) => x + y), path: combine(t.path, e.path, Math.max) },
|
|
148
|
+
(x, y) => x + y,
|
|
149
|
+
);
|
|
150
|
+
} else if (s.k === 'switch') {
|
|
151
|
+
const arms = s.cases.map((c) => countCalls(c.body));
|
|
152
|
+
const dflt = countCalls(s.default ?? []);
|
|
153
|
+
// A fall-through arm continues into the NEXT one emitted (the last into `default`), so a
|
|
154
|
+
// path through arm i runs the chain starting at i — the shape the fall-through round's
|
|
155
|
+
// CRITICAL took. Built from the end; `chain[i]` is that arm's per-path count.
|
|
156
|
+
const chain: CallCounts[] = new Array(arms.length);
|
|
157
|
+
for (let i = arms.length - 1; i >= 0; i--) {
|
|
158
|
+
const next = i + 1 < arms.length ? chain[i + 1] : dflt.path;
|
|
159
|
+
chain[i] = s.cases[i].fallsThrough ? combine(arms[i].path, next, (x, y) => x + y) : arms[i].path;
|
|
160
|
+
}
|
|
161
|
+
const armTotal = arms.reduce((acc, a) => combine(acc, a.total, (x, y) => x + y), dflt.total);
|
|
162
|
+
const armPath = chain.reduce((acc, c) => combine(acc, c, Math.max), dflt.path);
|
|
163
|
+
add({ total: armTotal, path: armPath }, (x, y) => x + y);
|
|
164
|
+
} else {
|
|
165
|
+
// Sequenced children (a loop body, a `for`'s init/inc): counted ONCE — a loop's dynamic trip
|
|
166
|
+
// count is not a syntactic occurrence, and the IR side is static too.
|
|
167
|
+
for (const c of stmtChildren(s)) {
|
|
168
|
+
add(countCalls([c]), (x, y) => x + y);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { total, path };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Post structuring: every call the asm makes is emitted, and none is emitted more times than the
|
|
177
|
+
* asm makes it on any one path. See the note above for what this deliberately does not cover.
|
|
178
|
+
*/
|
|
179
|
+
export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
180
|
+
// Reachable blocks only: an unreachable block's call is legitimately never emitted.
|
|
181
|
+
const seen = reachableBlocks(fn);
|
|
182
|
+
const irCalls: CallCounts = new Map();
|
|
183
|
+
// Unmodelled instructions, by the mnemonic the frontend stamped. Same "never dropped" property as
|
|
184
|
+
// a call, and it needs its own tally because an `opaque` carries no `target`.
|
|
185
|
+
const irOpaques = new Set<string>();
|
|
186
|
+
for (const b of seen) {
|
|
187
|
+
for (const op of b.ops) {
|
|
188
|
+
if (op.opcode === 'call' && typeof op.attrs.target === 'string') {
|
|
189
|
+
const t = op.attrs.target;
|
|
190
|
+
irCalls.set(t, (irCalls.get(t) ?? 0) + 1);
|
|
191
|
+
} else if (op.opcode === 'opaque') {
|
|
192
|
+
irOpaques.add(gapReasonFor(op.attrs.mnemonic));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// DROPPED only, not the RE-RUN half: a gap rendered twice is a diagnostic printed twice, which
|
|
197
|
+
// costs nothing because nothing recompiles it, and structuring legitimately duplicates a shared
|
|
198
|
+
// arm — so a per-path count here would fire on correct output.
|
|
199
|
+
//
|
|
200
|
+
// Bites only in ANNOTATE mode (under `strict` the gap is the `?` sentinel and structure() has
|
|
201
|
+
// already thrown), which is where it is needed: that is the CLI and benchmark default, and the
|
|
202
|
+
// only mode with no other backstop against a silently dropped opaque.
|
|
203
|
+
if (irOpaques.size) {
|
|
204
|
+
const emitted = new Set<string>();
|
|
205
|
+
for (const e of walkExprs(sfn.body)) {
|
|
206
|
+
if (e.k === 'marker') {
|
|
207
|
+
emitted.add(e.reason);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const reason of irOpaques) {
|
|
211
|
+
if (!emitted.has(reason)) {
|
|
212
|
+
throw new ContractError(
|
|
213
|
+
`structuring dropped the ${reason} in '${sfn.name}' — an instruction asmlift could not model left no trace`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!irCalls.size) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const { total, path } = countCalls(sfn.body);
|
|
222
|
+
for (const [name, n] of irCalls) {
|
|
223
|
+
if (!(total.get(name) ?? 0)) {
|
|
224
|
+
throw new ContractError(`structuring dropped the call to '${name}' in '${sfn.name}' — its effect is lost`);
|
|
225
|
+
}
|
|
226
|
+
const p = path.get(name) ?? 0;
|
|
227
|
+
if (p > n) {
|
|
228
|
+
throw new ContractError(
|
|
229
|
+
`structuring emitted ${p} calls to '${name}' on one path in '${sfn.name}', where the asm makes ${n}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Post structuring: a local the body READS must be WRITTEN somewhere in it. A materialized value
|
|
236
|
+
* renders as one `v = …` statement at its def's position while every use reads the bare name, so
|
|
237
|
+
* any pass that DISCARDS the statement's position — a collapsed switch test block, a suppressed
|
|
238
|
+
* edge copy — leaves the reads standing over whatever the register allocator left behind. That is
|
|
239
|
+
* the one wrongness the byte differ rewards rather than catches: the candidate compiles, scores,
|
|
240
|
+
* and can win.
|
|
241
|
+
*
|
|
242
|
+
* PRESENCE, not reaching definitions. The stronger question needs path sensitivity through
|
|
243
|
+
* `switch` fall-through, `do-while` and `break`, where a false positive DECLINES a function that
|
|
244
|
+
* is fine; assigned nowhere at all needs none of that and has no legitimate producer. Two local
|
|
245
|
+
* kinds are exempt and both say so in their declaration: an `uninit` local stands on an `undef`,
|
|
246
|
+
* where the missing assignment IS the recovery, and a `frame` local is the machine's own slot,
|
|
247
|
+
* whose store the readability passes between here and L3 may have dropped. */
|
|
248
|
+
export function assertLocalsWritten(sfn: SFn): void {
|
|
249
|
+
const suspect = new Set(sfn.locals.filter((l) => !l.frame && !l.uninit).map((l) => l.name));
|
|
250
|
+
if (!suspect.size) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const read = new Set<string>();
|
|
254
|
+
const written = new Set<string>();
|
|
255
|
+
// `&v` is a write channel this walk cannot follow — the callee/store behind it may fill the
|
|
256
|
+
// object — so it counts as one.
|
|
257
|
+
const walkExpr = (e: Expr): void => {
|
|
258
|
+
if ((e.k === 'var' || e.k === 'addr') && suspect.has(e.name)) {
|
|
259
|
+
(e.k === 'addr' ? written : read).add(e.name);
|
|
260
|
+
}
|
|
261
|
+
exprChildren(e).forEach(walkExpr);
|
|
262
|
+
};
|
|
263
|
+
const walkStmt = (st: Stmt): void => {
|
|
264
|
+
if (st.k === 'assign' && suspect.has(st.name)) {
|
|
265
|
+
written.add(st.name);
|
|
266
|
+
}
|
|
267
|
+
stmtExprs(st).forEach(walkExpr);
|
|
268
|
+
stmtChildren(st).forEach(walkStmt);
|
|
269
|
+
};
|
|
270
|
+
sfn.body.forEach(walkStmt);
|
|
271
|
+
const orphans = [...read].filter((n) => !written.has(n));
|
|
272
|
+
if (orphans.length) {
|
|
273
|
+
throw new ContractError(
|
|
274
|
+
`structuring emitted local(s) ${orphans.map((n) => `'${n}'`).join(', ')} in '${sfn.name}' read but ` +
|
|
275
|
+
`never assigned — a def whose assignment no render position emitted`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Post-lever: every read of a MINTED local — its ADDRESS being taken included — must sit where
|
|
281
|
+
* that local's assignment has already run.
|
|
282
|
+
*
|
|
283
|
+
* THE failure a placing lever can ship, and the only one the byte differ rewards: a base local whose
|
|
284
|
+
* assignment does not reach a use is a DIFFERENT VARIABLE — C that compiles, scores, and can win
|
|
285
|
+
* (the shape #106 shipped). `contracts.ts`'s `assertLocalsWritten` does not see it: it accumulates
|
|
286
|
+
* reads and writes as SETS over the whole body, so a local assigned in one arm and read after the
|
|
287
|
+
* `if` is written somewhere and passes.
|
|
288
|
+
*
|
|
289
|
+
* Checked on the EMITTED tree rather than argued from the plan, because the plan is what a bug
|
|
290
|
+
* would be in. `rank.ts`'s `respell` catches the throw and drops the candidate, so the wrong
|
|
291
|
+
* answer becomes a reported lever error instead of a scored spelling.
|
|
292
|
+
*
|
|
293
|
+
* IT LIVES HERE, beside `assertLocalsWritten`, because it has that check's population and that
|
|
294
|
+
* check's call site: levers that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
|
|
295
|
+
* l3/nearbase.ts, l3/reindex.ts, l3/scopebase.ts, l3/argbase.ts — are the population that can
|
|
296
|
+
* produce the failure, so the check belongs on every lever tree rather than on one lever's.
|
|
297
|
+
*
|
|
298
|
+
* Called ABSOLUTELY by the placing levers that put an init inside a nested list, each over its own
|
|
299
|
+
* plan; everywhere else it is reached through `assertPlacementSurvives` below, which is a
|
|
300
|
+
* DIFFERENTIAL — so a placement no lever's tree ever satisfied is not judged, and a lever that
|
|
301
|
+
* mints nothing is not judged at all.
|
|
302
|
+
*
|
|
303
|
+
* A nested list gets a COPY of the reaching set, so an assignment inside one arm does not count as
|
|
304
|
+
* reaching anything after the `if`. */
|
|
305
|
+
export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): void {
|
|
306
|
+
if (minted.size === 0) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const readUndominated = (e: Expr, live: ReadonlySet<string>): string | null => {
|
|
310
|
+
// `&p` COUNTS, the same mention the placing passes query on (l3/hoist.ts): the address is what
|
|
311
|
+
// a callee reads the cell through, so an init has to precede it as surely as it must precede a
|
|
312
|
+
// read.
|
|
313
|
+
if ((e.k === 'var' || e.k === 'addr') && minted.has(e.name) && !live.has(e.name)) {
|
|
314
|
+
return e.name;
|
|
315
|
+
}
|
|
316
|
+
let bad: string | null = null;
|
|
317
|
+
mapExprChildren(e, (c) => {
|
|
318
|
+
bad ??= readUndominated(c, live);
|
|
319
|
+
return c;
|
|
320
|
+
});
|
|
321
|
+
return bad;
|
|
322
|
+
};
|
|
323
|
+
const judge = (heads: readonly Expr[], live: ReadonlySet<string>): void => {
|
|
324
|
+
for (const e of heads) {
|
|
325
|
+
const bad = readUndominated(e, live);
|
|
326
|
+
if (bad) {
|
|
327
|
+
throw new ContractError(
|
|
328
|
+
`'${sfn.name}' reads '${bad}' where its assignment does not reach — ` +
|
|
329
|
+
`a def placed below a use it claims to serve`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const walk = (list: Stmt[], live: Set<string>): void => {
|
|
335
|
+
for (const st of list) {
|
|
336
|
+
// A `for`'s INIT runs once, before the condition, the inc and the body — so its assignment
|
|
337
|
+
// reaches all three, and the loop's own parts are statements with their own nested lists.
|
|
338
|
+
// `l3/reindex.ts` mints an induction variable whose ONLY def is that init, so reading the
|
|
339
|
+
// `for` as one flat head list rejects every counted walk it spells.
|
|
340
|
+
if (st.k === 'for') {
|
|
341
|
+
walk([st.init], live);
|
|
342
|
+
judge(stmtExprs(st), live);
|
|
343
|
+
walk([st.inc], new Set(live));
|
|
344
|
+
walk(st.body, new Set(live));
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
judge(stmtExprs(st), live);
|
|
348
|
+
for (const child of stmtLists(st)) {
|
|
349
|
+
walk(child, new Set(live));
|
|
350
|
+
}
|
|
351
|
+
if (st.k === 'assign' && minted.has(st.name)) {
|
|
352
|
+
live.add(st.name);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
walk(sfn.body, new Set());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The same guarantee across a re-spelling that MOVES statements over a placement another pass
|
|
360
|
+
* already made — `rank.ts`'s statement shapes (`/initfirst`, `/pollguard`, `/pollread`), derived
|
|
361
|
+
* onto every lever tree after the lever placed its defs, and the lever-on-lever compositions in
|
|
362
|
+
* the same file where a def-moving pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
|
|
363
|
+
* `reindexWalks`) runs on a tree a placing lever built. `pollReads` folds a materialized re-read
|
|
364
|
+
* back into a loop condition, which is exactly such a move.
|
|
365
|
+
*
|
|
366
|
+
* A DIFFERENTIAL, which is what makes it safe on every lever: the walk judges the reshaped tree
|
|
367
|
+
* only where it already described the unshaped one, so a placement it cannot model (a def inside
|
|
368
|
+
* a loop body read earlier in the same body is assigned on every iteration but the first) is not
|
|
369
|
+
* judged either way. `minted` may name a local `before` does not carry — a mover mints its own —
|
|
370
|
+
* and that one is judged absolutely, which is the same thing: a name absent from `before` is
|
|
371
|
+
* never read there. */
|
|
372
|
+
export function assertPlacementSurvives(before: SFn, after: SFn, minted: ReadonlySet<string>): void {
|
|
373
|
+
if (minted.size === 0) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
assertHoistsDominate(before, minted);
|
|
378
|
+
} catch {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
assertHoistsDominate(after, minted);
|
|
382
|
+
}
|
|
383
|
+
|
|
61
384
|
/** Post structuring: the AST's memory accesses and operators must be SPELLABLE — a `field`
|
|
62
385
|
* node's base a pointer-to-struct (`->`) or a struct value (`.`, an array element) carrying
|
|
63
386
|
* that field; no pointer operand under an operator C rejects; and every SCALAR `index` node's
|
|
@@ -85,7 +408,7 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
85
408
|
}
|
|
86
409
|
}
|
|
87
410
|
// Ops C rejects outright on a pointer operand (the additive ops and &&/|| are legal C).
|
|
88
|
-
const NO_PTR_OPS = new Set<BinOp>(['&', '|', '^', '<<', '>>', '>>>', '*', '/', '%']);
|
|
411
|
+
const NO_PTR_OPS = new Set<BinOp>(['&', '|', '^', '<<', '>>', '>>>', '*', '/', '/u', '%', '%u']);
|
|
89
412
|
// The comparison operators — where a bare `&SYM` operand is SIGN-ambiguous, not ill-formed.
|
|
90
413
|
const CMP_OPS = new Set(['<', '<=', '>', '>=', '==', '!=']);
|
|
91
414
|
// 1/2/4 only: the decomp typedef vocabulary (C_TYPEDEFS) has no 64-bit scalar, so a width-8
|
|
@@ -95,11 +418,13 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
95
418
|
// Dot-form field bases (struct-array elements) carry the struct STRIDE as their width — any
|
|
96
419
|
// stride matching the element size is legal there (the tree-level struct cast governs the
|
|
97
420
|
// spelling; a stride/size MISMATCH types scalar in exprCType and the field rule flags it).
|
|
98
|
-
// Collected as fields are visited, BEFORE recursing into their children
|
|
99
|
-
//
|
|
100
|
-
//
|
|
421
|
+
// Collected as fields are visited, BEFORE recursing into their children — which is what makes
|
|
422
|
+
// `walkExprs`' PRE-ORDER load-bearing here rather than incidental: the exemption is recorded on
|
|
423
|
+
// the `field` node and read at the `index` node beneath it. Identity-keyed: a future
|
|
424
|
+
// subtree-SHARING pass (CSE-style) would leak the exemption to aliased bare uses — trees are
|
|
425
|
+
// freshly built per node today (structure.ts), which this relies on.
|
|
101
426
|
const structElem = new Set<Expr>();
|
|
102
|
-
const
|
|
427
|
+
for (const e of walkExprs(sfn.body)) {
|
|
103
428
|
if (e.k === 'index' && !structElem.has(e) && !SCALAR_WIDTHS.has(e.width)) {
|
|
104
429
|
bad.push(`index width ${e.width} is not a C scalar width`);
|
|
105
430
|
}
|
|
@@ -159,13 +484,7 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
159
484
|
}
|
|
160
485
|
}
|
|
161
486
|
}
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
const checkStmt = (s: Stmt): void => {
|
|
165
|
-
stmtExprs(s).forEach(checkExpr);
|
|
166
|
-
stmtChildren(s).forEach(checkStmt);
|
|
167
|
-
};
|
|
168
|
-
sfn.body.forEach(checkStmt);
|
|
487
|
+
}
|
|
169
488
|
if (bad.length) {
|
|
170
489
|
throw new ContractError(
|
|
171
490
|
`structuring emitted ill-typed C in '${sfn.name}': ${bad[0]}${bad.length > 1 ? ` (+${bad.length - 1} more)` : ''}`,
|
package/src/declare.ts
CHANGED
|
@@ -29,7 +29,25 @@
|
|
|
29
29
|
// field) can only LOSE score — the target bytes derive from the truth decls, so a
|
|
30
30
|
// divergent compile can never false-match. Exception two is the NAME-ONLY data symbol
|
|
31
31
|
// (`extern u32 name;` — see the default case): required to reproduce symtab-only map
|
|
32
|
-
// rows outside project headers
|
|
32
|
+
// rows outside project headers.
|
|
33
|
+
//
|
|
34
|
+
// WHERE THE ONLY-LOSES-SCORE ARGUMENT STOPS. It rests on the target bytes coming from the
|
|
35
|
+
// project's own TRUTH declarations, so a divergent decl compiles to different bytes and simply
|
|
36
|
+
// scores worse. The line is not map-derived vs. synthesized, it is whether the ref carries
|
|
37
|
+
// `access`: that field is read out of the candidate's own IR — the very asm it is then scored
|
|
38
|
+
// against — so a declaration wearing it is FITTED and can only manufacture agreement. Every
|
|
39
|
+
// `synthesized` ref can wear it, and so can a MAP-KNOWN name whose map entry has no shape
|
|
40
|
+
// (symtab-only projects), which is the one fitted case `synthesized` does not mark. Measured
|
|
41
|
+
// over the 252 real benchmark rows with their vendored maps: fitted-and-marked 2, fitted-but-
|
|
42
|
+
// unmarked 0 — so the marker covers today's population, and a symtab-only project is where it
|
|
43
|
+
// would stop.
|
|
44
|
+
//
|
|
45
|
+
// A fitted declaration is a sound ARTIFACT (decls + source really do compile to those bytes) and
|
|
46
|
+
// an unsound CLAIM if the decls are hidden, so a consumer publishing a verdict must show the
|
|
47
|
+
// block beside the source. Its price, against the benchmark's own vendored maps: of 28 fitted
|
|
48
|
+
// NARROW declarations over the 126 rankable agbcc rows, 26 agree with the project's real
|
|
49
|
+
// declaration and 2 do not (27 of 28 agree on the offset-0 ACCESS WIDTH the declaration
|
|
50
|
+
// produces, which is the weaker question of whether the same load is emitted).
|
|
33
51
|
import { type StructFieldDecl, renderStructDecl } from './backend/cfamily';
|
|
34
52
|
import { T } from './ir/types';
|
|
35
53
|
import type { SymbolRef } from './l3/symbol-refs';
|
|
@@ -42,6 +60,7 @@ import {
|
|
|
42
60
|
pointeeFields,
|
|
43
61
|
symbolFieldType,
|
|
44
62
|
} from './symbols';
|
|
63
|
+
import { C_TYPEDEFS } from './target';
|
|
45
64
|
|
|
46
65
|
/** The u8/s8/u16/s16/u32/s32 spelling for a 1/2/4-byte cell, or null (no faithful narrow type). */
|
|
47
66
|
function intType(size: number, signed: boolean): string | null {
|
|
@@ -223,9 +242,11 @@ export function renderDeclarations(refs: SymbolRef[]): string {
|
|
|
223
242
|
// tree performed only under a decl of that exact width (`extern u16 g;` is `sh` where
|
|
224
243
|
// a guessed u32 is `sw`). Without a bare off-0 access fact, every core spelling goes
|
|
225
244
|
// through `&name` casts, where any object decl is address-identical — u32 is the
|
|
226
|
-
// fallback cell.
|
|
227
|
-
//
|
|
228
|
-
//
|
|
245
|
+
// fallback cell.
|
|
246
|
+
// For a MAP-derived name-only symbol (symtab-only projects) the only-loses-score
|
|
247
|
+
// argument applies. For a `synthesized` one it does not — the width came from the
|
|
248
|
+
// target's own asm, so this line is a hypothesis fitted to the bytes; see the module
|
|
249
|
+
// note's "WHERE THE ONLY-LOSES-SCORE ARGUMENT STOPS".
|
|
229
250
|
const t = access ? intType(access.width, access.signed) : null;
|
|
230
251
|
lines.push(`extern ${quals(info)}${t ?? 'u32'} ${name};`);
|
|
231
252
|
break;
|
|
@@ -249,3 +270,19 @@ export function macroDefinesOf(declarations: string | undefined): string {
|
|
|
249
270
|
const lines = declarations.split('\n').filter((l) => l.startsWith('#define '));
|
|
250
271
|
return lines.length ? lines.join('\n') + '\n' : '';
|
|
251
272
|
}
|
|
273
|
+
|
|
274
|
+
/** THE self-declared world's compilation context: asmlift's typedef prelude followed by this
|
|
275
|
+
* candidate's declaration block. One composition with two callers — the cli's compile seam
|
|
276
|
+
* (compile-command.ts, whose probe decides whether the world is self-declared at all) and the
|
|
277
|
+
* webapp's wasm scorer, which is ALWAYS in it — because two hand-rolled copies of
|
|
278
|
+
* `C_TYPEDEFS + decls` is exactly how the two scoring worlds come to disagree about what a
|
|
279
|
+
* candidate was compiled in. */
|
|
280
|
+
export function selfDeclaredContext(declarations: string | undefined): string {
|
|
281
|
+
return C_TYPEDEFS + (declarations ?? '');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The same context straight from a candidate's refs — what a scorer holding `Candidate`s (the
|
|
285
|
+
* webapp) needs, and the one place that decides an empty ref list renders no block at all. */
|
|
286
|
+
export function selfDeclaredContextFor(refs: SymbolRef[] | undefined): string {
|
|
287
|
+
return selfDeclaredContext(refs?.length ? renderDeclarations(refs) : undefined);
|
|
288
|
+
}
|
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']);
|
|
@@ -517,6 +517,17 @@ export function lift(
|
|
|
517
517
|
}
|
|
518
518
|
});
|
|
519
519
|
|
|
520
|
+
// NO FRAME PARTITION IS CLAIMED, so every def-less slot read refuses (frontend/ssa.ts,
|
|
521
|
+
// LiveInModel). This frontend has no frame bound at all — `addiu sp,sp,±N` is transparent and
|
|
522
|
+
// every word sp-relative access becomes `sp@<rawOff>` — so its slot keys span O32's CALLER-owned
|
|
523
|
+
// register-parameter home area and the incoming stack arguments above it, where a def-less read
|
|
524
|
+
// is argument 5, not an uninitialised local.
|
|
525
|
+
//
|
|
526
|
+
// Claiming one needs the frame SIZE those offsets are measured against, which is not computed
|
|
527
|
+
// here, plus ensureParam for the register half. The ranges themselves are known: O32 reserves
|
|
528
|
+
// `[0,16)` as the caller-owned home area (in NEITHER range — caller-owned, but not an argument)
|
|
529
|
+
// with stack arguments from 16 up, which is what `mips32be.cspec`'s `<localrange>` and stack
|
|
530
|
+
// `<pentry offset="16">` encode.
|
|
520
531
|
const ssa = makeSsaBuilder(name, blocks.length, preds);
|
|
521
532
|
const { irBlocks, readVar, writeVar, paramReg } = ssa;
|
|
522
533
|
const RET = target.returnReg;
|
|
@@ -866,8 +877,8 @@ export function lift(
|
|
|
866
877
|
}
|
|
867
878
|
};
|
|
868
879
|
// TRUSTWORTHINESS GUARD (mirrors the PPC frontend): an unmodelled instruction must not silently
|
|
869
|
-
// drop its destination register — emit an honest `opaque
|
|
870
|
-
//
|
|
880
|
+
// drop its destination register — emit an honest `opaque`, which fails LOUD at assertResolved
|
|
881
|
+
// whether or not anything reads that register (see frontend/opaque.ts for the policy).
|
|
871
882
|
const emitOpaqueDest = (ins: Instr) => {
|
|
872
883
|
// A `%hi`/`%lo` operand on an instruction NOT modelled as a global consumer — an FP load/store
|
|
873
884
|
// (`lwc1`/`ldc1`), or any unmodelled op — reaches here (the modelled consumers handle their own
|
|
@@ -889,8 +900,8 @@ export function lift(
|
|
|
889
900
|
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
890
901
|
isReg: isMipsReg,
|
|
891
902
|
isZero,
|
|
892
|
-
storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)
|
|
893
|
-
skipSafe: /^(nop|ssnop|break)
|
|
903
|
+
storeClass: /^(sb|sh|sw|swl|swr|sc|sd|sdl|sdr|swc1|sdc1)$/i,
|
|
904
|
+
skipSafe: /^(nop|ssnop|break)$/i,
|
|
894
905
|
context: `${name} @0x${ins.addr.toString(16)}`,
|
|
895
906
|
});
|
|
896
907
|
if (!od) {
|
|
@@ -938,6 +949,13 @@ export function lift(
|
|
|
938
949
|
// incoming STACK-PASSED argument (5th+ param, O32) or an uninitialised local — neither modelled.
|
|
939
950
|
// Without this, readVar would FABRICATE a phantom entry parameter for the slot, silently emitting a
|
|
940
951
|
// function of wrong arity that returns the wrong argument. Loud-fail instead of miscompiling.
|
|
952
|
+
//
|
|
953
|
+
// Those two cases are SEPARABLE, and the Thumb frontend now separates them (frontend/thumb.ts,
|
|
954
|
+
// incomingArgIndex): a slot at or above the callee's own frame cannot have been written by this
|
|
955
|
+
// function, so it is an incoming argument; below the frame top it is a local. Doing the same here
|
|
956
|
+
// needs O32's own frame rule — the 16-byte home area means a stack argument is NOT simply "above
|
|
957
|
+
// the frame", so the Thumb arithmetic does not carry over — plus ssa.ensureParam for the register
|
|
958
|
+
// half. Until then this stays one loud decline for both.
|
|
941
959
|
if (!ssa.hasReachingDef(stackSlot(off), bi)) {
|
|
942
960
|
throw new FrontendUnsupportedError(
|
|
943
961
|
`cannot lift '${name}': load from stack slot sp@${off} that was never stored ` +
|