@asmlift/core 0.2.0 → 0.3.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/package.json +1 -1
- package/src/backend/cfamily.ts +30 -4
- package/src/contracts.ts +22 -3
- package/src/declare.ts +225 -0
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/thumb.ts +119 -6
- package/src/l3/ast.ts +8 -2
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +126 -0
- package/src/pipeline.ts +15 -4
- package/src/proto.ts +55 -0
- package/src/rank.ts +215 -76
- package/src/structure/structure.ts +462 -45
- package/src/symbols.ts +426 -0
- package/src/trace.ts +8 -2
package/src/l3/typing.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type VarTypes = (name: string) => IrType | undefined;
|
|
|
27
27
|
|
|
28
28
|
export function declaredTypes(fn: SFn): VarTypes {
|
|
29
29
|
const m = new Map<string, IrType>();
|
|
30
|
+
// shape-known project globals first, so a (theoretical) local of the same name wins
|
|
31
|
+
for (const g of fn.globals ?? []) {
|
|
32
|
+
m.set(g.name, g.type);
|
|
33
|
+
}
|
|
30
34
|
for (const p of fn.params) {
|
|
31
35
|
m.set(p.name, p.type);
|
|
32
36
|
}
|
package/src/macros.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// asmlift — address-cast macros: the OTHER way a project names a fixed RAM cell.
|
|
2
|
+
//
|
|
3
|
+
// Some decomp projects declare `extern u16 gCounter;` and let the linker place it; others write
|
|
4
|
+
// `#define gCounter (*(u16 *)0x03001234)`. Both read the same cell, but they are NOT
|
|
5
|
+
// interchangeable in the bytes an old compiler emits: an `extern` produces a RELOCATED literal-pool
|
|
6
|
+
// word (`.word gCounter`), the macro a NUMERIC one (`.word 0x3001234`). A target that shows the
|
|
7
|
+
// numeric word can therefore only be matched by the macro spelling — a symtab name will not do,
|
|
8
|
+
// and no `.symtab` carries these names in the first place (a macro is not a symbol).
|
|
9
|
+
//
|
|
10
|
+
// This module is the PURE recognizer over preprocessor output (`cpp -dD`). Everything it accepts is
|
|
11
|
+
// a fact it can name exactly; everything else is refused, because a wrong width or a dropped
|
|
12
|
+
// `volatile` is the plausible-but-wrong class — see the guards on {@link addressCastMacros}.
|
|
13
|
+
|
|
14
|
+
/** One recognized address-cast macro. */
|
|
15
|
+
export interface AddressMacro {
|
|
16
|
+
name: string;
|
|
17
|
+
/** the cell's address — the map key this macro names */
|
|
18
|
+
address: number;
|
|
19
|
+
/** the macro body VERBATIM, as the declaration must reproduce it */
|
|
20
|
+
body: string;
|
|
21
|
+
/** the cast's byte width */
|
|
22
|
+
size: number;
|
|
23
|
+
/** the cast type's signedness */
|
|
24
|
+
signed: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The scalar type spellings a cast may use, and what each one means. Deliberately a CLOSED table:
|
|
28
|
+
* an unrecognized spelling (a project typedef, an enum, a struct) is refused rather than guessed,
|
|
29
|
+
* and every `volatile` alias is absent so it can never be silently dropped — the qualifier changes
|
|
30
|
+
* whether repeated reads may be folded, which is both a byte and a semantic difference. */
|
|
31
|
+
const SCALAR_TYPES: Record<string, { size: number; signed: boolean }> = {
|
|
32
|
+
u8: { size: 1, signed: false },
|
|
33
|
+
s8: { size: 1, signed: true },
|
|
34
|
+
u16: { size: 2, signed: false },
|
|
35
|
+
s16: { size: 2, signed: true },
|
|
36
|
+
u32: { size: 4, signed: false },
|
|
37
|
+
s32: { size: 4, signed: true },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** `#define NAME (*(TYPE *)0xADDR)` — the ONE shape recognized. Anything else (a two-level
|
|
41
|
+
* indirection, an offset expression, a function-like macro, a bare integer constant) does not
|
|
42
|
+
* match and is therefore refused by construction. */
|
|
43
|
+
const ADDRESS_CAST = /^\s*#define\s+([A-Za-z_]\w*)\s+(\(\s*\*\s*\(\s*(\w+)\s*\*\s*\)\s*(0[xX][0-9A-Fa-f]+)\s*\))\s*$/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Recognize the address-cast macros in `cpp -dD` output, keyed by the address each names.
|
|
47
|
+
*
|
|
48
|
+
* REFUSALS, all of them because the alternative is a plausible-but-wrong spelling:
|
|
49
|
+
* - a cast type outside {@link SCALAR_TYPES} — including every `volatile` alias (`vu16`), whose
|
|
50
|
+
* qualifier must not be silently dropped;
|
|
51
|
+
* - two macros naming the SAME address (`REG_VCOUNT`/`REG_VCOUNT_L`/`REG_VCOUNT_H` at 0x04000006
|
|
52
|
+
* differ in width, and picking wrong turns an `ldrh` into an `ldrb`) — both are dropped;
|
|
53
|
+
* - one name defined at two addresses, which no correct spelling can disambiguate.
|
|
54
|
+
*/
|
|
55
|
+
export function addressCastMacros(cppOutput: string): Map<number, AddressMacro> {
|
|
56
|
+
return addressCastMacrosFrom(cppOutput.split('\n'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The same recognizer over already-split `#define NAME body` lines — what a DWARF
|
|
60
|
+
* `.debug_macinfo` reader produces once each definition is re-spelled as a directive. */
|
|
61
|
+
export function addressCastMacrosFrom(defineLines: readonly string[]): Map<number, AddressMacro> {
|
|
62
|
+
const byAddress = new Map<number, AddressMacro>();
|
|
63
|
+
const collided = new Set<number>();
|
|
64
|
+
const seenNames = new Map<string, number>();
|
|
65
|
+
for (const line of defineLines) {
|
|
66
|
+
const m = ADDRESS_CAST.exec(line);
|
|
67
|
+
if (!m) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const [, name, body, typeName, addrText] = m;
|
|
71
|
+
const type = SCALAR_TYPES[typeName];
|
|
72
|
+
if (!type) {
|
|
73
|
+
continue; // unknown or volatile-qualified spelling — refuse
|
|
74
|
+
}
|
|
75
|
+
const address = Number.parseInt(addrText, 16);
|
|
76
|
+
if (!Number.isFinite(address)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const priorAddr = seenNames.get(name);
|
|
80
|
+
if (priorAddr !== undefined && priorAddr !== address) {
|
|
81
|
+
collided.add(priorAddr);
|
|
82
|
+
collided.add(address);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
seenNames.set(name, address);
|
|
86
|
+
const prior = byAddress.get(address);
|
|
87
|
+
if (prior && prior.name !== name) {
|
|
88
|
+
collided.add(address);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
byAddress.set(address, { name, address, body, size: type.size, signed: type.signed });
|
|
92
|
+
}
|
|
93
|
+
for (const addr of collided) {
|
|
94
|
+
byAddress.delete(addr);
|
|
95
|
+
}
|
|
96
|
+
return byAddress;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The `#define` lines for every address-cast macro in `symbols` that `source` actually names.
|
|
101
|
+
*
|
|
102
|
+
* A published source that spells `gCollisionMapPtr` only compiles where that macro is defined —
|
|
103
|
+
* and a REPRODUCTION of it must therefore carry the definition, or the script the benchmark
|
|
104
|
+
* publishes cannot build the very source it publishes. Selected by the names the source uses
|
|
105
|
+
* rather than by the whole map, so a reproduction context stays the size of what it needs.
|
|
106
|
+
*
|
|
107
|
+
* Name-sorted and deduplicated: the materialized context must be byte-stable across machines.
|
|
108
|
+
*/
|
|
109
|
+
export function macroDefinesUsedBy(
|
|
110
|
+
symbols: Map<number, { name: string; macroBody?: string }[]>,
|
|
111
|
+
source: string,
|
|
112
|
+
): string {
|
|
113
|
+
const used = new Map<string, string>();
|
|
114
|
+
for (const infos of symbols.values()) {
|
|
115
|
+
for (const info of infos) {
|
|
116
|
+
if (info.macroBody === undefined || used.has(info.name)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (new RegExp(`\\b${info.name}\\b`).test(source)) {
|
|
120
|
+
used.set(info.name, info.macroBody);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const names = [...used.keys()].sort();
|
|
125
|
+
return names.length ? names.map((n) => `#define ${n} ${used.get(n)}`).join('\n') + '\n' : '';
|
|
126
|
+
}
|
package/src/pipeline.ts
CHANGED
|
@@ -13,12 +13,13 @@ import { Expr, LanguageBackend, SFn, Stmt, exprChildren, stmtChildren, stmtExprs
|
|
|
13
13
|
import { hoistReusedGlobalBases } from './l3/basecse';
|
|
14
14
|
import { eliminateDeadStores } from './l3/dce';
|
|
15
15
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
16
|
-
import type
|
|
16
|
+
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
17
17
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
18
18
|
import { type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
|
|
19
19
|
import { recoverTypes } from './raise/recover';
|
|
20
20
|
import { sinkReturns } from './raise/retsink';
|
|
21
21
|
import { StructureError, structure } from './structure/structure';
|
|
22
|
+
import { type SymbolMap, symbolsByName } from './symbols';
|
|
22
23
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
23
24
|
|
|
24
25
|
/** How a gap (a construct asmlift cannot faithfully model) degrades:
|
|
@@ -53,6 +54,10 @@ export interface DecompileOptions {
|
|
|
53
54
|
* MIPS/PPC switch declines/loud-fails; present ⇒ the frontend recovers the `switch_br`.
|
|
54
55
|
* Produced by `extractAsmData(obj, target)` from the scoring object. */
|
|
55
56
|
asmData?: AsmData;
|
|
57
|
+
/** OPTIONAL address→symbol map (symbols.ts) — the project's own names (ELF symtab) and
|
|
58
|
+
* declaration shapes (DWARF types-sidecar). Drives the Thumb numeric-pool promotion and the
|
|
59
|
+
* byte-sensitive global spellings. Absent ⇒ byte-identical to today. */
|
|
60
|
+
symbols?: SymbolMap;
|
|
56
61
|
/** gap policy — see `OnGap`. Default "strict". */
|
|
57
62
|
onGap?: OnGap;
|
|
58
63
|
}
|
|
@@ -97,9 +102,11 @@ function runTower(
|
|
|
97
102
|
onGap: OnGap,
|
|
98
103
|
): DecompileResult {
|
|
99
104
|
const backend = opts.backend ?? cBackend;
|
|
100
|
-
|
|
105
|
+
// The project's own DWARF signatures fill in what the caller did not state — in practice the
|
|
106
|
+
// CALLEES (a function still in assembly has none), which is what makes this transferable.
|
|
107
|
+
const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
|
|
101
108
|
// (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
|
|
102
|
-
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
|
|
109
|
+
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
103
110
|
verify(fn);
|
|
104
111
|
const raw = print(fn);
|
|
105
112
|
|
|
@@ -115,7 +122,11 @@ function runTower(
|
|
|
115
122
|
|
|
116
123
|
// (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
|
|
117
124
|
// every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
|
|
118
|
-
const sfn = structureChecked(fn, {
|
|
125
|
+
const sfn = structureChecked(fn, {
|
|
126
|
+
...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
|
|
127
|
+
onGap,
|
|
128
|
+
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
129
|
+
});
|
|
119
130
|
|
|
120
131
|
// (5) lower + print: neutral AST → target language
|
|
121
132
|
const source = backend.emit(sfn);
|
package/src/proto.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SymbolMap } from './symbols';
|
|
2
|
+
|
|
1
3
|
// asmlift — function prototypes: the single carrier for the caller-supplied facts a
|
|
2
4
|
// matching-decomp project reads from its headers (arg counts, void-ness). One `Prototypes`
|
|
3
5
|
// map, keyed by symbol, is threaded through every entry point and resolved at the point of
|
|
@@ -40,3 +42,56 @@ export function protoArity(p: FnProto | undefined): number | undefined {
|
|
|
40
42
|
// fall back to the frontend's arg-register heuristic rather than misread a string's `.length`.
|
|
41
43
|
return undefined;
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
/** The C type spelling for one declared parameter/return, or null when the facts do not
|
|
47
|
+
* determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
|
|
48
|
+
* makes every stride explicit — so nothing is guessed about what it points at. */
|
|
49
|
+
function typeSpelling(t: { size: number | null; signed: boolean | null; pointer?: boolean }): ParamType | null {
|
|
50
|
+
if (t.pointer) {
|
|
51
|
+
return 'void *';
|
|
52
|
+
}
|
|
53
|
+
if (t.size === 1 || t.size === 2 || t.size === 4) {
|
|
54
|
+
// A signless 4-byte type is the C89 enum idiom (int); a signless NARROW one has no honest
|
|
55
|
+
// spelling, and the width alone would not fix its load, so it is refused.
|
|
56
|
+
if (t.signed === null) {
|
|
57
|
+
return t.size === 4 ? 's32' : null;
|
|
58
|
+
}
|
|
59
|
+
return `${t.signed ? 's' : 'u'}${t.size * 8}`;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prototypes the project's own DWARF states, merged UNDER the caller's.
|
|
66
|
+
*
|
|
67
|
+
* A caller-supplied proto always wins: it comes from the user's headers or the benchmark
|
|
68
|
+
* manifest, and it is the thing a real user actually has for the function they are decompiling.
|
|
69
|
+
* The map fills the rest — in practice the CALLEES, since a function still written in assembly
|
|
70
|
+
* has no signature in its project's ELF (see SymbolSignature).
|
|
71
|
+
*
|
|
72
|
+
* Every parameter must spell faithfully or the whole entry is dropped: a partly-typed list would
|
|
73
|
+
* be read for its LENGTH and give the right arity with the wrong widths, which is worse than the
|
|
74
|
+
* arg-register heuristic it would replace.
|
|
75
|
+
*/
|
|
76
|
+
export function prototypesFromSymbols(symbols: SymbolMap | undefined, base: Prototypes = {}): Prototypes {
|
|
77
|
+
if (!symbols) {
|
|
78
|
+
return base;
|
|
79
|
+
}
|
|
80
|
+
const out: Prototypes = { ...base };
|
|
81
|
+
for (const infos of symbols.values()) {
|
|
82
|
+
for (const info of infos) {
|
|
83
|
+
if (info.kind !== 'code' || !info.signature || out[info.name] !== undefined) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const params = info.signature.params.map(typeSpelling);
|
|
87
|
+
if (params.some((p) => p === null)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
out[info.name] = {
|
|
91
|
+
params: params as ParamType[],
|
|
92
|
+
...(info.signature.returns === null ? { returnsVoid: true } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
package/src/rank.ts
CHANGED
|
@@ -12,17 +12,19 @@ import { cBackend } from './backend/c';
|
|
|
12
12
|
import { assertDerefsTyped, assertResolved } from './contracts';
|
|
13
13
|
import type { AsmData } from './frontend/asmdata';
|
|
14
14
|
import { frontendFor } from './frontend/registry';
|
|
15
|
-
import { Fn } from './ir/core';
|
|
15
|
+
import { Fn, type Value, defOpMap } from './ir/core';
|
|
16
16
|
import { T } from './ir/types';
|
|
17
17
|
import { verify } from './ir/verify';
|
|
18
18
|
import type { LanguageBackend, SFn } from './l3/ast';
|
|
19
19
|
import { registerishSpellings } from './l3/regspell';
|
|
20
20
|
import { reindexWalks } from './l3/reindex';
|
|
21
|
+
import { type SymbolRef, collectSymbolRefs } from './l3/symbol-refs';
|
|
21
22
|
import { RewritePattern } from './pattern/engine';
|
|
22
23
|
import { applyIdiomPatterns, raiseRecovered, structureChecked } from './pipeline';
|
|
23
|
-
import type
|
|
24
|
+
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
24
25
|
import { runPreRecovery } from './raise/pre-recovery';
|
|
25
26
|
import { recoverTypes } from './raise/recover';
|
|
27
|
+
import { type SymbolMap, symbolsByName } from './symbols';
|
|
26
28
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
27
29
|
|
|
28
30
|
/** The signedness of the entry parameters — the classic ambiguity asm cannot resolve.
|
|
@@ -51,25 +53,104 @@ function pinScalarParams(fn: Fn, signed: boolean, ptrIdx: Set<number>): void {
|
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
/** Bare-global ACCESS FACTS for name-only map symbols — the width/signedness authority the
|
|
57
|
+
* declaration synthesis (declare.ts) uses when the map has no shape. The map knows only the
|
|
58
|
+
* NAME (symtab-only projects: marioparty3); the candidate's own IR knows exactly how the cell
|
|
59
|
+
* is accessed, and the bare `gSym = v` / `x = gSym` spelling compiles to those bytes only
|
|
60
|
+
* under a decl of that exact width (`extern u16 g;` is `sh` where a guessed u32 is `sw`).
|
|
61
|
+
* Mirrors structure()'s scalar-global rule: a fact is recorded only for a symbol accessed
|
|
62
|
+
* EXCLUSIVELY at offset 0 with ONE width and ONE load signedness — anything else (interior
|
|
63
|
+
* offsets, address arithmetic, width or sign conflicts) records nothing, because those
|
|
64
|
+
* spellings go through `&gSym` casts where every object decl is address-identical. */
|
|
65
|
+
function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; signed: boolean }> {
|
|
66
|
+
const defs = defOpMap(fn);
|
|
67
|
+
const symOf = (v: Value): string | null => {
|
|
68
|
+
const d = defs.get(v);
|
|
69
|
+
return d?.opcode === 'gaddr' && d.attrs.code !== true ? (d.attrs.sym as string) : null;
|
|
70
|
+
};
|
|
71
|
+
const acc = new Map<string, { widths: Set<number>; signs: Set<boolean>; interior: boolean }>();
|
|
72
|
+
const get = (s: string) => acc.get(s) ?? acc.set(s, { widths: new Set(), signs: new Set(), interior: false }).get(s)!;
|
|
73
|
+
for (const b of fn.blocks) {
|
|
74
|
+
for (const op of b.ops) {
|
|
75
|
+
if (op.opcode === 'load' || op.opcode === 'store') {
|
|
76
|
+
const s = symOf(op.operands[0]);
|
|
77
|
+
if (s) {
|
|
78
|
+
const a = get(s);
|
|
79
|
+
if ((op.attrs.off as number) !== 0) {
|
|
80
|
+
a.interior = true;
|
|
81
|
+
} else {
|
|
82
|
+
a.widths.add(op.attrs.width as number);
|
|
83
|
+
if (op.opcode === 'load') {
|
|
84
|
+
a.signs.add(((op.attrs.signed as boolean) ?? false) && (op.attrs.width as number) < 4);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (op.opcode === 'aload' || op.opcode === 'astore') {
|
|
89
|
+
const s = symOf(op.operands[0]);
|
|
90
|
+
if (s) {
|
|
91
|
+
get(s).interior = true;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// any other use of the address (arithmetic, a call arg, a comparison) is interior/escape
|
|
95
|
+
for (const o of op.operands) {
|
|
96
|
+
const s = symOf(o);
|
|
97
|
+
if (s) {
|
|
98
|
+
get(s).interior = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const out = new Map<string, { width: number; signed: boolean }>();
|
|
105
|
+
for (const [s, a] of acc) {
|
|
106
|
+
if (!a.interior && a.widths.size === 1 && a.signs.size <= 1) {
|
|
107
|
+
out.set(s, { width: [...a.widths][0], signed: a.signs.has(true) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
54
113
|
export interface EnumerateOptions {
|
|
55
114
|
patterns?: RewritePattern[];
|
|
56
115
|
backend?: LanguageBackend;
|
|
57
116
|
prototypes?: Prototypes;
|
|
58
117
|
asmData?: AsmData;
|
|
118
|
+
/** address→symbol map (symbols.ts) — same contract as DecompileOptions.symbols */
|
|
119
|
+
symbols?: SymbolMap;
|
|
59
120
|
}
|
|
60
121
|
|
|
61
122
|
/** One distinct candidate spelling (a signedness × branch-sense lever combination), emitted to source. */
|
|
62
123
|
export interface Candidate {
|
|
63
124
|
label: string;
|
|
64
125
|
source: string;
|
|
126
|
+
/** the map-derived VALUE references this candidate's tree contains — what the scoring
|
|
127
|
+
* layer's declaration synthesis renders. DERIVED, never carried: computed once from the
|
|
128
|
+
* exact tree this candidate's source was emitted from, at the moment the candidate is
|
|
129
|
+
* finalized (l3/symbol-refs.ts — no pipeline stage caches refs, so they cannot go stale).
|
|
130
|
+
* Present on EVERY spelling variant that names mapped symbols — including '/raw-globals',
|
|
131
|
+
* whose tree still names pool/reloc-derived globals (it only drops the map's shaped
|
|
132
|
+
* SPELLINGS). Absent without a map — synthesis then has nothing to do. */
|
|
133
|
+
symbolRefs?: SymbolRef[];
|
|
65
134
|
}
|
|
66
135
|
/** A candidate paired with its score `S` (the injected scorer's result shape — must carry `.score`). */
|
|
67
136
|
export interface Scored<S> extends Candidate {
|
|
68
137
|
score: S;
|
|
69
138
|
}
|
|
139
|
+
/** A candidate the scorer REFUSED — its C did not build. Recorded rather than discarded: a
|
|
140
|
+
* spelling that fails to compile is a defect in the emitter or in the facts it was given, and a
|
|
141
|
+
* scoring harness that shows only the surviving sibling reports a clean win over a hidden
|
|
142
|
+
* failure. */
|
|
143
|
+
export interface DroppedCandidate {
|
|
144
|
+
label: string;
|
|
145
|
+
/** the scorer's first error line (a compiler diagnostic, usually) */
|
|
146
|
+
error: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
70
149
|
export interface RankedResult<S> {
|
|
71
150
|
best: Scored<S>; // lowest score
|
|
72
151
|
candidates: Scored<S>[]; // sorted best (lowest) first
|
|
152
|
+
/** candidates whose scoreFn threw — empty when every spelling built */
|
|
153
|
+
dropped: DroppedCandidate[];
|
|
73
154
|
}
|
|
74
155
|
|
|
75
156
|
/** Emit the DISTINCT type/branch-sense candidate spellings for `name` — PURE, no scoring.
|
|
@@ -83,9 +164,14 @@ export function enumerateCandidates(
|
|
|
83
164
|
opts: EnumerateOptions = {},
|
|
84
165
|
): Candidate[] {
|
|
85
166
|
const backend = opts.backend ?? cBackend;
|
|
86
|
-
|
|
167
|
+
// Same merge as `decompile`: the project's DWARF signatures fill in what the caller did not
|
|
168
|
+
// state, so both the annotate pass and the ranked candidates reason about one prototype table.
|
|
169
|
+
const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
|
|
87
170
|
const frontend = frontendFor(target);
|
|
88
|
-
const baseOpts =
|
|
171
|
+
const baseOpts = {
|
|
172
|
+
...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
|
|
173
|
+
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
174
|
+
};
|
|
89
175
|
// Branch-sense is a differ-ranked LEVER, the same class as param signedness: a divergent `if`
|
|
90
176
|
// can be spelled with either sense (`if (c) A else B` vs `if (!c) B else A`), and which one the
|
|
91
177
|
// source compiler emitted is genuinely ambiguous from asm. There is no safe global heuristic
|
|
@@ -101,75 +187,120 @@ export function enumerateCandidates(
|
|
|
101
187
|
// so they are excluded from the signedness axis (see NO_PIN_KINDS). One extra lift+recover, no
|
|
102
188
|
// compile. (The probe deliberately stops after recoverTypes — it only reads the param KINDS, so
|
|
103
189
|
// 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);
|
|
190
|
+
const probe = frontend.lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
105
191
|
verify(probe);
|
|
106
192
|
applyIdiomPatterns(probe, target, opts.patterns);
|
|
107
193
|
runPreRecovery(probe, target, () => verify(probe));
|
|
108
194
|
recoverTypes(probe);
|
|
109
195
|
const ptrIdx = new Set<number>(probe.blocks[0].params.flatMap((p, i) => (NO_PIN_KINDS.has(p.type.kind) ? [i] : [])));
|
|
196
|
+
// Access facts for name-only symbol declarations (see bareGlobalAccessFacts) — derived once
|
|
197
|
+
// from the probe: widths/offsets are lift-time facts, identical across every candidate.
|
|
198
|
+
const accessFacts = opts.symbols ? bareGlobalAccessFacts(probe) : new Map<string, never>();
|
|
110
199
|
|
|
111
200
|
const seen = new Set<string>();
|
|
112
201
|
const out: Candidate[] = [];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
202
|
+
// The SYMBOL-MAP spelling is itself a ranked LEVER on the same footing as signedness/branch
|
|
203
|
+
// sense: naming a global changes agbcc's codegen (the eager-load effect), and which side
|
|
204
|
+
// byte-wins is genuinely per-function — the dogfood's landed matches split between extern
|
|
205
|
+
// spellings and raw-address macros. So when a map is present the raw-global spelling is ALSO
|
|
206
|
+
// enumerated ('/raw-globals') and the differ referees; the dedup below collapses the pair
|
|
207
|
+
// wherever the map changed nothing, so this never scores worse than either side alone.
|
|
208
|
+
const symbolVariants: { suffix: string; symbols?: typeof opts.symbols }[] = opts.symbols
|
|
209
|
+
? [
|
|
210
|
+
{ suffix: '', symbols: opts.symbols },
|
|
211
|
+
{ suffix: '/raw-globals', symbols: undefined },
|
|
212
|
+
]
|
|
213
|
+
: [{ suffix: '' }];
|
|
214
|
+
for (const sv of symbolVariants) {
|
|
215
|
+
const svOpts = sv.symbols ? baseOpts : { ...baseOpts, symbols: undefined };
|
|
216
|
+
for (const cand of SIGN_CANDS) {
|
|
217
|
+
const fn = frontend.lift(name, asm, target, prototypes, opts.asmData, sv.symbols);
|
|
218
|
+
verify(fn);
|
|
219
|
+
applyIdiomPatterns(fn, target, opts.patterns);
|
|
220
|
+
// The shared tower spine (pipeline.ts) — the candidate's ONE difference from decompile() is the
|
|
221
|
+
// signedness pin, injected between pre-recovery and recoverTypes via the beforeRecover hook.
|
|
222
|
+
raiseRecovered(fn, target, { beforeRecover: () => pinScalarParams(fn, cand.signed, ptrIdx) });
|
|
223
|
+
for (const s of senseCands) {
|
|
224
|
+
// structure() reads `fn` and produces a fresh SFn (it does not mutate `fn`), so both branch
|
|
225
|
+
// senses structure the same recovered function without re-lifting.
|
|
226
|
+
const sfn = structureChecked(fn, { ...svOpts, preserveDivergentBranchSense: s.sense });
|
|
227
|
+
// The walk→index re-spelling (l3/reindex.ts) is a THIRD lever on the same footing as
|
|
228
|
+
// signedness and branch sense: whether the source spelled `*p; p++` or `arr[i]` is
|
|
229
|
+
// genuinely ambiguous from asm (compilers strength-reduce the latter into the former), so
|
|
230
|
+
// when a loop re-spells, BOTH representations are emitted and the differ referees. The
|
|
231
|
+
// re-spelling passes the same boundary contracts as the primary; one that fails them is
|
|
232
|
+
// dropped here — never scored, never able to win.
|
|
233
|
+
// Each spelling's symbol refs are DERIVED from its own final tree right where the
|
|
234
|
+
// spelling is emitted — the single point a candidate comes into existence. No pipeline
|
|
235
|
+
// stage carries refs (SFn has no such field), so a future l3 pass that rewrites the tree
|
|
236
|
+
// can never leave a stale ref behind: whatever tree reaches emit is the tree the refs
|
|
237
|
+
// describe, by construction. Collected against the FULL name-keyed map for EVERY
|
|
238
|
+
// spelling variant — the '/raw-globals' sibling drops the map's shaped SPELLINGS, but
|
|
239
|
+
// its tree still NAMES pool/reloc-derived globals (ARM `.word gSym`, MIPS `%lo(gSym)`),
|
|
240
|
+
// and those references need declarations in the self-declared scoring world exactly
|
|
241
|
+
// like the named variant's (without them every raw sibling fails to compile there,
|
|
242
|
+
// and the eval-winning raw candidate becomes unreproducible outside project headers).
|
|
243
|
+
const refsOf = (tree: SFn): { symbolRefs?: SymbolRef[] } => {
|
|
244
|
+
const refs = baseOpts.symbols
|
|
245
|
+
? collectSymbolRefs(tree.body, baseOpts.symbols, tree.name).map((r) => {
|
|
246
|
+
// name-only symbols carry the IR-derived access facts — the width authority
|
|
247
|
+
// for their synthesized declaration (shaped symbols keep the map's truth)
|
|
248
|
+
const access = r.info.shape === undefined ? accessFacts.get(r.name) : undefined;
|
|
249
|
+
return access ? { ...r, access } : r;
|
|
250
|
+
})
|
|
251
|
+
: [];
|
|
252
|
+
return refs.length ? { symbolRefs: refs } : {};
|
|
253
|
+
};
|
|
254
|
+
const spellings: { suffix: string; source: string; symbolRefs?: SymbolRef[] }[] = [
|
|
255
|
+
{ suffix: '', source: backend.emit(sfn), ...refsOf(sfn) },
|
|
256
|
+
];
|
|
257
|
+
// Representation re-spellings — each a lever on the same footing as signedness/branch sense,
|
|
258
|
+
// each guarded: it must pass the same boundary contracts as the primary AND emit (a backend
|
|
259
|
+
// that declines by throwing — Pascal loud-fails unspellable shapes — drops the candidate,
|
|
260
|
+
// never aborts the enumeration). A dropped re-spelling loses nothing: the primary remains.
|
|
261
|
+
//
|
|
262
|
+
// POLICY: re-spellings derive from the BASE spelling only — levers do not compose
|
|
263
|
+
// (an /indexed + /regcopy product is deferred until a row demands it). And a lever must
|
|
264
|
+
// PRESERVE SEMANTICS by construction: the differ referees byte-exactness (a wrong candidate
|
|
265
|
+
// can never fake a score-0 match), but on a NONMATCH row the best-scoring source is shown
|
|
266
|
+
// to the user — a semantically-wrong re-spelling there is plausible-but-wrong output, the
|
|
267
|
+
// defect class this project exists to avoid. Hence each lever's decline-over-approximate
|
|
268
|
+
// gates, adversarially audited.
|
|
269
|
+
const respell = (suffix: string, alt: SFn): void => {
|
|
270
|
+
try {
|
|
271
|
+
assertResolved(alt);
|
|
272
|
+
assertDerefsTyped(alt);
|
|
273
|
+
spellings.push({ suffix, source: backend.emit(alt), ...refsOf(alt) });
|
|
274
|
+
} catch {
|
|
275
|
+
// contract-failing or unspellable re-spelling: drop it, keep the primary
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const indexed = reindexWalks(sfn);
|
|
279
|
+
if (indexed) {
|
|
280
|
+
respell('/indexed', indexed);
|
|
150
281
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
282
|
+
// the register-copy spelling (l3/regspell.ts): 0–3 variants (base; tail assign-back reusing
|
|
283
|
+
// the dead value var; tail assign-back into a fresh var — the tail choice is allocator-
|
|
284
|
+
// ambiguous, so both are ranked)
|
|
285
|
+
const REGCOPY_LABELS = ['/regcopy', '/regcopy-ret', '/regcopy-ret-fresh'];
|
|
286
|
+
registerishSpellings(sfn).forEach((alt, i) => respell(REGCOPY_LABELS[i] ?? `/regcopy-${i}`, alt));
|
|
287
|
+
for (const sp of spellings) {
|
|
288
|
+
const source = sp.source;
|
|
289
|
+
// Collapse a spelling that produced identical source (a function with no divergent `if`
|
|
290
|
+
// structures the same either way): no point scoring a duplicate spelling. Deduping the
|
|
291
|
+
// WHOLE emitted set (not just scored survivors) is equivalent — an identical source
|
|
292
|
+
// scores identically, so it can never change `best` — and it keeps the candidate set to
|
|
293
|
+
// the genuinely distinct spellings.
|
|
294
|
+
if (seen.has(source)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
seen.add(source);
|
|
298
|
+
out.push({
|
|
299
|
+
label: `${cand.label}${s.suffix}${sp.suffix}${sv.suffix}`,
|
|
300
|
+
source,
|
|
301
|
+
...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
|
|
302
|
+
});
|
|
170
303
|
}
|
|
171
|
-
seen.add(source);
|
|
172
|
-
out.push({ label: `${cand.label}${s.suffix}${sp.suffix}`, source });
|
|
173
304
|
}
|
|
174
305
|
}
|
|
175
306
|
}
|
|
@@ -185,24 +316,32 @@ export function enumerateCandidates(
|
|
|
185
316
|
export function rankBy<S extends { score: number }>(
|
|
186
317
|
candidates: Candidate[],
|
|
187
318
|
symbol: string,
|
|
188
|
-
scoreFn: (source: string, symbol: string) => S,
|
|
319
|
+
scoreFn: (source: string, symbol: string, candidate: Candidate) => S,
|
|
189
320
|
): RankedResult<S> {
|
|
190
|
-
const results: Scored<S>[] = [];
|
|
191
|
-
|
|
192
|
-
|
|
321
|
+
const results: (Scored<S> & { order: number })[] = [];
|
|
322
|
+
const dropped: DroppedCandidate[] = []; // spellings that failed to build; only fatal if ALL do
|
|
323
|
+
let lastScoreErr: unknown = null;
|
|
324
|
+
candidates.forEach((c, order) => {
|
|
193
325
|
try {
|
|
194
|
-
results.push({ ...c, score: scoreFn(c.source, symbol) });
|
|
326
|
+
results.push({ ...c, order, score: scoreFn(c.source, symbol, c) });
|
|
195
327
|
} catch (e) {
|
|
196
328
|
lastScoreErr = e;
|
|
329
|
+
dropped.push({ label: c.label, error: firstLine(e) });
|
|
197
330
|
}
|
|
198
|
-
}
|
|
331
|
+
});
|
|
199
332
|
if (results.length === 0) {
|
|
200
|
-
|
|
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 });
|
|
333
|
+
throw new Error(`no scorable candidate for '${symbol}': ${firstLine(lastScoreErr)}`, { cause: lastScoreErr });
|
|
205
334
|
}
|
|
206
|
-
|
|
207
|
-
|
|
335
|
+
// Score first; ENUMERATION ORDER breaks a tie. That order is meaningful, not incidental:
|
|
336
|
+
// enumerateCandidates emits the symbol-map spellings before their `/raw-globals` siblings, so
|
|
337
|
+
// when both compile to the same bytes the named one wins and the reader gets `gCounter` rather
|
|
338
|
+
// than a bare address. Spelled as an explicit comparator because relying on Array#sort's
|
|
339
|
+
// stability would make the preference an accident of two unrelated decisions.
|
|
340
|
+
results.sort((a, b) => a.score.score - b.score.score || a.order - b.order);
|
|
341
|
+
return { best: results[0], candidates: results.map(({ order: _order, ...c }) => c), dropped };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** First line of whatever the scorer threw — the compiler's own diagnostic, not a stack. */
|
|
345
|
+
function firstLine(e: unknown): string {
|
|
346
|
+
return e instanceof Error ? e.message.split('\n')[0] : String(e ?? 'no candidate produced');
|
|
208
347
|
}
|