@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/package.json +14 -0
  4. package/src/backend/c.ts +20 -0
  5. package/src/backend/cfamily.ts +352 -0
  6. package/src/backend/cpp.ts +145 -0
  7. package/src/backend/pascal.ts +279 -0
  8. package/src/contracts.ts +131 -0
  9. package/src/detect.ts +12 -0
  10. package/src/frontend/asmdata.ts +170 -0
  11. package/src/frontend/disasm.ts +102 -0
  12. package/src/frontend/emit.ts +57 -0
  13. package/src/frontend/errors.ts +14 -0
  14. package/src/frontend/format.ts +47 -0
  15. package/src/frontend/frontend.ts +22 -0
  16. package/src/frontend/mips.ts +875 -0
  17. package/src/frontend/opaque.ts +82 -0
  18. package/src/frontend/ppc.ts +990 -0
  19. package/src/frontend/registry.ts +34 -0
  20. package/src/frontend/ssa.ts +214 -0
  21. package/src/frontend/thumb.ts +1419 -0
  22. package/src/ir/core.ts +104 -0
  23. package/src/ir/opcodes.ts +143 -0
  24. package/src/ir/parse.ts +221 -0
  25. package/src/ir/print.ts +77 -0
  26. package/src/ir/types.ts +106 -0
  27. package/src/ir/verify.ts +221 -0
  28. package/src/l3/ast.ts +301 -0
  29. package/src/l3/basecse.ts +218 -0
  30. package/src/l3/dce.ts +256 -0
  31. package/src/l3/regspell.ts +331 -0
  32. package/src/l3/reindex.ts +447 -0
  33. package/src/l3/typing.ts +145 -0
  34. package/src/mangle.ts +135 -0
  35. package/src/pattern/engine.ts +392 -0
  36. package/src/pipeline.ts +272 -0
  37. package/src/proto.ts +42 -0
  38. package/src/raise/arrays.ts +84 -0
  39. package/src/raise/const.ts +52 -0
  40. package/src/raise/errors.ts +10 -0
  41. package/src/raise/magicdiv.ts +386 -0
  42. package/src/raise/pre-recovery.ts +71 -0
  43. package/src/raise/recover.ts +215 -0
  44. package/src/raise/retsink.ts +72 -0
  45. package/src/raise/shortcircuit.ts +207 -0
  46. package/src/raise/softdiv.ts +62 -0
  47. package/src/raise/struct-arrays.ts +257 -0
  48. package/src/raise/structs.ts +223 -0
  49. package/src/rank.ts +208 -0
  50. package/src/structure/analysis.ts +410 -0
  51. package/src/structure/hazards.ts +142 -0
  52. package/src/structure/loops.ts +169 -0
  53. package/src/structure/structure.ts +1726 -0
  54. package/src/structure/switch-recover.ts +410 -0
  55. package/src/target.ts +140 -0
  56. 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
+ };
@@ -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
+ }