@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,223 @@
|
|
|
1
|
+
// asmlift — STRUCT RECOVERY (L1 → typed struct pointers).
|
|
2
|
+
//
|
|
3
|
+
// THE PROBLEM. A struct-field read and an array-element read are the SAME load: `s->c` and
|
|
4
|
+
// `arr[2]` both lower to `lw v0, 8(a0)` — byte-identical AND representation-ambiguous, so the
|
|
5
|
+
// objdiff score cannot referee between them; there is no supplied layout yet (DWARF is future
|
|
6
|
+
// work). What DOES distinguish them is the ACCESS-PATTERN SHAPE on a given base: a homogeneous
|
|
7
|
+
// array produces uniform-width, uniform-stride accesses; a struct produces heterogeneous ones
|
|
8
|
+
// (mixed widths, or an offset no single element size can index).
|
|
9
|
+
//
|
|
10
|
+
// THE DISCRIMINATOR (evidence, not guess). A base's accesses form a valid homogeneous array iff
|
|
11
|
+
// there is a single width `w` with EVERY access of width `w` AND EVERY offset a multiple of `w`.
|
|
12
|
+
// If so → leave it as the array `base[idx]` (structure.ts). Otherwise → it is a struct: recover
|
|
13
|
+
// one field per distinct offset (type from the access width/signedness) and type the base
|
|
14
|
+
// `struct S *`, so structuring emits `base->field_<off>`.
|
|
15
|
+
//
|
|
16
|
+
// base @ {off0 w1, off4 w4} -> struct { u8 field_0; s32 field_4; } (mixed width)
|
|
17
|
+
// base @ {off2 w2, off4 w4} -> struct { u8 _pad0[2]; s16 field_2; s32 field_4; } (leading/
|
|
18
|
+
// gap fields the compiler had but this function never touched)
|
|
19
|
+
// base @ {off2 w4} -> LOUD decline (a 4-byte field at offset 2 is not 4-aligned —
|
|
20
|
+
// natural C alignment cannot place it there)
|
|
21
|
+
// base @ {off0 w4, off4 w4} -> array (uniform stride — untouched)
|
|
22
|
+
// base @ {off8 w4} -> array (single aligned access — no struct evidence)
|
|
23
|
+
// base @ aload(index) -> array (variable index — untouched)
|
|
24
|
+
//
|
|
25
|
+
// This recovery is BYTE-NEUTRAL — `->field_N` and `[idx]` compile identically, so it is a
|
|
26
|
+
// representation upgrade driven by access evidence, not a scored lever. GAPS between accessed
|
|
27
|
+
// offsets (unaccessed leading/interior fields) are filled with `u8[N]` PAD fields so the declared
|
|
28
|
+
// struct reproduces the observed offsets byte-for-byte and is self-describing (the same
|
|
29
|
+
// discipline raise/struct-arrays.ts withPadding uses). Each accessed field must still be
|
|
30
|
+
// naturally aligned to ITS OWN width (`off % width === 0`) — a genuinely packed layout (a field
|
|
31
|
+
// at an offset natural C alignment could not place it at) is rejected LOUD, as is an
|
|
32
|
+
// overlap/union.
|
|
33
|
+
import { Fn, Op, Value } from '../ir/core';
|
|
34
|
+
import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
35
|
+
import type { StructType } from '../l3/ast';
|
|
36
|
+
import { RaiseUnsupportedError } from './errors';
|
|
37
|
+
|
|
38
|
+
// A single observed access to a base: byte offset, access width (bytes), signedness (loads only).
|
|
39
|
+
interface Access {
|
|
40
|
+
off: number;
|
|
41
|
+
width: number;
|
|
42
|
+
signed: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Natural C size/alignment of a recovered scalar field type (all fields here are int/ptr ≤ 4 bytes,
|
|
46
|
+
// where size === align). Used to check that a plain struct decl reproduces the observed offsets.
|
|
47
|
+
//
|
|
48
|
+
// NOTE the deliberate divergence from raise/struct-arrays.ts withPadding, which looks similar but
|
|
49
|
+
// is a DIFFERENT operation: this pass is ALIGNMENT-AWARE (no explicit pad when C's own inter-field
|
|
50
|
+
// padding already lands the field) and carries NO trailing pad / struct `size` (a recovered struct
|
|
51
|
+
// here is only ever a `struct S *` pointee accessed by named field — never an array element or a
|
|
52
|
+
// by-value param, so sizeof is never taken). If the two are ever unified, PARAMETERIZE those axes
|
|
53
|
+
// — a naive merge would break the natural-alignment golden or silently mislay a struct that later
|
|
54
|
+
// becomes an element / ABI value.
|
|
55
|
+
const sizeAlign = (width: number): number => width;
|
|
56
|
+
const roundUp = (n: number, a: number) => Math.ceil(n / a) * a;
|
|
57
|
+
|
|
58
|
+
/** Does this access set describe a homogeneous array (uniform width, all offsets multiples of it)? */
|
|
59
|
+
function isArray(accesses: Access[]): boolean {
|
|
60
|
+
const w = accesses[0].width;
|
|
61
|
+
return accesses.every((a) => a.width === w && a.off % w === 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Build the struct type for a base whose accesses are NOT array-shaped. Unaccessed leading/
|
|
65
|
+
* interior gaps are FILLED with `u8[N]` pads so the declared struct reproduces the observed
|
|
66
|
+
* offsets. Throws LOUD only on a layout natural C alignment cannot reproduce: two accesses
|
|
67
|
+
* overlapping in bytes (a union — same offset with differing widths, OR distinct offsets whose
|
|
68
|
+
* ranges collide), or a field at an offset its own natural alignment could not place it at (a
|
|
69
|
+
* PACKED layout). */
|
|
70
|
+
function buildStruct(name: string, accesses: Access[]): IrType {
|
|
71
|
+
// One field per distinct offset; a load's signedness wins over a store's (more information).
|
|
72
|
+
const byOff = new Map<number, Access>();
|
|
73
|
+
for (const a of accesses) {
|
|
74
|
+
const prev = byOff.get(a.off);
|
|
75
|
+
if (!prev) {
|
|
76
|
+
byOff.set(a.off, a);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (prev.width !== a.width) {
|
|
80
|
+
throw new RaiseUnsupportedError(
|
|
81
|
+
`cannot recover struct '${name}': overlapping fields at offset ${a.off} (widths ${prev.width} and ${a.width}) — unions not modelled`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (a.signed && !prev.signed) {
|
|
85
|
+
byOff.set(a.off, a);
|
|
86
|
+
} // prefer the signed (load-derived) view
|
|
87
|
+
}
|
|
88
|
+
const dataFields: StructField[] = [...byOff.values()]
|
|
89
|
+
.sort((x, y) => x.off - y.off)
|
|
90
|
+
.map((a) => ({ off: a.off, type: scalarTypeForAccess(a.width, a.signed), name: `field_${a.off}` }));
|
|
91
|
+
// Each accessed field must be NATURALLY ALIGNED to its own width — a field the compiler would
|
|
92
|
+
// have placed at a different offset under natural C alignment is a packed layout this recovery
|
|
93
|
+
// cannot reproduce, so it is rejected LOUD (never a silently-wrong struct). The GAP before a
|
|
94
|
+
// field (an unaccessed leading/interior member) is legal: it is filled with a `u8[N]` pad below.
|
|
95
|
+
for (const f of dataFields) {
|
|
96
|
+
const sz = accessWidth(f);
|
|
97
|
+
if (f.off % sizeAlign(sz) !== 0) {
|
|
98
|
+
throw new RaiseUnsupportedError(
|
|
99
|
+
`cannot recover struct '${name}': field at offset ${f.off} (width ${sz}) is not naturally aligned — packed layout not modelled`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Place fields under natural C alignment, inserting an explicit `u8[N]` PAD only for a gap the
|
|
104
|
+
// alignment itself does NOT already cover (the same self-describing discipline as
|
|
105
|
+
// raise/struct-arrays.ts withPadding). For each field, `aligned` = where natural C alignment
|
|
106
|
+
// would put it after the running cursor:
|
|
107
|
+
// • aligned === off — natural padding lands it exactly (`{s8@0, s32@4}`): no explicit pad,
|
|
108
|
+
// C's own inter-field alignment reproduces the layout.
|
|
109
|
+
// • aligned < off — a leading/interior gap alignment can't fill (`{s16@2, s32@4}`, byte 0–1
|
|
110
|
+
// never read): insert a `u8[off - cursor]` pad so the field lands exactly.
|
|
111
|
+
// • aligned > off — the field's offset precedes where alignment would force it: it OVERLAPS
|
|
112
|
+
// the prior field (`{s32@0, s16@2}` — a union view the same-offset byOff check cannot see):
|
|
113
|
+
// reject LOUD, never a silently-mislaid field.
|
|
114
|
+
const fields: StructField[] = [];
|
|
115
|
+
let cursor = 0;
|
|
116
|
+
let pad = 0;
|
|
117
|
+
for (const f of dataFields) {
|
|
118
|
+
const aligned = roundUp(cursor, sizeAlign(accessWidth(f)));
|
|
119
|
+
if (aligned > f.off) {
|
|
120
|
+
throw new RaiseUnsupportedError(
|
|
121
|
+
`cannot recover struct '${name}': field at offset ${f.off} overlaps the prior field (aligned to ${aligned}) — unions not modelled`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (aligned < f.off) {
|
|
125
|
+
fields.push({ off: cursor, type: T.array(T.u(8), f.off - cursor), name: `_pad${pad++}` });
|
|
126
|
+
}
|
|
127
|
+
fields.push(f);
|
|
128
|
+
cursor = f.off + accessWidth(f);
|
|
129
|
+
}
|
|
130
|
+
return T.struct(name, fields);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Width in bytes of a recovered field's type (int width/8; pointer is word-sized 4).
|
|
134
|
+
function accessWidth(f: StructField): number {
|
|
135
|
+
return f.type.kind === 'int' ? f.type.width / 8 : 4;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
|
|
139
|
+
* before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
|
|
140
|
+
* plain pointer. Returns the number of bases recovered as structs. */
|
|
141
|
+
export function recognizeStructs(fn: Fn): number {
|
|
142
|
+
// Collect each base's constant-offset accesses, and the set of bases that are ALSO array bases
|
|
143
|
+
// (used by a variable-index aload/astore) — those are arrays, excluded from struct recovery.
|
|
144
|
+
const accessesOf = new Map<Value, Access[]>();
|
|
145
|
+
const arrayBases = new Set<Value>();
|
|
146
|
+
const order: Value[] = []; // first-appearance order → deterministic struct names
|
|
147
|
+
const note = (base: Value, a: Access) => {
|
|
148
|
+
let list = accessesOf.get(base);
|
|
149
|
+
if (!list) {
|
|
150
|
+
list = [];
|
|
151
|
+
accessesOf.set(base, list);
|
|
152
|
+
order.push(base);
|
|
153
|
+
}
|
|
154
|
+
list.push(a);
|
|
155
|
+
};
|
|
156
|
+
for (const b of fn.blocks) {
|
|
157
|
+
for (const op of b.ops as Op[]) {
|
|
158
|
+
switch (op.opcode) {
|
|
159
|
+
case 'load':
|
|
160
|
+
note(op.operands[0], {
|
|
161
|
+
off: op.attrs.off as number,
|
|
162
|
+
width: op.attrs.width as number,
|
|
163
|
+
signed: op.attrs.signed as boolean,
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
case 'store':
|
|
167
|
+
note(op.operands[0], {
|
|
168
|
+
off: op.attrs.off as number,
|
|
169
|
+
width: op.attrs.width as number,
|
|
170
|
+
signed: (op.attrs.width as number) === 4,
|
|
171
|
+
});
|
|
172
|
+
break;
|
|
173
|
+
case 'aload':
|
|
174
|
+
case 'astore':
|
|
175
|
+
arrayBases.add(op.operands[0]);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let count = 0;
|
|
182
|
+
for (const base of order) {
|
|
183
|
+
if (arrayBases.has(base)) {
|
|
184
|
+
continue;
|
|
185
|
+
} // a variable-index array base — leave it
|
|
186
|
+
if (base.type.kind !== 'unknown') {
|
|
187
|
+
continue;
|
|
188
|
+
} // already typed (not a bare recovery target)
|
|
189
|
+
const accesses = accessesOf.get(base)!;
|
|
190
|
+
if (isArray(accesses)) {
|
|
191
|
+
continue;
|
|
192
|
+
} // uniform stride / single aligned access → array
|
|
193
|
+
base.type = T.ptr(buildStruct(`Struct${count}`, accesses));
|
|
194
|
+
count++;
|
|
195
|
+
}
|
|
196
|
+
return count;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The distinct struct types this function references (unwrapping struct pointers on every value),
|
|
200
|
+
* deduped by name and sorted, for the backend to declare above the function. */
|
|
201
|
+
export function collectStructs(fn: Fn): StructType[] {
|
|
202
|
+
const seen = new Map<string, StructType>();
|
|
203
|
+
const consider = (t: IrType) => {
|
|
204
|
+
const s = t.kind === 'ptr' && t.to.kind === 'struct' ? t.to : t.kind === 'struct' ? t : null;
|
|
205
|
+
if (s && s.kind === 'struct' && !seen.has(s.name)) {
|
|
206
|
+
seen.set(s.name, { name: s.name, fields: s.fields, size: s.size });
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
for (const b of fn.blocks) {
|
|
210
|
+
for (const p of b.params) {
|
|
211
|
+
consider(p.type);
|
|
212
|
+
}
|
|
213
|
+
for (const op of b.ops as Op[]) {
|
|
214
|
+
for (const v of op.operands) {
|
|
215
|
+
consider(v.type);
|
|
216
|
+
}
|
|
217
|
+
for (const v of op.results) {
|
|
218
|
+
consider(v.type);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
223
|
+
}
|
package/src/rank.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// asmlift — candidate ENUMERATION, split from scoring. Type recovery is genuinely ambiguous
|
|
2
|
+
// from asm alone (is this value signed or unsigned? which branch sense did the source spell?).
|
|
3
|
+
// Rather than guess, asmlift emits a small set of CANDIDATES and lets an external differ score
|
|
4
|
+
// pick the winner — the differ is the fitness function; types/branch-sense are differ-ranked
|
|
5
|
+
// levers, not asserted truths.
|
|
6
|
+
//
|
|
7
|
+
// This module owns only the PURE half: producing the distinct candidate spellings. It has NO
|
|
8
|
+
// scorer (that stays out of @asmlift/core, which is browser-pure). `rankBy` takes an INJECTED
|
|
9
|
+
// scoreFn, so the same enumeration feeds the cli's Node/objdiff scorer and the webapp's
|
|
10
|
+
// wasm/objdiff scorer alike.
|
|
11
|
+
import { cBackend } from './backend/c';
|
|
12
|
+
import { assertDerefsTyped, assertResolved } from './contracts';
|
|
13
|
+
import type { AsmData } from './frontend/asmdata';
|
|
14
|
+
import { frontendFor } from './frontend/registry';
|
|
15
|
+
import { Fn } from './ir/core';
|
|
16
|
+
import { T } from './ir/types';
|
|
17
|
+
import { verify } from './ir/verify';
|
|
18
|
+
import type { LanguageBackend, SFn } from './l3/ast';
|
|
19
|
+
import { registerishSpellings } from './l3/regspell';
|
|
20
|
+
import { reindexWalks } from './l3/reindex';
|
|
21
|
+
import { RewritePattern } from './pattern/engine';
|
|
22
|
+
import { applyIdiomPatterns, raiseRecovered, structureChecked } from './pipeline';
|
|
23
|
+
import type { Prototypes } from './proto';
|
|
24
|
+
import { runPreRecovery } from './raise/pre-recovery';
|
|
25
|
+
import { recoverTypes } from './raise/recover';
|
|
26
|
+
import { type TargetDescription, structureOptionsFor } from './target';
|
|
27
|
+
|
|
28
|
+
/** The signedness of the entry parameters — the classic ambiguity asm cannot resolve.
|
|
29
|
+
*
|
|
30
|
+
* Struct LAYOUT is recovered structurally (raise/structs.ts), not as a scored axis here:
|
|
31
|
+
* `->field_N` and `[idx]` compile identically, so the differ cannot referee between them. */
|
|
32
|
+
const SIGN_CANDS = [
|
|
33
|
+
{ label: 'unsigned', signed: false },
|
|
34
|
+
{ label: 'signed', signed: true },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// A recovered POINTER/aggregate param must NOT be signedness-pinned: pinning a still-`unknown`
|
|
38
|
+
// pointer param to a scalar int BEFORE recovery blocks pointer recovery and emits uncompilable
|
|
39
|
+
// `*(s32)`. Only genuine scalars carry the signedness axis.
|
|
40
|
+
const NO_PIN_KINDS = new Set(['ptr', 'struct', 'array']);
|
|
41
|
+
|
|
42
|
+
/** Pin every SCALAR entry param (index not in `ptrIdx`) to the candidate signedness, before recovery. */
|
|
43
|
+
function pinScalarParams(fn: Fn, signed: boolean, ptrIdx: Set<number>): void {
|
|
44
|
+
fn.blocks[0].params.forEach((p, i) => {
|
|
45
|
+
if (ptrIdx.has(i)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (p.type.kind === 'unknown' || p.type.kind === 'int') {
|
|
49
|
+
p.type = signed ? T.s(32) : T.u(32);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EnumerateOptions {
|
|
55
|
+
patterns?: RewritePattern[];
|
|
56
|
+
backend?: LanguageBackend;
|
|
57
|
+
prototypes?: Prototypes;
|
|
58
|
+
asmData?: AsmData;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One distinct candidate spelling (a signedness × branch-sense lever combination), emitted to source. */
|
|
62
|
+
export interface Candidate {
|
|
63
|
+
label: string;
|
|
64
|
+
source: string;
|
|
65
|
+
}
|
|
66
|
+
/** A candidate paired with its score `S` (the injected scorer's result shape — must carry `.score`). */
|
|
67
|
+
export interface Scored<S> extends Candidate {
|
|
68
|
+
score: S;
|
|
69
|
+
}
|
|
70
|
+
export interface RankedResult<S> {
|
|
71
|
+
best: Scored<S>; // lowest score
|
|
72
|
+
candidates: Scored<S>[]; // sorted best (lowest) first
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Emit the DISTINCT type/branch-sense candidate spellings for `name` — PURE, no scoring.
|
|
76
|
+
* The ONE difference from `decompile()` is the signedness pin, injected between pre-recovery and
|
|
77
|
+
* recoverTypes via the `beforeRecover` hook. Duplicate sources are collapsed so the scorer never
|
|
78
|
+
* recompiles an identical spelling. */
|
|
79
|
+
export function enumerateCandidates(
|
|
80
|
+
name: string,
|
|
81
|
+
asm: string,
|
|
82
|
+
target: TargetDescription,
|
|
83
|
+
opts: EnumerateOptions = {},
|
|
84
|
+
): Candidate[] {
|
|
85
|
+
const backend = opts.backend ?? cBackend;
|
|
86
|
+
const prototypes = opts.prototypes ?? {};
|
|
87
|
+
const frontend = frontendFor(target);
|
|
88
|
+
const baseOpts = structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false);
|
|
89
|
+
// Branch-sense is a differ-ranked LEVER, the same class as param signedness: a divergent `if`
|
|
90
|
+
// can be spelled with either sense (`if (c) A else B` vs `if (!c) B else A`), and which one the
|
|
91
|
+
// source compiler emitted is genuinely ambiguous from asm. There is no safe global heuristic
|
|
92
|
+
// (`ifor` wants positive, `simpleif` wants negated, `diamond` wants positive) — emit BOTH senses
|
|
93
|
+
// and let the differ referee. The default sense is always among them, so this never scores
|
|
94
|
+
// worse; it only wins where the flip matches.
|
|
95
|
+
const defSense = baseOpts.preserveDivergentBranchSense ?? true;
|
|
96
|
+
const senseCands = [
|
|
97
|
+
{ suffix: '', sense: defSense },
|
|
98
|
+
{ suffix: '/flip-branch', sense: !defSense },
|
|
99
|
+
];
|
|
100
|
+
// Probe: recover ONCE with no signedness pin, to learn which entry params are pointers/aggregates
|
|
101
|
+
// so they are excluded from the signedness axis (see NO_PIN_KINDS). One extra lift+recover, no
|
|
102
|
+
// compile. (The probe deliberately stops after recoverTypes — it only reads the param KINDS, so
|
|
103
|
+
// the totality contract / return-sinking of the full spine are not run on it.)
|
|
104
|
+
const probe = frontend.lift(name, asm, target, prototypes, opts.asmData);
|
|
105
|
+
verify(probe);
|
|
106
|
+
applyIdiomPatterns(probe, target, opts.patterns);
|
|
107
|
+
runPreRecovery(probe, target, () => verify(probe));
|
|
108
|
+
recoverTypes(probe);
|
|
109
|
+
const ptrIdx = new Set<number>(probe.blocks[0].params.flatMap((p, i) => (NO_PIN_KINDS.has(p.type.kind) ? [i] : [])));
|
|
110
|
+
|
|
111
|
+
const seen = new Set<string>();
|
|
112
|
+
const out: Candidate[] = [];
|
|
113
|
+
for (const cand of SIGN_CANDS) {
|
|
114
|
+
const fn = frontend.lift(name, asm, target, prototypes, opts.asmData);
|
|
115
|
+
verify(fn);
|
|
116
|
+
applyIdiomPatterns(fn, target, opts.patterns);
|
|
117
|
+
// The shared tower spine (pipeline.ts) — the candidate's ONE difference from decompile() is the
|
|
118
|
+
// signedness pin, injected between pre-recovery and recoverTypes via the beforeRecover hook.
|
|
119
|
+
raiseRecovered(fn, target, { beforeRecover: () => pinScalarParams(fn, cand.signed, ptrIdx) });
|
|
120
|
+
for (const s of senseCands) {
|
|
121
|
+
// structure() reads `fn` and produces a fresh SFn (it does not mutate `fn`), so both branch
|
|
122
|
+
// senses structure the same recovered function without re-lifting.
|
|
123
|
+
const sfn = structureChecked(fn, { ...baseOpts, preserveDivergentBranchSense: s.sense });
|
|
124
|
+
// The walk→index re-spelling (l3/reindex.ts) is a THIRD lever on the same footing as
|
|
125
|
+
// signedness and branch sense: whether the source spelled `*p; p++` or `arr[i]` is
|
|
126
|
+
// genuinely ambiguous from asm (compilers strength-reduce the latter into the former), so
|
|
127
|
+
// when a loop re-spells, BOTH representations are emitted and the differ referees. The
|
|
128
|
+
// re-spelling passes the same boundary contracts as the primary; one that fails them is
|
|
129
|
+
// dropped here — never scored, never able to win.
|
|
130
|
+
const spellings: { suffix: string; source: string }[] = [{ suffix: '', source: backend.emit(sfn) }];
|
|
131
|
+
// Representation re-spellings — each a lever on the same footing as signedness/branch sense,
|
|
132
|
+
// each guarded: it must pass the same boundary contracts as the primary AND emit (a backend
|
|
133
|
+
// that declines by throwing — Pascal loud-fails unspellable shapes — drops the candidate,
|
|
134
|
+
// never aborts the enumeration). A dropped re-spelling loses nothing: the primary remains.
|
|
135
|
+
//
|
|
136
|
+
// POLICY: re-spellings derive from the BASE spelling only — levers do not compose
|
|
137
|
+
// (an /indexed + /regcopy product is deferred until a row demands it). And a lever must
|
|
138
|
+
// PRESERVE SEMANTICS by construction: the differ referees byte-exactness (a wrong candidate
|
|
139
|
+
// can never fake a score-0 match), but on a NONMATCH row the best-scoring source is shown
|
|
140
|
+
// to the user — a semantically-wrong re-spelling there is plausible-but-wrong output, the
|
|
141
|
+
// defect class this project exists to avoid. Hence each lever's decline-over-approximate
|
|
142
|
+
// gates, adversarially audited.
|
|
143
|
+
const respell = (suffix: string, alt: SFn): void => {
|
|
144
|
+
try {
|
|
145
|
+
assertResolved(alt);
|
|
146
|
+
assertDerefsTyped(alt);
|
|
147
|
+
spellings.push({ suffix, source: backend.emit(alt) });
|
|
148
|
+
} catch {
|
|
149
|
+
// contract-failing or unspellable re-spelling: drop it, keep the primary
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const indexed = reindexWalks(sfn);
|
|
153
|
+
if (indexed) {
|
|
154
|
+
respell('/indexed', indexed);
|
|
155
|
+
}
|
|
156
|
+
// the register-copy spelling (l3/regspell.ts): 0–3 variants (base; tail assign-back reusing
|
|
157
|
+
// the dead value var; tail assign-back into a fresh var — the tail choice is allocator-
|
|
158
|
+
// ambiguous, so both are ranked)
|
|
159
|
+
const REGCOPY_LABELS = ['/regcopy', '/regcopy-ret', '/regcopy-ret-fresh'];
|
|
160
|
+
registerishSpellings(sfn).forEach((alt, i) => respell(REGCOPY_LABELS[i] ?? `/regcopy-${i}`, alt));
|
|
161
|
+
for (const sp of spellings) {
|
|
162
|
+
const source = sp.source;
|
|
163
|
+
// Collapse a spelling that produced identical source (a function with no divergent `if`
|
|
164
|
+
// structures the same either way): no point scoring a duplicate spelling. Deduping the
|
|
165
|
+
// WHOLE emitted set (not just scored survivors) is equivalent — an identical source
|
|
166
|
+
// scores identically, so it can never change `best` — and it keeps the candidate set to
|
|
167
|
+
// the genuinely distinct spellings.
|
|
168
|
+
if (seen.has(source)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
seen.add(source);
|
|
172
|
+
out.push({ label: `${cand.label}${s.suffix}${sp.suffix}`, source });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Score each candidate with the injected `scoreFn` and rank by score (lowest first). A candidate
|
|
180
|
+
* whose `scoreFn` throws — e.g. its C failed to compile — is SKIPPED so it cannot sink a sibling
|
|
181
|
+
* that compiles and matches; only if EVERY candidate fails is the failure surfaced. Synchronous:
|
|
182
|
+
* the scorer must be sync (the cli/Node objdiff path). The webapp scores asynchronously and does
|
|
183
|
+
* its own await-loop over `enumerateCandidates`, reusing this module's `Candidate`/`RankedResult`
|
|
184
|
+
* types but not this driver. */
|
|
185
|
+
export function rankBy<S extends { score: number }>(
|
|
186
|
+
candidates: Candidate[],
|
|
187
|
+
symbol: string,
|
|
188
|
+
scoreFn: (source: string, symbol: string) => S,
|
|
189
|
+
): RankedResult<S> {
|
|
190
|
+
const results: Scored<S>[] = [];
|
|
191
|
+
let lastScoreErr: unknown = null; // a candidate's C that failed to compile; only fatal if ALL do
|
|
192
|
+
for (const c of candidates) {
|
|
193
|
+
try {
|
|
194
|
+
results.push({ ...c, score: scoreFn(c.source, symbol) });
|
|
195
|
+
} catch (e) {
|
|
196
|
+
lastScoreErr = e;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (results.length === 0) {
|
|
200
|
+
const why =
|
|
201
|
+
lastScoreErr instanceof Error
|
|
202
|
+
? lastScoreErr.message.split('\n')[0]
|
|
203
|
+
: String(lastScoreErr ?? 'no candidate produced');
|
|
204
|
+
throw new Error(`no scorable candidate for '${symbol}': ${why}`, { cause: lastScoreErr });
|
|
205
|
+
}
|
|
206
|
+
results.sort((a, b) => a.score.score - b.score.score);
|
|
207
|
+
return { best: results[0], candidates: results };
|
|
208
|
+
}
|