@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/pipeline.ts
ADDED
|
@@ -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
|
+
}
|