@asmlift/core 0.1.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/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// asmlift — the C++ language backend. Emits IDIOMATIC, de-mangled C++ — a member function with
|
|
2
|
+
// scope resolution (`Vec::dot`), an implicit `this`, and named member access — reusing the
|
|
3
|
+
// shared C-family printer (backend/cfamily.ts) for the body VERBATIM, because a CodeWarrior
|
|
4
|
+
// member function's body is byte-identical to the same C with `this` explicit. Only the
|
|
5
|
+
// DIVERGENT C++ surface lives here: the scoped/`this` signature, the class declaration, member
|
|
6
|
+
// access, and the mangled SYMBOL (src/mangle.ts) that objdiff aligns the candidate by.
|
|
7
|
+
//
|
|
8
|
+
// What it consumes beyond the neutral SFn — supplied like `prototypes` (a decomp project has its
|
|
9
|
+
// class layouts in headers, exactly as it has function prototypes): the owning class + method
|
|
10
|
+
// name, the explicit parameter names/types, and the field layout of each class touched (so an
|
|
11
|
+
// indexed load `this[1]` becomes the named `y`).
|
|
12
|
+
//
|
|
13
|
+
// Scope: free functions and non-virtual member functions with scalar/pointer params and named
|
|
14
|
+
// field access. Virtual dispatch, references, and constructors/destructors are deliberately not
|
|
15
|
+
// built ahead of an inhabitant.
|
|
16
|
+
import { Expr, LanguageBackend, SFn } from '../l3/ast';
|
|
17
|
+
import { type CppType, mangle, spellType } from '../mangle';
|
|
18
|
+
import { LeafHook, cComment, emitCFamily } from './cfamily';
|
|
19
|
+
|
|
20
|
+
export interface CppClass {
|
|
21
|
+
fields: { name: string; type: CppType }[];
|
|
22
|
+
} // field i at word offset i
|
|
23
|
+
export interface CppFnSpec {
|
|
24
|
+
method: string; // idiomatic function / method name
|
|
25
|
+
cls?: string; // owning class (member fn); omit for a free fn
|
|
26
|
+
retType: CppType;
|
|
27
|
+
params: { name: string; type: CppType }[]; // EXPLICIT params (the implicit `this` excluded)
|
|
28
|
+
classes?: Record<string, CppClass>; // layouts for named member access
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The mangled CodeWarrior symbol this spec compiles to — the objdiff alignment key. */
|
|
32
|
+
export function cppSymbol(spec: CppFnSpec): string {
|
|
33
|
+
return mangle({ name: spec.method, cls: spec.cls, params: spec.params.map((p) => p.type) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Build a C++ backend for one function, parameterized by its recovered C++ signature. The lifted
|
|
37
|
+
* SFn params are positional: for a member function SFn.params[0] is `this`, the rest are `params`. */
|
|
38
|
+
export function cppBackend(spec: CppFnSpec): LanguageBackend {
|
|
39
|
+
return {
|
|
40
|
+
id: 'cpp',
|
|
41
|
+
emit(fn: SFn): string {
|
|
42
|
+
// Map each lifted param var → its C++ meaning: `this` (bare member access) or a named param
|
|
43
|
+
// (a pointer-to-class param uses `->`). A pointer-to-known-class param is a member receiver.
|
|
44
|
+
const thisVar = spec.cls ? fn.params[0]?.name : undefined;
|
|
45
|
+
const explicitStart = spec.cls ? 1 : 0;
|
|
46
|
+
const rename = new Map<string, string>(); // lifted var → C++ name
|
|
47
|
+
const recv = new Map<string, { cls: string; via: 'this' | string }>(); // var → member receiver
|
|
48
|
+
if (thisVar) {
|
|
49
|
+
rename.set(thisVar, 'this');
|
|
50
|
+
recv.set(thisVar, { cls: spec.cls!, via: 'this' });
|
|
51
|
+
}
|
|
52
|
+
spec.params.forEach((p, i) => {
|
|
53
|
+
const v = fn.params[explicitStart + i]?.name;
|
|
54
|
+
if (!v) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
rename.set(v, p.name);
|
|
58
|
+
if (p.type.ptr === 1 && spec.classes?.[p.type.base]) {
|
|
59
|
+
recv.set(v, { cls: p.type.base, via: p.name });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const field = (cls: string, k: number): string => {
|
|
64
|
+
const fields = spec.classes?.[cls]?.fields ?? [];
|
|
65
|
+
// The lifted index `k` counts WORD offsets. It coincides with the sequential field
|
|
66
|
+
// position ONLY when every field is word-sized (4 bytes), so a `short`/`char`/mixed-width
|
|
67
|
+
// struct would map `k` to the WRONG field. Rather than emit silently-wrong idiomatic C++,
|
|
68
|
+
// fail LOUD: a mixed layout needs byte-offset field resolution, which is follow-on work.
|
|
69
|
+
if (!fields.every((f) => typeWidth(f.type) === 4)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`cpp backend: class ${cls} has a sub-word/mixed field layout — member access needs byte-offset recovery (not yet supported)`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const f = fields[k];
|
|
75
|
+
if (!f) {
|
|
76
|
+
throw new Error(`cpp backend: no field at word offset ${k} of class ${cls}`);
|
|
77
|
+
}
|
|
78
|
+
return f.name;
|
|
79
|
+
};
|
|
80
|
+
// Leaf hook: rewrite an indexed access on a receiver into named member access, and a bare
|
|
81
|
+
// receiver/param var into its C++ name. Everything else falls through to shared C spelling.
|
|
82
|
+
//
|
|
83
|
+
// The member rewrite fires ONLY for a WORD access (`width === 4`): the word-index `field()`
|
|
84
|
+
// mapping assumes idx counts words, and the all-word-layout guard above checks the CLASS,
|
|
85
|
+
// not the ACCESS — a sub-word access on a word field (`lhz` from offset 4) would map its
|
|
86
|
+
// byte-scaled idx to the wrong member and read the wrong width, silently. The sub-word
|
|
87
|
+
// receiver access is spelled HERE too (the honest reinterpret cast, `((s16 *)this)[2]`):
|
|
88
|
+
// it cannot fall through to the shared legalization, because the hook RENAMES the receiver
|
|
89
|
+
// — the shared printer would judge the SFn var's recovered type while the reader sees the
|
|
90
|
+
// class pointer, and print an unscaled `this[2]` that C++ strides by sizeof(class).
|
|
91
|
+
// Correct bytes over idiomatic spelling, never the reverse.
|
|
92
|
+
const leaf: LeafHook = (e: Expr) => {
|
|
93
|
+
if (e.k === 'index' && e.base.k === 'var' && e.idx.k === 'const') {
|
|
94
|
+
const r = recv.get(e.base.name);
|
|
95
|
+
if (r) {
|
|
96
|
+
if (e.width === 4) {
|
|
97
|
+
return r.via === 'this' ? field(r.cls, e.idx.value) : `${r.via}->${field(r.cls, e.idx.value)}`;
|
|
98
|
+
}
|
|
99
|
+
if (e.width === 1 || e.width === 2) {
|
|
100
|
+
return `((${e.signed ? 's' : 'u'}${e.width * 8} *)${r.via === 'this' ? 'this' : r.via})[${e.idx.value}]`;
|
|
101
|
+
}
|
|
102
|
+
// any other width is a struct-array STRIDE (a dot-form base) — not this rewrite's
|
|
103
|
+
// shape; fall through to the shared spelling so `.field` stays intact.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (e.k === 'var') {
|
|
107
|
+
const nm = rename.get(e.name);
|
|
108
|
+
if (nm) {
|
|
109
|
+
return nm;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const paramList = spec.params.map((p) => `${spellType(p.type)} ${p.name}`).join(', ');
|
|
116
|
+
const decls = classDecls(spec, paramList);
|
|
117
|
+
const signature = `${spellType(spec.retType)} ${spec.cls ? spec.cls + '::' : ''}${spec.method}(${paramList})`;
|
|
118
|
+
return (decls ? decls + '\n' : '') + emitCFamily(signature, fn, leaf);
|
|
119
|
+
},
|
|
120
|
+
comment: cComment, // C++ shares C's block-comment spelling
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Byte width of a C++ type (a pointer is always word-sized). Used to reject a sub-word field layout
|
|
125
|
+
// the word-index member-access mapping cannot represent.
|
|
126
|
+
function typeWidth(t: CppType): number {
|
|
127
|
+
if (t.ptr > 0) {
|
|
128
|
+
return 4;
|
|
129
|
+
}
|
|
130
|
+
return (
|
|
131
|
+
{ char: 1, bool: 1, 'unsigned char': 1, short: 2, 'unsigned short': 2, 'long long': 8, double: 8 }[t.base] ?? 4
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The class declaration(s) a member/field-accessing function needs to compile: fields in word-offset
|
|
136
|
+
// order, plus the method prototype inside its owning class.
|
|
137
|
+
function classDecls(spec: CppFnSpec, paramList: string): string {
|
|
138
|
+
const out: string[] = [];
|
|
139
|
+
for (const [cname, cdef] of Object.entries(spec.classes ?? {})) {
|
|
140
|
+
const fields = cdef.fields.map((f) => `${spellType(f.type)} ${f.name};`).join(' ');
|
|
141
|
+
const method = cname === spec.cls ? ` ${spellType(spec.retType)} ${spec.method}(${paramList});` : '';
|
|
142
|
+
out.push(`struct ${cname} { ${fields}${method} };`);
|
|
143
|
+
}
|
|
144
|
+
return out.join('\n');
|
|
145
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// asmlift — the IDO / SGI Pascal backend. Consumes the SAME
|
|
2
|
+
// language-neutral L3 AST as the C backend; every divergence lives here: `:=` assignment,
|
|
3
|
+
// `if..then..else`, and the neutral "return a value" node lowered to Pascal's name-assignment
|
|
4
|
+
// idiom (`FnName := expr`).
|
|
5
|
+
//
|
|
6
|
+
// DIALECT NOTE (verified against IDO 7.1 `upas`): in SGI Pascal `and`/`or`/`not` are BOOLEAN
|
|
7
|
+
// operators — using them on integers is a type error ("operand(s) must be boolean"). Bitwise
|
|
8
|
+
// and shift operations are INTRINSIC FUNCTIONS: bitand/bitor/bitxor/bitnot and lshift/rshift.
|
|
9
|
+
// (`rshift` is arithmetic on a signed Integer → `sra`.) This is the concrete difference from
|
|
10
|
+
// Turbo/Delphi/FreePascal.
|
|
11
|
+
import { IrType, typeToString } from '../ir/types';
|
|
12
|
+
import { BinOp, Expr, LanguageBackend, SFn, Stmt } from '../l3/ast';
|
|
13
|
+
import { type VarTypes, declaredTypes, derefStrideOk, exprCType } from '../l3/typing';
|
|
14
|
+
|
|
15
|
+
// Infix operators IDO Pascal spells directly.
|
|
16
|
+
const OP: Partial<Record<BinOp, string>> = {
|
|
17
|
+
// NOTE: no `%`. IDO Pascal `mod` is ISO (result in [0, n), sign of the DIVISOR), which does NOT
|
|
18
|
+
// match C's truncated `%` (sign of the DIVIDEND) — verified: `a mod 3` mis-scores against the
|
|
19
|
+
// IDO C `a % 3` codegen. There is no faithful IDO-Pascal spelling of a signed C remainder, so the
|
|
20
|
+
// backend fails LOUD on `%` (below) rather than emit a silently-wrong `mod`. `/`→`div` DOES match.
|
|
21
|
+
'+': '+',
|
|
22
|
+
'-': '-',
|
|
23
|
+
'*': '*',
|
|
24
|
+
'/': 'div',
|
|
25
|
+
'<': '<',
|
|
26
|
+
'<=': '<=',
|
|
27
|
+
'>': '>',
|
|
28
|
+
'>=': '>=',
|
|
29
|
+
'==': '=',
|
|
30
|
+
'!=': '<>',
|
|
31
|
+
};
|
|
32
|
+
// Bitwise/shift operations that IDO Pascal spells as intrinsic FUNCTION calls `fn(l, r)`.
|
|
33
|
+
const BIT_FN: Partial<Record<BinOp, string>> = {
|
|
34
|
+
'&': 'bitand',
|
|
35
|
+
'|': 'bitor',
|
|
36
|
+
'^': 'bitxor',
|
|
37
|
+
'<<': 'lshift',
|
|
38
|
+
'>>': 'rshift',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function pasType(t: IrType): string {
|
|
42
|
+
if (t.kind === 'ptr') {
|
|
43
|
+
return '^' + pasType(t.to);
|
|
44
|
+
}
|
|
45
|
+
if (t.kind === 'int') {
|
|
46
|
+
return t.signed ? 'Integer' : 'Cardinal';
|
|
47
|
+
}
|
|
48
|
+
// `unknown` reaching a backend means recovery's totality contract already failed upstream —
|
|
49
|
+
// spell it Integer (assertTypesRecovered is the real gate). struct/array have NO faithful
|
|
50
|
+
// spelling here yet — fail loud, like every other unspellable construct in this backend. A
|
|
51
|
+
// void RETURN type is deliberately spelled as `procedure` (see emit) and never reaches here.
|
|
52
|
+
if (t.kind === 'unknown') {
|
|
53
|
+
return 'Integer';
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`pascal backend: no faithful spelling for a ${t.kind}-typed value yet`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Pascal precedence is paren-hungry and differs from C; emit parens around every nested
|
|
59
|
+
// binary/unary subexpression (always safe). Minimal-paren Pascal is a later refinement.
|
|
60
|
+
//
|
|
61
|
+
// pe/ps live in a factory closing over `vt` (the declared type of each printed variable) so the
|
|
62
|
+
// deref check below can judge the Pascal the reader will see without threading an argument
|
|
63
|
+
// through every recursion site.
|
|
64
|
+
function makePrinter(vt: VarTypes) {
|
|
65
|
+
function pe(e: Expr): string {
|
|
66
|
+
switch (e.k) {
|
|
67
|
+
case 'var':
|
|
68
|
+
return e.name;
|
|
69
|
+
case 'addr':
|
|
70
|
+
// IDO Pascal address-of has no faithful spelling here yet — loud-decline (agbcc-only
|
|
71
|
+
// globals today; a deref of an addr is simplified away before it reaches this backend).
|
|
72
|
+
throw new Error(`pascal backend: address-of global '${e.name}' has no IDO Pascal spelling yet`);
|
|
73
|
+
case 'const':
|
|
74
|
+
return String(e.value);
|
|
75
|
+
case 'call':
|
|
76
|
+
return `${e.fn}(${e.args.map(pe).join(', ')})`;
|
|
77
|
+
case 'index': {
|
|
78
|
+
// The width-carrying access node (l3/ast.ts): each backend legalizes its own derefs. The
|
|
79
|
+
// C family inserts a reinterpret cast when the base's rendered type does not stride the
|
|
80
|
+
// access width; Pascal HAS no reinterpret cast, so a definite mismatch declines LOUD — a
|
|
81
|
+
// `p^` through the wrong-width pointer would silently read the wrong size. An UNKNOWABLE
|
|
82
|
+
// base (a call — its type lives outside this unit) prints ONLY for a word access, where
|
|
83
|
+
// any plausible `^Integer`-shaped callee agrees with the machine width; a sub-word access
|
|
84
|
+
// through an unknowable base would DISCARD the node's width (upas checks types, not
|
|
85
|
+
// machine widths), so it declines like a definite mismatch.
|
|
86
|
+
const bt = exprCType(e.base, vt);
|
|
87
|
+
if ((bt !== undefined && !derefStrideOk(bt, e.width)) || (bt === undefined && e.width !== 4)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`pascal backend: a ${e.width}-byte access through a base of type '${bt ? typeToString(bt) : '<unknowable>'}' has no faithful spelling (no reinterpret cast)`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return e.idx.k === 'const' && e.idx.value === 0 ? `${pe(e.base)}^` : `${pe(e.base)}[${pe(e.idx)}]`;
|
|
93
|
+
}
|
|
94
|
+
// Recovered struct field access has no faithful IDO-Pascal spelling yet (records + `.field`
|
|
95
|
+
// are future work) — fail LOUD rather than emit a silently-wrong access, as `%` does above.
|
|
96
|
+
case 'field':
|
|
97
|
+
throw new Error(`pascal backend: struct field access '${e.name}' has no IDO Pascal spelling yet`);
|
|
98
|
+
case 'un':
|
|
99
|
+
return e.op === '~' ? `bitnot(${pe(e.e)})` : `(${e.op === '!' ? 'not ' : e.op}${pe(e.e)})`;
|
|
100
|
+
// Casts have no faithful IDO-Pascal spelling yet — fail LOUD rather than emit silently-wrong
|
|
101
|
+
// source. Tree-level producers reaching here: the width-narrowing idiom casts (agbcc-gated,
|
|
102
|
+
// so never on this path today), structure.ts's STRUCT-pointer casts (unreachable too — the
|
|
103
|
+
// `field` case above throws first), and intify's `(s32)ptr` legalization (any target).
|
|
104
|
+
// Scalar deref casts never appear in the tree — the index case above owns that judgment.
|
|
105
|
+
case 'cast':
|
|
106
|
+
throw new Error(`pascal backend: cast has no IDO Pascal spelling yet`);
|
|
107
|
+
// A gap marker: a call to the UNDECLARED function ASMLIFT_ERROR — Pascal has no preprocessor,
|
|
108
|
+
// but an undeclared identifier fails `upas` all the same, so the loud-in-artifact property
|
|
109
|
+
// (the file cannot compile until the user consciously supplies the symbol) is preserved.
|
|
110
|
+
// Single quotes double to escape inside a Pascal string literal.
|
|
111
|
+
case 'marker':
|
|
112
|
+
return `ASMLIFT_ERROR(${[`'${e.reason.replace(/'/g, "''")}'`, ...e.args.map(pe)].join(', ')})`;
|
|
113
|
+
case 'bin': {
|
|
114
|
+
const fn = BIT_FN[e.op];
|
|
115
|
+
if (fn) {
|
|
116
|
+
return `${fn}(${pe(e.l)}, ${pe(e.r)})`;
|
|
117
|
+
}
|
|
118
|
+
const op = OP[e.op];
|
|
119
|
+
// Fail LOUD on an operator this backend cannot faithfully spell (e.g. `%`) rather than emit
|
|
120
|
+
// `(l undefined r)` — a silently-wrong Pascal expression.
|
|
121
|
+
if (!op) {
|
|
122
|
+
throw new Error(`pascal backend: operator '${e.op}' has no faithful IDO Pascal spelling`);
|
|
123
|
+
}
|
|
124
|
+
return `(${pe(e.l)} ${op} ${pe(e.r)})`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// `tail` = control falls off the END of the function once this statement (the LAST of its list)
|
|
130
|
+
// completes. Pascal has no early return — `fnName := v` only sets the result — so a `return` is
|
|
131
|
+
// faithful ONLY in tail position (the assignment-then-fall-off-end idiom; a bare tail `return;`
|
|
132
|
+
// simply falls off). A NON-tail return would render as a silent fall-through miscompile, so it
|
|
133
|
+
// fails LOUD, mirroring this file's `%`/`field`/`break` throws.
|
|
134
|
+
function ps(fnName: string, s: Stmt, indent: string, tail = false): string[] {
|
|
135
|
+
// render a statement list: only its LAST statement can inherit tail position
|
|
136
|
+
const list = (stmts: Stmt[], ind: string, tl: boolean): string[] =>
|
|
137
|
+
stmts.flatMap((x, i) => ps(fnName, x, ind, tl && i === stmts.length - 1));
|
|
138
|
+
switch (s.k) {
|
|
139
|
+
case 'assign': {
|
|
140
|
+
// The write-side sibling of the index case's deref discipline: Pascal has no reinterpret
|
|
141
|
+
// cast, so a definitely-non-pointer value assigned into a pointer-declared var (the shape
|
|
142
|
+
// the C family legalizes with `(u8 *)…`, cfamily.ts legalizePointerWrites) declines LOUD
|
|
143
|
+
// here instead of failing three stages later in upas.
|
|
144
|
+
const dt = vt(s.name);
|
|
145
|
+
const ct = exprCType(s.value, vt);
|
|
146
|
+
if (dt?.kind === 'ptr' && ct && ct.kind !== 'ptr' && ct.kind !== 'array') {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`pascal backend: assigning a ${typeToString(ct)} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return [`${indent}${s.name} := ${pe(s.value)};`];
|
|
152
|
+
}
|
|
153
|
+
case 'store':
|
|
154
|
+
return [`${indent}${pe(s.lval)} := ${pe(s.value)};`];
|
|
155
|
+
case 'exprstmt':
|
|
156
|
+
return [`${indent}${pe(s.value)};`];
|
|
157
|
+
case 'return':
|
|
158
|
+
if (!tail) {
|
|
159
|
+
throw new Error('pascal backend: early `return` (not in tail position) has no faithful IDO Pascal spelling');
|
|
160
|
+
}
|
|
161
|
+
return s.value ? [`${indent}${fnName} := ${pe(s.value)};`] : [];
|
|
162
|
+
case 'if': {
|
|
163
|
+
const cond = pe(s.cond);
|
|
164
|
+
const block = (stmts: Stmt[], ind: string) =>
|
|
165
|
+
stmts.length === 1
|
|
166
|
+
? ps(fnName, stmts[0], ind, tail)
|
|
167
|
+
: [`${ind}begin`, ...list(stmts, ind + ' ', tail), `${ind}end`];
|
|
168
|
+
// Pascal: no `;` before `else`; the branch statements already carry their own.
|
|
169
|
+
const out = [`${indent}if ${cond} then`];
|
|
170
|
+
out.push(...block(s.then, indent + ' '));
|
|
171
|
+
if (s.else.length) {
|
|
172
|
+
out.push(`${indent}else`);
|
|
173
|
+
out.push(...block(s.else, indent + ' '));
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
case 'while': {
|
|
178
|
+
// loop bodies are NEVER tail position — control returns to the test
|
|
179
|
+
const body =
|
|
180
|
+
s.body.length === 1
|
|
181
|
+
? ps(fnName, s.body[0], indent + ' ')
|
|
182
|
+
: [`${indent}begin`, ...s.body.flatMap((x) => ps(fnName, x, indent + ' ')), `${indent}end`];
|
|
183
|
+
return [`${indent}while ${pe(s.cond)} do`, ...body];
|
|
184
|
+
}
|
|
185
|
+
case 'dowhile':
|
|
186
|
+
// IDO/SGI Pascal `repeat S until C` runs the body once then tests — the exit test is the NEGATION
|
|
187
|
+
// of the C loop-continue condition (`do{}while(c)` re-enters while c is TRUE; `repeat until` exits
|
|
188
|
+
// when its test is TRUE). `repeat`/`until` need no begin/end (the keywords bracket the body).
|
|
189
|
+
return [
|
|
190
|
+
`${indent}repeat`,
|
|
191
|
+
...s.body.flatMap((x) => ps(fnName, x, indent + ' ')),
|
|
192
|
+
`${indent}until ${pe({ k: 'un', op: '!', e: s.cond })};`,
|
|
193
|
+
];
|
|
194
|
+
case 'for':
|
|
195
|
+
// IDO/SGI Pascal's native `for i := a to b do` is restricted to a unit-stride countable
|
|
196
|
+
// range, so rather than pattern-match that subset, render the ALWAYS-faithful desugaring
|
|
197
|
+
// `init; while cond do begin body; inc end` (reusing the `while` arm). The `for` node
|
|
198
|
+
// exists purely as a C/C++ readability spelling — no silently-wrong `to`-bound. (A native
|
|
199
|
+
// `for` spelling is future work.)
|
|
200
|
+
return [
|
|
201
|
+
...ps(fnName, s.init, indent),
|
|
202
|
+
...ps(fnName, { k: 'while', cond: s.cond, body: [...s.body, s.inc] }, indent),
|
|
203
|
+
];
|
|
204
|
+
// SGI/IDO Pascal has no loop `break`/`continue` — loud-fail rather than emit silently-wrong control
|
|
205
|
+
// flow (mirrors the `field`/`cast`/`%` throws). A `goto`-lowered form is future work.
|
|
206
|
+
case 'break':
|
|
207
|
+
throw new Error('pascal backend: `break` has no IDO Pascal spelling');
|
|
208
|
+
case 'continue':
|
|
209
|
+
throw new Error('pascal backend: `continue` has no IDO Pascal spelling');
|
|
210
|
+
case 'switch': {
|
|
211
|
+
// IDO/SGI Pascal `case E of L: S; … otherwise S end`. There is NO fall-through in Pascal case-of,
|
|
212
|
+
// so a `fallsThrough` arm has no faithful spelling → loud-fail (mirrors the `field`/`cast`/`%`
|
|
213
|
+
// throws above). Each arm's body is a single statement or a begin/end block.
|
|
214
|
+
const arm = (body: Stmt[], ind: string) =>
|
|
215
|
+
body.length === 1
|
|
216
|
+
? ps(fnName, body[0], ind + ' ')
|
|
217
|
+
: [`${ind} begin`, ...body.flatMap((x) => ps(fnName, x, ind + ' ')), `${ind} end`];
|
|
218
|
+
// Pascal has no early `return`: a `return v` inside a case renders `fnName := v` and then FALLS
|
|
219
|
+
// THROUGH the case-of into any post-switch code — a silent miscompile for a mixed return/break
|
|
220
|
+
// switch. Loud-fail if a case (or default) body contains a return, rather than emit wrong code.
|
|
221
|
+
const hasReturn = (body: Stmt[]): boolean =>
|
|
222
|
+
body.some(
|
|
223
|
+
(st) =>
|
|
224
|
+
st.k === 'return' ||
|
|
225
|
+
(st.k === 'if' && (hasReturn(st.then) || hasReturn(st.else))) ||
|
|
226
|
+
((st.k === 'while' || st.k === 'dowhile' || st.k === 'for') && hasReturn(st.body)) ||
|
|
227
|
+
(st.k === 'switch' && (st.cases.some((c) => hasReturn(c.body)) || hasReturn(st.default ?? []))),
|
|
228
|
+
);
|
|
229
|
+
if (s.cases.some((c) => hasReturn(c.body)) || hasReturn(s.default ?? [])) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
'pascal backend: `return` inside a switch case has no faithful IDO Pascal spelling (no early return)',
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const out = [`${indent}case ${pe(s.scrutinee)} of`];
|
|
235
|
+
for (const c of s.cases) {
|
|
236
|
+
if (c.fallsThrough) {
|
|
237
|
+
throw new Error('pascal backend: switch fall-through has no faithful IDO Pascal case-of spelling');
|
|
238
|
+
}
|
|
239
|
+
out.push(`${indent} ${c.values.join(', ')}:`, ...arm(c.body, indent + ' '));
|
|
240
|
+
}
|
|
241
|
+
if (s.default) {
|
|
242
|
+
out.push(`${indent} otherwise`);
|
|
243
|
+
out.push(...arm(s.default, indent + ' '));
|
|
244
|
+
}
|
|
245
|
+
out.push(`${indent}end;`);
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return ps;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const pascalBackend: LanguageBackend = {
|
|
254
|
+
id: 'pascal',
|
|
255
|
+
emit(fn: SFn): string {
|
|
256
|
+
// Same env discipline as the C family (cfamily.ts cFamilyBody): the printer judges derefs
|
|
257
|
+
// against the exact declarations it emits.
|
|
258
|
+
const ps = makePrinter(declaredTypes(fn));
|
|
259
|
+
const params = fn.params.map((p) => `${p.name}: ${pasType(p.type)}`).join('; ');
|
|
260
|
+
// A void return is a PROCEDURE — the honest Pascal spelling (the annotate-mode stub's SFn is
|
|
261
|
+
// void-typed by design); a valued function keeps the `function … : T` form.
|
|
262
|
+
const lines = [
|
|
263
|
+
fn.retType.kind === 'void'
|
|
264
|
+
? `procedure ${fn.name}(${params});`
|
|
265
|
+
: `function ${fn.name}(${params}): ${pasType(fn.retType)};`,
|
|
266
|
+
];
|
|
267
|
+
if (fn.locals.length) {
|
|
268
|
+
lines.push('var', ...fn.locals.map((l) => ` ${l.name}: ${pasType(l.type)};`));
|
|
269
|
+
}
|
|
270
|
+
lines.push('begin');
|
|
271
|
+
fn.body.forEach((s, i) => lines.push(...ps(fn.name, s, ' ', i === fn.body.length - 1)));
|
|
272
|
+
lines.push('end;');
|
|
273
|
+
return lines.join('\n') + '\n';
|
|
274
|
+
},
|
|
275
|
+
// ISO/IDO Pascal comment; `*)` inside the text would terminate it early — split it.
|
|
276
|
+
comment(text: string): string {
|
|
277
|
+
return `(* ${text.replace(/\*\)/g, '* )')} *)`;
|
|
278
|
+
},
|
|
279
|
+
};
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// asmlift — stage boundary contracts: semantic POSTCONDITIONS enforced in production at the
|
|
2
|
+
// stage boundaries, in every entry path (decompile / decompileTraced / the cli's
|
|
3
|
+
// decompileRanked / decompileWithReport).
|
|
4
|
+
// A pass that regresses fails AT its boundary with a diagnostic, not three stages later as
|
|
5
|
+
// wrong C.
|
|
6
|
+
import type { Fn, Value } from './ir/core';
|
|
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';
|
|
10
|
+
import { declaredTypes, exprCType } from './l3/typing';
|
|
11
|
+
|
|
12
|
+
export class ContractError extends Error {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'ContractError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// An `unknown` may hide NESTED inside a pointer (`ptr(unknown)` prints as `unk32 *` — uncompilable,
|
|
20
|
+
// since `unk32` isn't a real typedef). Check the whole type, not just its top-level kind.
|
|
21
|
+
function hasUnknown(t: IrType): boolean {
|
|
22
|
+
return t.kind === 'unknown' || (t.kind === 'ptr' && hasUnknown(t.to));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Post type-recovery: no SSA value may still be `unknown` (at any depth). Recovery is total by
|
|
26
|
+
* construction (it defaults every residual to s32), so a surviving `unknown` means a recovery pass
|
|
27
|
+
* stopped short — caught here, before it poisons a downstream type decision or the emitted
|
|
28
|
+
* signature. */
|
|
29
|
+
export function assertTypesRecovered(fn: Fn): void {
|
|
30
|
+
const check = (v: Value, what: string) => {
|
|
31
|
+
if (hasUnknown(v.type)) {
|
|
32
|
+
throw new ContractError(`type recovery left ${what} unknown in '${fn.name}'`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
for (const b of fn.blocks) {
|
|
36
|
+
b.params.forEach((p, i) => check(p, `param #${i}`));
|
|
37
|
+
for (const op of b.ops) {
|
|
38
|
+
op.results.forEach((r, i) => check(r, `${op.opcode} result #${i}`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Post structuring: the AST must reference no unresolved value. The structurer emits the
|
|
44
|
+
* sentinel var `"?"` when it cannot resolve a value (a dropped def, or an opcode it has no
|
|
45
|
+
* lowering for) — which would print as uncompilable source. Fail at the structuring boundary
|
|
46
|
+
* instead of emitting garbage. */
|
|
47
|
+
export function assertResolved(sfn: SFn): void {
|
|
48
|
+
// Derived from the shared exprChildren/stmtExprs/stmtChildren traversal so no statement kind
|
|
49
|
+
// 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 `"?"` — and args are
|
|
51
|
+
// exactly its children.
|
|
52
|
+
const badExpr = (e: Expr): boolean => (e.k === 'var' && e.name === '?') || exprChildren(e).some(badExpr);
|
|
53
|
+
const badStmt = (s: Stmt): boolean => stmtExprs(s).some(badExpr) || stmtChildren(s).some(badStmt);
|
|
54
|
+
if (sfn.body.some(badStmt)) {
|
|
55
|
+
throw new ContractError(
|
|
56
|
+
`structuring left an unresolved value ('?') in '${sfn.name}' — a dropped def or unlowered opcode`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Post structuring: the AST's memory accesses and operators must be SPELLABLE — a `field`
|
|
62
|
+
* node's base a pointer-to-struct (`->`) or a struct value (`.`, an array element) carrying
|
|
63
|
+
* that field; no pointer operand under an operator C rejects; and every SCALAR `index` node's
|
|
64
|
+
* width a real C scalar width (a regressing pass emitting width 3 would print as the
|
|
65
|
+
* nonexistent `(u24 *)` typedef and fail at candidate compile three stages later). Index BASES
|
|
66
|
+
* are deliberately not checked: the width-carrying node makes every base legalizable — the C
|
|
67
|
+
* family casts at the node's width, Pascal declines loud — so a non-pointer base is a backend
|
|
68
|
+
* spelling decision, not an ill-formed tree. Only DEFINITE violations throw: an expression
|
|
69
|
+
* whose C type is not statically knowable here (a call's return, a gap marker) passes. */
|
|
70
|
+
export function assertDerefsTyped(sfn: SFn): void {
|
|
71
|
+
const vt = declaredTypes(sfn);
|
|
72
|
+
const ctype = (e: Expr): IrType | undefined => exprCType(e, vt);
|
|
73
|
+
const bad: string[] = [];
|
|
74
|
+
// Ops C rejects outright on a pointer operand (the additive ops and &&/|| are legal C).
|
|
75
|
+
const NO_PTR_OPS = new Set(['&', '|', '^', '<<', '>>', '*', '/', '%']);
|
|
76
|
+
// 1/2/4 only: the decomp typedef vocabulary (C_TYPEDEFS) has no 64-bit scalar, so a width-8
|
|
77
|
+
// access would print as the nonexistent `(s64 *)` — exactly the three-stages-later failure
|
|
78
|
+
// this rule pre-empts. (If f64 loads ever land they are floats, not a scalar width here.)
|
|
79
|
+
const SCALAR_WIDTHS = new Set([1, 2, 4]);
|
|
80
|
+
// Dot-form field bases (struct-array elements) carry the struct STRIDE as their width — any
|
|
81
|
+
// stride matching the element size is legal there (the tree-level struct cast governs the
|
|
82
|
+
// spelling; a stride/size MISMATCH types scalar in exprCType and the field rule flags it).
|
|
83
|
+
// Collected as fields are visited, BEFORE recursing into their children. Identity-keyed: a
|
|
84
|
+
// future subtree-SHARING pass (CSE-style) would leak the exemption to aliased bare uses —
|
|
85
|
+
// trees are freshly built per node today (structure.ts), which this relies on.
|
|
86
|
+
const structElem = new Set<Expr>();
|
|
87
|
+
const checkExpr = (e: Expr): void => {
|
|
88
|
+
if (e.k === 'index' && !structElem.has(e) && !SCALAR_WIDTHS.has(e.width)) {
|
|
89
|
+
bad.push(`index width ${e.width} is not a C scalar width`);
|
|
90
|
+
}
|
|
91
|
+
// The emitter legalizes pointer operands away from these ops (structure.ts intify); a
|
|
92
|
+
// pointer surviving here is ill-typed C the compiler will reject.
|
|
93
|
+
if (e.k === 'bin' && NO_PTR_OPS.has(e.op)) {
|
|
94
|
+
for (const side of [e.l, e.r]) {
|
|
95
|
+
if (ctype(side)?.kind === 'ptr') {
|
|
96
|
+
bad.push(`pointer operand under '${e.op}'`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// `!p` is legal C (pointer truthiness); `-p`/`~p` are not.
|
|
101
|
+
if (e.k === 'un' && e.op !== '!' && ctype(e.e)?.kind === 'ptr') {
|
|
102
|
+
bad.push(`pointer operand under unary '${e.op}'`);
|
|
103
|
+
}
|
|
104
|
+
if (e.k === 'field') {
|
|
105
|
+
if (fieldSpellsDot(e)) {
|
|
106
|
+
structElem.add(e.base);
|
|
107
|
+
}
|
|
108
|
+
const bt = ctype(e.base);
|
|
109
|
+
if (bt) {
|
|
110
|
+
// type-check against the same dot-vs-arrow spelling the printer will use (shared rule)
|
|
111
|
+
const st = fieldSpellsDot(e) ? bt : bt.kind === 'ptr' ? bt.to : undefined;
|
|
112
|
+
if (!st || st.kind !== 'struct') {
|
|
113
|
+
bad.push(`member access '${e.name}' on a non-struct base (C type '${typeToString(bt)}')`);
|
|
114
|
+
} else if (!st.fields.some((f) => f.name === e.name)) {
|
|
115
|
+
bad.push(`member access '${e.name}' not declared on '${st.name}'`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exprChildren(e).forEach(checkExpr);
|
|
120
|
+
};
|
|
121
|
+
const checkStmt = (s: Stmt): void => {
|
|
122
|
+
stmtExprs(s).forEach(checkExpr);
|
|
123
|
+
stmtChildren(s).forEach(checkStmt);
|
|
124
|
+
};
|
|
125
|
+
sfn.body.forEach(checkStmt);
|
|
126
|
+
if (bad.length) {
|
|
127
|
+
throw new ContractError(
|
|
128
|
+
`structuring emitted ill-typed C in '${sfn.name}': ${bad[0]}${bad.length > 1 ? ` (+${bad.length - 1} more)` : ''}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// asmlift — small pure helpers over raw asm TEXT (no parsing): shared by the CLI and the
|
|
2
|
+
// web playground, which both need a function name before they can call `decompile`.
|
|
3
|
+
|
|
4
|
+
/** Best-effort function-name detection: the objdump symbol header, else the `.globl` name,
|
|
5
|
+
* else the first label. Returns undefined when the asm names nothing (caller asks the user). */
|
|
6
|
+
export function detectName(asm: string): string | undefined {
|
|
7
|
+
return (
|
|
8
|
+
asm.match(/^[0-9a-f]+ <([\w.$]+)>:/m)?.[1] ??
|
|
9
|
+
asm.match(/^\s*\.globl\s+([\w.$]+)/m)?.[1] ??
|
|
10
|
+
asm.match(/^([A-Za-z_]\w*):/m)?.[1]
|
|
11
|
+
);
|
|
12
|
+
}
|