@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
package/src/ir/core.ts ADDED
@@ -0,0 +1,104 @@
1
+ // asmlift IR — the MLIR-lite substrate shared by all levels.
2
+ //
3
+ // - a CFG of basic blocks with TYPED BLOCK-ARGUMENTS (functional-form SSA); no phi
4
+ // - exactly one terminator per block; terminators carry successors + block-arg lists
5
+ // - Value identity is OBJECT IDENTITY, owned by the graph — no module-global counter;
6
+ // textual names are assigned at print time by deterministic traversal
7
+ // - passes transform via replaceAllUsesWith, never in-place opcode/type mutation
8
+ //
9
+ // The two real representations are this `Fn` (typed-SSA) and the structured `SFn` AST; type
10
+ // recovery is an in-place pass on `Fn`.
11
+ import type { Opcode } from './opcodes';
12
+ import type { IrType } from './types';
13
+
14
+ export type AttrVal = number | boolean | string | number[];
15
+
16
+ /** An SSA value. Identity is the object itself; the type may be `unknown` at L1. */
17
+ export interface Value {
18
+ type: IrType;
19
+ }
20
+
21
+ /** A branch target: which block, and the arguments bound to its block-parameters. */
22
+ export interface Successor {
23
+ block: Block;
24
+ args: Value[];
25
+ }
26
+
27
+ export interface Op {
28
+ opcode: string;
29
+ operands: Value[];
30
+ results: Value[];
31
+ attrs: Record<string, AttrVal>;
32
+ successors: Successor[]; // non-empty only for terminators
33
+ }
34
+
35
+ /** A basic block. `params` are its block-arguments. Must end in exactly one terminator. */
36
+ export interface Block {
37
+ params: Value[];
38
+ ops: Op[];
39
+ }
40
+
41
+ /** A function. `blocks[0]` is the entry; its params are the function parameters. */
42
+ export interface Fn {
43
+ name: string;
44
+ blocks: Block[];
45
+ }
46
+
47
+ export function mkValue(type: IrType): Value {
48
+ return { type };
49
+ }
50
+
51
+ export function mkOp(opcode: Opcode, o: Partial<Op> = {}): Op {
52
+ return {
53
+ opcode,
54
+ operands: o.operands ?? [],
55
+ results: o.results ?? [],
56
+ attrs: o.attrs ?? {},
57
+ successors: o.successors ?? [],
58
+ };
59
+ }
60
+
61
+ /** The successor blocks of `b`, read off its terminator. */
62
+ export function successorsOf(b: Block): Block[] {
63
+ const term = b.ops[b.ops.length - 1];
64
+ return term ? term.successors.map((s) => s.block) : [];
65
+ }
66
+
67
+ /** Predecessor map for the whole function's CFG. */
68
+ export function predecessors(fn: Fn): Map<Block, Block[]> {
69
+ const preds = new Map<Block, Block[]>();
70
+ for (const b of fn.blocks) {
71
+ preds.set(b, []);
72
+ }
73
+ for (const b of fn.blocks) {
74
+ for (const s of successorsOf(b)) {
75
+ preds.get(s)!.push(b);
76
+ }
77
+ }
78
+ return preds;
79
+ }
80
+
81
+ /** Every value defined by an op result → its defining op (block params excluded). */
82
+ export function defOpMap(fn: Fn): Map<Value, Op> {
83
+ const m = new Map<Value, Op>();
84
+ for (const b of fn.blocks) {
85
+ for (const op of b.ops) {
86
+ for (const r of op.results) {
87
+ m.set(r, op);
88
+ }
89
+ }
90
+ }
91
+ return m;
92
+ }
93
+
94
+ /** Replace every use of `oldV` with `newV` (operands + successor args). No in-place op mutation. */
95
+ export function replaceAllUsesWith(fn: Fn, oldV: Value, newV: Value): void {
96
+ for (const b of fn.blocks) {
97
+ for (const op of b.ops) {
98
+ op.operands = op.operands.map((v) => (v === oldV ? newV : v));
99
+ for (const s of op.successors) {
100
+ s.args = s.args.map((v) => (v === oldV ? newV : v));
101
+ }
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,143 @@
1
+ // asmlift IR — the opcode signature registry.
2
+ //
3
+ // A closed table of signatures. The verifier and the parser are driven by it, so a mnemonic
4
+ // typo or an operand-count mismatch fails at its source instead of surfacing as wrong output
5
+ // several stages later.
6
+
7
+ export interface OpSig {
8
+ /** exact operand count, or "variadic" (e.g. ret takes 0 or 1). */
9
+ operands: number | 'variadic';
10
+ results: number;
11
+ terminator?: boolean;
12
+ /** required successor count (terminators only), or "variadic" (switch_br: N cases + 1 default). */
13
+ successors?: number | 'variadic';
14
+ requiredAttrs?: readonly string[];
15
+ /** observable side effect (memory write / call): never deleted when dead, never hoisted into
16
+ * an unconditional position. THE one effect vocabulary — DCE (pattern/engine.ts) and the
17
+ * short-circuit hoist guard (raise/shortcircuit.ts) both derive from this flag. */
18
+ effects?: boolean;
19
+ }
20
+
21
+ export const OPCODES = {
22
+ // --- pure integer ops ---
23
+ const: { operands: 0, results: 1, requiredAttrs: ['value'] },
24
+ add: { operands: 2, results: 1 },
25
+ sub: { operands: 2, results: 1 },
26
+ mul: { operands: 2, results: 1 },
27
+ // High word of the 32x32->64 product: `mulh` signed, `mulhu` unsigned. TRANSIENT — emitted by the
28
+ // frontend (MIPS `mfhi` after `mult`/`multu`; PPC `mulhw`/`mulhwu`) and rewritten away by the
29
+ // magic-division recognizer (raise/magicdiv.ts) before recovery. They carry no C spelling: a `mulh`
30
+ // that survives to the structurer hits the `"?"` loud-fail (like a bare `clz`) — never printed.
31
+ // Effect-free (no `effects` flag) so DCE reaps a dead one.
32
+ mulh: { operands: 2, results: 1 },
33
+ mulhu: { operands: 2, results: 1 },
34
+ neg: { operands: 1, results: 1 },
35
+ not: { operands: 1, results: 1 }, // bitwise complement (`mvn`) → ~x
36
+ or: { operands: 2, results: 1 },
37
+ and: { operands: 2, results: 1 },
38
+ xor: { operands: 2, results: 1 },
39
+ // shifts take EITHER 1 operand + `imm` attr (immediate: `lsl rD,rS,#n`) OR 2 operands
40
+ // (register: `lsl rD,rS,rN`). Variadic so both forms verify; the structurer branches on
41
+ // operand count (structure.ts) to print `x << n` vs `x << y`.
42
+ shl: { operands: 'variadic', results: 1 },
43
+ shr_u: { operands: 'variadic', results: 1 },
44
+ shr_s: { operands: 'variadic', results: 1 },
45
+ // Rotates, variadic like the shifts (immediate: PPC `rotlwi`; register: Thumb `ror`, PPC
46
+ // `rotlw`). Lowered by the structurer to the C rotate idiom (`x >> n | x << (32 - n)` /
47
+ // mirrored for rotl), which agbcc AND mwcc compile back to the single rotate instruction —
48
+ // byte-exact round-trip verified both ways before these ops landed.
49
+ rotr: { operands: 'variadic', results: 1 },
50
+ rotl: { operands: 'variadic', results: 1 },
51
+ // Count leading zeros (PPC `cntlzw`). TRANSIENT like mulh: the cntlzw-equality pattern
52
+ // (pattern/engine.ts CNTLZW_EQ0, mwcc-gated) folds `clz(x) >> 5` → `x == 0` (mwcc's spelling
53
+ // of ==0 and `!`); a bare clz that survives has no C spelling → the structurer's loud gap.
54
+ clz: { operands: 1, results: 1 },
55
+ // width-narrowing casts (S4): `zext`/`sext` take one operand and a `width` attr (8/16), and
56
+ // widen back to 32 with zero/sign extension — the recovered form of a compiler's byte/half
57
+ // extend idiom (`(x<<24)>>24` etc.). The backend prints them as a C cast `(u8)x` / `(s8)x`;
58
+ // recompiling the cast reproduces the extend sequence on the compilers that emit it. Produced
59
+ // by the cast idiom patterns (pattern/engine.ts), gated to those compilers.
60
+ zext: { operands: 1, results: 1, requiredAttrs: ['width'] },
61
+ sext: { operands: 1, results: 1, requiredAttrs: ['width'] },
62
+ // Division/remainder. `sdiv` is variadic like the shifts: the immediate form (1 operand +
63
+ // `imm` attr) is the strength-reduced constant divisor an idiom folds to (`sdiv X {imm=2}`);
64
+ // the register form (2 operands) is a real hardware divide (`div`/`divu` + `mflo`/`mfhi` on an
65
+ // ISA with `capabilities.hwDivide`); the structurer branches on count. `udiv`/`smod`/`umod`
66
+ // are 2-operand only. `sdiv`/`udiv` = quotient, `smod`/`umod` = remainder; signedness lives in
67
+ // the op (recovery types the operands to match), so the backend picks `/`/`%` over
68
+ // correctly-typed operands.
69
+ sdiv: { operands: 'variadic', results: 1 },
70
+ udiv: { operands: 2, results: 1 },
71
+ smod: { operands: 2, results: 1 },
72
+ umod: { operands: 2, results: 1 },
73
+ // signed/equality comparisons (result is a boolean-valued u32)
74
+ icmp_slt: { operands: 2, results: 1 },
75
+ icmp_sle: { operands: 2, results: 1 },
76
+ icmp_sgt: { operands: 2, results: 1 },
77
+ icmp_sge: { operands: 2, results: 1 },
78
+ // unsigned comparisons (MIPS `sltu`/`sltiu`; the operator is the same `<`, unsignedness lives
79
+ // in the operand TYPES — recover types their operands u32, so the backend emits `sltu`).
80
+ icmp_ult: { operands: 2, results: 1 },
81
+ icmp_ule: { operands: 2, results: 1 },
82
+ icmp_ugt: { operands: 2, results: 1 },
83
+ icmp_uge: { operands: 2, results: 1 },
84
+ icmp_eq: { operands: 2, results: 1 },
85
+ icmp_ne: { operands: 2, results: 1 },
86
+ // Short-circuit logical connectives (`&&`/`||`), produced by the boolean short-circuit recognizer
87
+ // (raise/shortcircuit.ts) from a value-merge diamond. Both operands are boolean-valued (0/1); the
88
+ // result is the 0/1 connective. Distinct from bitwise `and`/`or` — the backend prints `&&`/`||`,
89
+ // which recompiles to the branch diamond the source emitted.
90
+ logic_and: { operands: 2, results: 1 },
91
+ logic_or: { operands: 2, results: 1 },
92
+ // --- memory ---
93
+ load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'] },
94
+ store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
95
+ // Typed element-scaled array access. Unlike load/store's constant `off`, these carry an
96
+ // explicit runtime `index` operand plus the `elemSize` the index scales by, so the base is a
97
+ // genuine `elem *` and no byte-offset arithmetic leaks into the emitted source. Produced by
98
+ // the array-recognition legalization pass (raise/arrays.ts).
99
+ aload: { operands: 2, results: 1, requiredAttrs: ['elemSize', 'signed'] }, // aload base, index
100
+ astore: { operands: 3, results: 0, requiredAttrs: ['elemSize'], effects: true }, // astore base, index, value
101
+ // --- call: operands are the argument values (r0..), result is the return value (r0),
102
+ // `target` attr is the callee symbol. Caller-saved clobbering is implicit. ---
103
+ call: { operands: 'variadic', results: 1, requiredAttrs: ['target'], effects: true },
104
+ // The ADDRESS of a named global (agbcc `ldr rD, .Lpool` where the pool word is `.word gSym`).
105
+ // Pure, 0 operands. Globals come from the project headers, so they are referenced by name, never
106
+ // declared as locals. The structurer lowers it three ways (see scalarGlobals in structure.ts):
107
+ // - a load/store through an off-0 SCALAR gaddr → a bare global `gSym` / `gSym = v`;
108
+ // - an indexed or non-zero-offset AGGREGATE access → the address-cast `((T *)&gSym)[i]`;
109
+ // - any other use (e.g. `&gSym` passed to a call) → the `{k:'addr'}` L3 node, printed `&gSym`.
110
+ gaddr: { operands: 0, results: 1, requiredAttrs: ['sym'] },
111
+ // --- black-box escape hatch (keeps lifting total) ---
112
+ opaque: { operands: 'variadic', results: 1 },
113
+ // --- terminators ---
114
+ ret: { operands: 'variadic', results: 0, terminator: true, successors: 0 },
115
+ br: { operands: 0, results: 0, terminator: true, successors: 1 },
116
+ cond_br: { operands: 1, results: 0, terminator: true, successors: 2 },
117
+ // Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
118
+ // successors are the N case blocks followed by the default block (the LAST successor);
119
+ // `cases` is the index-aligned list of the first N successors' case values.
120
+ switch_br: { operands: 1, results: 0, terminator: true, successors: 'variadic', requiredAttrs: ['cases'] },
121
+ } as const satisfies Record<string, OpSig>;
122
+
123
+ /** The registered opcode vocabulary as a TYPE — `mkOp("add", …)` compiles, `mkOp("addd", …)`
124
+ * does not. */
125
+ export type Opcode = keyof typeof OPCODES;
126
+
127
+ /** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
128
+ * on it); undefined for an unregistered opcode. */
129
+ export function opSig(opcode: string): OpSig | undefined {
130
+ return (OPCODES as Record<string, OpSig | undefined>)[opcode];
131
+ }
132
+
133
+ /** Ops with an observable side effect — the derived view raise/shortcircuit.ts consumes. */
134
+ export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
135
+ (Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
136
+ );
137
+
138
+ /** May a dead result of this opcode be deleted? Registered, no observable effects, not control
139
+ * flow. Deliberately includes `opaque` — a dead opaque vanishing is designed behavior. */
140
+ export function isDceSafe(opcode: string): boolean {
141
+ const sig = opSig(opcode);
142
+ return !!sig && !sig.effects && !sig.terminator;
143
+ }
@@ -0,0 +1,221 @@
1
+ // asmlift IR — the textual parser (the inverse of print.ts).
2
+ //
3
+ // The parser exists so that `parse(print(fn))` round-trips: the same textual artifact is
4
+ // both the debug dump and the test oracle. The parser builds the graph but enforces NO
5
+ // semantics — that is the verifier's job — so malformed-but-well-formed-syntax IR can be
6
+ // constructed and then rejected by verify().
7
+ // ROUND-TRIP DOMAIN: parse(print(fn)) holds for L1/scalar types only — `unkN`/`sN`/`uN` and
8
+ // `*`-pointers to them. STRUCT/ARRAY/VOID types print (typeToString) but do NOT parse back; a
9
+ // post-type-recovery dump is a one-way debugging artifact, not a test oracle.
10
+ import { Block, Fn, Op, Successor, Value, mkOp, mkValue } from './core';
11
+ import type { Opcode } from './opcodes';
12
+ import { IrType, parseType } from './types';
13
+
14
+ export function parse(text: string): Fn {
15
+ const raw = text.split('\n').map((l) => l.replace(/\r$/, ''));
16
+ let i = 0;
17
+ while (i < raw.length && raw[i].trim() === '') {
18
+ i++;
19
+ }
20
+ const fnM = raw[i]?.match(/^fn (\w+) \{$/);
21
+ if (!fnM) {
22
+ throw new Error(`expected 'fn NAME {', got '${raw[i] ?? '<eof>'}'`);
23
+ }
24
+ const name = fnM[1];
25
+ i++;
26
+
27
+ const body: string[] = [];
28
+ for (; i < raw.length; i++) {
29
+ if (raw[i] === '}') {
30
+ break;
31
+ }
32
+ if (raw[i].trim() === '') {
33
+ continue;
34
+ }
35
+ body.push(raw[i].trim());
36
+ }
37
+
38
+ // PASS A — create all blocks and pre-declare every value by its textual name, so
39
+ // operands can resolve regardless of definition order (incl. loop back-edges).
40
+ const valueByName = new Map<string, Value>();
41
+ const declValue = (nm: string, ty: IrType): Value => {
42
+ let v = valueByName.get(nm);
43
+ if (!v) {
44
+ v = mkValue(ty);
45
+ valueByName.set(nm, v);
46
+ } else {
47
+ v.type = ty;
48
+ }
49
+ return v;
50
+ };
51
+
52
+ interface RawBlock {
53
+ block: Block;
54
+ opLines: string[];
55
+ }
56
+ const rawBlocks: RawBlock[] = [];
57
+ const blockByLabel = new Map<string, Block>();
58
+ let cur: RawBlock | null = null;
59
+
60
+ for (const line of body) {
61
+ const bh = line.match(/^\^(\w+)\((.*)\):$/);
62
+ if (bh) {
63
+ const block: Block = { params: [], ops: [] };
64
+ cur = { block, opLines: [] };
65
+ rawBlocks.push(cur);
66
+ if (blockByLabel.has(bh[1])) {
67
+ throw new Error(`duplicate block label '${bh[1]}'`);
68
+ }
69
+ blockByLabel.set(bh[1], block);
70
+ for (const p of splitTop(bh[2])) {
71
+ const pm = p.match(/^(%\w+):\s*(.+)$/);
72
+ if (!pm) {
73
+ throw new Error(`bad block param '${p}'`);
74
+ }
75
+ block.params.push(declValue(pm[1], parseType(pm[2])));
76
+ }
77
+ continue;
78
+ }
79
+ if (!cur) {
80
+ throw new Error(`op outside any block: '${line}'`);
81
+ }
82
+ cur.opLines.push(line);
83
+ const eq = splitEquals(line);
84
+ if (eq) {
85
+ for (const r of splitTop(eq.results)) {
86
+ const rm = r.match(/^(%\w+):\s*(.+)$/);
87
+ if (!rm) {
88
+ throw new Error(`bad result decl '${r}'`);
89
+ }
90
+ declValue(rm[1], parseType(rm[2]));
91
+ }
92
+ }
93
+ }
94
+
95
+ // PASS B — wire operands / successor args / results.
96
+ const refValue = (nm: string): Value => {
97
+ const v = valueByName.get(nm);
98
+ if (!v) {
99
+ throw new Error(`reference to undefined value '${nm}'`);
100
+ }
101
+ return v;
102
+ };
103
+ const refBlock = (label: string): Block => {
104
+ const b = blockByLabel.get(label);
105
+ if (!b) {
106
+ throw new Error(`reference to undefined block '^${label}'`);
107
+ }
108
+ return b;
109
+ };
110
+ for (const rb of rawBlocks) {
111
+ for (const line of rb.opLines) {
112
+ rb.block.ops.push(parseOp(line, refValue, refBlock));
113
+ }
114
+ }
115
+
116
+ return { name, blocks: rawBlocks.map((r) => r.block) };
117
+ }
118
+
119
+ function parseOp(line: string, refValue: (nm: string) => Value, refBlock: (label: string) => Block): Op {
120
+ const eq = splitEquals(line);
121
+ const results: Value[] = eq ? splitTop(eq.results).map((r) => refValue(r.match(/^(%\w+):/)![1])) : [];
122
+ let rest = eq ? eq.rest : line;
123
+
124
+ let attrs: Record<string, number | boolean | string | number[]> = {};
125
+ const am = rest.match(/\s*\{([^}]*)\}\s*$/);
126
+ if (am) {
127
+ attrs = parseAttrs(am[1]);
128
+ rest = rest.slice(0, am.index).trim();
129
+ }
130
+
131
+ const sp = rest.indexOf(' ');
132
+ const opcode = sp < 0 ? rest : rest.slice(0, sp);
133
+ const argStr = sp < 0 ? '' : rest.slice(sp + 1);
134
+
135
+ const operands: Value[] = [];
136
+ const successors: Successor[] = [];
137
+ for (const arg of splitTop(argStr)) {
138
+ if (arg.startsWith('^')) {
139
+ const sm = arg.match(/^\^(\w+)\((.*)\)$/);
140
+ if (!sm) {
141
+ throw new Error(`bad successor '${arg}'`);
142
+ }
143
+ successors.push({ block: refBlock(sm[1]), args: splitTop(sm[2]).map(refValue) });
144
+ } else if (arg.startsWith('%')) {
145
+ operands.push(refValue(arg));
146
+ } else {
147
+ throw new Error(`unexpected operand '${arg}'`);
148
+ }
149
+ }
150
+ return mkOp(opcode as Opcode, { operands, results, attrs, successors }); // data boundary: verify() rejects unknowns
151
+ }
152
+
153
+ // --- small text helpers ---
154
+
155
+ /** Split on top-level commas only (ignore commas inside (...), {...} or [...] — print.ts's
156
+ * list attrs are bracketed and rely on this tracking). */
157
+ function splitTop(s: string): string[] {
158
+ const out: string[] = [];
159
+ let depth = 0,
160
+ cur = '';
161
+ for (const c of s) {
162
+ if (c === '(' || c === '{' || c === '[') {
163
+ depth++;
164
+ } else if (c === ')' || c === '}' || c === ']') {
165
+ depth--;
166
+ }
167
+ if (c === ',' && depth === 0) {
168
+ if (cur.trim()) {
169
+ out.push(cur.trim());
170
+ }
171
+ cur = '';
172
+ } else {
173
+ cur += c;
174
+ }
175
+ }
176
+ if (cur.trim()) {
177
+ out.push(cur.trim());
178
+ }
179
+ return out;
180
+ }
181
+
182
+ /** Split at the first top-level " = " (not inside attrs braces). */
183
+ function splitEquals(t: string): { results: string; rest: string } | null {
184
+ let depth = 0;
185
+ for (let k = 0; k + 3 <= t.length; k++) {
186
+ const c = t[k];
187
+ if (c === '(' || c === '{') {
188
+ depth++;
189
+ } else if (c === ')' || c === '}') {
190
+ depth--;
191
+ } else if (depth === 0 && t.startsWith(' = ', k)) {
192
+ return { results: t.slice(0, k).trim(), rest: t.slice(k + 3).trim() };
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+
198
+ function parseAttrs(s: string): Record<string, number | boolean | string | number[]> {
199
+ const a: Record<string, number | boolean | string | number[]> = {};
200
+ for (const pair of splitTop(s)) {
201
+ const eqi = pair.indexOf('=');
202
+ if (eqi < 0) {
203
+ throw new Error(`bad attr '${pair}'`);
204
+ }
205
+ const k = pair.slice(0, eqi).trim();
206
+ const raw = pair.slice(eqi + 1).trim();
207
+ if (raw.startsWith('"')) {
208
+ a[k] = JSON.parse(raw);
209
+ } else if (raw === 'true') {
210
+ a[k] = true;
211
+ } else if (raw === 'false') {
212
+ a[k] = false;
213
+ } else if (raw.startsWith('[')) {
214
+ a[k] = raw.slice(1, -1).split(';').filter(Boolean).map(Number);
215
+ } // switch_br cases
216
+ else {
217
+ a[k] = Number(raw);
218
+ }
219
+ }
220
+ return a;
221
+ }
@@ -0,0 +1,77 @@
1
+ // asmlift IR — the canonical textual printer.
2
+ //
3
+ // Determinism: value names are assigned HERE, at print time, by a fixed traversal
4
+ // (blocks in order; within a block, params then op-results). Two
5
+ // structurally-identical functions therefore print byte-identically regardless of the
6
+ // order their Value objects were created — there is no global counter to leak order.
7
+ import type { AttrVal, Block, Fn, Value } from './core';
8
+ import { typeToString } from './types';
9
+
10
+ export function print(fn: Fn): string {
11
+ const blockLabel = new Map<Block, string>();
12
+ fn.blocks.forEach((b, i) => blockLabel.set(b, `bb${i}`));
13
+
14
+ const name = new Map<Value, string>();
15
+ let counter = 0;
16
+ const assign = (v: Value) => {
17
+ if (!name.has(v)) {
18
+ name.set(v, `%${counter++}`);
19
+ }
20
+ return name.get(v)!;
21
+ };
22
+ for (const b of fn.blocks) {
23
+ for (const p of b.params) {
24
+ assign(p);
25
+ }
26
+ for (const op of b.ops) {
27
+ for (const r of op.results) {
28
+ assign(r);
29
+ }
30
+ }
31
+ }
32
+ const ref = (v: Value) => name.get(v) ?? '%<undef>';
33
+
34
+ const lines: string[] = [`fn ${fn.name} {`];
35
+ for (const b of fn.blocks) {
36
+ const params = b.params.map((p) => `${ref(p)}: ${typeToString(p.type)}`).join(', ');
37
+ lines.push(`^${blockLabel.get(b)}(${params}):`);
38
+ for (const op of b.ops) {
39
+ let s = ' ';
40
+ if (op.results.length) {
41
+ s += op.results.map((r) => `${ref(r)}: ${typeToString(r.type)}`).join(', ') + ' = ';
42
+ }
43
+ s += op.opcode;
44
+ const args: string[] = [];
45
+ for (const o of op.operands) {
46
+ args.push(ref(o));
47
+ }
48
+ for (const su of op.successors) {
49
+ args.push(`^${blockLabel.get(su.block)}(${su.args.map(ref).join(', ')})`);
50
+ }
51
+ if (args.length) {
52
+ s += ' ' + args.join(', ');
53
+ }
54
+ s += fmtAttrs(op.attrs);
55
+ lines.push(s);
56
+ }
57
+ }
58
+ lines.push('}');
59
+ return lines.join('\n') + '\n';
60
+ }
61
+
62
+ function fmtAttrs(a: Record<string, AttrVal>): string {
63
+ const keys = Object.keys(a).sort();
64
+ if (keys.length === 0) {
65
+ return '';
66
+ }
67
+ return ' {' + keys.map((k) => `${k}=${fmtAttr(a[k])}`).join(', ') + '}';
68
+ }
69
+
70
+ function fmtAttr(v: AttrVal): string {
71
+ // A list attr (switch_br `cases`) prints bracketed so `parseAttrs`' top-level comma split (which
72
+ // tracks `[`/`(`/`{` depth) keeps it as one token and round-trips it back to a number[].
73
+ if (Array.isArray(v)) {
74
+ return `[${v.join(';')}]`;
75
+ }
76
+ return typeof v === 'string' ? JSON.stringify(v) : String(v);
77
+ }
@@ -0,0 +1,106 @@
1
+ // asmlift IR — semantic types. NOT language type strings (a hard requirement from
2
+ // the language-backend study: each backend picks its own spelling from these).
3
+
4
+ /** One recovered field of a struct: its byte offset within the struct, its recovered
5
+ * scalar/pointer type, and its (currently synthetic, offset-derived) name. */
6
+ export interface StructField {
7
+ off: number;
8
+ type: IrType;
9
+ name: string;
10
+ }
11
+
12
+ export type IrType =
13
+ | { kind: 'unknown'; width: number } // width in bits; type not yet recovered
14
+ | { kind: 'int'; width: number; signed: boolean }
15
+ | { kind: 'ptr'; to: IrType }
16
+ // A recovered aggregate: heterogeneous fields at byte offsets (raise/structs.ts). Distinct
17
+ // from `ptr(int)`+array-index because its access pattern is inconsistent with a homogeneous
18
+ // array (mixed widths / non-uniform offsets). `name` is synthetic today (`Struct0`); a later
19
+ // DWARF pass supplies real names. Fields are sorted by `off`.
20
+ | { kind: 'struct'; name: string; fields: StructField[]; size?: number }
21
+ // A fixed-length array `elem[count]`. Today its sole inhabitant is struct padding (a `u8[N]`
22
+ // pad member seats fields at their exact offsets, raise/struct-arrays.ts) — a REAL type,
23
+ // not a printed string. Array-typed fields declare with the length AFTER the name in C
24
+ // (`u8 _pad[4]`), so the backend routes them through a declarator-aware `cDeclare`, not the
25
+ // prefix `cType`.
26
+ | { kind: 'array'; elem: IrType; count: number }
27
+ | { kind: 'void' }; // a function that returns nothing
28
+
29
+ /** The scalar type of a memory access of `width` bytes: word ⇒ the s32 integer default;
30
+ * narrower widths carry the access's signedness. THE one copy of a match-critical rule (it
31
+ * decides emitted decl types), consumed by recover.ts, structs.ts, and struct-arrays — a
32
+ * per-consumer copy would silently diverge struct fields from pointer pointees. */
33
+ export function scalarTypeForAccess(width: number, signed: boolean): IrType {
34
+ return width === 4 ? T.s(32) : T.int(width * 8, signed);
35
+ }
36
+
37
+ export const T = {
38
+ unk: (width = 32): IrType => ({ kind: 'unknown', width }),
39
+ int: (width: number, signed: boolean): IrType => ({ kind: 'int', width, signed }),
40
+ s: (width = 32): IrType => ({ kind: 'int', width, signed: true }),
41
+ u: (width = 32): IrType => ({ kind: 'int', width, signed: false }),
42
+ ptr: (to: IrType): IrType => ({ kind: 'ptr', to }),
43
+ struct: (name: string, fields: StructField[], size?: number): IrType => ({ kind: 'struct', name, fields, size }),
44
+ array: (elem: IrType, count: number): IrType => ({ kind: 'array', elem, count }),
45
+ void: (): IrType => ({ kind: 'void' }),
46
+ };
47
+
48
+ export function typeToString(t: IrType): string {
49
+ switch (t.kind) {
50
+ case 'unknown':
51
+ return `unk${t.width}`;
52
+ case 'int':
53
+ return `${t.signed ? 's' : 'u'}${t.width}`;
54
+ case 'ptr':
55
+ return `${typeToString(t.to)}*`;
56
+ case 'struct':
57
+ return t.name;
58
+ case 'array':
59
+ return `${typeToString(t.elem)}[${t.count}]`;
60
+ case 'void':
61
+ return 'void';
62
+ }
63
+ }
64
+
65
+ export function parseType(s: string): IrType {
66
+ s = s.trim();
67
+ if (s.endsWith('*')) {
68
+ return T.ptr(parseType(s.slice(0, -1)));
69
+ }
70
+ const m = s.match(/^(unk|s|u)(\d+)$/);
71
+ if (!m) {
72
+ throw new Error(`bad type '${s}'`);
73
+ }
74
+ const width = parseInt(m[2], 10);
75
+ if (m[1] === 'unk') {
76
+ return T.unk(width);
77
+ }
78
+ return T.int(width, m[1] === 's');
79
+ }
80
+
81
+ export function typeEquals(a: IrType, b: IrType): boolean {
82
+ if (a.kind === 'ptr' && b.kind === 'ptr') {
83
+ return typeEquals(a.to, b.to);
84
+ }
85
+ if (a.kind === 'int' && b.kind === 'int') {
86
+ return a.width === b.width && a.signed === b.signed;
87
+ }
88
+ if (a.kind === 'unknown' && b.kind === 'unknown') {
89
+ return a.width === b.width;
90
+ }
91
+ if (a.kind === 'array' && b.kind === 'array') {
92
+ return a.count === b.count && typeEquals(a.elem, b.elem);
93
+ }
94
+ // Two structs are equal when their name + field layout match (recovered structs are named
95
+ // by layout-discovery order, so equal name ⇒ equal layout in practice).
96
+ if (a.kind === 'struct' && b.kind === 'struct') {
97
+ return (
98
+ a.name === b.name &&
99
+ a.fields.length === b.fields.length &&
100
+ a.fields.every(
101
+ (f, i) => f.off === b.fields[i].off && f.name === b.fields[i].name && typeEquals(f.type, b.fields[i].type),
102
+ )
103
+ );
104
+ }
105
+ return false;
106
+ }