@asmlift/core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +154 -5
  4. package/src/backend/cpp.ts +3 -1
  5. package/src/backend/pascal.ts +11 -0
  6. package/src/contracts.ts +37 -5
  7. package/src/declare.ts +251 -0
  8. package/src/frontend/frontend.ts +12 -2
  9. package/src/frontend/mips.ts +24 -23
  10. package/src/frontend/opaque.ts +39 -2
  11. package/src/frontend/ssa.ts +32 -53
  12. package/src/frontend/thumb.ts +420 -32
  13. package/src/ir/opcodes.ts +44 -0
  14. package/src/ir/simplify.ts +72 -0
  15. package/src/l3/argbase.ts +216 -0
  16. package/src/l3/ast.ts +126 -6
  17. package/src/l3/basecse.ts +3 -40
  18. package/src/l3/coalesce.ts +146 -0
  19. package/src/l3/dce.ts +2 -23
  20. package/src/l3/hoist.ts +65 -0
  21. package/src/l3/reindex.ts +7 -0
  22. package/src/l3/scopebase.ts +436 -0
  23. package/src/l3/symbol-refs.ts +61 -0
  24. package/src/l3/tailmerge.ts +120 -0
  25. package/src/l3/typing.ts +4 -0
  26. package/src/macros.ts +335 -0
  27. package/src/pattern/engine.ts +99 -6
  28. package/src/pipeline.ts +20 -6
  29. package/src/proto.ts +55 -0
  30. package/src/raise/divpow2.ts +226 -0
  31. package/src/raise/gvn.ts +141 -0
  32. package/src/raise/pre-recovery.ts +37 -3
  33. package/src/raise/recover.ts +24 -7
  34. package/src/raise/retsink.ts +36 -7
  35. package/src/raise/shortcircuit.ts +264 -22
  36. package/src/raise/structs.ts +12 -2
  37. package/src/rank.ts +370 -79
  38. package/src/structure/analysis.ts +42 -1
  39. package/src/structure/structure.ts +852 -67
  40. package/src/structure/switch-recover.ts +21 -3
  41. package/src/symbols.ts +541 -0
  42. package/src/target.ts +4 -2
  43. package/src/trace.ts +17 -2
@@ -18,6 +18,12 @@ export interface SwitchRecoverDeps {
18
18
  /** is this opcode an integer comparison? */
19
19
  isCmpOpcode: (opcode: string) => boolean;
20
20
  switchAllowsNeqCase: boolean;
21
+ /** does emitting this block's ops carry a statement beyond the ops themselves? A def-site
22
+ * ANCHORED merge copy (structure.ts anchorConstCopies) is attached to a const op and emitted
23
+ * with the block's side effects — a test block carrying one is not pure however pure its
24
+ * opcodes look, because collapsing it into a `switch` discards the write while the edge copy
25
+ * it replaced stays suppressed. */
26
+ emitsAnchoredWrite: (blk: Block) => boolean;
21
27
  expr: (v: Value) => Expr;
22
28
  structureRegion: (b: Block, stop: Block | null) => Stmt[];
23
29
  }
@@ -29,7 +35,19 @@ export interface SwitchRecovery {
29
35
  }
30
36
 
31
37
  export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
32
- const { fn, defs, dom, ipdom, opBlock, isNamed, isCmpOpcode, switchAllowsNeqCase, expr, structureRegion } = deps;
38
+ const {
39
+ fn,
40
+ defs,
41
+ dom,
42
+ ipdom,
43
+ opBlock,
44
+ isNamed,
45
+ isCmpOpcode,
46
+ switchAllowsNeqCase,
47
+ emitsAnchoredWrite,
48
+ expr,
49
+ structureRegion,
50
+ } = deps;
33
51
 
34
52
  // --- Regime A: comparison-tree switch recovery ----------------------------------------------------
35
53
  // Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
@@ -128,9 +146,9 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
128
146
  if (!cmp || !isCmpOpcode(cmp.opcode)) {
129
147
  return null;
130
148
  }
131
- if (!isRoot && blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode))) {
149
+ if (!isRoot && (blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode)) || emitsAnchoredWrite(blk))) {
132
150
  return null;
133
- } // PRE4
151
+ } // PRE4 — anchored writes included: discarded with the block, while their edge copies stay suppressed
134
152
  // Which operand is the scrutinee, which is the constant?
135
153
  const [lo, ro] = cmp.operands;
136
154
  const lc = evalConst(lo),
package/src/symbols.ts ADDED
@@ -0,0 +1,541 @@
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
+ /** BITFIELD field only: the field's width in BITS. Its PRESENCE is what marks a field a
56
+ * bitfield — `size` above stays the byte span its bits touch (the read width the compiler
57
+ * uses), which is why the exact (offset,size) scalar-field rules must exclude it. The
58
+ * provider only emits these for LITTLE-ENDIAN ELFs: both the extract equation the access
59
+ * recognizer solves and the `u32 name : n` layout model the synthesis verifies are LE-GCC
60
+ * semantics, so a big-endian map carries no bitfield members at all (today's behavior). */
61
+ bitWidth?: number;
62
+ /** BITFIELD field only: the bit position of the field's LOW bit within the byte at `offset`
63
+ * (LSB-first) — the field's absolute low bit is `offset*8 + bitOffset`. Required alongside
64
+ * `bitWidth`; a bitfield missing it is malformed and declines the whole layout. */
65
+ bitOffset?: number;
66
+ }
67
+
68
+ /** What a `shape:'pointer'` global POINTS AT, when the sidecar says its target is a struct/union.
69
+ * The pointer cell itself is 4 bytes whatever it addresses; this is about the OTHER end — it is
70
+ * what lets an access through the LOADED pointer spell `gPtr->field` instead of byte arithmetic
71
+ * on the cell's value. Absent when the target is not a struct (a scalar/pointer/function target). */
72
+ export interface SymbolPointee {
73
+ /** the name the pointee type is declared under — a struct tag, or the typedef alias for the
74
+ * `typedef struct {…} T;` idiom (absent when the DWARF gives the target no name at all) */
75
+ structName?: string;
76
+ /** total byte size of the pointee type */
77
+ size?: number;
78
+ /** the pointee's fields — absent when the sidecar carries no layout for the named type, which
79
+ * is exactly when no field spelling may be attempted */
80
+ layout?: SymbolStructField[];
81
+ /** the POINTEE type is volatile-qualified (`volatile struct S *g`) — a fact about the OTHER end
82
+ * of the pointer, independent of the cell's own qualifiers (`struct S *volatile g`, which is
83
+ * `SymbolInfo.volatile`). Synthesis must reproduce it, and it forbids the named spelling
84
+ * outright: `gPtr->m` would be a volatile access where the cast form it replaces was plain */
85
+ volatile?: boolean;
86
+ /** the POINTEE type is const-qualified (`const struct S *g`) — same independence from the
87
+ * cell's own `const`. A STORE through a member's name is then a hard error */
88
+ const?: boolean;
89
+ }
90
+
91
+ /** One declared type in a signature — width, signedness, pointer-ness. Deliberately the same
92
+ * vocabulary a struct member uses, so a parameter and a field of the same C type describe
93
+ * identically. `size: null` = the DWARF did not size it. */
94
+ export interface SymbolTypeFacts {
95
+ size: number | null;
96
+ signed: boolean | null;
97
+ pointer?: boolean;
98
+ }
99
+
100
+ /** A CODE symbol's declared signature, read from the project's own DWARF.
101
+ *
102
+ * LEAKAGE WARNING, and it is the whole reason `asIfUndecompiled` exists: a compiler emits this
103
+ * only for a function it COMPILED. Every benchmark row is already decompiled, so the row's own
104
+ * signature is present there and absent for the user, who is decompiling the one function whose
105
+ * definition their project does not have. Only CALLEE signatures transfer. */
106
+ export interface SymbolSignature {
107
+ /** the return type, or null for `void` */
108
+ returns: SymbolTypeFacts | null;
109
+ /** the definition's own parameter list — authoritative (a definition records what it takes) */
110
+ params: SymbolTypeFacts[];
111
+ }
112
+
113
+ export interface SymbolInfo {
114
+ name: string;
115
+ kind: 'code' | 'data';
116
+ /** `kind: 'code'` only — the declared signature from the project's DWARF. DEFINITION-DERIVED:
117
+ * see {@link SymbolSignature} and {@link asIfUndecompiled}. */
118
+ signature?: SymbolSignature;
119
+ /** a DWARF DIE exists for this name ⇒ the project headers declare it (safe to emit) */
120
+ declared?: boolean;
121
+ /** total byte size — complete-typed globals only; an unsized extern array has none */
122
+ size?: number;
123
+ /** the byte-sensitive declaration shape (drives P2 rendering; absent ⇒ name-only) */
124
+ shape?: 'scalar' | 'array' | 'struct' | 'pointer';
125
+ /** scalar signedness for `shape:'scalar'` (absent = not a base type, e.g. an enum) — types
126
+ * the synthesized `extern T name;` declaration */
127
+ signed?: boolean;
128
+ /** element byte width for `shape:'array'` — enables the bare `gSym[i]` spelling */
129
+ elemSize?: number;
130
+ /** element signedness for `shape:'array'` (default unsigned) — types the env entry */
131
+ elemSigned?: boolean;
132
+ /** ARRAY RANK for `shape:'array'` — the per-dimension extents, outermost first (`u16
133
+ * g[4][0x400]` → `[4, 1024]`), `null` for an unbounded one. It is NOT `size`/`elemSize`
134
+ * restated: those size the object, this says how many subscripts reach an ELEMENT. `gSym[i]`
135
+ * on a rank-2 array is a ROW — against the project's own header that is a type error, or,
136
+ * where the row address flows into an integer context, silently the wrong address. So the
137
+ * bare spelling needs the leading subscripts (`gSym[0][i]`), and its ABSENCE is what forbids
138
+ * the bare spelling from being attempted at all (see the provider's dims capability gate:
139
+ * a package that cannot report rank must not be read as "rank 1"). */
140
+ dims?: (number | null)[];
141
+ /** the real struct tag for `shape:'struct'` — names the synthesized struct declaration
142
+ * (absent ⇒ synthesis mints a placeholder tag; the tag is codegen-arbitrary) */
143
+ structName?: string;
144
+ /** the declaration is volatile-qualified — load-bearing for synthesis: a non-volatile decl
145
+ * of an MMIO global lets the compiler fold/reorder accesses (wrong bytes AND semantics) */
146
+ volatile?: boolean;
147
+ /** the declaration is const-qualified (ROM tables) — spelling fidelity */
148
+ const?: boolean;
149
+ /** field names/offsets for `shape:'struct'` — enables `gSym.field` interior spelling */
150
+ layout?: SymbolStructField[];
151
+ /** the pointee facts for `shape:'pointer'` — enables the `gPtr->field` interior spelling
152
+ * (absent ⇒ the target is not a struct, or the sidecar named no layout for it) */
153
+ pointee?: SymbolPointee;
154
+ /** This name is an ADDRESS-CAST MACRO, and this is its body verbatim from the project header
155
+ * (`(*(u32 *)0x03005290)`). Some projects name a fixed RAM cell that way instead of declaring
156
+ * an `extern` — and the two are not interchangeable in the bytes: an `extern` makes the
157
+ * compiler emit a RELOCATED pool word (`.word gSym`), while the macro expands to a literal
158
+ * address and emits a NUMERIC one (`.word 0x3005290`). Matching a target that shows the
159
+ * numeric word therefore requires the macro spelling, not merely a name.
160
+ *
161
+ * Everything else about it is already the global machinery: the macro expands to an lvalue, so
162
+ * `gName`, `gName = v` and `&gName` all mean what they mean for an `extern`. Only the
163
+ * DECLARATION differs — `#define name body` instead of `extern T name;` — which is why the
164
+ * body is carried rather than reconstructed. */
165
+ macroBody?: string;
166
+ }
167
+
168
+ /** THE one reading of {@link SymbolInfo.dims} for spelling C, shared by the access side
169
+ * (structure.ts's bare-name gate) and the declaration side (declare.ts) so the two cannot
170
+ * disagree about an array's shape.
171
+ *
172
+ * Returns the INNER extents — every dimension but the outermost. The outermost is excluded
173
+ * because C lets a declaration omit it, and the inner ones are exactly what scales a leading
174
+ * subscript. `[]` is the rank-1 answer: one subscript, `extern T gSym[];`, the spelling this has
175
+ * always had.
176
+ *
177
+ * An ABSENT `dims` also reads as rank 1, because that is what the author of such a map said: the
178
+ * ELF provider's capability gate refuses a @gba-kit/debug-info that cannot report rank, so
179
+ * absence here can only come from a hand-written map whose `shape:'array'` states a plain array.
180
+ * Absence never means "the package could not say" — that case fails loudly at load.
181
+ *
182
+ * Null means NO consistent pair is available (a stated rank with an unknown inner extent, which
183
+ * neither a declaration nor a subscript can spell). Both sides honour it the same way: the access
184
+ * falls back to `((T *)&gSym)[i]`, the declaration to the flat `extern T gSym[];` — valid
185
+ * together under whatever the project's own header says. */
186
+ export function arrayInnerExtents(info: SymbolInfo): number[] | null {
187
+ const dims = info.shape === 'array' ? info.dims : undefined;
188
+ if (dims === undefined || dims.length <= 1) {
189
+ return [];
190
+ }
191
+ const inner = dims.slice(1);
192
+ return inner.every((d) => typeof d === 'number' && d > 0) ? (inner as number[]) : null;
193
+ }
194
+
195
+ /** address → symbols at that address; `[0]` is the provider's canonical pick. */
196
+ export type SymbolMap = Map<number, SymbolInfo[]>;
197
+
198
+ /** THE one test for "is this field an array". The PRESENCE of `elemSize` is what marks one (see
199
+ * the field doc) — `length` is a separate fact that a flexible array member legitimately lacks,
200
+ * so testing it instead silently reclassifies such a member as a scalar cell. */
201
+ export function isArrayField(f: SymbolStructField): boolean {
202
+ return f.elemSize !== undefined;
203
+ }
204
+
205
+ /** THE one test for "is this field a bitfield" — the PRESENCE of `bitWidth` (see the field doc).
206
+ * The exact (offset,size) scalar-field rules must exclude these: a 7-bit field whose bits span
207
+ * 2 bytes carries `size: 2` and would otherwise match a plain u16 read at its offset. */
208
+ export function isBitfieldField(f: SymbolStructField): boolean {
209
+ return f.bitWidth !== undefined;
210
+ }
211
+
212
+ /** A layout member that {@link declaredFields} passed: sizable, and seated at an offset no
213
+ * earlier member already covers. */
214
+ export type DeclaredField = SymbolStructField & { size: number };
215
+
216
+ /** Is `f` shaped like a layout member at all? `SymbolMap` is public API — a caller-supplied map
217
+ * (the webapp accepts one) must be DECLINED, never crash the pipeline. */
218
+ function wellFormedField(f: unknown): f is SymbolStructField {
219
+ if (typeof f !== 'object' || f === null) {
220
+ return false;
221
+ }
222
+ const m = f as Partial<SymbolStructField>;
223
+ if (
224
+ typeof m.name !== 'string' ||
225
+ typeof m.offset !== 'number' ||
226
+ !Number.isFinite(m.offset) ||
227
+ !(m.size === null || (typeof m.size === 'number' && Number.isFinite(m.size) && m.size >= 0))
228
+ ) {
229
+ return false;
230
+ }
231
+ // A bitfield's two facts must be present TOGETHER and internally consistent — a bitWidth with
232
+ // no bitOffset (or bits outside the byte span `size` claims) leaves the field unseatable, so
233
+ // the member is malformed and the layout declines whole like any other malformed member.
234
+ if (m.bitWidth !== undefined) {
235
+ return (
236
+ typeof m.bitWidth === 'number' &&
237
+ Number.isInteger(m.bitWidth) &&
238
+ m.bitWidth > 0 &&
239
+ typeof m.bitOffset === 'number' &&
240
+ Number.isInteger(m.bitOffset) &&
241
+ m.bitOffset >= 0 &&
242
+ typeof m.size === 'number' &&
243
+ m.bitOffset + m.bitWidth <= m.size * 8
244
+ );
245
+ }
246
+ return true;
247
+ }
248
+
249
+ /**
250
+ * THE one definition of "which members of this layout exist", for every consumer: the declaration
251
+ * SYNTHESIS that PRINTS them (declare.ts) and the access rules that NAME them (structure.ts).
252
+ * Returns the members in offset order, or null when the layout cannot be reproduced faithfully at
253
+ * all — and the two answers must be the same answer, because core naming a member that synthesis
254
+ * does not declare is non-compiling C.
255
+ *
256
+ * Declines the WHOLE layout on an unsizable member (its successors' offsets are then unknowable,
257
+ * so no member of it can be seated), on a malformed one, and on an array member whose
258
+ * `elemSize * length` does not account for its `size` (the three facts contradict each other, so
259
+ * none of them can be trusted). SELECTS by dropping a member an earlier one already covers — the
260
+ * union-alias rule: `struct { u32 word; u16 half; }` at one offset declares the first view only,
261
+ * so `half` is a name no declaration carries and no access may spell.
262
+ */
263
+ export function declaredFields(layout: SymbolStructField[] | undefined): DeclaredField[] | null {
264
+ if (!Array.isArray(layout)) {
265
+ return null;
266
+ }
267
+ // Validate BEFORE sorting: the comparator reads `.offset`, so a malformed entry would throw
268
+ // there rather than decline here — the crash this function exists to prevent.
269
+ for (const m of layout) {
270
+ if (!wellFormedField(m) || m.size === null) {
271
+ return null;
272
+ }
273
+ if (isArrayField(m) && m.length !== undefined && m.elemSize! * m.length !== m.size) {
274
+ return null;
275
+ }
276
+ }
277
+ // The cursor is in BITS so co-located bitfields seat correctly: `u32 a:2; u32 b:3;` are two
278
+ // members at byte offset 0, not a union alias. For plain members the arithmetic is the old
279
+ // byte cursor times 8 — behavior-identical for every bitfield-free layout.
280
+ const lowBitOf = (m: DeclaredField): number => m.offset * 8 + (m.bitWidth !== undefined ? m.bitOffset! : 0);
281
+ const members = (layout as DeclaredField[])
282
+ .slice()
283
+ .sort((a, b) => lowBitOf(a) - lowBitOf(b) || (a.bitWidth ?? -1) - (b.bitWidth ?? -1));
284
+ const out: DeclaredField[] = [];
285
+ let bitCursor = 0;
286
+ for (const m of members) {
287
+ const lo = lowBitOf(m);
288
+ if (lo < bitCursor) {
289
+ continue; // an overlapping (union) member: the first view is declared, the alias is not
290
+ }
291
+ if (m.bitWidth !== undefined) {
292
+ // The synthesis lays bitfields as LE-GCC `u32 name : n`, whose allocation never straddles
293
+ // a 32-bit unit — a field that would cannot be reproduced, so it is not declared (its bits
294
+ // pad instead) and no access may name it. The cursor does NOT advance: the bits stay a hole.
295
+ if (Math.floor(lo / 32) !== Math.floor((lo + m.bitWidth - 1) / 32)) {
296
+ continue;
297
+ }
298
+ out.push(m);
299
+ bitCursor = lo + m.bitWidth;
300
+ } else {
301
+ out.push(m);
302
+ bitCursor = (m.offset + m.size) * 8;
303
+ }
304
+ }
305
+ return out;
306
+ }
307
+
308
+ /**
309
+ * THE one copy of "what C type does a map field declare", consumed by the declaration SYNTHESIS
310
+ * (declare.ts, which prints it). A per-consumer copy would let the emitted declaration and the
311
+ * type a consumer reasoned against disagree about what a member is.
312
+ *
313
+ * An ARRAY field declares its own element type and length — spelling `u16 x[8]` as `u8 x[16]`
314
+ * keeps the layout but makes `x[i]` index BYTES, a wrong address. That spelling is used ONLY when
315
+ * the element is a 1/2/4-byte BASE type, `elemSigned` being the witness that it is one: an array
316
+ * of 2-byte STRUCTS declared `u16 x[n]` acquires an alignment the real member does not have, and
317
+ * at an odd offset the compiler then inserts padding that shifts every later member. Such a
318
+ * member declares the byte array of its own size instead, which has no alignment to acquire.
319
+ *
320
+ * A POINTER field types `void *` (an integer guess flips relational compares of the loaded value).
321
+ * Everything else is the 1/2/4 scalar cell at its declared signedness — with the 4-byte
322
+ * no-base-type case (an enum member) spelled s32 on the C89 enum=int rule — or a `u8 name[size]`
323
+ * byte array when it is no scalar cell at all (a nested struct, an 8-byte member).
324
+ */
325
+ export function symbolFieldType(f: DeclaredField): IrType {
326
+ if (isArrayField(f)) {
327
+ const scalarElem = f.elemSigned !== undefined && (f.elemSize === 1 || f.elemSize === 2 || f.elemSize === 4);
328
+ return scalarElem && f.length !== undefined && f.elemSize! * f.length === f.size
329
+ ? T.array(T.int(f.elemSize! * 8, f.elemSigned!), f.length)
330
+ : T.array(T.u(8), f.size);
331
+ }
332
+ if (f.pointer && f.size === 4) {
333
+ // The pointee width is byte-load-bearing: arithmetic on the loaded pointer scales by it, so
334
+ // `p - 4` through the header's `u16 *` and through a guessed `void *` reach different bytes.
335
+ // Only a base-type target is spelled; anything else keeps `void *`, which is address-identical
336
+ // for any object pointer and never derefs.
337
+ const scalarPointee = f.pointeeSize === 1 || f.pointeeSize === 2 || f.pointeeSize === 4;
338
+ return T.ptr(scalarPointee ? T.int(f.pointeeSize! * 8, f.pointeeSigned ?? false) : T.void());
339
+ }
340
+ if (isBitfieldField(f)) {
341
+ // The BASE type of the synthesized `u32 name : n` — the `: n` itself is the declaration
342
+ // renderer's job (StructFieldDecl.bits). 32-bit base always: that is the LE-GCC unit model
343
+ // declaredFields verified the layout against. A signless bitfield declares unsigned — the
344
+ // extract recognizer refuses to NAME one anyway, so the choice only types padding.
345
+ return T.int(32, f.signed ?? false);
346
+ }
347
+ if (isScalarCellSize(f.size)) {
348
+ return scalarCellType(f.size, f.signed);
349
+ }
350
+ return T.array(T.u(8), f.size);
351
+ }
352
+ /** A 4-byte member/scalar with NO base-type signedness is the enum idiom — C89 says int. */
353
+ export const ENUM_IS_SIGNED = true;
354
+
355
+ /** Is this byte size one a base type can spell? */
356
+ export function isScalarCellSize(size: number | undefined): size is 1 | 2 | 4 {
357
+ return size === 1 || size === 2 || size === 4;
358
+ }
359
+
360
+ /** THE DECLARED type of a 1/2/4-byte scalar cell — what `extern T gSym;` synthesis writes, and
361
+ * therefore what `&gSym` actually points to.
362
+ *
363
+ * Extracted because this rule had drifted into a fourth copy. It is NOT `scalarTypeForAccess`,
364
+ * which answers a different question — the type an ACCESS of that width reads — and collapses
365
+ * every 4-byte access to `s32` whatever the signedness. Using that one to decide "does `&gSym`
366
+ * already have the destination's type" silently answered YES for a `u32` cell reaching an
367
+ * `s32 *`, and the incompatible-pointer assignment survived. Declaration side and access side are
368
+ * separate facts; this is the declaration one. */
369
+ export function scalarCellType(size: 1 | 2 | 4, signed: boolean | undefined): IrType {
370
+ return T.int(size * 8, signed ?? (size === 4 ? ENUM_IS_SIGNED : false));
371
+ }
372
+
373
+ /**
374
+ * THE gate on every spelling through a POINTER global's value: the members a `gPtr->member`
375
+ * spelling may name, or null when nothing may be named through this pointee at all. Null unless
376
+ * the pointee is named (synthesis has no tag to declare it under otherwise), sized (the struct
377
+ * type is incomplete otherwise), and its layout is declarable ({@link declaredFields}) — the same
378
+ * three conditions declare.ts needs to emit `struct Tag *gPtr;` rather than falling back to
379
+ * `extern void *gPtr;`. Both must decline together: core naming a member of an undeclared pointee
380
+ * is non-compiling C.
381
+ */
382
+ export function pointeeFields(pointee: SymbolPointee | undefined): DeclaredField[] | null {
383
+ if (pointee?.structName === undefined || pointee.size === undefined) {
384
+ return null;
385
+ }
386
+ return declaredFields(pointee.layout);
387
+ }
388
+
389
+ /** Kind-aware two-probe lookup for a pool-loaded 32-bit value. Exact match first (any kind);
390
+ * on miss, `value & ~1` — accepted ONLY when the hit is code, because ELF function addresses
391
+ * are stored with the Thumb bit cleared while a Thumb code pointer in a pool is odd. An exact
392
+ * odd-DATA hit therefore wins over a masked code hit (odd data addresses are real). */
393
+ export function lookupSymbol(map: SymbolMap, value: number): SymbolInfo | null {
394
+ const exact = map.get(value)?.[0];
395
+ if (exact) {
396
+ return exact;
397
+ }
398
+ if ((value & 1) === 1) {
399
+ const masked = map.get(value & ~1)?.[0];
400
+ if (masked?.kind === 'code') {
401
+ return masked;
402
+ }
403
+ }
404
+ return null;
405
+ }
406
+
407
+ /** Interior attribution: the data symbol whose `[address, address+size)` range contains
408
+ * `value` strictly inside (offset > 0 — exact bases go through `lookupSymbol`). Only
409
+ * complete-typed globals carry a size, so unsized arrays never attribute. */
410
+ export function lookupInterior(map: SymbolMap, value: number): { info: SymbolInfo; offset: number } | null {
411
+ for (const [addr, infos] of map) {
412
+ const info = infos[0];
413
+ if (info.kind !== 'data' || info.size === undefined) {
414
+ continue;
415
+ }
416
+ if (value > addr && value < addr + info.size) {
417
+ return { info, offset: value - addr };
418
+ }
419
+ }
420
+ return null;
421
+ }
422
+
423
+ /** The `SymbolInfo` keys a project's DWARF can only carry because the symbol's DEFINITION was
424
+ * compiled from C. Everything else in the map survives a function that is still `INCLUDE_ASM`:
425
+ * its `.symtab` entry exists (the asm defines the label), and its globals are typed by the OTHER
426
+ * translation units that declare them. Listed here, once, so {@link asIfUndecompiled} and any
427
+ * later definition-derived fact (a signature, a local's type, a register location) stay in sync. */
428
+ const DEFINITION_DERIVED_KEYS = ['declared', 'signature'] as const satisfies readonly (keyof SymbolInfo)[];
429
+
430
+ /** The map a user actually has while decompiling `fn` — i.e. with `fn` still an `INCLUDE_ASM`
431
+ * stub in their project.
432
+ *
433
+ * Every benchmark row is a function someone ALREADY decompiled, so the project ELF carries
434
+ * facts about it that exist only *because* the work is done. Scoring against those facts
435
+ * measures the harness, not the tool: it flatters any feature that reads them and transfers
436
+ * nothing to the user, who is decompiling the one function whose definition is absent. This
437
+ * rebuilds the map as that user's ELF would give it.
438
+ *
439
+ * What it strips is the row's own DEFINITION-derived facts ({@link DEFINITION_DERIVED_KEYS}),
440
+ * NOT its name: an `INCLUDE_ASM` function still has a `.symtab` entry, so dropping the symbol
441
+ * outright would understate what a user has and make the map look worse than it is. Callee
442
+ * signatures, globals and struct layouts all stay — those are the transferable facts, and they
443
+ * are the point.
444
+ *
445
+ * Address identity is preserved (aliases keep their order, `[0]` stays canonical) so a filtered
446
+ * map is a drop-in for the unfiltered one. */
447
+ export function asIfUndecompiled(map: SymbolMap, fn: string): SymbolMap {
448
+ const leaks = (info: SymbolInfo): boolean =>
449
+ info.kind === 'code' && info.name === fn && DEFINITION_DERIVED_KEYS.some((k) => info[k] !== undefined);
450
+ // Return the SAME map when the row's own symbol carries no definition-derived fact — the common
451
+ // case today, and it keeps the filter free to apply unconditionally on every row of a run.
452
+ let any = false;
453
+ for (const infos of map.values()) {
454
+ if (infos.some(leaks)) {
455
+ any = true;
456
+ break;
457
+ }
458
+ }
459
+ if (!any) {
460
+ return map;
461
+ }
462
+ const out: SymbolMap = new Map();
463
+ for (const [addr, infos] of map) {
464
+ out.set(
465
+ addr,
466
+ infos.map((info) => {
467
+ if (!leaks(info)) {
468
+ return info;
469
+ }
470
+ const stripped = { ...info };
471
+ for (const k of DEFINITION_DERIVED_KEYS) {
472
+ delete stripped[k];
473
+ }
474
+ return stripped;
475
+ }),
476
+ );
477
+ }
478
+ return out;
479
+ }
480
+
481
+ /** Every fact but the name, canonically ordered — the equality a name collision is judged by. */
482
+ function factsOf(info: SymbolInfo): string {
483
+ return JSON.stringify(
484
+ Object.keys(info)
485
+ .filter((k) => k !== 'name')
486
+ .sort()
487
+ .map((k) => [k, info[k as keyof SymbolInfo]]),
488
+ );
489
+ }
490
+
491
+ /** NAME-keyed view over every symbol in the map — what the structurer consumes (it sees gaddr
492
+ * symbol names, not addresses). Aliases at one address each appear under their own name.
493
+ *
494
+ * One name can sit at SEVERAL addresses in a real project (file-static `sMenu` in two
495
+ * translation units, a `.symtab` full of same-named locals). Where those entries agree on their
496
+ * facts the collision is harmless — `InitSprite` at 16 sa3 addresses is 16 identical name-only
497
+ * entries. Where they DISAGREE, silently keeping whichever the map iterated last would apply one
498
+ * address's declaration shape to another address's global: the same layout, the wrong struct.
499
+ *
500
+ * So a disagreeing name degrades to NAME-ONLY rather than picking. The name survives (dropping it
501
+ * outright would leave the reference undeclarable in the self-declared scoring world, turning a
502
+ * spelling question into a compile failure); only the shape facts, which are what could be wrong,
503
+ * are withheld — the honest cast spellings take over. `kind` is kept: it never disagrees in the
504
+ * vendored maps, and it is settled address-side by `lookupSymbol` before a name is ever used. */
505
+ export function symbolsByName(map: SymbolMap): Map<string, SymbolInfo> {
506
+ const byName = new Map<string, SymbolInfo>();
507
+ const conflicted = new Set<string>();
508
+ for (const infos of map.values()) {
509
+ for (const info of infos) {
510
+ const prev = byName.get(info.name);
511
+ if (prev === undefined) {
512
+ byName.set(info.name, info);
513
+ } else if (factsOf(prev) !== factsOf(info)) {
514
+ conflicted.add(info.name);
515
+ }
516
+ }
517
+ }
518
+ for (const name of conflicted) {
519
+ byName.set(name, { name, kind: byName.get(name)!.kind });
520
+ }
521
+ return byName;
522
+ }
523
+
524
+ /** Serialize a SymbolMap to a byte-stable JSON object (hex keys, sorted; array order kept —
525
+ * `[0]` is the canonical pick). The benchmark vendors this; the ELF itself never leaves the
526
+ * project checkout. */
527
+ export function symbolMapToJson(map: SymbolMap): Record<string, SymbolInfo[]> {
528
+ const out: Record<string, SymbolInfo[]> = {};
529
+ for (const addr of [...map.keys()].sort((a, b) => a - b)) {
530
+ out[`0x${addr.toString(16).padStart(8, '0')}`] = map.get(addr)!;
531
+ }
532
+ return out;
533
+ }
534
+
535
+ export function symbolMapFromJson(obj: Record<string, SymbolInfo[]>): SymbolMap {
536
+ const map: SymbolMap = new Map();
537
+ for (const [k, infos] of Object.entries(obj)) {
538
+ map.set(Number.parseInt(k, 16), infos);
539
+ }
540
+ return map;
541
+ }
package/src/target.ts CHANGED
@@ -34,7 +34,7 @@ export interface TargetDescription {
34
34
  returnReg: string;
35
35
  // HARDWARE / ISA facts — independent of the compiler.
36
36
  capabilities: {
37
- endianness: 'little' | 'big'; // RESERVED no pass reads it yet (byte-addressing will)
37
+ endianness: 'little' | 'big'; // consumed by structureOptionsFor (bitfield extract recognition is LSB-first)
38
38
  hwDivide: boolean; // consumed by patternApplies (idiom gating)
39
39
  hwFloat: boolean; // consumed by patternApplies (idiom gating)
40
40
  flags: boolean; // RESERVED — no pass reads it yet (PPC condition regs will)
@@ -132,7 +132,9 @@ export const PPC_MWCC: TargetDescription = {
132
132
  * target's compiler behaviors flow into the target-agnostic structurer — a new behavior lever
133
133
  * is a field in `compilerBehaviors`, consumed automatically. */
134
134
  export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
135
- return { returnsVoid, ...t.compilerBehaviors };
135
+ // `littleEndian` is the one HARDWARE capability the structurer consumes (bitfield extract
136
+ // recognition is LSB-first); everything else is a compiler behavior.
137
+ return { returnsVoid, littleEndian: t.capabilities.endianness === 'little', ...t.compilerBehaviors };
136
138
  }
137
139
 
138
140
  export const C_TYPEDEFS =
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. */
@@ -70,12 +72,21 @@ export interface TraceOptions {
70
72
  // (not in pre-recovery.ts) because these strings are a trace concern — the driver itself is
71
73
  // trace-agnostic. `title` is a function so `arrays` can fold its scaled-access count in.
72
74
  const PRE_RECOVERY_TRACE: Record<string, { stage: string; title: (result: number | boolean) => string }> = {
75
+ addrnum: {
76
+ stage: 'stage:addrnum',
77
+ title: (r) => `Address numbering (${r} duplicate address def(s) / trivial phi(s) collapsed)`,
78
+ },
73
79
  const: { stage: 'stage:const', title: () => 'Const materialize (lui;ori → one 32-bit const)' },
74
80
  magicdiv: { stage: 'stage:magicdiv', title: () => 'Magic-number division recovery (mulh/mulhu → sdiv/udiv)' },
75
81
  softdiv: { stage: 'stage:softdiv', title: () => 'Soft-division lower (bl __divsi3 → division op)' },
76
82
  arrays: { stage: 'stage:legalize', title: (r) => `Array legalize (${r} scaled access(es) → aload/astore)` },
77
83
  structs: { stage: 'stage:structs', title: () => 'Struct-pointer recovery (access-pattern evidence)' },
78
84
  shortcircuit: { stage: 'stage:shortcircuit', title: () => 'Short-circuit recovery (boolean && / ||)' },
85
+ 'branch-shortcircuit': {
86
+ stage: 'stage:branch-shortcircuit',
87
+ title: () => 'Short-circuit recovery (control-flow && / ||)',
88
+ },
89
+ 'struct-arrays': { stage: 'stage:struct-arrays', title: () => 'Struct-array recovery (element stride evidence)' },
79
90
  };
80
91
 
81
92
  /** Run the tower while recording a TraceReport. Strict mode throws on any gap (like decompile);
@@ -130,7 +141,7 @@ function traceTower(
130
141
  const patternEvents: PatternEvent[] = [];
131
142
 
132
143
  // (1) lift → typed-SSA IR
133
- const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
144
+ const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
134
145
  verify(fn);
135
146
  trace.push({ id: 'stage:lift', title: 'Lift (ISA frontend → typed-SSA IR)', irDump: print(fn), verified: true });
136
147
 
@@ -200,7 +211,11 @@ function traceTower(
200
211
 
201
212
  // (4) structure → neutral AST; boundary contract: no unresolved value leaked (strict) or
202
213
  // 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' });
214
+ const sfn = structureChecked(fn, {
215
+ ...structureOptionsFor(target, returnsVoid),
216
+ onGap: opts.onGap ?? 'strict',
217
+ ...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
218
+ });
204
219
  trace.push({
205
220
  id: 'stage:structure',
206
221
  title: 'Structuring (IR → neutral AST)',