@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
package/src/ir/verify.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// asmlift IR — the verifier. Runs after every pass; a bad edit fails HERE, at its source,
|
|
2
|
+
// not three stages later as wrong output. Invariants:
|
|
3
|
+
// 1. every block ends in exactly one terminator (and it is the last op)
|
|
4
|
+
// 2. operands well-formed: opcode registered, correct arity/attrs
|
|
5
|
+
// 3. SSA: each value defined once; every use is defined; def dominates use
|
|
6
|
+
import { Block, Fn, Value, predecessors } from './core';
|
|
7
|
+
import { opSig } from './opcodes';
|
|
8
|
+
|
|
9
|
+
export class VerifyError extends Error {}
|
|
10
|
+
|
|
11
|
+
// Opcodes admitting EITHER a 2-operand register form OR a 1-operand + `imm` attr form.
|
|
12
|
+
const TWO_OR_IMM = new Set(['sdiv', 'shl', 'shr_u', 'shr_s']);
|
|
13
|
+
|
|
14
|
+
export function verify(fn: Fn): void {
|
|
15
|
+
if (fn.blocks.length === 0) {
|
|
16
|
+
throw new VerifyError(`fn '${fn.name}' has no blocks`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --- collect definitions; reject double-definition ---
|
|
20
|
+
const defined = new Set<Value>();
|
|
21
|
+
const defBlock = new Map<Value, Block>();
|
|
22
|
+
const defIndex = new Map<Value, number>(); // -1 for block params
|
|
23
|
+
const define = (v: Value, b: Block, idx: number, what: string) => {
|
|
24
|
+
if (defined.has(v)) {
|
|
25
|
+
throw new VerifyError(`value defined twice (${what})`);
|
|
26
|
+
}
|
|
27
|
+
defined.add(v);
|
|
28
|
+
defBlock.set(v, b);
|
|
29
|
+
defIndex.set(v, idx);
|
|
30
|
+
};
|
|
31
|
+
for (const b of fn.blocks) {
|
|
32
|
+
for (const p of b.params) {
|
|
33
|
+
define(p, b, -1, 'block param');
|
|
34
|
+
}
|
|
35
|
+
b.ops.forEach((op, idx) => op.results.forEach((r) => define(r, b, idx, `result of '${op.opcode}'`)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- per-op structural + arity + level checks ---
|
|
39
|
+
const at = (b: Block, idx: number) => `(fn '${fn.name}', block ^bb${fn.blocks.indexOf(b)}, op ${idx})`;
|
|
40
|
+
for (const b of fn.blocks) {
|
|
41
|
+
if (b.ops.length === 0) {
|
|
42
|
+
throw new VerifyError(`empty block ^bb${fn.blocks.indexOf(b)} in '${fn.name}'`);
|
|
43
|
+
}
|
|
44
|
+
b.ops.forEach((op, idx) =>
|
|
45
|
+
locate(
|
|
46
|
+
() => {
|
|
47
|
+
const sig = opSig(op.opcode);
|
|
48
|
+
if (!sig) {
|
|
49
|
+
throw new VerifyError(`unknown opcode '${op.opcode}'`);
|
|
50
|
+
}
|
|
51
|
+
if (sig.operands !== 'variadic' && op.operands.length !== sig.operands) {
|
|
52
|
+
throw new VerifyError(`'${op.opcode}' expects ${sig.operands} operands, got ${op.operands.length}`);
|
|
53
|
+
}
|
|
54
|
+
if (op.results.length !== sig.results) {
|
|
55
|
+
throw new VerifyError(`'${op.opcode}' expects ${sig.results} results, got ${op.results.length}`);
|
|
56
|
+
}
|
|
57
|
+
// `sdiv` and the shifts are variadic to admit BOTH forms (2-operand register form, or
|
|
58
|
+
// 1-operand + `imm`), so the generic arity check can't guard them. Enforce the real
|
|
59
|
+
// invariant here — otherwise a malformed op (0 operands, or 1 with no `imm`) would slip
|
|
60
|
+
// through and render `/ undefined` / `<< undefined` downstream instead of failing at its
|
|
61
|
+
// source.
|
|
62
|
+
if (
|
|
63
|
+
TWO_OR_IMM.has(op.opcode) &&
|
|
64
|
+
!(op.operands.length === 2 || (op.operands.length === 1 && 'imm' in op.attrs))
|
|
65
|
+
) {
|
|
66
|
+
throw new VerifyError(
|
|
67
|
+
`'${op.opcode}' must be 2 operands OR 1 operand with an 'imm' attr, got ${op.operands.length} operands`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
// `ret` is variadic to admit the void form; anything past one returned value is malformed.
|
|
71
|
+
if (op.opcode === 'ret' && op.operands.length > 1) {
|
|
72
|
+
throw new VerifyError(`'ret' takes at most 1 operand, got ${op.operands.length}`);
|
|
73
|
+
}
|
|
74
|
+
const isTerm = !!sig.terminator;
|
|
75
|
+
const isLast = idx === b.ops.length - 1;
|
|
76
|
+
if (isTerm && !isLast) {
|
|
77
|
+
throw new VerifyError(`terminator '${op.opcode}' is not the last op in its block`);
|
|
78
|
+
}
|
|
79
|
+
if (!isTerm && isLast) {
|
|
80
|
+
throw new VerifyError(`block does not end in a terminator (ends with '${op.opcode}')`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof sig.successors === 'number' && op.successors.length !== sig.successors) {
|
|
83
|
+
throw new VerifyError(`'${op.opcode}' expects ${sig.successors} successors, got ${op.successors.length}`);
|
|
84
|
+
}
|
|
85
|
+
// `switch_br` has variadic successors (N cases + 1 default). Enforce its real invariants here (as
|
|
86
|
+
// the generic count check can't): ≥2 successors, a `cases` list index-aligned with the first N,
|
|
87
|
+
// and DISTINCT case values (a duplicate would only surface as a `duplicate case` error at recompile).
|
|
88
|
+
if (op.opcode === 'switch_br') {
|
|
89
|
+
if (op.successors.length < 2) {
|
|
90
|
+
throw new VerifyError(`'switch_br' needs ≥2 successors (cases + default), got ${op.successors.length}`);
|
|
91
|
+
}
|
|
92
|
+
const cases = op.attrs.cases;
|
|
93
|
+
if (!Array.isArray(cases) || cases.length !== op.successors.length - 1) {
|
|
94
|
+
throw new VerifyError(
|
|
95
|
+
`'switch_br' 'cases' must have (successors - 1) = ${op.successors.length - 1} entries`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (new Set(cases as number[]).size !== cases.length) {
|
|
99
|
+
throw new VerifyError(`'switch_br' has duplicate case values`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!isTerm && op.successors.length) {
|
|
103
|
+
throw new VerifyError(`non-terminator '${op.opcode}' has successors`);
|
|
104
|
+
}
|
|
105
|
+
for (const k of sig.requiredAttrs ?? []) {
|
|
106
|
+
if (!(k in op.attrs)) {
|
|
107
|
+
throw new VerifyError(`'${op.opcode}' missing required attr '${k}'`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const u of op.operands) {
|
|
111
|
+
if (!defined.has(u)) {
|
|
112
|
+
throw new VerifyError(`use of undefined value in '${op.opcode}'`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const s of op.successors) {
|
|
116
|
+
if (!fn.blocks.includes(s.block)) {
|
|
117
|
+
throw new VerifyError(`successor of '${op.opcode}' is not a block of this fn`);
|
|
118
|
+
}
|
|
119
|
+
if (s.args.length !== s.block.params.length) {
|
|
120
|
+
throw new VerifyError(
|
|
121
|
+
`successor of '${op.opcode}' passes ${s.args.length} args to a block with ${s.block.params.length} params`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
for (const u of s.args) {
|
|
125
|
+
if (!defined.has(u)) {
|
|
126
|
+
throw new VerifyError(`use of undefined value in successor args of '${op.opcode}'`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
() => at(b, idx),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- dominance (iterative dominators over the CFG) ---
|
|
137
|
+
const entry = fn.blocks[0];
|
|
138
|
+
const preds = predecessors(fn);
|
|
139
|
+
const dom = new Map<Block, Set<Block>>();
|
|
140
|
+
const allBlocks = new Set(fn.blocks);
|
|
141
|
+
for (const b of fn.blocks) {
|
|
142
|
+
dom.set(b, b === entry ? new Set([entry]) : new Set(allBlocks));
|
|
143
|
+
}
|
|
144
|
+
let changed = true;
|
|
145
|
+
while (changed) {
|
|
146
|
+
changed = false;
|
|
147
|
+
for (const b of fn.blocks) {
|
|
148
|
+
if (b === entry) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
let inter: Set<Block> | null = null;
|
|
152
|
+
for (const p of preds.get(b)!) {
|
|
153
|
+
const dp = dom.get(p)!;
|
|
154
|
+
if (inter === null) {
|
|
155
|
+
inter = new Set(dp);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
for (const x of inter) {
|
|
159
|
+
if (!dp.has(x)) {
|
|
160
|
+
inter.delete(x);
|
|
161
|
+
}
|
|
162
|
+
} // intersect in place (spec-safe delete-in-iter)
|
|
163
|
+
}
|
|
164
|
+
const next = new Set<Block>(inter ?? []);
|
|
165
|
+
next.add(b);
|
|
166
|
+
if (!setEq(next, dom.get(b)!)) {
|
|
167
|
+
dom.set(b, next);
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const dominates = (a: Block, b: Block) => dom.get(b)!.has(a);
|
|
173
|
+
|
|
174
|
+
for (const b of fn.blocks) {
|
|
175
|
+
b.ops.forEach((op, idx) =>
|
|
176
|
+
locate(
|
|
177
|
+
() => {
|
|
178
|
+
const checkUse = (u: Value) => {
|
|
179
|
+
const db = defBlock.get(u)!;
|
|
180
|
+
if (db === b) {
|
|
181
|
+
const di = defIndex.get(u)!;
|
|
182
|
+
if (di >= 0 && di >= idx) {
|
|
183
|
+
throw new VerifyError(`use before def in '${op.opcode}'`);
|
|
184
|
+
}
|
|
185
|
+
} else if (!dominates(db, b)) {
|
|
186
|
+
throw new VerifyError(`def does not dominate use in '${op.opcode}'`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
op.operands.forEach(checkUse);
|
|
190
|
+
op.successors.forEach((s) => s.args.forEach(checkUse));
|
|
191
|
+
},
|
|
192
|
+
() => at(b, idx),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Run a check body; a VerifyError it throws is re-thrown with the op's location appended —
|
|
199
|
+
* "value defined twice" is unactionable without WHICH block/op. */
|
|
200
|
+
function locate(body: () => void, where: () => string): void {
|
|
201
|
+
try {
|
|
202
|
+
body();
|
|
203
|
+
} catch (e) {
|
|
204
|
+
if (e instanceof VerifyError) {
|
|
205
|
+
throw new VerifyError(`${e.message} ${where()}`);
|
|
206
|
+
}
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function setEq(a: Set<Block>, b: Set<Block>): boolean {
|
|
212
|
+
if (a.size !== b.size) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
for (const x of a) {
|
|
216
|
+
if (!b.has(x)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
package/src/l3/ast.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// asmlift L3 — the language-NEUTRAL structured AST. A LanguageBackend lowers this to a
|
|
2
|
+
// concrete language (C / Pascal / C++) and prints it. "Return a value" and binary ops
|
|
3
|
+
// are neutral nodes here; each backend owns its own spelling.
|
|
4
|
+
import type { IrType } from '../ir/types';
|
|
5
|
+
|
|
6
|
+
export type Expr =
|
|
7
|
+
| { k: 'var'; name: string }
|
|
8
|
+
| { k: 'const'; value: number }
|
|
9
|
+
| { k: 'bin'; op: BinOp; l: Expr; r: Expr }
|
|
10
|
+
| { k: 'un'; op: '-' | '~' | '!'; e: Expr }
|
|
11
|
+
// A C-style value cast `(T)e`. Tree-level producers: a width-narrowing cast `(u8)e` (the
|
|
12
|
+
// recovered form of a byte/half extend idiom — zext/sext IR ops), the STRUCT-pointer cast
|
|
13
|
+
// (structure.ts memAccess/arrayAccess struct paths — see the note on `field` below), and the
|
|
14
|
+
// integer legalization of a pointer operand under an operator C rejects (structure.ts intify,
|
|
15
|
+
// `3 & (s32)p`). Scalar deref casts are backend-owned — the C-family printer synthesizes
|
|
16
|
+
// them from the `index` node's width. Each backend spells the cast in its own syntax
|
|
17
|
+
// (C: `(u8)e`; Pascal: no spelling yet → fails loud).
|
|
18
|
+
| { k: 'cast'; to: IrType; e: Expr }
|
|
19
|
+
| { k: 'call'; fn: string; args: Expr[] }
|
|
20
|
+
// The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
|
|
21
|
+
// DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
|
|
22
|
+
// and `(&gSym)[i]` as `gSym[i]` (a global name decays to a pointer). Only a genuinely
|
|
23
|
+
// address-TAKEN global (passed by address, `&gSym` as a call arg) prints the `&` form. The
|
|
24
|
+
// global's type comes from the project headers, so it is never declared as a local.
|
|
25
|
+
| { k: 'addr'; name: string }
|
|
26
|
+
// A memory access `base[idx]` (printed `*base` when idx is the constant 0), CARRYING the
|
|
27
|
+
// access's element width (bytes) and signedness. `idx` counts elements of `width` bytes.
|
|
28
|
+
// Because the node carries the width, EACH BACKEND owns its own legalization: the C family
|
|
29
|
+
// checks whether `base`'s rendered C type strides `width` and inserts the reinterpret cast
|
|
30
|
+
// itself when it does not (`*(u8 *)(a0 + a1)`); Pascal loud-declines a base it cannot spell
|
|
31
|
+
// faithfully (see also cpp.ts's sub-word guard and `field`'s name-encoded offset).
|
|
32
|
+
//
|
|
33
|
+
// Still not fully language-neutral — the idx ≠ 0 form is a known C-idiom:
|
|
34
|
+
// • C backend: `*base` (idx 0) and `base[idx]` (idx ≠ 0) — both valid.
|
|
35
|
+
// • Pascal backend: `base^` (idx 0) is valid IDO Pascal, but `base[idx]` (idx ≠ 0) is
|
|
36
|
+
// REJECTED by `upas` — SGI Pascal has no bare-pointer indexing
|
|
37
|
+
// (packages/cli/test/matching/mips-memory.test.ts).
|
|
38
|
+
// Variable-index `a[i]` is recovered at the IR level (`aload`/`astore` carry elemSize;
|
|
39
|
+
// raise/arrays.ts) but still LOWERS to this one C-shaped `index` node, so it stays C-only
|
|
40
|
+
// (a Pascal array-access spelling is future work). Treat `index` with idx ≠ 0 as C-shaped.
|
|
41
|
+
| { k: 'index'; base: Expr; idx: Expr; width: number; signed: boolean }
|
|
42
|
+
// A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
|
|
43
|
+
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
44
|
+
// Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
|
|
45
|
+
// not a width-scaled number — the byte-offset-carrying member access cpp.ts's sub-word guard needs.
|
|
46
|
+
| { k: 'field'; base: Expr; name: string }
|
|
47
|
+
// A GAP MARKER — the annotate-mode (`onGap: "annotate"`) spelling of a value asmlift could not
|
|
48
|
+
// faithfully lift (an unmodelled instruction's `opaque` result, an unlowered transient op, a
|
|
49
|
+
// dropped def). Every backend spells it as a call to the UNDEFINED symbol `ASMLIFT_ERROR("reason",
|
|
50
|
+
// args…)` (the m2c `M2C_ERROR` discipline): the surrounding function is complete and readable, but
|
|
51
|
+
// the source does NOT compile until the user consciously defines the macro — loud in the ARTIFACT
|
|
52
|
+
// instead of loud in the process. `args` carry the source operands for context. Strict mode (the
|
|
53
|
+
// default) never produces this node; it keeps the `"?"` sentinel → ContractError behavior.
|
|
54
|
+
| { k: 'marker'; reason: string; args: Expr[] };
|
|
55
|
+
|
|
56
|
+
export type BinOp =
|
|
57
|
+
'+' | '-' | '*' | '/' | '%' | '<' | '<=' | '>' | '>=' | '==' | '!=' | '&' | '|' | '^' | '<<' | '>>' | '&&' | '||';
|
|
58
|
+
|
|
59
|
+
export type Stmt =
|
|
60
|
+
| { k: 'assign'; name: string; value: Expr }
|
|
61
|
+
// A memory write to an lvalue expression (`index` → `base[idx] = value` / `*base = value`;
|
|
62
|
+
// `field` → `base->name = value`). Carrying the lvalue as an Expr keeps stores symmetric with
|
|
63
|
+
// the load side, so the same leaf-hook / field spelling serves reads and writes alike.
|
|
64
|
+
| { k: 'store'; lval: Expr; value: Expr }
|
|
65
|
+
| { k: 'exprstmt'; value: Expr } // a side-effecting expression (e.g. a void call)
|
|
66
|
+
| { k: 'if'; cond: Expr; then: Stmt[]; else: Stmt[] }
|
|
67
|
+
| { k: 'while'; cond: Expr; body: Stmt[] }
|
|
68
|
+
// A bottom-tested loop `do { body } while (cond);` — the body runs at least once, then the test at
|
|
69
|
+
// the BOTTOM decides re-entry. This is the shape a compiler emits for a loop whose trip count it can
|
|
70
|
+
// prove ≥1 (guard elided) or a source `do-while`. Distinct from `while` (test-at-top, body may run
|
|
71
|
+
// 0 times) — the two are NOT interchangeable for matching.
|
|
72
|
+
| { k: 'dowhile'; cond: Expr; body: Stmt[] }
|
|
73
|
+
// A counted loop `for (init; cond; inc) { body }` — a PURE RE-SPELLING of a test-at-top `while`
|
|
74
|
+
// (quality only). Produced ONLY by recognizing a `while` whose induction variable's init literally
|
|
75
|
+
// precedes it and whose increment is literally the body's last statement, WITHOUT moving any op:
|
|
76
|
+
// `assign(iv,e0); while(c){ …; assign(iv,e1) }` becomes `for(assign(iv,e0); c; assign(iv,e1)){ … }`.
|
|
77
|
+
// Semantically identical to that desugaring — with ONE exception the recognizer guards against: a
|
|
78
|
+
// `continue` in the body RUNS `inc` under `for` but SKIPS it under `while`, so a body containing a
|
|
79
|
+
// same-level `continue` is NOT converted. `init`/`inc` are Stmts (an `assign`); a backend that
|
|
80
|
+
// cannot spell native `for` may always fall back to the `while` desugaring (Pascal does), so this
|
|
81
|
+
// node never forces a loud-fail.
|
|
82
|
+
| { k: 'for'; init: Stmt; cond: Expr; inc: Stmt; body: Stmt[] }
|
|
83
|
+
// `break;` / `continue;` — a loop early-exit / next-iteration jump. Emitted ONLY when the target is
|
|
84
|
+
// the innermost enclosing loop (bare C break/continue cannot express a multi-level exit; a deeper
|
|
85
|
+
// target declines). SGI/IDO Pascal has neither, so its backend loud-fails them (like `field`/`cast`).
|
|
86
|
+
| { k: 'break' }
|
|
87
|
+
| { k: 'continue' }
|
|
88
|
+
// A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
|
|
89
|
+
// a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
|
|
90
|
+
// is emitted last.
|
|
91
|
+
//
|
|
92
|
+
// NON-NEUTRALITY NOTE (like the `index` node above): `fallsThrough` encodes a C/C++ control-flow
|
|
93
|
+
// concept POSITIONALLY — `cases[i].fallsThrough === true` means control continues into
|
|
94
|
+
// `cases[i+1].body`, so the array ORDER is semantically load-bearing (a backend that reorders cases
|
|
95
|
+
// would break fall-through). C/C++ spell it natively; Pascal `case-of` has NO fall-through, so the
|
|
96
|
+
// Pascal backend MUST loud-fail a `fallsThrough` case (it has no faithful spelling), exactly as it
|
|
97
|
+
// loud-fails `field`/`cast`. Recovery must therefore only set `fallsThrough` when the fall-through
|
|
98
|
+
// target is the emission-adjacent case.
|
|
99
|
+
| { k: 'switch'; scrutinee: Expr; cases: SwitchCase[]; default?: Stmt[] }
|
|
100
|
+
| { k: 'return'; value?: Expr };
|
|
101
|
+
|
|
102
|
+
/** One arm of a `switch`. `values` stacks multiple `case K:` labels onto one body (`case 1: case 2:`).
|
|
103
|
+
* `fallsThrough` true ⇒ the body flows into the NEXT arm (no `break;`); see the non-neutrality note. */
|
|
104
|
+
export interface SwitchCase {
|
|
105
|
+
values: number[];
|
|
106
|
+
body: Stmt[];
|
|
107
|
+
fallsThrough: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface SFn {
|
|
111
|
+
name: string;
|
|
112
|
+
params: { name: string; type: IrType }[];
|
|
113
|
+
locals: { name: string; type: IrType }[]; // recovered locals, declared at function top
|
|
114
|
+
retType: IrType;
|
|
115
|
+
body: Stmt[];
|
|
116
|
+
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
117
|
+
* unless raise/structs.ts recovered a struct. Sorted by name for deterministic output. */
|
|
118
|
+
structs?: StructType[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
|
|
122
|
+
* type but lives in the neutral AST so a backend can print `struct N { ... };`. */
|
|
123
|
+
export interface StructType {
|
|
124
|
+
name: string;
|
|
125
|
+
fields: { off: number; type: IrType; name: string }[];
|
|
126
|
+
size?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A language backend: emits one L3 AST as concrete-language source, plus the language's
|
|
130
|
+
* comment spelling. */
|
|
131
|
+
export interface LanguageBackend {
|
|
132
|
+
readonly id: 'c' | 'cpp' | 'pascal';
|
|
133
|
+
emit(fn: SFn): string;
|
|
134
|
+
// Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
|
|
135
|
+
// Used by the annotate-mode stub path to carry the failure reason + the original asm
|
|
136
|
+
// alongside the emitted marker, so a human/LLM has the raw material to finish by hand.
|
|
137
|
+
comment(text: string): string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The dot-form base of a `field` node — the array-element `index` node under an
|
|
141
|
+
* `arr[i].field` access — or undefined for the arrow form. THE one copy of the dot-vs-arrow
|
|
142
|
+
* rule, as a NARROWING accessor (no bare `as` at the consumers): the C-family printer spells
|
|
143
|
+
* from it and the deref contract (assertDerefsTyped) type-checks against it; a per-consumer
|
|
144
|
+
* copy would let the two silently disagree on the same AST. */
|
|
145
|
+
export function dotBase(f: Extract<Expr, { k: 'field' }>): Extract<Expr, { k: 'index' }> | undefined {
|
|
146
|
+
return f.base.k === 'index' ? f.base : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Boolean projection of `dotBase` for conditions that need no narrowing. */
|
|
150
|
+
export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
|
|
151
|
+
return dotBase(f) !== undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Structural equality of two expression trees. THE one copy of Expr deep-equal (like
|
|
155
|
+
* fieldSpellsDot/derefStrideOk): key-order-independent by construction (a switch, not a
|
|
156
|
+
* stringify), exhaustive under noImplicitReturns like the walkers below. */
|
|
157
|
+
export function exprEquals(a: Expr, b: Expr): boolean {
|
|
158
|
+
if (a.k !== b.k) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
switch (a.k) {
|
|
162
|
+
case 'var':
|
|
163
|
+
return a.name === (b as typeof a).name;
|
|
164
|
+
case 'addr':
|
|
165
|
+
return a.name === (b as typeof a).name;
|
|
166
|
+
case 'const':
|
|
167
|
+
return a.value === (b as typeof a).value;
|
|
168
|
+
case 'bin': {
|
|
169
|
+
const bb = b as typeof a;
|
|
170
|
+
return a.op === bb.op && exprEquals(a.l, bb.l) && exprEquals(a.r, bb.r);
|
|
171
|
+
}
|
|
172
|
+
case 'un': {
|
|
173
|
+
const bb = b as typeof a;
|
|
174
|
+
return a.op === bb.op && exprEquals(a.e, bb.e);
|
|
175
|
+
}
|
|
176
|
+
case 'cast': {
|
|
177
|
+
const bb = b as typeof a;
|
|
178
|
+
return JSON.stringify(a.to) === JSON.stringify(bb.to) && exprEquals(a.e, bb.e);
|
|
179
|
+
}
|
|
180
|
+
case 'call': {
|
|
181
|
+
const bb = b as typeof a;
|
|
182
|
+
return a.fn === bb.fn && a.args.length === bb.args.length && a.args.every((x, i) => exprEquals(x, bb.args[i]));
|
|
183
|
+
}
|
|
184
|
+
case 'index': {
|
|
185
|
+
const bb = b as typeof a;
|
|
186
|
+
return a.width === bb.width && a.signed === bb.signed && exprEquals(a.base, bb.base) && exprEquals(a.idx, bb.idx);
|
|
187
|
+
}
|
|
188
|
+
case 'field': {
|
|
189
|
+
const bb = b as typeof a;
|
|
190
|
+
return a.name === bb.name && exprEquals(a.base, bb.base);
|
|
191
|
+
}
|
|
192
|
+
case 'marker': {
|
|
193
|
+
const bb = b as typeof a;
|
|
194
|
+
return (
|
|
195
|
+
a.reason === bb.reason && a.args.length === bb.args.length && a.args.every((x, i) => exprEquals(x, bb.args[i]))
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
|
|
202
|
+
// Every generic walker derives from these helpers, so a NEW node kind is a compile error in
|
|
203
|
+
// exactly one place per union (the switches are exhaustive under noImplicitReturns) — a
|
|
204
|
+
// hand-rolled walker that misses a node kind is a silent bug. Specialized walkers with per-kind
|
|
205
|
+
// SEMANTICS (loop-boundary scans like hasEnclosingContinue, rebuilding transforms like
|
|
206
|
+
// recognizeForLoops) rightly keep their own switches.
|
|
207
|
+
|
|
208
|
+
/** The direct sub-expressions of `e`, in syntactic order. */
|
|
209
|
+
export function exprChildren(e: Expr): Expr[] {
|
|
210
|
+
switch (e.k) {
|
|
211
|
+
case 'var':
|
|
212
|
+
case 'const':
|
|
213
|
+
case 'addr':
|
|
214
|
+
return [];
|
|
215
|
+
case 'bin':
|
|
216
|
+
return [e.l, e.r];
|
|
217
|
+
case 'un':
|
|
218
|
+
case 'cast':
|
|
219
|
+
return [e.e];
|
|
220
|
+
case 'call':
|
|
221
|
+
return e.args;
|
|
222
|
+
case 'index':
|
|
223
|
+
return [e.base, e.idx];
|
|
224
|
+
case 'field':
|
|
225
|
+
return [e.base];
|
|
226
|
+
case 'marker':
|
|
227
|
+
return e.args;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Rebuild `e` with each direct sub-expression mapped through `f` (shallow; recurse in `f`). */
|
|
232
|
+
export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
|
|
233
|
+
switch (e.k) {
|
|
234
|
+
case 'var':
|
|
235
|
+
case 'const':
|
|
236
|
+
case 'addr':
|
|
237
|
+
return e;
|
|
238
|
+
case 'bin':
|
|
239
|
+
return { ...e, l: f(e.l), r: f(e.r) };
|
|
240
|
+
case 'un':
|
|
241
|
+
case 'cast':
|
|
242
|
+
return { ...e, e: f(e.e) };
|
|
243
|
+
case 'call':
|
|
244
|
+
return { ...e, args: e.args.map(f) };
|
|
245
|
+
case 'index':
|
|
246
|
+
return { ...e, base: f(e.base), idx: f(e.idx) };
|
|
247
|
+
case 'field':
|
|
248
|
+
return { ...e, base: f(e.base) };
|
|
249
|
+
case 'marker':
|
|
250
|
+
return { ...e, args: e.args.map(f) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The expressions a statement DIRECTLY contains, in syntactic order. */
|
|
255
|
+
export function stmtExprs(s: Stmt): Expr[] {
|
|
256
|
+
switch (s.k) {
|
|
257
|
+
case 'assign':
|
|
258
|
+
return [s.value];
|
|
259
|
+
case 'store':
|
|
260
|
+
return [s.lval, s.value];
|
|
261
|
+
case 'exprstmt':
|
|
262
|
+
return [s.value];
|
|
263
|
+
case 'return':
|
|
264
|
+
return s.value ? [s.value] : [];
|
|
265
|
+
case 'if':
|
|
266
|
+
case 'while':
|
|
267
|
+
case 'dowhile':
|
|
268
|
+
return [s.cond];
|
|
269
|
+
case 'for':
|
|
270
|
+
return [s.cond];
|
|
271
|
+
case 'switch':
|
|
272
|
+
return [s.scrutinee];
|
|
273
|
+
case 'break':
|
|
274
|
+
case 'continue':
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The statements a statement DIRECTLY contains. NOTE for document-order walks: a `for`'s
|
|
280
|
+
* init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
|
|
281
|
+
* sees the cond before the init. */
|
|
282
|
+
export function stmtChildren(s: Stmt): Stmt[] {
|
|
283
|
+
switch (s.k) {
|
|
284
|
+
case 'assign':
|
|
285
|
+
case 'store':
|
|
286
|
+
case 'exprstmt':
|
|
287
|
+
case 'return':
|
|
288
|
+
case 'break':
|
|
289
|
+
case 'continue':
|
|
290
|
+
return [];
|
|
291
|
+
case 'if':
|
|
292
|
+
return [...s.then, ...s.else];
|
|
293
|
+
case 'while':
|
|
294
|
+
case 'dowhile':
|
|
295
|
+
return s.body;
|
|
296
|
+
case 'for':
|
|
297
|
+
return [s.init, s.inc, ...s.body];
|
|
298
|
+
case 'switch':
|
|
299
|
+
return [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])];
|
|
300
|
+
}
|
|
301
|
+
}
|