@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.
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +154 -5
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +37 -5
- package/src/declare.ts +251 -0
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +420 -32
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +126 -6
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +335 -0
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +20 -6
- package/src/proto.ts +55 -0
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +370 -79
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +852 -67
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +541 -0
- package/src/target.ts +4 -2
- 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 {
|
|
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'; //
|
|
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
|
-
|
|
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, {
|
|
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)',
|