@asmlift/core 0.3.0 → 0.5.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 +130 -4
  4. package/src/backend/cpp.ts +3 -1
  5. package/src/backend/pascal.ts +11 -0
  6. package/src/contracts.ts +181 -4
  7. package/src/declare.ts +35 -9
  8. package/src/frontend/mips.ts +37 -29
  9. package/src/frontend/opaque.ts +70 -20
  10. package/src/frontend/ppc.ts +18 -7
  11. package/src/frontend/ssa.ts +279 -56
  12. package/src/frontend/thumb.ts +1372 -87
  13. package/src/ir/alias.ts +75 -0
  14. package/src/ir/opcodes.ts +57 -3
  15. package/src/ir/simplify.ts +72 -0
  16. package/src/l3/argbase.ts +221 -0
  17. package/src/l3/ast.ts +127 -5
  18. package/src/l3/basecse.ts +58 -62
  19. package/src/l3/coalesce.ts +215 -0
  20. package/src/l3/dce.ts +33 -41
  21. package/src/l3/gates.ts +67 -0
  22. package/src/l3/hoist.ts +65 -0
  23. package/src/l3/reindex.ts +7 -0
  24. package/src/l3/scopebase.ts +440 -0
  25. package/src/l3/tailmerge.ts +124 -0
  26. package/src/macros.ts +222 -13
  27. package/src/pattern/engine.ts +99 -6
  28. package/src/pipeline.ts +65 -6
  29. package/src/raise/divpow2.ts +227 -0
  30. package/src/raise/gvn.ts +151 -0
  31. package/src/raise/pre-recovery.ts +39 -3
  32. package/src/raise/recover.ts +24 -7
  33. package/src/raise/retsink.ts +37 -7
  34. package/src/raise/shortcircuit.ts +262 -22
  35. package/src/raise/struct-arrays.ts +2 -1
  36. package/src/raise/structs.ts +41 -3
  37. package/src/rank.ts +196 -20
  38. package/src/structure/analysis.ts +175 -89
  39. package/src/structure/structure.ts +588 -55
  40. package/src/structure/switch-recover.ts +117 -30
  41. package/src/symbols.ts +128 -13
  42. package/src/target.ts +4 -2
  43. package/src/trace.ts +9 -0
@@ -18,18 +18,49 @@ 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
  }
24
30
 
31
+ /** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
32
+ * as C at all, and with or without a `break`.
33
+ *
34
+ * - `break` every path out of the arm reaches the switch's merge (or returns / loops
35
+ * inside the arm). The ordinary closed arm.
36
+ * - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
37
+ * Only spellable when that sibling is the arm emitted NEXT (the caller checks
38
+ * emission adjacency — see the l3/ast.ts non-neutrality note).
39
+ * - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
40
+ * "out to the merge". C needs a `goto` for those, so callers decline LOUD. */
41
+ export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
42
+
25
43
  export interface SwitchRecovery {
26
44
  recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
27
- /** shared with the Regime-B (`switch_br`) path in structure.ts, which throws where A declines */
28
- caseRegionReachesSibling: (targets: Set<Block>, b: Block, merge: Block | null) => boolean;
45
+ /** shared with the Regime-B (`switch_br`) path in structure.ts, which recovers the fall-through
46
+ * this returns; Regime A only accepts `break` arms and otherwise declines to if-recovery. */
47
+ analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>) => ArmExit;
29
48
  }
30
49
 
31
50
  export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
32
- const { fn, defs, dom, ipdom, opBlock, isNamed, isCmpOpcode, switchAllowsNeqCase, expr, structureRegion } = deps;
51
+ const {
52
+ fn,
53
+ defs,
54
+ dom,
55
+ ipdom,
56
+ opBlock,
57
+ isNamed,
58
+ isCmpOpcode,
59
+ switchAllowsNeqCase,
60
+ emitsAnchoredWrite,
61
+ expr,
62
+ structureRegion,
63
+ } = deps;
33
64
 
34
65
  // --- Regime A: comparison-tree switch recovery ----------------------------------------------------
35
66
  // Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
@@ -128,9 +159,9 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
128
159
  if (!cmp || !isCmpOpcode(cmp.opcode)) {
129
160
  return null;
130
161
  }
131
- if (!isRoot && blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode))) {
162
+ if (!isRoot && (blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode)) || emitsAnchoredWrite(blk))) {
132
163
  return null;
133
- } // PRE4
164
+ } // PRE4 — anchored writes included: discarded with the block, while their edge copies stay suppressed
134
165
  // Which operand is the scrutinee, which is the constant?
135
166
  const [lo, ro] = cmp.operands;
136
167
  const lc = evalConst(lo),
@@ -180,31 +211,83 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
180
211
  }
181
212
  };
182
213
 
183
- // Can any case/default entry's region reach a SIBLING entry (switch fall-through)? Region =
184
- // blocks strictly dominated by `b`, short of `merge`. Shared by Regime A (declines to
185
- // if-recovery) and Regime B (throwsa jump-table has no fallback).
186
- const caseRegionReachesSibling = (targets: Set<Block>, b: Block, merge: Block | null): boolean => {
187
- const inRegion = (blk: Block) => blk !== merge && dom.get(blk)!.has(b);
188
- for (const entry of targets) {
189
- const rseen = new Set<Block>([entry]);
190
- const q = [entry];
191
- while (q.length) {
192
- const cur = q.pop()!;
193
- for (const s of successorsOf(cur)) {
194
- if (s === entry) {
214
+ // Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
215
+ // sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
216
+ // caller can emit a `case`/`default` label for the merge is deliberately not among them, so a
217
+ // switch whose default block IS the merge (agbcc's usual "the default just leaves") reads as an
218
+ // ordinary `break`, not as falling into the default.
219
+ //
220
+ // Region membership is `dom(blk) ∋ b` as before: a block NOT dominated by the switch is outside
221
+ // this switch's region and is not walked. It IS recorded as an escape, because an arm that can
222
+ // leave sideways does not fall into the next case — but only the fall-through verdict consults
223
+ // that, so no arm that used to be accepted as closed becomes a decline.
224
+ //
225
+ // A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
226
+ // arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
227
+ // under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
228
+ // already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
229
+ // back. Costly for matching, correct to run.
230
+ const analyzeArmExit = (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>): ArmExit => {
231
+ if (entry === merge) {
232
+ return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
233
+ }
234
+ const into = new Set<Block>(); // sibling entries this arm flows into
235
+ let toMerge = false,
236
+ escapes = false;
237
+ const seen = new Set<Block>([entry]);
238
+ const q = [entry];
239
+ while (q.length) {
240
+ const cur = q.pop()!;
241
+ for (const s of successorsOf(cur)) {
242
+ if (s === merge) {
243
+ toMerge = true;
244
+ } else if (s !== entry && siblings.has(s)) {
245
+ into.add(s);
246
+ } else if (s !== entry && !seen.has(s)) {
247
+ if (!dom.get(s)!.has(b)) {
248
+ escapes = true;
195
249
  continue;
196
250
  }
197
- if (targets.has(s)) {
198
- return true;
199
- }
200
- if (inRegion(s) && !rseen.has(s)) {
201
- rseen.add(s);
202
- q.push(s);
203
- }
251
+ seen.add(s);
252
+ q.push(s);
204
253
  }
205
254
  }
206
255
  }
207
- return false;
256
+ if (into.size === 0) {
257
+ return { kind: 'break' };
258
+ }
259
+ if (into.size === 1 && !toMerge && !escapes) {
260
+ return { kind: 'fallthrough', to: [...into][0] };
261
+ }
262
+ // Name what is actually missing. These three are different facts, and only the first is a shape
263
+ // C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
264
+ const names = () => [...into].map((x) => `#${fn.blocks.indexOf(x)}`).join(', ');
265
+ if (into.size > 1) {
266
+ return {
267
+ kind: 'unstructurable',
268
+ why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
269
+ };
270
+ }
271
+ if (escapes) {
272
+ return {
273
+ kind: 'unstructurable',
274
+ why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
275
+ };
276
+ }
277
+ return {
278
+ kind: 'unstructurable',
279
+ // `case 0: if (c) { …; break; } /* fall through */ case 1:` is the C for this, and the reason
280
+ // asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
281
+ // (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
282
+ why: `a case body reaches sibling case ${names()} on one path and the end of the switch on another — a switch-scoped \`break\` inside a case body is not emitted yet`,
283
+ };
284
+ };
285
+
286
+ /** Every arm closed (`break`)? The precondition Regime A needs — it has a behaviourally identical
287
+ * fallback (if-recovery), so it declines on anything else instead of recovering fall-through. */
288
+ const allArmsClosed = (targets: Set<Block>, b: Block, merge: Block | null): boolean => {
289
+ const siblings = new Set([...targets].filter((t) => t !== merge));
290
+ return [...siblings].every((t) => analyzeArmExit(t, b, merge, siblings).kind === 'break');
208
291
  };
209
292
 
210
293
  const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
@@ -367,11 +450,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
367
450
  }
368
451
 
369
452
  // PRE2 (fall-through): only NON-fall-through switches are handled — decline if any case body
370
- // can reach ANOTHER case body (or the default) while staying inside the region. (The SAME
371
- // predicate serves the Regime-B path, which throws instead.)
453
+ // can reach ANOTHER case body (or a default that has its own block) while staying inside the
454
+ // region. (The SAME analysis serves the Regime-B path, which RECOVERS the adjacent-sibling
455
+ // case as C fall-through instead of declining; A has if-recovery to fall back on, B does not.)
372
456
  const merge = ipdom.get(b) ?? stop;
373
457
  const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
374
- if (caseRegionReachesSibling(targets, b, merge)) {
458
+ if (!allArmsClosed(targets, b, merge)) {
375
459
  return null;
376
460
  }
377
461
 
@@ -394,11 +478,14 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
394
478
  body: structureRegion(blk, merge),
395
479
  fallsThrough: false,
396
480
  }));
481
+ // An empty default arm is not a default (see the Regime-B note in structure.ts): the label
482
+ // would carry no statement, which says nothing and is not valid C89.
483
+ const defBody = defaultBlk ? structureRegion(defaultBlk, merge) : [];
397
484
  const sw: Stmt = {
398
485
  k: 'switch',
399
486
  scrutinee: scrutExpr,
400
487
  cases: outCases,
401
- ...(defaultBlk ? { default: structureRegion(defaultBlk, merge) } : {}),
488
+ ...(defBody.length ? { default: defBody } : {}),
402
489
  };
403
490
  const out: Stmt[] = [sw];
404
491
  if (merge && merge !== stop) {
@@ -406,5 +493,5 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
406
493
  }
407
494
  return out;
408
495
  };
409
- return { recognizeSwitch, caseRegionReachesSibling };
496
+ return { recognizeSwitch, analyzeArmExit };
410
497
  }
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);