@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.
package/src/symbols.ts ADDED
@@ -0,0 +1,426 @@
1
+ // asmlift — the address→symbol map seam (research/symbol-map-plan-2026-07-22.md).
2
+ //
3
+ // A `SymbolMap` tells the pipeline what the project knows about its absolute addresses: the
4
+ // name (from the ELF `.symtab`), and optionally the byte-sensitive declaration shape (from the
5
+ // project's DWARF types-sidecar). Core only consumes the VALUE — providers that read files live
6
+ // in @asmlift/cli; tests and the webapp hand-build maps. Absent map ⇒ behavior byte-identical
7
+ // (the `prototypes`/`asmData` optionality contract).
8
+ //
9
+ // An address legitimately carries SEVERAL symbols in real projects (ldscript aliases, rename
10
+ // leftovers, deliberate typed views of one RAM region), hence `SymbolInfo[]` per address with
11
+ // the provider's canonical pick at index 0.
12
+ import { type IrType, T } from './ir/types';
13
+
14
+ /** One field of a struct-shaped global, from the sidecar DWARF layout. */
15
+ export interface SymbolStructField {
16
+ name: string;
17
+ /** byte offset from the struct start */
18
+ offset: number;
19
+ /** bytes read at `offset` (null for flexible/unknown members) */
20
+ size: number | null;
21
+ /** the field type's base-type signedness (absent = not a base type / unknown) — drives the
22
+ * u8-vs-s8 spelling of a SYNTHESIZED field decl (an s8 read is ldrb+lsl+asr, u8 is ldrb) */
23
+ signed?: boolean;
24
+ /** the field's resolved type is a pointer — synthesis must spell it as one, or relational
25
+ * compares of the loaded value flip signedness (s32 `blt` vs the pointer truth's `bcc`) */
26
+ pointer?: boolean;
27
+ /** POINTER field only: the byte width of what it points AT, when that is a base type
28
+ * (`u16 *p` → 2). The cell is 4 bytes whatever it addresses; this is the OTHER end, and it is
29
+ * byte-load-bearing because POINTER ARITHMETIC SCALES BY IT — `p - 4` through a `u16 *` and
30
+ * through a `void *` address different memory. Absent ⇒ the target is not a base type
31
+ * (`void *`, `struct S *`) and `void *` remains the honest spelling. */
32
+ pointeeSize?: number;
33
+ /** POINTER field only: signedness of the pointed-at base type, on the same terms as
34
+ * {@link pointeeSize} — it types the LOAD through the pointer (`s8` is ldrsb, `u8` ldrb). */
35
+ pointeeSigned?: boolean;
36
+ /** the field's type chain is volatile-qualified (the `vu16 field;` MMIO idiom) — a decl that
37
+ * drops it lets the compiler fold repeated reads (wrong bytes AND wrong semantics) */
38
+ volatile?: boolean;
39
+ /** the field's type chain is const-qualified — a STORE through the member's name is a hard
40
+ * error where the cast spelling it replaces only warned, so a named store declines on it */
41
+ const?: boolean;
42
+ /** ARRAY field only: the byte size of ONE element — `size` above is the WHOLE member (`u8
43
+ * x[16]` → 16), so this is the stride an indexed `field[i]` spelling needs. Its PRESENCE is
44
+ * what marks a field an array, which the exact-match field rules must exclude: a one-element
45
+ * array (`u8 x[1]`, size 1) would otherwise match a byte access and spell `->x`, which is not
46
+ * an lvalue of that width. */
47
+ elemSize?: number;
48
+ /** ARRAY field only: the ELEMENT's base-type signedness (absent = not a base type) — the same
49
+ * u8-vs-s8 fact `signed` carries for a scalar field, and the guard an indexed spelling needs
50
+ * (an s8 element read is ldrb+lsl+asr where u8 is ldrb alone) */
51
+ elemSigned?: boolean;
52
+ /** ARRAY field only: the element count (absent for a flexible array member, which declares a
53
+ * stride but no bound) — types the synthesized `T name[n];` field decl */
54
+ length?: number;
55
+ }
56
+
57
+ /** What a `shape:'pointer'` global POINTS AT, when the sidecar says its target is a struct/union.
58
+ * The pointer cell itself is 4 bytes whatever it addresses; this is about the OTHER end — it is
59
+ * what lets an access through the LOADED pointer spell `gPtr->field` instead of byte arithmetic
60
+ * on the cell's value. Absent when the target is not a struct (a scalar/pointer/function target). */
61
+ export interface SymbolPointee {
62
+ /** the name the pointee type is declared under — a struct tag, or the typedef alias for the
63
+ * `typedef struct {…} T;` idiom (absent when the DWARF gives the target no name at all) */
64
+ structName?: string;
65
+ /** total byte size of the pointee type */
66
+ size?: number;
67
+ /** the pointee's fields — absent when the sidecar carries no layout for the named type, which
68
+ * is exactly when no field spelling may be attempted */
69
+ layout?: SymbolStructField[];
70
+ /** the POINTEE type is volatile-qualified (`volatile struct S *g`) — a fact about the OTHER end
71
+ * of the pointer, independent of the cell's own qualifiers (`struct S *volatile g`, which is
72
+ * `SymbolInfo.volatile`). Synthesis must reproduce it, and it forbids the named spelling
73
+ * outright: `gPtr->m` would be a volatile access where the cast form it replaces was plain */
74
+ volatile?: boolean;
75
+ /** the POINTEE type is const-qualified (`const struct S *g`) — same independence from the
76
+ * cell's own `const`. A STORE through a member's name is then a hard error */
77
+ const?: boolean;
78
+ }
79
+
80
+ /** One declared type in a signature — width, signedness, pointer-ness. Deliberately the same
81
+ * vocabulary a struct member uses, so a parameter and a field of the same C type describe
82
+ * identically. `size: null` = the DWARF did not size it. */
83
+ export interface SymbolTypeFacts {
84
+ size: number | null;
85
+ signed: boolean | null;
86
+ pointer?: boolean;
87
+ }
88
+
89
+ /** A CODE symbol's declared signature, read from the project's own DWARF.
90
+ *
91
+ * LEAKAGE WARNING, and it is the whole reason `asIfUndecompiled` exists: a compiler emits this
92
+ * only for a function it COMPILED. Every benchmark row is already decompiled, so the row's own
93
+ * signature is present there and absent for the user, who is decompiling the one function whose
94
+ * definition their project does not have. Only CALLEE signatures transfer. */
95
+ export interface SymbolSignature {
96
+ /** the return type, or null for `void` */
97
+ returns: SymbolTypeFacts | null;
98
+ /** the definition's own parameter list — authoritative (a definition records what it takes) */
99
+ params: SymbolTypeFacts[];
100
+ }
101
+
102
+ export interface SymbolInfo {
103
+ name: string;
104
+ kind: 'code' | 'data';
105
+ /** `kind: 'code'` only — the declared signature from the project's DWARF. DEFINITION-DERIVED:
106
+ * see {@link SymbolSignature} and {@link asIfUndecompiled}. */
107
+ signature?: SymbolSignature;
108
+ /** a DWARF DIE exists for this name ⇒ the project headers declare it (safe to emit) */
109
+ declared?: boolean;
110
+ /** total byte size — complete-typed globals only; an unsized extern array has none */
111
+ size?: number;
112
+ /** the byte-sensitive declaration shape (drives P2 rendering; absent ⇒ name-only) */
113
+ shape?: 'scalar' | 'array' | 'struct' | 'pointer';
114
+ /** scalar signedness for `shape:'scalar'` (absent = not a base type, e.g. an enum) — types
115
+ * the synthesized `extern T name;` declaration */
116
+ signed?: boolean;
117
+ /** element byte width for `shape:'array'` — enables the bare `gSym[i]` spelling */
118
+ elemSize?: number;
119
+ /** element signedness for `shape:'array'` (default unsigned) — types the env entry */
120
+ elemSigned?: boolean;
121
+ /** the real struct tag for `shape:'struct'` — names the synthesized struct declaration
122
+ * (absent ⇒ synthesis mints a placeholder tag; the tag is codegen-arbitrary) */
123
+ structName?: string;
124
+ /** the declaration is volatile-qualified — load-bearing for synthesis: a non-volatile decl
125
+ * of an MMIO global lets the compiler fold/reorder accesses (wrong bytes AND semantics) */
126
+ volatile?: boolean;
127
+ /** the declaration is const-qualified (ROM tables) — spelling fidelity */
128
+ const?: boolean;
129
+ /** field names/offsets for `shape:'struct'` — enables `gSym.field` interior spelling */
130
+ layout?: SymbolStructField[];
131
+ /** the pointee facts for `shape:'pointer'` — enables the `gPtr->field` interior spelling
132
+ * (absent ⇒ the target is not a struct, or the sidecar named no layout for it) */
133
+ pointee?: SymbolPointee;
134
+ /** This name is an ADDRESS-CAST MACRO, and this is its body verbatim from the project header
135
+ * (`(*(u32 *)0x03005290)`). Some projects name a fixed RAM cell that way instead of declaring
136
+ * an `extern` — and the two are not interchangeable in the bytes: an `extern` makes the
137
+ * compiler emit a RELOCATED pool word (`.word gSym`), while the macro expands to a literal
138
+ * address and emits a NUMERIC one (`.word 0x3005290`). Matching a target that shows the
139
+ * numeric word therefore requires the macro spelling, not merely a name.
140
+ *
141
+ * Everything else about it is already the global machinery: the macro expands to an lvalue, so
142
+ * `gName`, `gName = v` and `&gName` all mean what they mean for an `extern`. Only the
143
+ * DECLARATION differs — `#define name body` instead of `extern T name;` — which is why the
144
+ * body is carried rather than reconstructed. */
145
+ macroBody?: string;
146
+ }
147
+
148
+ /** address → symbols at that address; `[0]` is the provider's canonical pick. */
149
+ export type SymbolMap = Map<number, SymbolInfo[]>;
150
+
151
+ /** THE one test for "is this field an array". The PRESENCE of `elemSize` is what marks one (see
152
+ * the field doc) — `length` is a separate fact that a flexible array member legitimately lacks,
153
+ * so testing it instead silently reclassifies such a member as a scalar cell. */
154
+ export function isArrayField(f: SymbolStructField): boolean {
155
+ return f.elemSize !== undefined;
156
+ }
157
+
158
+ /** A layout member that {@link declaredFields} passed: sizable, and seated at an offset no
159
+ * earlier member already covers. */
160
+ export type DeclaredField = SymbolStructField & { size: number };
161
+
162
+ /** Is `f` shaped like a layout member at all? `SymbolMap` is public API — a caller-supplied map
163
+ * (the webapp accepts one) must be DECLINED, never crash the pipeline. */
164
+ function wellFormedField(f: unknown): f is SymbolStructField {
165
+ if (typeof f !== 'object' || f === null) {
166
+ return false;
167
+ }
168
+ const m = f as Partial<SymbolStructField>;
169
+ return (
170
+ typeof m.name === 'string' &&
171
+ typeof m.offset === 'number' &&
172
+ Number.isFinite(m.offset) &&
173
+ (m.size === null || (typeof m.size === 'number' && Number.isFinite(m.size) && m.size >= 0))
174
+ );
175
+ }
176
+
177
+ /**
178
+ * THE one definition of "which members of this layout exist", for every consumer: the declaration
179
+ * SYNTHESIS that PRINTS them (declare.ts) and the access rules that NAME them (structure.ts).
180
+ * Returns the members in offset order, or null when the layout cannot be reproduced faithfully at
181
+ * all — and the two answers must be the same answer, because core naming a member that synthesis
182
+ * does not declare is non-compiling C.
183
+ *
184
+ * Declines the WHOLE layout on an unsizable member (its successors' offsets are then unknowable,
185
+ * so no member of it can be seated), on a malformed one, and on an array member whose
186
+ * `elemSize * length` does not account for its `size` (the three facts contradict each other, so
187
+ * none of them can be trusted). SELECTS by dropping a member an earlier one already covers — the
188
+ * union-alias rule: `struct { u32 word; u16 half; }` at one offset declares the first view only,
189
+ * so `half` is a name no declaration carries and no access may spell.
190
+ */
191
+ export function declaredFields(layout: SymbolStructField[] | undefined): DeclaredField[] | null {
192
+ if (!Array.isArray(layout)) {
193
+ return null;
194
+ }
195
+ // Validate BEFORE sorting: the comparator reads `.offset`, so a malformed entry would throw
196
+ // there rather than decline here — the crash this function exists to prevent.
197
+ for (const m of layout) {
198
+ if (!wellFormedField(m) || m.size === null) {
199
+ return null;
200
+ }
201
+ if (isArrayField(m) && m.length !== undefined && m.elemSize! * m.length !== m.size) {
202
+ return null;
203
+ }
204
+ }
205
+ const members = (layout as DeclaredField[]).slice().sort((a, b) => a.offset - b.offset);
206
+ const out: DeclaredField[] = [];
207
+ let cursor = 0;
208
+ for (const m of members) {
209
+ if (m.offset < cursor) {
210
+ continue; // an overlapping (union) member: the first view is declared, the alias is not
211
+ }
212
+ out.push(m);
213
+ cursor = m.offset + m.size;
214
+ }
215
+ return out;
216
+ }
217
+
218
+ /**
219
+ * THE one copy of "what C type does a map field declare", consumed by the declaration SYNTHESIS
220
+ * (declare.ts, which prints it). A per-consumer copy would let the emitted declaration and the
221
+ * type a consumer reasoned against disagree about what a member is.
222
+ *
223
+ * An ARRAY field declares its own element type and length — spelling `u16 x[8]` as `u8 x[16]`
224
+ * keeps the layout but makes `x[i]` index BYTES, a wrong address. That spelling is used ONLY when
225
+ * the element is a 1/2/4-byte BASE type, `elemSigned` being the witness that it is one: an array
226
+ * of 2-byte STRUCTS declared `u16 x[n]` acquires an alignment the real member does not have, and
227
+ * at an odd offset the compiler then inserts padding that shifts every later member. Such a
228
+ * member declares the byte array of its own size instead, which has no alignment to acquire.
229
+ *
230
+ * A POINTER field types `void *` (an integer guess flips relational compares of the loaded value).
231
+ * Everything else is the 1/2/4 scalar cell at its declared signedness — with the 4-byte
232
+ * no-base-type case (an enum member) spelled s32 on the C89 enum=int rule — or a `u8 name[size]`
233
+ * byte array when it is no scalar cell at all (a nested struct, an 8-byte member).
234
+ */
235
+ export function symbolFieldType(f: DeclaredField): IrType {
236
+ if (isArrayField(f)) {
237
+ const scalarElem = f.elemSigned !== undefined && (f.elemSize === 1 || f.elemSize === 2 || f.elemSize === 4);
238
+ return scalarElem && f.length !== undefined && f.elemSize! * f.length === f.size
239
+ ? T.array(T.int(f.elemSize! * 8, f.elemSigned!), f.length)
240
+ : T.array(T.u(8), f.size);
241
+ }
242
+ if (f.pointer && f.size === 4) {
243
+ // The pointee width is byte-load-bearing: arithmetic on the loaded pointer scales by it, so
244
+ // `p - 4` through the header's `u16 *` and through a guessed `void *` reach different bytes.
245
+ // Only a base-type target is spelled; anything else keeps `void *`, which is address-identical
246
+ // for any object pointer and never derefs.
247
+ const scalarPointee = f.pointeeSize === 1 || f.pointeeSize === 2 || f.pointeeSize === 4;
248
+ return T.ptr(scalarPointee ? T.int(f.pointeeSize! * 8, f.pointeeSigned ?? false) : T.void());
249
+ }
250
+ if (f.size === 1 || f.size === 2 || f.size === 4) {
251
+ return T.int(f.size * 8, f.signed ?? (f.size === 4 ? ENUM_IS_SIGNED : false));
252
+ }
253
+ return T.array(T.u(8), f.size);
254
+ }
255
+ /** A 4-byte member/scalar with NO base-type signedness is the enum idiom — C89 says int. */
256
+ export const ENUM_IS_SIGNED = true;
257
+
258
+ /**
259
+ * THE gate on every spelling through a POINTER global's value: the members a `gPtr->member`
260
+ * spelling may name, or null when nothing may be named through this pointee at all. Null unless
261
+ * the pointee is named (synthesis has no tag to declare it under otherwise), sized (the struct
262
+ * type is incomplete otherwise), and its layout is declarable ({@link declaredFields}) — the same
263
+ * three conditions declare.ts needs to emit `struct Tag *gPtr;` rather than falling back to
264
+ * `extern void *gPtr;`. Both must decline together: core naming a member of an undeclared pointee
265
+ * is non-compiling C.
266
+ */
267
+ export function pointeeFields(pointee: SymbolPointee | undefined): DeclaredField[] | null {
268
+ if (pointee?.structName === undefined || pointee.size === undefined) {
269
+ return null;
270
+ }
271
+ return declaredFields(pointee.layout);
272
+ }
273
+
274
+ /** Kind-aware two-probe lookup for a pool-loaded 32-bit value. Exact match first (any kind);
275
+ * on miss, `value & ~1` — accepted ONLY when the hit is code, because ELF function addresses
276
+ * are stored with the Thumb bit cleared while a Thumb code pointer in a pool is odd. An exact
277
+ * odd-DATA hit therefore wins over a masked code hit (odd data addresses are real). */
278
+ export function lookupSymbol(map: SymbolMap, value: number): SymbolInfo | null {
279
+ const exact = map.get(value)?.[0];
280
+ if (exact) {
281
+ return exact;
282
+ }
283
+ if ((value & 1) === 1) {
284
+ const masked = map.get(value & ~1)?.[0];
285
+ if (masked?.kind === 'code') {
286
+ return masked;
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+
292
+ /** Interior attribution: the data symbol whose `[address, address+size)` range contains
293
+ * `value` strictly inside (offset > 0 — exact bases go through `lookupSymbol`). Only
294
+ * complete-typed globals carry a size, so unsized arrays never attribute. */
295
+ export function lookupInterior(map: SymbolMap, value: number): { info: SymbolInfo; offset: number } | null {
296
+ for (const [addr, infos] of map) {
297
+ const info = infos[0];
298
+ if (info.kind !== 'data' || info.size === undefined) {
299
+ continue;
300
+ }
301
+ if (value > addr && value < addr + info.size) {
302
+ return { info, offset: value - addr };
303
+ }
304
+ }
305
+ return null;
306
+ }
307
+
308
+ /** The `SymbolInfo` keys a project's DWARF can only carry because the symbol's DEFINITION was
309
+ * compiled from C. Everything else in the map survives a function that is still `INCLUDE_ASM`:
310
+ * its `.symtab` entry exists (the asm defines the label), and its globals are typed by the OTHER
311
+ * translation units that declare them. Listed here, once, so {@link asIfUndecompiled} and any
312
+ * later definition-derived fact (a signature, a local's type, a register location) stay in sync. */
313
+ const DEFINITION_DERIVED_KEYS = ['declared', 'signature'] as const satisfies readonly (keyof SymbolInfo)[];
314
+
315
+ /** The map a user actually has while decompiling `fn` — i.e. with `fn` still an `INCLUDE_ASM`
316
+ * stub in their project.
317
+ *
318
+ * Every benchmark row is a function someone ALREADY decompiled, so the project ELF carries
319
+ * facts about it that exist only *because* the work is done. Scoring against those facts
320
+ * measures the harness, not the tool: it flatters any feature that reads them and transfers
321
+ * nothing to the user, who is decompiling the one function whose definition is absent. This
322
+ * rebuilds the map as that user's ELF would give it.
323
+ *
324
+ * What it strips is the row's own DEFINITION-derived facts ({@link DEFINITION_DERIVED_KEYS}),
325
+ * NOT its name: an `INCLUDE_ASM` function still has a `.symtab` entry, so dropping the symbol
326
+ * outright would understate what a user has and make the map look worse than it is. Callee
327
+ * signatures, globals and struct layouts all stay — those are the transferable facts, and they
328
+ * are the point.
329
+ *
330
+ * Address identity is preserved (aliases keep their order, `[0]` stays canonical) so a filtered
331
+ * map is a drop-in for the unfiltered one. */
332
+ export function asIfUndecompiled(map: SymbolMap, fn: string): SymbolMap {
333
+ const leaks = (info: SymbolInfo): boolean =>
334
+ info.kind === 'code' && info.name === fn && DEFINITION_DERIVED_KEYS.some((k) => info[k] !== undefined);
335
+ // Return the SAME map when the row's own symbol carries no definition-derived fact — the common
336
+ // case today, and it keeps the filter free to apply unconditionally on every row of a run.
337
+ let any = false;
338
+ for (const infos of map.values()) {
339
+ if (infos.some(leaks)) {
340
+ any = true;
341
+ break;
342
+ }
343
+ }
344
+ if (!any) {
345
+ return map;
346
+ }
347
+ const out: SymbolMap = new Map();
348
+ for (const [addr, infos] of map) {
349
+ out.set(
350
+ addr,
351
+ infos.map((info) => {
352
+ if (!leaks(info)) {
353
+ return info;
354
+ }
355
+ const stripped = { ...info };
356
+ for (const k of DEFINITION_DERIVED_KEYS) {
357
+ delete stripped[k];
358
+ }
359
+ return stripped;
360
+ }),
361
+ );
362
+ }
363
+ return out;
364
+ }
365
+
366
+ /** Every fact but the name, canonically ordered — the equality a name collision is judged by. */
367
+ function factsOf(info: SymbolInfo): string {
368
+ return JSON.stringify(
369
+ Object.keys(info)
370
+ .filter((k) => k !== 'name')
371
+ .sort()
372
+ .map((k) => [k, info[k as keyof SymbolInfo]]),
373
+ );
374
+ }
375
+
376
+ /** NAME-keyed view over every symbol in the map — what the structurer consumes (it sees gaddr
377
+ * symbol names, not addresses). Aliases at one address each appear under their own name.
378
+ *
379
+ * One name can sit at SEVERAL addresses in a real project (file-static `sMenu` in two
380
+ * translation units, a `.symtab` full of same-named locals). Where those entries agree on their
381
+ * facts the collision is harmless — `InitSprite` at 16 sa3 addresses is 16 identical name-only
382
+ * entries. Where they DISAGREE, silently keeping whichever the map iterated last would apply one
383
+ * address's declaration shape to another address's global: the same layout, the wrong struct.
384
+ *
385
+ * So a disagreeing name degrades to NAME-ONLY rather than picking. The name survives (dropping it
386
+ * outright would leave the reference undeclarable in the self-declared scoring world, turning a
387
+ * spelling question into a compile failure); only the shape facts, which are what could be wrong,
388
+ * are withheld — the honest cast spellings take over. `kind` is kept: it never disagrees in the
389
+ * vendored maps, and it is settled address-side by `lookupSymbol` before a name is ever used. */
390
+ export function symbolsByName(map: SymbolMap): Map<string, SymbolInfo> {
391
+ const byName = new Map<string, SymbolInfo>();
392
+ const conflicted = new Set<string>();
393
+ for (const infos of map.values()) {
394
+ for (const info of infos) {
395
+ const prev = byName.get(info.name);
396
+ if (prev === undefined) {
397
+ byName.set(info.name, info);
398
+ } else if (factsOf(prev) !== factsOf(info)) {
399
+ conflicted.add(info.name);
400
+ }
401
+ }
402
+ }
403
+ for (const name of conflicted) {
404
+ byName.set(name, { name, kind: byName.get(name)!.kind });
405
+ }
406
+ return byName;
407
+ }
408
+
409
+ /** Serialize a SymbolMap to a byte-stable JSON object (hex keys, sorted; array order kept —
410
+ * `[0]` is the canonical pick). The benchmark vendors this; the ELF itself never leaves the
411
+ * project checkout. */
412
+ export function symbolMapToJson(map: SymbolMap): Record<string, SymbolInfo[]> {
413
+ const out: Record<string, SymbolInfo[]> = {};
414
+ for (const addr of [...map.keys()].sort((a, b) => a - b)) {
415
+ out[`0x${addr.toString(16).padStart(8, '0')}`] = map.get(addr)!;
416
+ }
417
+ return out;
418
+ }
419
+
420
+ export function symbolMapFromJson(obj: Record<string, SymbolInfo[]>): SymbolMap {
421
+ const map: SymbolMap = new Map();
422
+ for (const [k, infos] of Object.entries(obj)) {
423
+ map.set(Number.parseInt(k, 16), infos);
424
+ }
425
+ return map;
426
+ }
package/src/trace.ts CHANGED
@@ -14,6 +14,7 @@ import type { LanguageBackend } from './l3/ast';
14
14
  import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
15
15
  import { type OnGap, raiseRecovered, structureChecked, stubResult } from './pipeline';
16
16
  import type { Prototypes } from './proto';
17
+ import { type SymbolMap, symbolsByName } from './symbols';
17
18
  import { type TargetDescription, structureOptionsFor } from './target';
18
19
 
19
20
  export interface StageTrace {
@@ -60,6 +61,7 @@ export interface TraceOptions {
60
61
  backend?: LanguageBackend;
61
62
  prototypes?: Prototypes; // header facts (callee arities + void-ness), keyed by symbol
62
63
  asmData?: AsmData; // data-section side table (Regime-B jump tables), as in decompile()
64
+ symbols?: SymbolMap; // address→symbol map (symbols.ts), as in decompile(); absent ⇒ inert
63
65
  onGap?: OnGap; // "strict" (default) | "annotate", as in decompile()
64
66
  /** Score probe at pattern boundaries (cli report's objdiff hook). One call per boundary:
65
67
  * pattern N's after-score is pattern N+1's before-score. Absent ⇒ score fields stay unset. */
@@ -130,7 +132,7 @@ function traceTower(
130
132
  const patternEvents: PatternEvent[] = [];
131
133
 
132
134
  // (1) lift → typed-SSA IR
133
- const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
135
+ const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
134
136
  verify(fn);
135
137
  trace.push({ id: 'stage:lift', title: 'Lift (ISA frontend → typed-SSA IR)', irDump: print(fn), verified: true });
136
138
 
@@ -200,7 +202,11 @@ function traceTower(
200
202
 
201
203
  // (4) structure → neutral AST; boundary contract: no unresolved value leaked (strict) or
202
204
  // spelled as a loud ASMLIFT_ERROR marker (annotate) — same onGap lever as decompile()
203
- const sfn = structureChecked(fn, { ...structureOptionsFor(target, returnsVoid), onGap: opts.onGap ?? 'strict' });
205
+ const sfn = structureChecked(fn, {
206
+ ...structureOptionsFor(target, returnsVoid),
207
+ onGap: opts.onGap ?? 'strict',
208
+ ...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
209
+ });
204
210
  trace.push({
205
211
  id: 'stage:structure',
206
212
  title: 'Structuring (IR → neutral AST)',