@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,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
+ }