@asmlift/core 0.1.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.
@@ -19,7 +19,9 @@ import type { Opcode } from '../ir/opcodes';
19
19
  import { T } from '../ir/types';
20
20
  import { type Prototypes, protoArity } from '../proto';
21
21
  import { RUNTIME_HELPERS } from '../raise/softdiv';
22
+ import { type SymbolMap, lookupInterior, lookupSymbol } from '../symbols';
22
23
  import type { TargetDescription } from '../target';
24
+ import type { AsmData } from './asmdata';
23
25
  import { pushSwitchBr } from './emit';
24
26
  import { FrontendUnsupportedError } from './errors';
25
27
  import { assertInputFormat } from './format';
@@ -171,10 +173,15 @@ function splitOperands(s: string): string[] {
171
173
  // Parse a Thumb memory addressing operand `[base]` or `[base, #off]` into base register +
172
174
  // constant byte offset. (Register-scaled indices like `[base, r1, lsl #2]` are not handled
173
175
  // yet — agbcc materialises those as explicit add/lsl before the load in the cases we target.)
174
- function parseAddr(operand: string): { base: string; off: number } {
176
+ function parseAddr(operand: string): { base: string; off: number; regOff?: string } {
175
177
  const inner = operand.replace(/[[\]]/g, '').trim();
176
178
  const parts = inner.split(',').map((s) => s.trim());
177
179
  const base = parts[0];
180
+ // `[rB, rX]` — REGISTER-offset addressing. Surfaced to the caller so load/store DECLINE
181
+ // loud: silently reading `[rB]` (the old behavior) dropped the index — a silent miscompile.
182
+ if (parts[1] !== undefined && !parts[1].startsWith('#')) {
183
+ return { base, off: 0, regOff: parts[1] };
184
+ }
178
185
  const off = parts[1]?.startsWith('#') ? imm(parts[1]) : 0;
179
186
  return { base, off };
180
187
  }
@@ -724,6 +731,37 @@ function poolRef(operand: string, dataWords: Map<string, string[]>): PoolRef | n
724
731
  return { kind: 'unmodelled', why: `pool word '${w}' is a symbol offset or code label` };
725
732
  }
726
733
 
734
+ /** Does this function's literal pool name at least one EXTERNAL symbol?
735
+ *
736
+ * agbcc emits a pool word symbolically exactly when the source expression named a linker symbol,
737
+ * and numerically when it did not (`*(vu16 *)0x4000130`, an address-cast macro). So within one
738
+ * function, a numeric word sitting alongside a symbolic one is numeric *by the source's choice* —
739
+ * which is what lets {@link liftThumb} refuse to invent a name for it.
740
+ *
741
+ * The witness is required because that inference only holds for asm that KEPT its symbols. A
742
+ * linked-ROM disassembly resolves every relocation to a number, and there "numeric" says nothing
743
+ * about the source; vetoing on it would disable the map's naming for the users who need it most.
744
+ * A function whose pool names nothing external is therefore left alone (both spellings still
745
+ * enumerate, and the differ referees) rather than being read as evidence of anything.
746
+ *
747
+ * Labels DEFINED in this same asm — the jump-table pointer word, a pret-style `_08012358` pool
748
+ * label — are not external symbols and never witness: they survive disassembly whether or not
749
+ * relocations did. */
750
+ function poolNamesASymbol(dataWords: Map<string, string[]>, blockLabels: Set<string>): boolean {
751
+ for (const [, words] of dataWords) {
752
+ for (const raw of words) {
753
+ const w = raw.trim();
754
+ if (!/^[A-Za-z_]\w*$/.test(w) || w.startsWith('.L')) {
755
+ continue;
756
+ }
757
+ if (!dataWords.has(w) && !blockLabels.has(w)) {
758
+ return true;
759
+ }
760
+ }
761
+ }
762
+ return false;
763
+ }
764
+
727
765
  // Recover an agbcc Thumb jump-table dispatch. Given a dispatch block `disp` ending in `mov pc, rV`
728
766
  // and its unique bounds predecessor `bounds` ending in `cmp rX,#(N-1); bhi DEF`, verify the exact
729
767
  // idiom and read the inline table — else return null (→ the indirect-jump loud-fail fires). The
@@ -816,7 +854,14 @@ function recoverJumpTable(
816
854
  /** Lift decoded asm → an L1 Fn with block-argument SSA. `prototypes` supplies each callee's
817
855
  * declared parameter count (from the project's headers); it is authoritative for recovering
818
856
  * how many argument registers a `bl` passes (falling back to a heuristic when absent). */
819
- export function lift(name: string, asm: string, target: TargetDescription, prototypes: Prototypes = {}): Fn {
857
+ export function lift(
858
+ name: string,
859
+ asm: string,
860
+ target: TargetDescription,
861
+ prototypes: Prototypes = {},
862
+ _asmData?: AsmData,
863
+ symbols?: SymbolMap,
864
+ ): Fn {
820
865
  assertInputFormat('thumb', 'gnu-as', asm);
821
866
  const { blocks: rawBlocks, dataWords } = decode(name, asm);
822
867
 
@@ -825,6 +870,9 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
825
870
  // dispatch block is ELIDED from the CFG. A `mov pc` that is NOT a recognised table falls through
826
871
  // to the loud-fail below.
827
872
  const blockLabels = new Set(rawBlocks.map((b) => b.label));
873
+ // Whether THIS asm preserves symbol names in its literal pools — the witness the numeric-pool
874
+ // naming veto needs (see poolNamesASymbol).
875
+ const poolNamesSymbols = poolNamesASymbol(dataWords, blockLabels);
828
876
  // Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
829
877
  // predecessor — a `b disp` from elsewhere — which would dangle after elision; decline if so).
830
878
  const branchTargets = new Set<string>();
@@ -1278,6 +1326,54 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
1278
1326
  if (ins.mnemonic === 'ldr' && b !== undefined) {
1279
1327
  const pr = poolRef(b, dataWords);
1280
1328
  if (pr?.kind === 'const') {
1329
+ // Numeric-pool PROMOTION (symbols.ts): a pool-loaded word whose value the
1330
+ // project's symbol map knows becomes the NAMED global's address — the same
1331
+ // `gaddr` the symbol-pool path emits, so everything downstream is the existing
1332
+ // named-global machinery. Only pool-loaded words promote (an address built by
1333
+ // arithmetic never reaches here); a promoted `code` symbol carries `code: true`
1334
+ // so the structurer spells it `(u32)Name`, not `&Name`.
1335
+ //
1336
+ // VETOED when this asm's pool names other symbols (poolNamesASymbol): agbcc would
1337
+ // have emitted THIS word symbolically too had the source named it, so promoting it
1338
+ // spells a name the source did not use.
1339
+ //
1340
+ // …but the veto is really about RELOCATION, not about naming. An `extern` name makes
1341
+ // the compiler emit a relocated pool word, which contradicts the numeric word the
1342
+ // target shows. An address-cast MACRO expands to that same numeric literal, so it is
1343
+ // COMPATIBLE with the evidence by construction and is never vetoed — indeed it is
1344
+ // the spelling the numeric word is evidence FOR (klonoa's true source reaches these
1345
+ // cells through exactly such macros). Nothing is guessed in either case: a vetoed
1346
+ // word stays the raw constant the target says it is.
1347
+ const found = symbols ? lookupSymbol(symbols, pr.value) : null;
1348
+ const si = found && (!poolNamesSymbols || found.macroBody !== undefined) ? found : null;
1349
+ if (si) {
1350
+ const res = mkValue(T.unk(32));
1351
+ irb.ops.push(
1352
+ mkOp('gaddr', {
1353
+ results: [res],
1354
+ attrs: { sym: si.name, ...(si.kind === 'code' ? { code: true } : {}) },
1355
+ }),
1356
+ );
1357
+ writeVar(reg(a), bi, res);
1358
+ break;
1359
+ }
1360
+ // INTERIOR attribution: a value strictly inside a sized data symbol becomes
1361
+ // `gaddr sym + offset` — the `&gSym + K` tree structure.ts already lowers (and,
1362
+ // with a struct layout, spells as the named field). Sized symbols only; an
1363
+ // unattributed address stays a raw const — nothing guesses.
1364
+ // Interior attribution is always an `&gSym + K` spelling — extern-shaped, hence
1365
+ // relocated — so the veto applies to it without the macro exemption above.
1366
+ const interior = symbols && !poolNamesSymbols ? lookupInterior(symbols, pr.value) : null;
1367
+ if (interior) {
1368
+ const g = mkValue(T.unk(32));
1369
+ const k = mkValue(T.unk(32));
1370
+ const res = mkValue(T.unk(32));
1371
+ irb.ops.push(mkOp('gaddr', { results: [g], attrs: { sym: interior.info.name } }));
1372
+ irb.ops.push(mkOp('const', { results: [k], attrs: { value: interior.offset } }));
1373
+ irb.ops.push(mkOp('add', { operands: [g, k], results: [res] }));
1374
+ writeVar(reg(a), bi, res);
1375
+ break;
1376
+ }
1281
1377
  const res = mkValue(T.unk(32));
1282
1378
  irb.ops.push(mkOp('const', { results: [res], attrs: { value: pr.value } }));
1283
1379
  writeVar(reg(a), bi, res);
@@ -1303,9 +1399,19 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
1303
1399
  }
1304
1400
  const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
1305
1401
  const signed = ins.mnemonic === 'ldr' || /s/.test(ins.mnemonic.slice(3));
1306
- const { base, off } = parseAddr(b);
1402
+ const { base, off, regOff } = parseAddr(b);
1403
+ // `[rB, rX]` register-offset: lower EXACTLY as `rB + rX` then a load at offset 0 —
1404
+ // the same address arithmetic the encoding performs. (parseAddr used to silently
1405
+ // read `[rB]`, dropping the index — a silent miscompile; ldrsh exists ONLY in this
1406
+ // form in Thumb-1, so every ldrsh went through here.)
1407
+ let baseVal = readData(base, bi);
1408
+ if (regOff !== undefined) {
1409
+ const sum = mkValue(T.unk(32));
1410
+ irb.ops.push(mkOp('add', { operands: [baseVal, readData(regOff, bi)], results: [sum] }));
1411
+ baseVal = sum;
1412
+ }
1307
1413
  const res = mkValue(T.unk(32));
1308
- irb.ops.push(mkOp('load', { operands: [readData(base, bi)], results: [res], attrs: { off, width, signed } }));
1414
+ irb.ops.push(mkOp('load', { operands: [baseVal], results: [res], attrs: { off, width, signed } }));
1309
1415
  writeVar(reg(a), bi, res);
1310
1416
  break;
1311
1417
  }
@@ -1318,8 +1424,15 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
1318
1424
  break;
1319
1425
  }
1320
1426
  const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
1321
- const { base, off } = parseAddr(b);
1322
- irb.ops.push(mkOp('store', { operands: [readData(base, bi), readData(reg(a), bi)], attrs: { off, width } }));
1427
+ const { base, off, regOff } = parseAddr(b);
1428
+ let storeBase = readData(base, bi);
1429
+ if (regOff !== undefined) {
1430
+ // register-offset store: same exact `rB + rX` lowering as the load path above
1431
+ const sum = mkValue(T.unk(32));
1432
+ irb.ops.push(mkOp('add', { operands: [storeBase, readData(regOff, bi)], results: [sum] }));
1433
+ storeBase = sum;
1434
+ }
1435
+ irb.ops.push(mkOp('store', { operands: [storeBase, readData(reg(a), bi)], attrs: { off, width } }));
1323
1436
  break;
1324
1437
  }
1325
1438
  case 'bl':
package/src/l3/ast.ts CHANGED
@@ -43,7 +43,7 @@ export type Expr =
43
43
  // pointer, so the byte offset resolves to a named field instead of a scaled array index).
44
44
  // Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
45
45
  // not a width-scaled number — the byte-offset-carrying member access cpp.ts's sub-word guard needs.
46
- | { k: 'field'; base: Expr; name: string }
46
+ | { k: 'field'; base: Expr; name: string; dot?: true }
47
47
  // A GAP MARKER — the annotate-mode (`onGap: "annotate"`) spelling of a value asmlift could not
48
48
  // faithfully lift (an unmodelled instruction's `opaque` result, an unlowered transient op, a
49
49
  // dropped def). Every backend spells it as a call to the UNDEFINED symbol `ASMLIFT_ERROR("reason",
@@ -111,6 +111,10 @@ export interface SFn {
111
111
  name: string;
112
112
  params: { name: string; type: IrType }[];
113
113
  locals: { name: string; type: IrType }[]; // recovered locals, declared at function top
114
+ /** project globals referenced with a known declaration shape (symbol map) — typed for the
115
+ * legalization env (exprCType) but NEVER declared by a backend: the project's own headers
116
+ * declare them, exactly like every other global name asmlift emits. */
117
+ globals?: { name: string; type: IrType }[];
114
118
  retType: IrType;
115
119
  body: Stmt[];
116
120
  /** Struct types this function's fields reference, declared above it by the backend. Empty
@@ -148,7 +152,9 @@ export function dotBase(f: Extract<Expr, { k: 'field' }>): Extract<Expr, { k: 'i
148
152
 
149
153
  /** Boolean projection of `dotBase` for conditions that need no narrowing. */
150
154
  export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
151
- return dotBase(f) !== undefined;
155
+ // dot also spells a STRUCT-VALUE global's field (`gSym.field`, the symbol-map layout path)
156
+ // marked explicitly by the structurer via `dot: true` since the base is a `var`, not an index.
157
+ return dotBase(f) !== undefined || f.dot === true;
152
158
  }
153
159
 
154
160
  /** Structural equality of two expression trees. THE one copy of Expr deep-equal (like
@@ -0,0 +1,61 @@
1
+ // asmlift — SELF-DECLARING CANDIDATES: the pure map-reference query
2
+ // (research/self-declaring-candidates-2026-07-26.md).
3
+ //
4
+ // `collectSymbolRefs` derives, from a FINAL structured tree, every map-derived symbol the body
5
+ // references in a VALUE context — the input to the scoring layer's declaration synthesis
6
+ // (@asmlift/cli declare.ts). It is a pure tree query with no pipeline state: the enumeration
7
+ // layer (rank.ts) calls it exactly once per candidate, on the tree the candidate's source was
8
+ // emitted from, at the moment the candidate is finalized. There is deliberately NO cached
9
+ // `symbolRefs` field on `SFn` — a carried field would oblige every future l3 pass to remember
10
+ // to recompute it (a dead-store DCE that drops a tree's only reference would otherwise leave a
11
+ // stale ref, transitively reintroducing the hazards the collector excludes). Deriving at the
12
+ // consumption point makes staleness impossible by construction.
13
+ import type { SymbolInfo } from '../symbols';
14
+ import { Expr, Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
15
+
16
+ /** One recorded map-symbol VALUE reference — a name the tree references plus its map facts. */
17
+ export interface SymbolRef {
18
+ name: string;
19
+ info: SymbolInfo;
20
+ /** NAME-ONLY symbols (no map shape): the bare off-0 access facts observed in the candidate's
21
+ * own IR — attached by the enumeration (rank.ts bareGlobalAccessFacts), consumed by the
22
+ * declaration synthesis (declare.ts) as the width/signedness authority for `extern T name;`. */
23
+ access?: { width: number; signed: boolean };
24
+ }
25
+
26
+ /** The map-derived symbols a structured body references in a VALUE context — the input to the
27
+ * scoring layer's declaration synthesis. A name counts when it appears as a `var`/`addr` leaf
28
+ * and the map knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
29
+ * those leaves). A name that is ANY call's target is excluded entirely, even if also
30
+ * value-referenced: prototyping a called symbol `void F(void);` hard-errors under gcc-2.9
31
+ * when the call passes args, while leaving it undeclared keeps today's implicit-declaration
32
+ * behavior (the one honest option without arity knowledge). The function's OWN name
33
+ * (`selfName`) is excluded too — the candidate's definition IS its declaration, and a
34
+ * synthesized `void F(void);` above `s32 F(...)` is a conflicting-types hard error (a
35
+ * self-address reference resolves against the definition itself). */
36
+ export function collectSymbolRefs(body: Stmt[], symbols: Map<string, SymbolInfo>, selfName: string): SymbolRef[] {
37
+ const called = new Set<string>();
38
+ const valueRefs = new Set<string>();
39
+ const visitExpr = (e: Expr): void => {
40
+ if (e.k === 'call') {
41
+ called.add(e.fn);
42
+ } else if ((e.k === 'var' || e.k === 'addr') && symbols.has(e.name)) {
43
+ valueRefs.add(e.name);
44
+ }
45
+ exprChildren(e).forEach(visitExpr);
46
+ };
47
+ const visitStmt = (s: Stmt): void => {
48
+ // an `assign` carries its target as a NAME, not an Expr — a scalar global WRITE
49
+ // (`gSym = x;`) references the symbol every bit as much as a read does
50
+ if (s.k === 'assign' && symbols.has(s.name)) {
51
+ valueRefs.add(s.name);
52
+ }
53
+ stmtExprs(s).forEach(visitExpr);
54
+ stmtChildren(s).forEach(visitStmt);
55
+ };
56
+ body.forEach(visitStmt);
57
+ return [...valueRefs]
58
+ .filter((n) => !called.has(n) && n !== selfName)
59
+ .sort()
60
+ .map((n) => ({ name: n, info: symbols.get(n)! }));
61
+ }
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 { Prototypes } from './proto';
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
- const prototypes = opts.prototypes ?? {};
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, { ...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false), onGap });
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
+ }
@@ -1,4 +1,4 @@
1
- // asmlift — magic-number constant-division recovery (L1 recognition; gcc-mips + mwcc-ppc).
1
+ // asmlift — magic-number constant-division recovery (L1 recognition; gcc2.7.2kmc + mwcc_242_81).
2
2
  //
3
3
  // A compiler replaces `x / C` for a non-power-of-2 constant `C` with a HIGH-WORD MULTIPLY by a
4
4
  // precomputed "magic" reciprocal `M`, a shift `s`, and a sign correction. The frontend lifts the