@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/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Input is **text**, following what each target's toolchain produces:
|
|
|
38
38
|
| Option | Meaning |
|
|
39
39
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
40
|
| `backend` | `cBackend` (default) or `pascalBackend` — values from `@asmlift/core/backend/*`. C++ is `cppBackend(spec)`, a per-function factory: it takes a `CppFnSpec` (class/method name, explicit param types, class field layouts — what a project's headers supply) and covers free and non-virtual member functions with word-sized fields; virtual dispatch, references, ctors/dtors decline |
|
|
41
|
-
| `patterns` | Idiom rewrite patterns. Omitted = `DEFAULT_IDIOM_PATTERNS` (
|
|
41
|
+
| `patterns` | Idiom rewrite patterns. Omitted = `DEFAULT_IDIOM_PATTERNS` (self-selects per target: most are compiler-gated, the boolean-negation folds are universal); `[]` = none |
|
|
42
42
|
| `prototypes` | Callee arities + void-ness, as a real project takes them from headers — drives call-argument recovery |
|
|
43
43
|
| `asmData` | Optional `objdump -s -r -t` side-table; required to recover MIPS/PPC jump-table switches |
|
|
44
44
|
| `onGap` | `"strict"` (default): throw on any gap. `"annotate"`: emit best-effort source with `ASMLIFT_ERROR` markers; every gap is also returned in the structured `diagnostics` array (empty ⇔ gap-free) |
|
|
@@ -86,7 +86,7 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
|
|
|
86
86
|
| `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
|
|
87
87
|
| `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list (`pre-recovery.ts`): const materialize → magic division (`magicdiv.ts`, Hacker's Delight inverse) → soft division → array legalize → struct-array → struct-pointer → short-circuit; plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `errors.ts` (`RaiseUnsupportedError`) |
|
|
88
88
|
| `structure/*.ts` | L2→L3 in four modules: `loops.ts` (natural-loop discovery), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
|
|
89
|
-
| `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share)
|
|
89
|
+
| `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts` + `argbase.ts` + `scopebase.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
|
|
90
90
|
| `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
|
|
91
91
|
| `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
|
|
92
92
|
| `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
|
|
@@ -104,7 +104,9 @@ Recovered today: straight-line, if/else diamonds, natural loops (`while` / `do-w
|
|
|
104
104
|
properly nested, in-body `break`/early-`return`), comparison-tree and jump-table switches,
|
|
105
105
|
direct calls, constant-offset and variable-index memory (`*p`, `p[n]`, `a[i]`, struct fields),
|
|
106
106
|
magic-number and soft division, short-circuit booleans, width casts. Still DECLINED (loud, never
|
|
107
|
-
wrong code): **local stack frames** (address-taken locals / sp-as-data
|
|
107
|
+
wrong code): **local stack frames** (address-taken locals / sp-as-data; MIPS models word `sp`
|
|
108
|
+
slots and PPC elides callee-saved save slots, so a spill/reload pair is modelled on those two —
|
|
109
|
+
anything the narrow models cannot honour declines),
|
|
108
110
|
**cross-block condition flags** on PPC (a `cmpw` whose branch lands in another block — the
|
|
109
111
|
capability gap behind the mwcc switch stubs), computed tail calls, PIC/`gp`/SDA global access,
|
|
110
112
|
switch fall-through, multi-latch/irreducible loops, floats, and 64-bit memory ops. Prototypes
|
package/package.json
CHANGED
package/src/backend/cfamily.ts
CHANGED
|
@@ -9,6 +9,78 @@ import { IrType, T, scalarTypeForAccess, typeToString } from '../ir/types';
|
|
|
9
9
|
import { BinOp, Expr, SFn, Stmt, dotBase } from '../l3/ast';
|
|
10
10
|
import { type VarTypes, declaredTypes, derefStrideOk, exprCType } from '../l3/typing';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The C SIGNEDNESS a rendered integer expression actually has — `true`/`false`, or `undefined`
|
|
14
|
+
* when it is not determinable here. The deliberate complement to l3/typing's `exprCType`, which is
|
|
15
|
+
* pointer-ness-accurate and reports every integer as `s32` by contract; this models the two C
|
|
16
|
+
* rules that contract omits, integer PROMOTION and the usual arithmetic CONVERSIONS.
|
|
17
|
+
*
|
|
18
|
+
* It lives HERE, in the C-family backend, because it is a model of C's own rules with no meaning
|
|
19
|
+
* for another language — the same reason the cast it feeds is synthesized here rather than in the
|
|
20
|
+
* tower. `exprCType` stays in l3/ because Pascal consults it too.
|
|
21
|
+
*
|
|
22
|
+
* It exists for one question, and the question is byte-load-bearing: C spells both `>>>` and `>>`
|
|
23
|
+
* as `>>` and chooses between them from the left operand's type. A logical shift rendered over a
|
|
24
|
+
* signed expression recompiles to `asr` where the target has `lsr`, and evaluates to a different
|
|
25
|
+
* value. The C-family backend casts the operand whenever this returns anything but the signedness
|
|
26
|
+
* the operator needs, so `undefined` is the safe answer in every case the model does not cover — a
|
|
27
|
+
* redundant cast is codegen-identical, a missing one is a miscompile.
|
|
28
|
+
*
|
|
29
|
+
* Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
|
|
30
|
+
* declared. Pointers, calls and markers are `undefined`.
|
|
31
|
+
*/
|
|
32
|
+
function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | undefined {
|
|
33
|
+
const rec = (x: Expr): boolean | undefined => renderedIntSignedness(x, varType);
|
|
34
|
+
// an lvalue-ish leaf: its C type is a declaration / an explicit cast / a carried access width
|
|
35
|
+
const promoted = (t: IrType | undefined): boolean | undefined =>
|
|
36
|
+
t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 ? t.signed : undefined;
|
|
37
|
+
switch (e.k) {
|
|
38
|
+
case 'var':
|
|
39
|
+
case 'cast':
|
|
40
|
+
case 'index':
|
|
41
|
+
case 'field':
|
|
42
|
+
return promoted(exprCType(e, varType));
|
|
43
|
+
// A decimal literal is `int` when it fits in one; C89 gives a larger one an unsigned type,
|
|
44
|
+
// which is not the same operand — so it is left undetermined rather than assumed. INT_MIN is
|
|
45
|
+
// in that larger class despite fitting: the backend prints it as `-2147483648`, which C lexes
|
|
46
|
+
// as unary minus applied to `2147483648` — a constant too big for `int`, hence unsigned long.
|
|
47
|
+
case 'const':
|
|
48
|
+
return e.value > -2147483648 && e.value <= 2147483647 ? true : undefined;
|
|
49
|
+
// `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`.
|
|
50
|
+
case 'un':
|
|
51
|
+
return e.op === '!' ? true : rec(e.e);
|
|
52
|
+
case 'bin': {
|
|
53
|
+
// Shifts take the type of the LEFT operand alone — the right is promoted independently.
|
|
54
|
+
if (e.op === '<<' || e.op === '>>' || e.op === '>>>') {
|
|
55
|
+
return rec(e.l);
|
|
56
|
+
}
|
|
57
|
+
// Comparisons and the logical connectives yield `int`.
|
|
58
|
+
if (['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// Usual arithmetic conversions over the remaining binary operators: at equal rank, unsigned
|
|
62
|
+
// wins. Either side unknown leaves the result unknown — EXCEPT when the known side is
|
|
63
|
+
// unsigned, which already decides it.
|
|
64
|
+
//
|
|
65
|
+
// That exception is the one place this returns a DEFINITE answer from an unknown operand,
|
|
66
|
+
// and it is sound only because every integer here is rank `int`: at UNEQUAL rank C converts
|
|
67
|
+
// to the wider type first, so `unsigned int & long long` is SIGNED. Core has no 64-bit
|
|
68
|
+
// integer type at all (the decomp typedef vocabulary stops at 32 — see contracts.ts
|
|
69
|
+
// SCALAR_WIDTHS), so the unequal-rank case cannot arise. Adding one would invalidate this.
|
|
70
|
+
const l = rec(e.l);
|
|
71
|
+
const r = rec(e.r);
|
|
72
|
+
if (l === false || r === false) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return l === true && r === true ? true : undefined;
|
|
76
|
+
}
|
|
77
|
+
case 'call':
|
|
78
|
+
case 'marker':
|
|
79
|
+
case 'addr':
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
12
84
|
// C operator precedence (lower binds tighter). Used to emit MINIMAL parentheses. Shared: C++ has
|
|
13
85
|
// the same precedence for these operators.
|
|
14
86
|
const PREC: Record<BinOp, number> = {
|
|
@@ -19,6 +91,7 @@ const PREC: Record<BinOp, number> = {
|
|
|
19
91
|
'-': 4,
|
|
20
92
|
'<<': 5,
|
|
21
93
|
'>>': 5,
|
|
94
|
+
'>>>': 5, // spells as C's `>>` (see printExpr's shift rule) — same precedence
|
|
22
95
|
'<': 6,
|
|
23
96
|
'<=': 6,
|
|
24
97
|
'>': 6,
|
|
@@ -69,6 +142,8 @@ export interface StructFieldDecl {
|
|
|
69
142
|
name: string;
|
|
70
143
|
type: IrType;
|
|
71
144
|
volatile?: boolean;
|
|
145
|
+
/** bitfield width — spells `u32 name : n;` (the map-layout synthesis is the only producer) */
|
|
146
|
+
bits?: number;
|
|
72
147
|
}
|
|
73
148
|
|
|
74
149
|
/** THE struct-declaration spelling — every `struct N { ... };` asmlift prints comes from here,
|
|
@@ -76,7 +151,9 @@ export interface StructFieldDecl {
|
|
|
76
151
|
* drift apart. One line, fields in caller order (the type is self-describing: padding is the
|
|
77
152
|
* caller's discipline, already present as real fields). */
|
|
78
153
|
export function renderStructDecl(name: string, fields: StructFieldDecl[]): string {
|
|
79
|
-
|
|
154
|
+
const one = (f: StructFieldDecl) =>
|
|
155
|
+
`${f.volatile ? 'volatile ' : ''}${cDeclare(f.type, f.name)}${f.bits !== undefined ? ` : ${f.bits}` : ''};`;
|
|
156
|
+
return `struct ${name} { ${fields.map(one).join(' ')} };`;
|
|
80
157
|
}
|
|
81
158
|
|
|
82
159
|
// A LEAF hook lets a C-family backend override how a `var` or `index` node spells WITHOUT
|
|
@@ -103,6 +180,28 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
103
180
|
derefStrideOk(exprCType(ix.base, vt), ix.width)
|
|
104
181
|
? ix.base
|
|
105
182
|
: { k: 'cast', to: T.ptr(scalarTypeForAccess(ix.width, ix.signed)), e: ix.base };
|
|
183
|
+
// C-FAMILY SHIFT LEGALIZATION, the same discipline one operator over. The tower keeps the two
|
|
184
|
+
// right shifts apart (`>>>` logical, `>>` arithmetic); C spells BOTH `>>` and picks between them
|
|
185
|
+
// from the LEFT OPERAND'S TYPE. So the operand must be made to carry the choice, or an `shr_u`
|
|
186
|
+
// recompiles to `asr` where the target has `lsr` AND evaluates differently —
|
|
187
|
+
// `*(u8 *)&g << 30 >> 30` promotes to `int`, so a 2-bit field holding 2 comes out -1.
|
|
188
|
+
//
|
|
189
|
+
// (engine.ts's zext fold covers the same hazard for widths C can NAME, by folding the whole
|
|
190
|
+
// shift pair to a cast op. Every other extract width — every bitfield read — lands here.)
|
|
191
|
+
//
|
|
192
|
+
// The cast is added unless the operand PROVABLY renders with the signedness the op needs:
|
|
193
|
+
// renderedIntSignedness answers `undefined` wherever its model does not reach, and a redundant
|
|
194
|
+
// cast is codegen-identical while a missing one is a miscompile. An existing 32-bit integer cast
|
|
195
|
+
// is REPLACED rather than wrapped — `(u32)(s32)&g` and `(u32)&g` are the same bytes, and the
|
|
196
|
+
// arithmetic rules upstream do emit that inner cast (intifyAddr).
|
|
197
|
+
const shiftOperand = (e0: Extract<Expr, { k: 'bin' }>): Expr => {
|
|
198
|
+
const wantSigned = e0.op === '>>';
|
|
199
|
+
if (renderedIntSignedness(e0.l, vt) === wantSigned) {
|
|
200
|
+
return e0.l;
|
|
201
|
+
}
|
|
202
|
+
const inner = e0.l.k === 'cast' && e0.l.to.kind === 'int' && e0.l.to.width === 32 ? e0.l.e : e0.l;
|
|
203
|
+
return { k: 'cast', to: T.int(32, wantSigned), e: inner };
|
|
204
|
+
};
|
|
106
205
|
switch (e.k) {
|
|
107
206
|
case 'var':
|
|
108
207
|
return e.name;
|
|
@@ -123,6 +222,21 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
123
222
|
// binds tighter than any prefix operator, so a cast/unary/deref base is printed at prec 1
|
|
124
223
|
// and parenthesizes itself: `((u8 *)p)[1]`). The postfix form needs no outer parentheses.
|
|
125
224
|
const base = legalized(e);
|
|
225
|
+
// Leading constant subscripts (a multidimensional array global's bare spelling) keep the
|
|
226
|
+
// postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
|
|
227
|
+
//
|
|
228
|
+
// `lead` implies the base already strides the access width — its only producer registers a
|
|
229
|
+
// matching element type for the global (structure.ts bareArrayLead + noteGlobal). Nothing
|
|
230
|
+
// else enforced that, and the failure would be quiet-ish: legalization would wrap the base,
|
|
231
|
+
// spelling `((u16 *)g)[0][i]`, which subscripts a `u16` twice. Check it rather than assume.
|
|
232
|
+
if (e.lead && e.lead.length > 0) {
|
|
233
|
+
if (base !== e.base) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`c backend: a multidimensional array access needs a base that strides ${e.width} bytes as spelled`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return `${rec(base, 1)}${e.lead.map((l) => `[${l}]`).join('')}[${rec(e.idx, 99)}]`;
|
|
239
|
+
}
|
|
126
240
|
if (e.idx.k === 'const' && e.idx.value === 0) {
|
|
127
241
|
const s = `*${rec(base, 2)}`;
|
|
128
242
|
return parentPrec < 2 ? `(${s})` : s;
|
|
@@ -141,6 +255,13 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
141
255
|
// first (the C++ member-access rewrite).
|
|
142
256
|
const ix = dotBase(e);
|
|
143
257
|
if (ix) {
|
|
258
|
+
// This path spells the index node from parts, so a `lead` would be DROPPED — an element
|
|
259
|
+
// access silently becoming a row's. Unreachable today (arrayAccess's lead branch requires
|
|
260
|
+
// `fieldOff === undefined`, which is exclusive with the dot form), but this is a
|
|
261
|
+
// text-returning path with no other guard, so it refuses rather than assumes.
|
|
262
|
+
if (ix.lead && ix.lead.length > 0) {
|
|
263
|
+
throw new Error(`c backend: a multidimensional array element has no struct-field spelling yet`);
|
|
264
|
+
}
|
|
144
265
|
const hooked = leaf?.(ix, rec);
|
|
145
266
|
const baseTxt = hooked ?? `${rec(ix.base, 1)}[${rec(ix.idx, 99)}]`;
|
|
146
267
|
return `${baseTxt}.${e.name}`;
|
|
@@ -173,7 +294,9 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
173
294
|
}
|
|
174
295
|
case 'bin': {
|
|
175
296
|
const p = PREC[e.op];
|
|
176
|
-
|
|
297
|
+
// Both right shifts spell C's `>>`; `shiftOperand` supplies the operand cast that says which.
|
|
298
|
+
const shift = e.op === '>>' || e.op === '>>>';
|
|
299
|
+
const s = `${rec(shift ? shiftOperand(e) : e.l, p)} ${shift ? '>>' : e.op} ${rec(e.r, p - 1)}`;
|
|
177
300
|
return p > parentPrec ? `(${s})` : s;
|
|
178
301
|
}
|
|
179
302
|
}
|
|
@@ -265,7 +388,10 @@ function printStmt(s: Stmt, indent: string, vt: VarTypes, leaf?: LeafHook): stri
|
|
|
265
388
|
out.push(`${bi}break;`);
|
|
266
389
|
}
|
|
267
390
|
}
|
|
268
|
-
|
|
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) {
|
|
269
395
|
out.push(`${ci}default:`);
|
|
270
396
|
for (const t of s.default) {
|
|
271
397
|
out.push(...printStmt(t, bi, vt, leaf));
|
|
@@ -347,7 +473,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
|
347
473
|
const vt: VarTypes = declaredTypes(fn);
|
|
348
474
|
const lines: string[] = [];
|
|
349
475
|
for (const l of fn.locals) {
|
|
350
|
-
lines.push(` ${cType(l.type)} ${l.name};`);
|
|
476
|
+
lines.push(` ${l.volatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
|
|
351
477
|
}
|
|
352
478
|
for (const s of fn.body) {
|
|
353
479
|
lines.push(...printStmt(s, ' ', vt, leaf));
|
package/src/backend/cpp.ts
CHANGED
|
@@ -90,7 +90,9 @@ export function cppBackend(spec: CppFnSpec): LanguageBackend {
|
|
|
90
90
|
// class pointer, and print an unscaled `this[2]` that C++ strides by sizeof(class).
|
|
91
91
|
// Correct bytes over idiomatic spelling, never the reverse.
|
|
92
92
|
const leaf: LeafHook = (e: Expr) => {
|
|
93
|
-
|
|
93
|
+
// `lead` (a multidimensional array global) is not a receiver access and must not be
|
|
94
|
+
// rewritten to one — the hook returns text, so a dropped subscript would be silent.
|
|
95
|
+
if (e.k === 'index' && e.base.k === 'var' && e.idx.k === 'const' && !e.lead?.length) {
|
|
94
96
|
const r = recv.get(e.base.name);
|
|
95
97
|
if (r) {
|
|
96
98
|
if (e.width === 4) {
|
package/src/backend/pascal.ts
CHANGED
|
@@ -35,6 +35,10 @@ const BIT_FN: Partial<Record<BinOp, string>> = {
|
|
|
35
35
|
'|': 'bitor',
|
|
36
36
|
'^': 'bitxor',
|
|
37
37
|
'<<': 'lshift',
|
|
38
|
+
// `rshift` over this backend's signed `Integer` reproduces IDO's `sra` — verified byte-exact
|
|
39
|
+
// against upas (pascal-ido.test.ts `asr2`). `>>>`, the LOGICAL shift, has no verified spelling
|
|
40
|
+
// here and is therefore absent: it reaches the loud decline below rather than borrowing this
|
|
41
|
+
// one, which would emit an arithmetic shift where the machine did a logical one.
|
|
38
42
|
'>>': 'rshift',
|
|
39
43
|
};
|
|
40
44
|
|
|
@@ -83,6 +87,13 @@ function makePrinter(vt: VarTypes) {
|
|
|
83
87
|
// any plausible `^Integer`-shaped callee agrees with the machine width; a sub-word access
|
|
84
88
|
// through an unknowable base would DISCARD the node's width (upas checks types, not
|
|
85
89
|
// machine widths), so it declines like a definite mismatch.
|
|
90
|
+
// Leading constant subscripts (a multidimensional array global) have no IDO Pascal
|
|
91
|
+
// spelling yet, and dropping them would read a ROW's address as an element — decline
|
|
92
|
+
// LOUD, like the address-of case above. Unreachable today (the symbol map is agbcc-only),
|
|
93
|
+
// but silence here would be the wrong kind of unreachable.
|
|
94
|
+
if (e.lead && e.lead.length > 0) {
|
|
95
|
+
throw new Error(`pascal backend: a multidimensional array access has no IDO Pascal spelling yet`);
|
|
96
|
+
}
|
|
86
97
|
const bt = exprCType(e.base, vt);
|
|
87
98
|
if ((bt !== undefined && !derefStrideOk(bt, e.width)) || (bt === undefined && e.width !== 4)) {
|
|
88
99
|
throw new Error(
|
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
|
-
import type { Expr, SFn, Stmt } from './l3/ast';
|
|
9
|
-
import { exprChildren, fieldSpellsDot, stmtChildren, stmtExprs } from './l3/ast';
|
|
8
|
+
import type { BinOp, Expr, SFn, Stmt } 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
|
|
@@ -71,8 +235,21 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
71
235
|
const vt = declaredTypes(sfn);
|
|
72
236
|
const ctype = (e: Expr): IrType | undefined => exprCType(e, vt);
|
|
73
237
|
const bad: string[] = [];
|
|
238
|
+
// A `void` function must not RETURN A VALUE. Holds by construction today — returnType() answers
|
|
239
|
+
// void only when every `ret` is operand-less — but `retType` has two producers (the recovered
|
|
240
|
+
// type and the prototype's `returnsVoid`) and the value-suppression lives in a third place
|
|
241
|
+
// (structure.ts's return lowering), so a regressing edit to any of them prints `return expr;`
|
|
242
|
+
// inside a void function. That is ill-formed C the candidate compiler only rejects two stages
|
|
243
|
+
// later, with a diagnostic pointing at the symptom rather than the pass. Cheap to state here.
|
|
244
|
+
if (sfn.retType.kind === 'void') {
|
|
245
|
+
const valued = (stmts: Stmt[]): boolean =>
|
|
246
|
+
stmts.some((s) => (s.k === 'return' && s.value !== undefined) || valued(stmtChildren(s)));
|
|
247
|
+
if (valued(sfn.body)) {
|
|
248
|
+
bad.push(`function '${sfn.name}' is typed void but a return carries a value`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
74
251
|
// Ops C rejects outright on a pointer operand (the additive ops and &&/|| are legal C).
|
|
75
|
-
const NO_PTR_OPS = new Set(['&', '|', '^', '<<', '>>', '*', '/', '%']);
|
|
252
|
+
const NO_PTR_OPS = new Set<BinOp>(['&', '|', '^', '<<', '>>', '>>>', '*', '/', '%']);
|
|
76
253
|
// The comparison operators — where a bare `&SYM` operand is SIGN-ambiguous, not ill-formed.
|
|
77
254
|
const CMP_OPS = new Set(['<', '<=', '>', '>=', '==', '!=']);
|
|
78
255
|
// 1/2/4 only: the decomp typedef vocabulary (C_TYPEDEFS) has no 64-bit scalar, so a width-8
|
package/src/declare.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
ENUM_IS_SIGNED,
|
|
38
38
|
type SymbolInfo,
|
|
39
39
|
type SymbolStructField,
|
|
40
|
+
arrayInnerExtents,
|
|
40
41
|
declaredFields,
|
|
41
42
|
pointeeFields,
|
|
42
43
|
symbolFieldType,
|
|
@@ -75,23 +76,41 @@ function structDecl(tag: string, layout: SymbolStructField[] | undefined, size:
|
|
|
75
76
|
return null;
|
|
76
77
|
}
|
|
77
78
|
const fields: StructFieldDecl[] = [];
|
|
78
|
-
|
|
79
|
+
// The cursor is in BITS (declaredFields' own discipline) so bitfield members seat exactly.
|
|
80
|
+
// Gaps pad as the u8 arrays they always were when both ends are byte-aligned, and as named
|
|
81
|
+
// `u32 asmlift_pad_N : k` bitfields otherwise — split at 32-bit unit boundaries, matching the
|
|
82
|
+
// no-straddle allocation rule declaredFields verified each kept member against. For a
|
|
83
|
+
// bitfield-free layout every gap is byte-aligned, so the emitted text is unchanged.
|
|
84
|
+
let bitCursor = 0;
|
|
79
85
|
let pad = 0;
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
const padTo = (lo: number): void => {
|
|
87
|
+
while (bitCursor < lo) {
|
|
82
88
|
// asmlift_-prefixed so a REAL member named pad_N (a decomp-header idiom) never collides
|
|
83
|
-
|
|
89
|
+
const name = `asmlift_pad_${pad++}`;
|
|
90
|
+
if (bitCursor % 8 === 0 && lo % 8 === 0) {
|
|
91
|
+
fields.push({ name, type: T.array(T.u(8), (lo - bitCursor) / 8) });
|
|
92
|
+
bitCursor = lo;
|
|
93
|
+
} else {
|
|
94
|
+
const k = Math.min(lo - bitCursor, 32 - (bitCursor % 32));
|
|
95
|
+
fields.push({ name, type: T.u(32), bits: k });
|
|
96
|
+
bitCursor += k;
|
|
97
|
+
}
|
|
84
98
|
}
|
|
99
|
+
};
|
|
100
|
+
for (const m of members) {
|
|
101
|
+
const bits = m.bitWidth !== undefined;
|
|
102
|
+
const lo = m.offset * 8 + (bits ? m.bitOffset! : 0);
|
|
103
|
+
padTo(lo);
|
|
85
104
|
fields.push({
|
|
86
105
|
name: m.name,
|
|
87
106
|
type: fieldType(m),
|
|
88
107
|
...(m.volatile ? { volatile: true } : {}),
|
|
108
|
+
...(bits ? { bits: m.bitWidth } : {}),
|
|
89
109
|
});
|
|
90
|
-
|
|
110
|
+
bitCursor = bits ? lo + m.bitWidth! : (m.offset + m.size) * 8;
|
|
91
111
|
}
|
|
92
|
-
if (size !== undefined
|
|
93
|
-
// tail padding to the declared size
|
|
94
|
-
fields.push({ name: `asmlift_pad_${pad}`, type: T.array(T.u(8), size - cursor) });
|
|
112
|
+
if (size !== undefined) {
|
|
113
|
+
padTo(size * 8); // tail padding to the declared size
|
|
95
114
|
}
|
|
96
115
|
return renderStructDecl(tag, fields);
|
|
97
116
|
}
|
|
@@ -139,7 +158,14 @@ export function renderDeclarations(refs: SymbolRef[]): string {
|
|
|
139
158
|
// A non-1/2/4 element width is never bare-indexed by core (only &gSym cast forms), so
|
|
140
159
|
// an unsized u8[] decl is codegen-identical for every spelling core emits.
|
|
141
160
|
const elem = info.elemSize !== undefined ? intType(info.elemSize, info.elemSigned ?? false) : null;
|
|
142
|
-
|
|
161
|
+
// The RANK must be reproduced, or the declaration disagrees with the access core spells:
|
|
162
|
+
// a `gSym[0][i]` needs a 2-D declaration to be an element rather than a type error. The
|
|
163
|
+
// OUTERMOST extent is always left unsized — it is the one C lets a declaration omit, and
|
|
164
|
+
// omitting it keeps this decl compatible with the project's real one whatever its size
|
|
165
|
+
// (the same reason the rank-1 form has always been `[]`). Inner extents are load-bearing:
|
|
166
|
+
// they are what scales each leading subscript, so they are spelled exactly.
|
|
167
|
+
const rank = (arrayInnerExtents(info) ?? []).map((d) => `[${d}]`).join('');
|
|
168
|
+
lines.push(`extern ${quals(info)}${elem ?? 'u8'} ${name}[]${rank};`);
|
|
143
169
|
break;
|
|
144
170
|
}
|
|
145
171
|
case 'struct': {
|