@asmlift/core 0.3.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.
@@ -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 CHANGED
@@ -52,6 +52,17 @@ export interface SymbolStructField {
52
52
  /** ARRAY field only: the element count (absent for a flexible array member, which declares a
53
53
  * stride but no bound) — types the synthesized `T name[n];` field decl */
54
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;
55
66
  }
56
67
 
57
68
  /** What a `shape:'pointer'` global POINTS AT, when the sidecar says its target is a struct/union.
@@ -118,6 +129,15 @@ export interface SymbolInfo {
118
129
  elemSize?: number;
119
130
  /** element signedness for `shape:'array'` (default unsigned) — types the env entry */
120
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)[];
121
141
  /** the real struct tag for `shape:'struct'` — names the synthesized struct declaration
122
142
  * (absent ⇒ synthesis mints a placeholder tag; the tag is codegen-arbitrary) */
123
143
  structName?: string;
@@ -145,6 +165,33 @@ export interface SymbolInfo {
145
165
  macroBody?: string;
146
166
  }
147
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
+
148
195
  /** address → symbols at that address; `[0]` is the provider's canonical pick. */
149
196
  export type SymbolMap = Map<number, SymbolInfo[]>;
150
197
 
@@ -155,6 +202,13 @@ export function isArrayField(f: SymbolStructField): boolean {
155
202
  return f.elemSize !== undefined;
156
203
  }
157
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
+
158
212
  /** A layout member that {@link declaredFields} passed: sizable, and seated at an offset no
159
213
  * earlier member already covers. */
160
214
  export type DeclaredField = SymbolStructField & { size: number };
@@ -166,12 +220,30 @@ function wellFormedField(f: unknown): f is SymbolStructField {
166
220
  return false;
167
221
  }
168
222
  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
- );
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;
175
247
  }
176
248
 
177
249
  /**
@@ -202,15 +274,33 @@ export function declaredFields(layout: SymbolStructField[] | undefined): Declare
202
274
  return null;
203
275
  }
204
276
  }
205
- const members = (layout as DeclaredField[]).slice().sort((a, b) => a.offset - b.offset);
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));
206
284
  const out: DeclaredField[] = [];
207
- let cursor = 0;
285
+ let bitCursor = 0;
208
286
  for (const m of members) {
209
- if (m.offset < cursor) {
287
+ const lo = lowBitOf(m);
288
+ if (lo < bitCursor) {
210
289
  continue; // an overlapping (union) member: the first view is declared, the alias is not
211
290
  }
212
- out.push(m);
213
- cursor = m.offset + m.size;
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
+ }
214
304
  }
215
305
  return out;
216
306
  }
@@ -247,14 +337,39 @@ export function symbolFieldType(f: DeclaredField): IrType {
247
337
  const scalarPointee = f.pointeeSize === 1 || f.pointeeSize === 2 || f.pointeeSize === 4;
248
338
  return T.ptr(scalarPointee ? T.int(f.pointeeSize! * 8, f.pointeeSigned ?? false) : T.void());
249
339
  }
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));
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);
252
349
  }
253
350
  return T.array(T.u(8), f.size);
254
351
  }
255
352
  /** A 4-byte member/scalar with NO base-type signedness is the enum idiom — C89 says int. */
256
353
  export const ENUM_IS_SIGNED = true;
257
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
+
258
373
  /**
259
374
  * THE gate on every spelling through a POINTER global's value: the members a `gPtr->member`
260
375
  * spelling may name, or null when nothing may be named through this pointee at all. Null unless
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
@@ -72,12 +72,21 @@ export interface TraceOptions {
72
72
  // (not in pre-recovery.ts) because these strings are a trace concern — the driver itself is
73
73
  // trace-agnostic. `title` is a function so `arrays` can fold its scaled-access count in.
74
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
+ },
75
79
  const: { stage: 'stage:const', title: () => 'Const materialize (lui;ori → one 32-bit const)' },
76
80
  magicdiv: { stage: 'stage:magicdiv', title: () => 'Magic-number division recovery (mulh/mulhu → sdiv/udiv)' },
77
81
  softdiv: { stage: 'stage:softdiv', title: () => 'Soft-division lower (bl __divsi3 → division op)' },
78
82
  arrays: { stage: 'stage:legalize', title: (r) => `Array legalize (${r} scaled access(es) → aload/astore)` },
79
83
  structs: { stage: 'stage:structs', title: () => 'Struct-pointer recovery (access-pattern evidence)' },
80
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)' },
81
90
  };
82
91
 
83
92
  /** Run the tower while recording a TraceReport. Strict mode throws on any gap (like decompile);