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