@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,272 @@
1
+ // asmlift — the library entry point. `decompile(name, asm, target)` runs the raising tower and
2
+ // returns structured results: the source, the per-level IR dumps, and diagnostics.
3
+ import { cBackend } from './backend/c';
4
+ import { ContractError, assertDerefsTyped, assertResolved, assertTypesRecovered } from './contracts';
5
+ import type { AsmData } from './frontend/asmdata';
6
+ import { FrontendUnsupportedError } from './frontend/errors';
7
+ import { frontendFor } from './frontend/registry';
8
+ import type { Fn } from './ir/core';
9
+ import { print } from './ir/print';
10
+ import { T } from './ir/types';
11
+ import { VerifyError, verify } from './ir/verify';
12
+ import { Expr, LanguageBackend, SFn, Stmt, exprChildren, stmtChildren, stmtExprs } from './l3/ast';
13
+ import { hoistReusedGlobalBases } from './l3/basecse';
14
+ import { eliminateDeadStores } from './l3/dce';
15
+ import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
16
+ import type { Prototypes } from './proto';
17
+ import { RaiseUnsupportedError } from './raise/errors';
18
+ import { type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
19
+ import { recoverTypes } from './raise/recover';
20
+ import { sinkReturns } from './raise/retsink';
21
+ import { StructureError, structure } from './structure/structure';
22
+ import { type TargetDescription, structureOptionsFor } from './target';
23
+
24
+ /** How a gap (a construct asmlift cannot faithfully model) degrades:
25
+ * "strict" — throw / `"?"`-sentinel → ContractError. Loud in the PROCESS. The default, and
26
+ * what every contract/loud-fail test pins: asmlift knows when it doesn't know.
27
+ * "annotate" — always emit SOMETHING (the m2c usefulness property), but every gap is loud in
28
+ * the ARTIFACT: a localizable gap becomes an undefined ASMLIFT_ERROR("reason", …)
29
+ * marker inline; a non-localizable failure (unliftable control flow, a broken
30
+ * invariant) degrades to a stub carrying the reason + the original asm as a
31
+ * comment. Either way the source cannot compile un-acknowledged, and the gaps are
32
+ * ALSO returned as structured `diagnostics` for the harness / self-improve loop. */
33
+ export type OnGap = 'strict' | 'annotate';
34
+
35
+ /** One machine-readable gap: which stage declined and why. The comments/markers in the emitted
36
+ * source are the human projection of these entries — a tool should read THIS, not parse text. */
37
+ export interface Diagnostic {
38
+ stage: 'lift' | 'raise' | 'structure' | 'contract' | 'verify' | 'internal';
39
+ reason: string;
40
+ }
41
+
42
+ export interface DecompileOptions {
43
+ backend?: LanguageBackend;
44
+ /** idiom patterns applied at L1. DEFAULTS to `DEFAULT_IDIOM_PATTERNS` (every idiom asmlift
45
+ * owns, each `{compilers}`-gated so it self-selects per target). Pass an explicit list to
46
+ * override, or `[]` to run the naive lift with no idiom folding. */
47
+ patterns?: RewritePattern[];
48
+ /** function prototypes from the project's headers, keyed by symbol: a callee's `params`
49
+ * drives its `bl` argument recovery; the current function's own entry supplies its
50
+ * `returnsVoid`. One table, resolved at the point of use (see proto.ts). */
51
+ prototypes?: Prototypes;
52
+ /** OPTIONAL Regime-B side-table (data-section jump tables + relocations). Absent ⇒ a dense
53
+ * MIPS/PPC switch declines/loud-fails; present ⇒ the frontend recovers the `switch_br`.
54
+ * Produced by `extractAsmData(obj, target)` from the scoring object. */
55
+ asmData?: AsmData;
56
+ /** gap policy — see `OnGap`. Default "strict". */
57
+ onGap?: OnGap;
58
+ }
59
+
60
+ export interface DecompileResult {
61
+ source: string;
62
+ sfn: SFn;
63
+ ir: { raw: string; folded: string; recovered: string }; // IR dumps: post-lift, post-idiom, post-recovery
64
+ patternHits: number;
65
+ /** structured gap list — ALWAYS present; empty ⇔ the emission is gap-free (compiles + candidate
66
+ * for scoring). Non-empty ⇔ the source contains ASMLIFT_ERROR markers / a stub and will NOT
67
+ * compile until the user defines that symbol (the loud-in-artifact contract). */
68
+ diagnostics: Diagnostic[];
69
+ }
70
+
71
+ export function decompile(
72
+ name: string,
73
+ asm: string,
74
+ target: TargetDescription,
75
+ opts: DecompileOptions = {},
76
+ ): DecompileResult {
77
+ const onGap = opts.onGap ?? 'strict';
78
+ if (onGap === 'strict') {
79
+ return runTower(name, asm, target, opts, 'strict');
80
+ }
81
+ try {
82
+ return runTower(name, asm, target, opts, 'annotate');
83
+ } catch (e) {
84
+ // A NON-localizable failure (unliftable control transfer, frame model, a broken internal
85
+ // invariant): there is no line to mark, so degrade to a stub — the failure reason + the
86
+ // original asm as comments, and one ASMLIFT_ERROR marker so the file stays uncompilable
87
+ // un-acknowledged. The user/LLM gets the raw material to finish by hand instead of a throw.
88
+ return stubResult(name, asm, opts.backend ?? cBackend, e);
89
+ }
90
+ }
91
+
92
+ function runTower(
93
+ name: string,
94
+ asm: string,
95
+ target: TargetDescription,
96
+ opts: DecompileOptions,
97
+ onGap: OnGap,
98
+ ): DecompileResult {
99
+ const backend = opts.backend ?? cBackend;
100
+ const prototypes = opts.prototypes ?? {};
101
+ // (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
102
+ const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
103
+ verify(fn);
104
+ const raw = print(fn);
105
+
106
+ // (2) idiom fold: apply serializable patterns on the IR (the AI-improvement surface),
107
+ // gated generically by the Target's capabilities (not an `arch ==` branch).
108
+ const patternHits = applyIdiomPatterns(fn, target, opts.patterns);
109
+ const folded = print(fn);
110
+
111
+ // (2.35–3.5) pre-recovery recognizers → type recovery → return-sinking, the ONE shared spine
112
+ // (`raiseRecovered`) that trace.ts and the cli's rank.ts/report.ts also run.
113
+ raiseRecovered(fn, target);
114
+ const recovered = print(fn);
115
+
116
+ // (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
117
+ // every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
118
+ const sfn = structureChecked(fn, { ...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false), onGap });
119
+
120
+ // (5) lower + print: neutral AST → target language
121
+ const source = backend.emit(sfn);
122
+
123
+ return { source, sfn, ir: { raw, folded, recovered }, patternHits, diagnostics: collectMarkers(sfn) };
124
+ }
125
+
126
+ // ── the shared raising tower ────────────────────────────────────────────────────────────────
127
+ // decompile(), decompileTraced (trace.ts), and the cli's decompileRanked (rank.ts) /
128
+ // decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME
129
+ // stage sequence. The optional hooks are the only per-caller
130
+ // differences: rank pins its signedness candidate `beforeRecover`; the report pushes trace
131
+ // entries after each stage. Every hook fires AFTER the stage's verify, so a hook can never
132
+ // observe unverified IR.
133
+
134
+ /** Stage 2 — idiom fold: filter the pattern set by target capabilities, apply, dce + verify.
135
+ * Returns total hits. `patterns` defaults to DEFAULT_IDIOM_PATTERNS exactly like decompile(). */
136
+ export function applyIdiomPatterns(fn: Fn, target: TargetDescription, patterns?: RewritePattern[]): number {
137
+ const active = (patterns ?? DEFAULT_IDIOM_PATTERNS).filter((p) => patternApplies(p, target));
138
+ let hits = 0;
139
+ for (const p of active) {
140
+ hits += applyPattern(fn, p);
141
+ }
142
+ if (active.length) {
143
+ dce(fn);
144
+ verify(fn);
145
+ }
146
+ return hits;
147
+ }
148
+
149
+ export interface RaiseHooks {
150
+ /** after each pre-recovery pass that changed the IR (fires after its verify) */
151
+ afterPass?: (pass: PreRecoveryPass, result: number | boolean) => void;
152
+ /** between pre-recovery and recoverTypes — rank.ts pins candidate signedness here */
153
+ beforeRecover?: () => void;
154
+ /** after recoverTypes + verify + assertTypesRecovered */
155
+ afterRecover?: () => void;
156
+ /** after return-sinking, only when it changed the fn (fires after its verify) */
157
+ afterRetsink?: () => void;
158
+ }
159
+
160
+ /** Stages 2.35–3.5 — pre-recovery recognizers (the shared ordered list in raise/pre-recovery.ts)
161
+ * → type recovery (boundary contract: no `unknown` survives) → return-sinking (tail-duplicate a
162
+ * return-only merge so short-circuits emit early returns). `verify` after every pass that
163
+ * changed the IR. */
164
+ export function raiseRecovered(fn: Fn, target: TargetDescription, hooks: RaiseHooks = {}): void {
165
+ runPreRecovery(fn, target, (pass, result) => {
166
+ verify(fn);
167
+ hooks.afterPass?.(pass, result);
168
+ });
169
+ hooks.beforeRecover?.();
170
+ recoverTypes(fn);
171
+ verify(fn);
172
+ assertTypesRecovered(fn);
173
+ hooks.afterRecover?.();
174
+ if (sinkReturns(fn)) {
175
+ verify(fn);
176
+ hooks.afterRetsink?.();
177
+ }
178
+ }
179
+
180
+ /** Stage 4 — structure + its boundary contracts, always as a pair. */
181
+ export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]): SFn {
182
+ const raw = structure(fn, opts);
183
+ // BOTH boundary contracts run on the pre-DCE tree: the readability pass must never be able to
184
+ // hide a structuring defect by dropping the dead statement that carries it. assertResolved
185
+ // catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
186
+ // under a rejected operator) — even one sitting in dead code structure emitted. DCE then only
187
+ // removes statements/flips branches over an already-validated tree.
188
+ assertResolved(raw);
189
+ assertDerefsTyped(raw);
190
+ // Then the readability/quality rewrites: drop dead stores, then hoist a reused aggregate-global
191
+ // base into a typed local pointer. The hoist moves the deref cast from each `index` node onto the
192
+ // local's initializer, so re-validate deref typing on the rewritten tree.
193
+ const sfn = hoistReusedGlobalBases(eliminateDeadStores(raw));
194
+ assertDerefsTyped(sfn);
195
+ return sfn;
196
+ }
197
+
198
+ // ── annotate-mode support ─────────────────────────────────────────────────────────────────
199
+
200
+ /** Classify a caught failure by which stage's designed signal it is. `instanceof`, not name
201
+ * strings: a renamed or newly-subclassed error class (PpcUnsupported subclasses
202
+ * FrontendUnsupported) stays correctly classified instead of silently degrading to "internal". */
203
+ function stageOf(e: unknown): Diagnostic['stage'] {
204
+ if (e instanceof FrontendUnsupportedError) {
205
+ return 'lift';
206
+ }
207
+ if (e instanceof RaiseUnsupportedError) {
208
+ return 'raise';
209
+ }
210
+ if (e instanceof StructureError) {
211
+ return 'structure';
212
+ }
213
+ if (e instanceof ContractError) {
214
+ return 'contract';
215
+ }
216
+ if (e instanceof VerifyError) {
217
+ return 'verify';
218
+ }
219
+ return 'internal';
220
+ }
221
+
222
+ /** The annotate-mode fallback for a failure with no line to mark: a compilable-shaped stub whose
223
+ * body is one ASMLIFT_ERROR marker, headed by the reason + the ORIGINAL ASM as comments.
224
+ * Exported for trace.ts (decompileTraced), whose annotate mode must degrade to the SAME stub
225
+ * as decompile(). */
226
+ export function stubResult(name: string, asm: string, backend: LanguageBackend, e: unknown): DecompileResult {
227
+ const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
228
+ const stage = stageOf(e);
229
+ const sfn: SFn = {
230
+ name,
231
+ params: [],
232
+ locals: [],
233
+ retType: T.void(),
234
+ body: [{ k: 'exprstmt', value: { k: 'marker', reason: `could not decompile (${stage}): ${msg}`, args: [] } }],
235
+ };
236
+ const header = [
237
+ backend.comment(`asmlift could not decompile '${name}' — ${stage}: ${msg}`),
238
+ backend.comment('original assembly:'),
239
+ ...asm
240
+ .split('\n')
241
+ .filter((l) => l.trim() !== '')
242
+ .map((l) => backend.comment(` ${l}`)),
243
+ ];
244
+ return {
245
+ source: header.join('\n') + '\n' + backend.emit(sfn),
246
+ sfn,
247
+ ir: { raw: '', folded: '', recovered: '' },
248
+ patternHits: 0,
249
+ diagnostics: [{ stage, reason: msg }],
250
+ };
251
+ }
252
+
253
+ /** Every ASMLIFT_ERROR marker in the emitted AST, as a structured diagnostic (one per marker,
254
+ * document order). The harness/self-improve loop reads THIS; the source text is for humans. */
255
+ function collectMarkers(sfn: SFn): Diagnostic[] {
256
+ // On the shared exprChildren/stmtExprs/stmtChildren traversal. Order is exprs-then-children
257
+ // per statement — deterministic and near-document-order (a `for`'s cond is visited before its
258
+ // init; see the note on stmtChildren).
259
+ const out: Diagnostic[] = [];
260
+ const we = (e: Expr): void => {
261
+ if (e.k === 'marker') {
262
+ out.push({ stage: 'structure', reason: e.reason });
263
+ }
264
+ exprChildren(e).forEach(we);
265
+ };
266
+ const ws = (s: Stmt): void => {
267
+ stmtExprs(s).forEach(we);
268
+ stmtChildren(s).forEach(ws);
269
+ };
270
+ sfn.body.forEach(ws);
271
+ return out;
272
+ }
package/src/proto.ts ADDED
@@ -0,0 +1,42 @@
1
+ // asmlift — function prototypes: the single carrier for the caller-supplied facts a
2
+ // matching-decomp project reads from its headers (arg counts, void-ness). One `Prototypes`
3
+ // map, keyed by symbol, is threaded through every entry point and resolved at the point of
4
+ // use — a callee's `params` gives its call-site arity, a function's own entry gives its
5
+ // `returnsVoid`. It also keeps the frontend seam honest: a frontend receives prototypes,
6
+ // not a grab-bag of ISA-specific options.
7
+
8
+ /** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`). asmlift consumes
9
+ * only the COUNT today (call-site arity), but a project's header extraction naturally produces
10
+ * the typed list, and keeping it lets a later pass pin an argument's width/signedness. */
11
+ export type ParamType = string;
12
+
13
+ /** What the headers know about one function. All fields optional: a partial table (only
14
+ * callee arities, or only the current function's void-ness) is the common case. */
15
+ export interface FnProto {
16
+ /** declared parameters — either a bare arity COUNT or the typed parameter list a header
17
+ * extraction produces (`["u8", "s32"]`). BOTH forms yield the call-site arity via
18
+ * `protoArity`; omit to let the frontend fall back to its contiguous-arg-register heuristic. */
19
+ params?: number | ParamType[];
20
+ /** the declared return type is `void`, so a trailing `bx lr` leaves a meaningless
21
+ * return register that must not surface as a `return` value. */
22
+ returnsVoid?: boolean;
23
+ }
24
+
25
+ /** symbol → prototype. The function under decompilation and its callees share one table. */
26
+ export type Prototypes = Record<string, FnProto>;
27
+
28
+ /** The call-site arity a proto declares, normalizing the count form (`2`) and the typed-list
29
+ * form (`["u8", "s32"]`) to one number. `undefined` when `params` is omitted — the caller then
30
+ * falls back to its arg-register heuristic. Reading a typed list as its length is what lets a
31
+ * header-derived proto (`params: ["u8"]`) recover its argument instead of silently dropping it. */
32
+ export function protoArity(p: FnProto | undefined): number | undefined {
33
+ if (typeof p?.params === 'number') {
34
+ return p.params;
35
+ }
36
+ if (Array.isArray(p?.params)) {
37
+ return p.params.length;
38
+ }
39
+ // Omitted OR malformed (e.g. a bare `"u8"` string reaching the untyped CLI `--proto` JSON):
40
+ // fall back to the frontend's arg-register heuristic rather than misread a string's `.length`.
41
+ return undefined;
42
+ }
@@ -0,0 +1,84 @@
1
+ // asmlift — array-access LEGALIZATION (L1 → typed `aload`/`astore`).
2
+ //
3
+ // A compiler materialises a VARIABLE-index array access `a[i]` as an explicit scaled-address
4
+ // computation before the memory op —
5
+ //
6
+ // sll t6, a1, 0x2 %s = shl %i {imm=2} (index << log2 elemSize)
7
+ // addu t7, a0, t6 %p = add %base, %s (base + scaled index)
8
+ // lw v0, 0(t7) %v = load %p {off=0,width=4,signed}
9
+ //
10
+ // which lifts to `load(add(base, shl(index, k)))` — an untyped byte-address the backend can
11
+ // only spell as the uncompilable `*(base + (index << k))` (base was never typed a pointer, and
12
+ // the shift would double-scale). This pass recognises that address shape and rewrites the
13
+ // access to a typed `aload`/`astore` carrying `elemSize = 1 << k`, so type recovery types
14
+ // `base` as `elem *` and structuring lowers it to the neutral `base[index]` index node.
15
+ //
16
+ // It is LEGALIZATION, not an idiom rewrite: the match needs the relation
17
+ // `1 << shiftImm == accessWidth`, which the patterns-as-data idiom layer's `attrEquals`
18
+ // (pattern/engine.ts) cannot state. Only the shift-scaled form is handled — elemSize = 1 << k
19
+ // (2 or 4 in practice), where the shifted operand is unambiguously the index. The unscaled
20
+ // byte form (`add(base, index)`, elemSize 1) is deferred: without types, base and index are
21
+ // indistinguishable there.
22
+ import { Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
23
+
24
+ // If `addr` is `add(base, shl(index, k))` with `1 << k === width`, return {base, index}.
25
+ // The `add` is commutative, so the scaled side may be either operand.
26
+ function scaledAddress(addr: Value, width: number, defs: Map<Value, Op>): { base: Value; index: Value } | null {
27
+ const add = defs.get(addr);
28
+ if (!add || add.opcode !== 'add' || add.operands.length !== 2) {
29
+ return null;
30
+ }
31
+ for (const [scaledSide, baseSide] of [
32
+ [0, 1],
33
+ [1, 0],
34
+ ] as const) {
35
+ const shl = defs.get(add.operands[scaledSide]);
36
+ if (!shl || shl.opcode !== 'shl' || shl.operands.length !== 1) {
37
+ continue;
38
+ } // must be `shl x {imm}`
39
+ if (1 << (shl.attrs.imm as number) !== width) {
40
+ continue;
41
+ }
42
+ return { base: add.operands[baseSide], index: shl.operands[0] };
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /** Rewrite variable-index `load`/`store` (scaled-address form) into typed `aload`/`astore`.
48
+ * Returns the number of accesses legalised. Dead address ops are then DCE'd. */
49
+ export function recognizeArrays(fn: Fn): number {
50
+ let count = 0;
51
+ const defs = defOpMap(fn);
52
+ for (const b of fn.blocks) {
53
+ for (let i = 0; i < b.ops.length; i++) {
54
+ const op = b.ops[i];
55
+ if (op.opcode === 'load' && (op.attrs.off as number) === 0) {
56
+ const m = scaledAddress(op.operands[0], op.attrs.width as number, defs);
57
+ if (!m) {
58
+ continue;
59
+ }
60
+ const res = mkValue(op.results[0].type);
61
+ b.ops[i] = mkOp('aload', {
62
+ operands: [m.base, m.index],
63
+ results: [res],
64
+ attrs: { elemSize: op.attrs.width as number, signed: op.attrs.signed as boolean },
65
+ });
66
+ replaceAllUsesWith(fn, op.results[0], res);
67
+ count++;
68
+ } else if (op.opcode === 'store' && (op.attrs.off as number) === 0) {
69
+ const m = scaledAddress(op.operands[0], op.attrs.width as number, defs);
70
+ if (!m) {
71
+ continue;
72
+ }
73
+ b.ops[i] = mkOp('astore', {
74
+ operands: [m.base, m.index, op.operands[1]],
75
+ attrs: { elemSize: op.attrs.width as number },
76
+ });
77
+ count++;
78
+ }
79
+ }
80
+ }
81
+ // the now-dead `add`/`shl` address computation is reaped by the DRIVER's dce
82
+ // (pre-recovery.ts declares `dce: true` for this pass)
83
+ return count;
84
+ }
@@ -0,0 +1,52 @@
1
+ // asmlift — 32-bit constant materialisation (F-CONST; L1 recognition, ISA-neutral).
2
+ //
3
+ // A RISC target builds a 32-bit literal in two halves: a high-half load (MIPS `lui`, PPC `lis`) then a
4
+ // low-half `ori`/`addiu`. The frontends lift that pair faithfully as `or(const(hi<<16), const(lo))` /
5
+ // `add(const(hi<<16), const(lo))` — a live binary op over two `const` ops — because neither frontend can
6
+ // see across the two instructions. This pass folds any such const/const `or`/`add` into a single `const`,
7
+ // which is the form that (a) type-recovers as one 32-bit literal and (b) recompiles to the exact
8
+ // `lui;ori` / `lis;ori` pair. Without it a magic-division reciprocal or an address literal is never a
9
+ // single value the later passes can reason about.
10
+ //
11
+ // This cannot be a data-`RewritePattern`: the fold's result is COMPUTED from the two operands' values,
12
+ // which the pattern engine's numeric-exact `attrEquals` cannot express. So it lives here as an always-on
13
+ // recognizer, run before type recovery. Value-preserving and local; a single left-to-right pass suffices
14
+ // (SSA guarantees each const is defined before the op that consumes it, and a folded result feeds
15
+ // forward for any chained materialisation).
16
+ import { Fn, Op, defOpMap, mkOp } from '../ir/core';
17
+
18
+ // The binary opcodes whose const/const form is a constant. `>> 0` normalises to a signed 32-bit result
19
+ // (hardware wraparound): `|` already yields int32, `+` may exceed it and is truncated to match `addu`/`add`.
20
+ const FOLD: Record<string, (a: number, b: number) => number> = {
21
+ or: (a, b) => (a | b) >> 0,
22
+ add: (a, b) => (a + b) >> 0,
23
+ };
24
+
25
+ /** Fold each const/const `or`/`add` into one `const`, in place. Returns whether anything changed. The
26
+ * now-dead source consts are left for DCE (they may still have other uses; liveness is not our concern). */
27
+ export function recognizeConsts(fn: Fn): boolean {
28
+ let changed = false;
29
+ const defs = defOpMap(fn);
30
+ const constOf = (op: Op | undefined): number | null =>
31
+ op && op.opcode === 'const' ? (op.attrs.value as number) : null;
32
+ for (const b of fn.blocks) {
33
+ for (let i = 0; i < b.ops.length; i++) {
34
+ const op = b.ops[i];
35
+ const fold = FOLD[op.opcode];
36
+ if (!fold || op.operands.length !== 2 || op.results.length !== 1) {
37
+ continue;
38
+ }
39
+ const a = constOf(defs.get(op.operands[0]));
40
+ const c = constOf(defs.get(op.operands[1]));
41
+ if (a === null || c === null) {
42
+ continue;
43
+ }
44
+ // Reuse the SAME result Value → every existing use already points at it (no RAUW needed).
45
+ const folded = mkOp('const', { results: [op.results[0]], attrs: { value: fold(a, c) } });
46
+ b.ops.splice(i, 1, folded);
47
+ defs.set(op.results[0], folded); // keep the def map current so a chained fold sees this const
48
+ changed = true;
49
+ }
50
+ }
51
+ return changed;
52
+ }
@@ -0,0 +1,10 @@
1
+ // asmlift — the shared DESIGNED loud-failure signal for RAISE passes (the frontend twin is
2
+ // `FrontendUnsupportedError`). A raise pass throws this when it meets a shape it cannot faithfully
3
+ // recover (an overlapping/packed struct layout, …). The class stays distinct from the frontend's
4
+ // so `stageOf` (pipeline.ts) routes a raise decline to the "raise" Diagnostic stage, not "lift".
5
+ export class RaiseUnsupportedError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = 'RaiseUnsupportedError';
9
+ }
10
+ }