@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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// asmlift — target→frontend dispatch. The entry points (pipeline / rank / report) ask for a
|
|
2
|
+
// frontend BY TARGET, never by importing a concrete lift. Registering a second ISA is a one-
|
|
3
|
+
// line addition here plus its own frontend module — not an edit to every entry point.
|
|
4
|
+
import type { TargetDescription } from '../target';
|
|
5
|
+
import type { Frontend } from './frontend';
|
|
6
|
+
import { mipsFrontend } from './mips';
|
|
7
|
+
import { ppcFrontend } from './ppc';
|
|
8
|
+
import { thumbFrontend } from './thumb';
|
|
9
|
+
|
|
10
|
+
// Keyed by TargetDescription.id. A target names its ISA; the frontend implements it.
|
|
11
|
+
const FRONTENDS: Record<string, Frontend> = {
|
|
12
|
+
armv4t: thumbFrontend,
|
|
13
|
+
mips: mipsFrontend,
|
|
14
|
+
ppc: ppcFrontend,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** The registered ISA ids (registry keys). test/contract-invariant.test.ts reflects over this
|
|
18
|
+
* so the frontend-contract property test covers EVERY registered frontend — a new ISA added
|
|
19
|
+
* here without a contract probe fails that test, rather than silently shipping a frontend
|
|
20
|
+
* that was never held to the "unmodelled ⇒ loud" contract. */
|
|
21
|
+
export function registeredFrontendIds(): string[] {
|
|
22
|
+
return Object.keys(FRONTENDS);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve the frontend for a target, or throw a clear error naming the missing id. */
|
|
26
|
+
export function frontendFor(target: TargetDescription): Frontend {
|
|
27
|
+
const f = FRONTENDS[target.id];
|
|
28
|
+
if (!f) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`no ISA frontend registered for target '${target.id}' (known: ${Object.keys(FRONTENDS).join(', ')})`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return f;
|
|
34
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// asmlift — ISA-neutral on-the-fly SSA construction (Braun et al. 2013, "Simple and
|
|
2
|
+
// Efficient Construction of SSA Form"), shared by every frontend. The frontend supplies the
|
|
3
|
+
// CFG (predecessors per block) and, per block, emits ops through `readVar`/`writeVar`; this
|
|
4
|
+
// module materialises block-argument phis at joins and back-edges.
|
|
5
|
+
//
|
|
6
|
+
// Protocol: create the builder, then fill blocks in index order. For each block, emit its
|
|
7
|
+
// computation via read/writeVar, push its terminator op last (successors referencing
|
|
8
|
+
// `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
|
|
9
|
+
// blocks are filled, call `finish()` to remove trivial phis.
|
|
10
|
+
import { Block, Fn, Successor, Value, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
11
|
+
import { T } from '../ir/types';
|
|
12
|
+
|
|
13
|
+
export interface SsaBuilder {
|
|
14
|
+
fn: Fn;
|
|
15
|
+
irBlocks: Block[];
|
|
16
|
+
/** Current SSA value of `reg` on entry to block `b` (creating phis/params as needed). */
|
|
17
|
+
readVar(reg: string, b: number): Value;
|
|
18
|
+
/** Record that `reg` now holds `v` within block `b`. */
|
|
19
|
+
writeVar(reg: string, b: number, v: Value): void;
|
|
20
|
+
/** Mark block `b` fully emitted (terminator pushed); seals any now-ready successors. */
|
|
21
|
+
markFilled(b: number): void;
|
|
22
|
+
/** Live-in parameter value → the ABI register it arrived on (for calling-convention order). */
|
|
23
|
+
paramReg: Map<Value, string>;
|
|
24
|
+
/** Whether `reg` has a definition reaching block `b` (best-effort call-arity heuristic). */
|
|
25
|
+
hasReachingDef(reg: string, b: number, seen?: Set<number>): boolean;
|
|
26
|
+
/** Remove trivial phis; call once every block is filled. */
|
|
27
|
+
finish(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function makeSsaBuilder(name: string, blockCount: number, preds: number[][]): SsaBuilder {
|
|
31
|
+
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
32
|
+
const fn: Fn = { name, blocks: irBlocks };
|
|
33
|
+
|
|
34
|
+
const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
35
|
+
const sealed: boolean[] = irBlocks.map(() => false);
|
|
36
|
+
const filled: boolean[] = irBlocks.map(() => false);
|
|
37
|
+
const incompletePhis: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
38
|
+
const phiBlock = new Map<Value, number>();
|
|
39
|
+
const paramReg = new Map<Value, string>();
|
|
40
|
+
|
|
41
|
+
const writeVar = (reg: string, b: number, v: Value) => defs[b].set(reg, v);
|
|
42
|
+
const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
|
|
43
|
+
|
|
44
|
+
const newPhi = (reg: string, b: number): Value => {
|
|
45
|
+
const phi = mkValue(T.unk(32));
|
|
46
|
+
irBlocks[b].params.push(phi);
|
|
47
|
+
phiBlock.set(phi, b);
|
|
48
|
+
defs[b].set(reg, phi); // set before wiring operands to break cycles
|
|
49
|
+
return phi;
|
|
50
|
+
};
|
|
51
|
+
const readRecursive = (reg: string, b: number): Value => {
|
|
52
|
+
if (!sealed[b]) {
|
|
53
|
+
// predecessors not all filled yet (e.g. a loop back-edge): defer operand wiring.
|
|
54
|
+
const phi = newPhi(reg, b);
|
|
55
|
+
incompletePhis[b].set(reg, phi);
|
|
56
|
+
return phi;
|
|
57
|
+
}
|
|
58
|
+
const ps = preds[b];
|
|
59
|
+
if (ps.length === 0) {
|
|
60
|
+
// live-in with no predecessor: an incoming argument register → function parameter.
|
|
61
|
+
const p = mkValue(T.unk(32));
|
|
62
|
+
irBlocks[b].params.push(p);
|
|
63
|
+
defs[b].set(reg, p);
|
|
64
|
+
paramReg.set(p, reg);
|
|
65
|
+
return p;
|
|
66
|
+
}
|
|
67
|
+
if (ps.length === 1) {
|
|
68
|
+
const v = readVar(reg, ps[0]);
|
|
69
|
+
defs[b].set(reg, v);
|
|
70
|
+
return v;
|
|
71
|
+
}
|
|
72
|
+
// sealed join: create the phi and wire every predecessor's terminator arg now.
|
|
73
|
+
const phi = newPhi(reg, b);
|
|
74
|
+
addPhiOperands(reg, b);
|
|
75
|
+
return phi;
|
|
76
|
+
};
|
|
77
|
+
const addPhiOperands = (reg: string, b: number) => {
|
|
78
|
+
for (const p of preds[b]) {
|
|
79
|
+
appendSuccessorArg(p, b, readVar(reg, p));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
// Append `arg` to predecessor p's terminator successor that targets block b.
|
|
83
|
+
const appendSuccessorArg = (p: number, b: number, arg: Value) => {
|
|
84
|
+
const term = irBlocks[p].ops[irBlocks[p].ops.length - 1];
|
|
85
|
+
const s = term.successors.find((su) => su.block === irBlocks[b]);
|
|
86
|
+
if (s) {
|
|
87
|
+
s.args.push(arg);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const sealBlock = (b: number) => {
|
|
91
|
+
if (sealed[b]) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
sealed[b] = true; // set first: addPhiOperands may recurse back here
|
|
95
|
+
for (const reg of incompletePhis[b].keys()) {
|
|
96
|
+
addPhiOperands(reg, b);
|
|
97
|
+
}
|
|
98
|
+
incompletePhis[b].clear();
|
|
99
|
+
};
|
|
100
|
+
const sealReadyBlocks = () => {
|
|
101
|
+
for (let b = 0; b < irBlocks.length; b++) {
|
|
102
|
+
if (!sealed[b] && preds[b].every((p) => filled[p])) {
|
|
103
|
+
sealBlock(b);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
sealReadyBlocks(); // seals the entry (no predecessors) up front
|
|
108
|
+
|
|
109
|
+
const hasReachingDef = (reg: string, b: number, seen = new Set<number>()): boolean => {
|
|
110
|
+
if (defs[b].has(reg)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (seen.has(b)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
seen.add(b);
|
|
117
|
+
return preds[b].length > 0 && preds[b].some((p) => hasReachingDef(reg, p, seen));
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
fn,
|
|
122
|
+
irBlocks,
|
|
123
|
+
readVar,
|
|
124
|
+
writeVar,
|
|
125
|
+
paramReg,
|
|
126
|
+
hasReachingDef,
|
|
127
|
+
markFilled: (b: number) => {
|
|
128
|
+
filled[b] = true;
|
|
129
|
+
sealReadyBlocks();
|
|
130
|
+
},
|
|
131
|
+
finish: () => simplifyTrivialPhis(fn, phiBlock),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── shared frontend tail helpers ──
|
|
136
|
+
|
|
137
|
+
/** Best-effort call arity when a callee has no prototype: the count of contiguous argument
|
|
138
|
+
* registers with a value reaching the call's block. Correct when the arguments are set up in
|
|
139
|
+
* the calling block; it can under-count pass-through parameters — which is why a prototype's
|
|
140
|
+
* declared `params` is authoritative when available. */
|
|
141
|
+
export function fallbackArgc(
|
|
142
|
+
ssa: { hasReachingDef(reg: string, b: number): boolean },
|
|
143
|
+
argRegs: string[],
|
|
144
|
+
bi: number,
|
|
145
|
+
): number {
|
|
146
|
+
let n = 0;
|
|
147
|
+
while (n < argRegs.length && ssa.hasReachingDef(argRegs[n], bi)) {
|
|
148
|
+
n++;
|
|
149
|
+
}
|
|
150
|
+
return n;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Order the TRUE entry block's parameters by ABI argument register, so downstream naming
|
|
154
|
+
* (`a0`, `a1`, …) matches the calling convention, not first-read order (a callee-saved copy can
|
|
155
|
+
* read a later argument register first). No-op when the entry has predecessors — a loop
|
|
156
|
+
* header's params are phis position-aligned with predecessor terminator args and must not be
|
|
157
|
+
* reordered. `rank` is per-ISA: the tie-break for a non-ABI live-in deliberately differs
|
|
158
|
+
* (Thumb sorts it LAST via 99, MIPS/PPC FIRST via indexOf's -1) to keep each frontend's
|
|
159
|
+
* output byte-exact. */
|
|
160
|
+
export function abiSortEntryParams(
|
|
161
|
+
entry: { params: Value[] },
|
|
162
|
+
entryHasPreds: boolean,
|
|
163
|
+
rank: (v: Value) => number,
|
|
164
|
+
): void {
|
|
165
|
+
if (entryHasPreds) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Remove block-parameters that are really trivial phis: those whose incoming operands (across
|
|
172
|
+
// every predecessor edge, ignoring self-references from a back-edge) are all the same single
|
|
173
|
+
// value. Such a parameter carries no join information — a loop-invariant register or a value
|
|
174
|
+
// defined before the join — so it is replaced by that value and the corresponding argument
|
|
175
|
+
// dropped from each predecessor's terminator. Iterated to fixpoint because removing one phi
|
|
176
|
+
// can make another trivial.
|
|
177
|
+
function simplifyTrivialPhis(fn: Fn, phiBlock: Map<Value, number>): void {
|
|
178
|
+
const edgesTo = (b: Block): Successor[] => {
|
|
179
|
+
const out: Successor[] = [];
|
|
180
|
+
for (const pb of fn.blocks) {
|
|
181
|
+
for (const op of pb.ops) {
|
|
182
|
+
for (const s of op.successors) {
|
|
183
|
+
if (s.block === b) {
|
|
184
|
+
out.push(s);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
};
|
|
191
|
+
let changed = true;
|
|
192
|
+
while (changed) {
|
|
193
|
+
changed = false;
|
|
194
|
+
for (const b of fn.blocks) {
|
|
195
|
+
const incoming = edgesTo(b);
|
|
196
|
+
for (let i = b.params.length - 1; i >= 0; i--) {
|
|
197
|
+
const param = b.params[i];
|
|
198
|
+
const operands = incoming.map((s) => s.args[i]);
|
|
199
|
+
const distinct = [...new Set(operands.filter((v) => v !== param))];
|
|
200
|
+
if (distinct.length !== 1) {
|
|
201
|
+
continue;
|
|
202
|
+
} // a genuine join (or unreachable) — keep it
|
|
203
|
+
const v = distinct[0];
|
|
204
|
+
replaceAllUsesWith(fn, param, v);
|
|
205
|
+
b.params.splice(i, 1);
|
|
206
|
+
for (const s of incoming) {
|
|
207
|
+
s.args.splice(i, 1);
|
|
208
|
+
}
|
|
209
|
+
phiBlock.delete(param);
|
|
210
|
+
changed = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|