@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/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
|
+
}
|
package/src/ir/parse.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ir/print.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ir/types.ts
ADDED
|
@@ -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
|
+
}
|