@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
package/src/trace.ts ADDED
@@ -0,0 +1,233 @@
1
+ // asmlift — the traced tower: decompile() plus a per-stage PROCESS record (the reasoning
2
+ // trail), browser-pure. Runs the SAME stage sequence as pipeline.ts and captures a TraceReport:
3
+ // per-stage IR dumps (each post-verify) and per-pattern before/after events. Scoring lives on
4
+ // the other side of the seam: @asmlift/cli/report enriches this into the full DecompileReport
5
+ // (objdiff score, per-pattern score deltas via the `probeScore` hook, ranked candidates) when a
6
+ // target object is available; the web playground renders the TraceReport as-is.
7
+ import { cBackend } from './backend/c';
8
+ import type { AsmData } from './frontend/asmdata';
9
+ import { frontendFor } from './frontend/registry';
10
+ import type { Fn } from './ir/core';
11
+ import { print } from './ir/print';
12
+ import { verify } from './ir/verify';
13
+ import type { LanguageBackend } from './l3/ast';
14
+ import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
15
+ import { type OnGap, raiseRecovered, structureChecked, stubResult } from './pipeline';
16
+ import type { Prototypes } from './proto';
17
+ import { type TargetDescription, structureOptionsFor } from './target';
18
+
19
+ export interface StageTrace {
20
+ id: string; // stable localization anchor: "stage:lift", "stage:recover", …
21
+ title: string; // human label = the transform this stage performs
22
+ irDump?: string; // textual IR (or emitted source for the backend stage)
23
+ verified: boolean; // verifier passed after this stage
24
+ note?: string;
25
+ }
26
+ export interface PatternEvent {
27
+ id: string; // "pattern:sdiv-pow2/2"
28
+ patternId: string;
29
+ hits: number;
30
+ beforeIr: string;
31
+ afterIr: string;
32
+ scoreBefore?: number; // filled only when a probeScore hook is supplied (cli report)
33
+ scoreAfter?: number;
34
+ scoreDelta?: number; // negative = improved toward match
35
+ }
36
+
37
+ export interface TraceReport {
38
+ version: 1;
39
+ type: 'decompile';
40
+ symbol: string;
41
+ target: {
42
+ isa: string;
43
+ compiler: string;
44
+ capabilities: TargetDescription['capabilities'];
45
+ compilerBehaviors: TargetDescription['compilerBehaviors'];
46
+ };
47
+ asm: string; // the original input assembly the run decompiled
48
+ trace: StageTrace[];
49
+ patternEvents: PatternEvent[];
50
+ source: string;
51
+ /** Set ONLY on the annotate-mode stub path (empty trace): the failure reason, machine-readable —
52
+ * so a consumer of the reasoning trail is not reduced to parsing the stub's source comments. */
53
+ declineReason?: string;
54
+ }
55
+
56
+ // Every knob decompile() takes must exist here with the SAME default — a surface that disagrees
57
+ // with pipeline.ts makes the report's headline source diverge from decompile()'s.
58
+ export interface TraceOptions {
59
+ patterns?: RewritePattern[]; // DEFAULTS to DEFAULT_IDIOM_PATTERNS, exactly like decompile()
60
+ backend?: LanguageBackend;
61
+ prototypes?: Prototypes; // header facts (callee arities + void-ness), keyed by symbol
62
+ asmData?: AsmData; // data-section side table (Regime-B jump tables), as in decompile()
63
+ onGap?: OnGap; // "strict" (default) | "annotate", as in decompile()
64
+ /** Score probe at pattern boundaries (cli report's objdiff hook). One call per boundary:
65
+ * pattern N's after-score is pattern N+1's before-score. Absent ⇒ score fields stay unset. */
66
+ probeScore?: (fn: Fn) => number | undefined;
67
+ }
68
+
69
+ // The report's per-pass trace stage id + title, keyed by the shared PreRecoveryPass.id. Kept HERE
70
+ // (not in pre-recovery.ts) because these strings are a trace concern — the driver itself is
71
+ // trace-agnostic. `title` is a function so `arrays` can fold its scaled-access count in.
72
+ const PRE_RECOVERY_TRACE: Record<string, { stage: string; title: (result: number | boolean) => string }> = {
73
+ const: { stage: 'stage:const', title: () => 'Const materialize (lui;ori → one 32-bit const)' },
74
+ magicdiv: { stage: 'stage:magicdiv', title: () => 'Magic-number division recovery (mulh/mulhu → sdiv/udiv)' },
75
+ softdiv: { stage: 'stage:softdiv', title: () => 'Soft-division lower (bl __divsi3 → division op)' },
76
+ arrays: { stage: 'stage:legalize', title: (r) => `Array legalize (${r} scaled access(es) → aload/astore)` },
77
+ structs: { stage: 'stage:structs', title: () => 'Struct-pointer recovery (access-pattern evidence)' },
78
+ shortcircuit: { stage: 'stage:shortcircuit', title: () => 'Short-circuit recovery (boolean && / ||)' },
79
+ };
80
+
81
+ /** Run the tower while recording a TraceReport. Strict mode throws on any gap (like decompile);
82
+ * annotate mode never throws — a non-localizable failure degrades to the same stub. */
83
+ export function decompileTraced(
84
+ name: string,
85
+ asm: string,
86
+ target: TargetDescription,
87
+ opts: TraceOptions = {},
88
+ ): { source: string; report: TraceReport } {
89
+ if ((opts.onGap ?? 'strict') === 'strict') {
90
+ return traceTower(name, asm, target, opts);
91
+ }
92
+ try {
93
+ return traceTower(name, asm, target, opts);
94
+ } catch (e) {
95
+ // Annotate-mode parity with decompile(): a NON-localizable failure degrades to the SAME
96
+ // stub (reason + original asm as comments) instead of a throw.
97
+ const stub = stubResult(name, asm, opts.backend ?? cBackend, e);
98
+ return {
99
+ source: stub.source,
100
+ report: {
101
+ version: 1,
102
+ type: 'decompile',
103
+ symbol: name,
104
+ target: {
105
+ isa: target.id,
106
+ compiler: target.compiler,
107
+ capabilities: target.capabilities,
108
+ compilerBehaviors: target.compilerBehaviors,
109
+ },
110
+ asm,
111
+ trace: [],
112
+ patternEvents: [],
113
+ source: stub.source,
114
+ declineReason: e instanceof Error ? e.message : String(e),
115
+ },
116
+ };
117
+ }
118
+ }
119
+
120
+ function traceTower(
121
+ name: string,
122
+ asm: string,
123
+ target: TargetDescription,
124
+ opts: TraceOptions,
125
+ ): { source: string; report: TraceReport } {
126
+ const backend = opts.backend ?? cBackend;
127
+ const prototypes = opts.prototypes ?? {};
128
+ const returnsVoid = prototypes[name]?.returnsVoid ?? false;
129
+ const trace: StageTrace[] = [];
130
+ const patternEvents: PatternEvent[] = [];
131
+
132
+ // (1) lift → typed-SSA IR
133
+ const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
134
+ verify(fn);
135
+ trace.push({ id: 'stage:lift', title: 'Lift (ISA frontend → typed-SSA IR)', irDump: print(fn), verified: true });
136
+
137
+ // (2) idiom fold (capability-gated), with an optional probed score per pattern boundary —
138
+ // the SAME default set as decompile()/decompileRanked
139
+ const active = (opts.patterns ?? DEFAULT_IDIOM_PATTERNS).filter((p) => patternApplies(p, target));
140
+ // Probe economy: ONE probe per pattern boundary — pattern N's after-score IS pattern N+1's
141
+ // before-score (the state is identical), and each probe costs a full clone + tower + external
142
+ // compile + objdiff. Zero-hit patterns emit NO event (the IR is unchanged).
143
+ let scoreBefore = opts.probeScore?.(fn);
144
+ for (const p of active) {
145
+ const beforeIr = print(fn);
146
+ const hits = applyPattern(fn, p);
147
+ dce(fn);
148
+ verify(fn);
149
+ if (hits === 0) {
150
+ continue;
151
+ }
152
+ const afterIr = print(fn);
153
+ const scoreAfter = opts.probeScore?.(fn);
154
+ patternEvents.push({
155
+ id: `pattern:${p.id}`,
156
+ patternId: p.id,
157
+ hits,
158
+ beforeIr,
159
+ afterIr,
160
+ scoreBefore,
161
+ scoreAfter,
162
+ scoreDelta: scoreBefore !== undefined && scoreAfter !== undefined ? scoreAfter - scoreBefore : undefined,
163
+ });
164
+ scoreBefore = scoreAfter;
165
+ }
166
+ if (active.length) {
167
+ trace.push({
168
+ id: 'stage:idiom',
169
+ title: `Idiom fold (${active.length} pattern(s), capability-gated)`,
170
+ irDump: print(fn),
171
+ verified: true,
172
+ });
173
+ }
174
+
175
+ // (2.35–3.5) the SHARED tower spine (pipeline.ts raiseRecovered) — pre-recovery recognizers →
176
+ // type recovery → return-sinking, byte-identical to decompile()/decompileRanked by
177
+ // construction. The trace's only additions are the entries, injected via the hooks
178
+ // (each fires post-verify).
179
+ raiseRecovered(fn, target, {
180
+ afterPass: (pass, result) => {
181
+ // A pass with no registered strings still traces under a generic title: the traced tower
182
+ // must never crash (and so diverge from decompile()) just because a NEW pre-recovery pass
183
+ // landed before its trace entry did.
184
+ const t = PRE_RECOVERY_TRACE[pass.id] ?? {
185
+ stage: `stage:${pass.id}`,
186
+ title: () => `${pass.id} (pre-recovery pass)`,
187
+ };
188
+ trace.push({ id: t.stage, title: t.title(result), irDump: print(fn), verified: true });
189
+ },
190
+ afterRecover: () =>
191
+ trace.push({ id: 'stage:recover', title: 'Type recovery (in-place on IR)', irDump: print(fn), verified: true }),
192
+ afterRetsink: () =>
193
+ trace.push({
194
+ id: 'stage:retsink',
195
+ title: 'Return-sinking (tail-duplicate return-only merge)',
196
+ irDump: print(fn),
197
+ verified: true,
198
+ }),
199
+ });
200
+
201
+ // (4) structure → neutral AST; boundary contract: no unresolved value leaked (strict) or
202
+ // spelled as a loud ASMLIFT_ERROR marker (annotate) — same onGap lever as decompile()
203
+ const sfn = structureChecked(fn, { ...structureOptionsFor(target, returnsVoid), onGap: opts.onGap ?? 'strict' });
204
+ trace.push({
205
+ id: 'stage:structure',
206
+ title: 'Structuring (IR → neutral AST)',
207
+ verified: true,
208
+ note: `${sfn.body.length} top-level statement(s)`,
209
+ });
210
+
211
+ // (5) lower + print → source text
212
+ const source = backend.emit(sfn);
213
+ trace.push({ id: 'stage:emit', title: `Backend emit (neutral AST → ${backend.id})`, irDump: source, verified: true });
214
+
215
+ return {
216
+ source,
217
+ report: {
218
+ version: 1,
219
+ type: 'decompile',
220
+ symbol: name,
221
+ target: {
222
+ isa: target.id,
223
+ compiler: target.compiler,
224
+ capabilities: target.capabilities,
225
+ compilerBehaviors: target.compilerBehaviors,
226
+ },
227
+ asm,
228
+ trace,
229
+ patternEvents,
230
+ source,
231
+ },
232
+ };
233
+ }