@asmlift/core 0.6.0 → 0.7.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 (74) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/contracts.ts +60 -11
  5. package/src/frontend/ssa.ts +1 -1
  6. package/src/frontend/thumb.ts +2 -2
  7. package/src/ir/alias.ts +24 -0
  8. package/src/ir/core.ts +8 -0
  9. package/src/ir/opcodes.ts +43 -7
  10. package/src/ir/simplify.ts +1 -1
  11. package/src/l3/address.ts +2 -2
  12. package/src/l3/advance.ts +373 -0
  13. package/src/l3/argbase.ts +4 -4
  14. package/src/l3/ast.ts +65 -21
  15. package/src/l3/basecse.ts +48 -28
  16. package/src/l3/coalesce.ts +9 -9
  17. package/src/l3/gates.ts +75 -1
  18. package/src/l3/hoist.ts +1 -1
  19. package/src/l3/homesplit.ts +13 -13
  20. package/src/l3/initfirst.ts +3 -3
  21. package/src/l3/inlinebase.ts +16 -16
  22. package/src/l3/mentions.ts +68 -5
  23. package/src/l3/mulfirst.ts +3 -3
  24. package/src/l3/nearbase.ts +4 -4
  25. package/src/l3/offmember.ts +5 -5
  26. package/src/l3/parkfirst.ts +6 -6
  27. package/src/l3/pollguard.ts +3 -3
  28. package/src/l3/ptrfield.ts +4 -4
  29. package/src/l3/regspell.ts +8 -8
  30. package/src/l3/reindex.ts +22 -17
  31. package/src/l3/scopebase.ts +28 -25
  32. package/src/l3/sinkinit.ts +7 -7
  33. package/src/l3/slotorder.ts +3 -3
  34. package/src/l3/storage.ts +1 -1
  35. package/src/l3/tailmerge.ts +2 -2
  36. package/src/l3/typing.ts +3 -3
  37. package/src/l3/unmerge.ts +483 -59
  38. package/src/l3/unreduce.ts +13 -13
  39. package/src/l3/volatileptr.ts +11 -11
  40. package/src/l3/volatileval.ts +11 -11
  41. package/src/l3/volstore.ts +16 -16
  42. package/src/l3/zerosub.ts +6 -6
  43. package/src/pattern/engine.ts +4 -4
  44. package/src/pipeline.ts +17 -5
  45. package/src/proto.ts +2 -2
  46. package/src/raise/const.ts +203 -3
  47. package/src/raise/divpow2.ts +2 -2
  48. package/src/raise/extscale.ts +342 -0
  49. package/src/raise/globalshape.ts +32 -12
  50. package/src/raise/gvn.ts +2 -2
  51. package/src/raise/magicdiv.ts +2 -2
  52. package/src/raise/memberarrays.ts +4 -4
  53. package/src/raise/narrowlocal.ts +18 -2
  54. package/src/raise/paramwidth.ts +24 -2
  55. package/src/raise/pre-recovery.ts +90 -25
  56. package/src/raise/retsink.ts +381 -15
  57. package/src/raise/shortcircuit.ts +595 -34
  58. package/src/raise/structs.ts +4 -4
  59. package/src/raise/tailsink.ts +126 -0
  60. package/src/rank-declare.ts +4 -4
  61. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  62. package/src/rank.ts +1148 -803
  63. package/src/structure/analysis.ts +87 -90
  64. package/src/structure/bitfields.ts +130 -30
  65. package/src/structure/globalaccess.ts +30 -4
  66. package/src/structure/namecoalesce.ts +32 -13
  67. package/src/structure/structure.ts +1415 -200
  68. package/src/structure/switch-recover.ts +100 -7
  69. package/src/symbols.ts +127 -6
  70. package/src/target.ts +155 -35
  71. package/src/trace.ts +1 -1
  72. package/src/variation-definitions.ts +1540 -0
  73. package/src/variation-gates.ts +89 -0
  74. package/src/variation-tokens.ts +355 -0
@@ -23,6 +23,12 @@ export interface SwitchRecoverDeps {
23
23
  switchAllowsBoundCase: boolean;
24
24
  /** emit the case arms in the ASSEMBLY's block-layout order rather than by ascending case value */
25
25
  switchArmsFollowLayout: boolean;
26
+ /** DECLINE a recovered tree whose own layout INTERLEAVES a test with a case body. A source
27
+ * `switch` front-loads its whole dispatch ahead of every arm body; an if/else-if LADDER emits
28
+ * each test directly above its own body. See StructureOptions for the compiled evidence, for
29
+ * the frontend premise this shares with `switchArmsFollowLayout`, and for what the decline
30
+ * costs when it is wrong. */
31
+ switchRequiresFrontLoadedTests: boolean;
26
32
  /** may the emitted SOURCE say "this arm runs on into the next one"? False for a language whose
27
33
  * `case` cannot fall through (Pascal), and then Regime A declines a falling arm to if-recovery
28
34
  * — the behaviourally identical recovery that backend CAN print. See StructureOptions. */
@@ -247,6 +253,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
247
253
  switchAllowsNeqCase,
248
254
  switchAllowsBoundCase,
249
255
  switchArmsFollowLayout,
256
+ switchRequiresFrontLoadedTests,
250
257
  spellSwitchFallthrough,
251
258
  emitsOwnStatement,
252
259
  blockOf,
@@ -255,17 +262,25 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
255
262
  structureRegion,
256
263
  } = deps;
257
264
 
258
- // A block's index in `fn.blocks` as its position in the ASSEMBLY — the sole warrant for reading a
259
- // source's arm order off block indices below, and true PER FRONTEND rather than of the IR:
265
+ // A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
266
+ // of layout below, and true PER FRONTEND rather than of the IR:
260
267
  // - thumb.ts and mips.ts build the list by scanning the instruction stream in address order;
261
268
  // - ppc.ts does not. It APPENDS a synthetic return block (`synthReturn`) at the end of the list
262
269
  // for every conditional-return branch, wherever in the stream that branch sits, so its list
263
270
  // is not address order at all;
264
271
  // - raising only ever REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}
265
272
  // .ts all `filter`), never inserts or reorders, so the frontend's order is what survives.
266
- // `switchArmsFollowLayout` is therefore a claim about a target's FRONTEND as much as about its
267
- // compiler, and a target opts in on bothwhich is why PPC_MWCC, whose frontend fails the first
268
- // half, does not.
273
+ // TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
274
+ // - `switchArmsFollowLayout` (arm ORDER, the sort below) needs the full claimno block moved
275
+ // at all because it PLACES the arms from the layout;
276
+ // - `switchRequiresFrontLoadedTests` (PRE5, whether to recover at all) needs only that no case
277
+ // BODY was moved above a dispatch test, which a compiler with a scheduler can still satisfy;
278
+ // `MIPS_GCC` declares `switchRequiresFrontLoadedTests` and NOT `switchArmsFollowLayout` for
279
+ // exactly that reason — it has a scheduler and fills delay slots (target.ts).
280
+ // Both are therefore claims about a target's FRONTEND as much as about its compiler, and a target
281
+ // opts in on both halves — which is why PPC_MWCC, whose frontend fails the frontend half outright,
282
+ // declares neither. Anything added below that reads `layoutIndex` inherits the frontend half and
283
+ // owes a statement of which strength of the compiler half it needs.
269
284
  const blockIndex = new Map(fn.blocks.map((blk, i) => [blk, i] as const));
270
285
  const layoutIndex = (blk: Block): number => blockIndex.get(blk) ?? -1;
271
286
 
@@ -336,9 +351,10 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
336
351
  };
337
352
 
338
353
  // --- Regime A: comparison-tree switch recovery ----------------------------------------------------
339
- // Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
354
+ // Every ambiguity declines. Five preconditions are enforced below, annotated PRE1..PRE5:
340
355
  // scrutinee identity/dominance, ARM EXITS (per site: `break`, `fallthrough` — which `chainArms`
341
- // then places — or a decline), concrete interval consistency, test purity.
356
+ // then places — or a decline), concrete interval consistency, test purity, and — where the target
357
+ // has declared that its layout answers it — which SPELLING the source wrote.
342
358
 
343
359
  // Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
344
360
  // `250 << 2` for a large sparse case) to a number — else null.
@@ -772,6 +788,83 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
772
788
  }
773
789
  }
774
790
 
791
+ // PRE5 (WHICH SPELLING THE SOURCE WROTE), per SITE, and only where the caller has declared
792
+ // that the layout answers it. Everything above establishes that this tree CAN be spelled as a
793
+ // `switch`; this asks whether it WAS. The two spellings are different objects, not one object
794
+ // two ways: a source `switch` front-loads its whole dispatch ahead of every arm body (agbcc's
795
+ // `expand_end_case` closes with a `reorder_insns` that moves the dispatch in front of the
796
+ // bodies expanded before it — the same sources `switchArmsFollowLayout` is read off), while an
797
+ // if/else-if ladder emits each test directly above the body it guards. So a test block laid
798
+ // out AFTER a case body cannot have come from a source `switch`, and recovering one here spells
799
+ // a ladder as a `switch` with no dual anywhere in the fan for a differ to prefer.
800
+ //
801
+ // READ PER SITE, off THIS recovery's own blocks, never off the function. A function holding a
802
+ // ladder and a real `switch` must keep both readings, and a function-wide OR would collapse
803
+ // them into one boolean and be wrong on one site by construction.
804
+ //
805
+ // WHAT IT REFUSES TO DO. The reading runs BACKWARDS, from emission to spelling, so it inherits
806
+ // `layoutIndex`'s premise (above) at the weaker of the two strengths stated there. A frontend
807
+ // or compiler that breaks it can hand this a front-loaded layout for a ladder, and then the
808
+ // gate simply does not fire. The converse error, a real `switch` whose layout interleaves,
809
+ // costs only the `switch` SPELLING: the recovery declines to if-recovery, behaviourally
810
+ // identical (the file header) and scored by the differ. This gate can lose a match; it can
811
+ // never produce a wrong answer.
812
+ //
813
+ // WHAT THE DECLINE PRODUCES, AND IT IS NOT ALWAYS A CLEAN LADDER. Recovery is RECURSIVE:
814
+ // declining a tree here hands the tree back to if-recovery, which then runs recovery AGAIN on
815
+ // each sub-tree it structures. On a flat tree that is a ladder over every case value. On a
816
+ // NESTED tree it is not: `corpus/agbcc-swnested.s` (a source `switch` nested inside an
817
+ // if/else-if ladder) declines HERE on the outer tree — correctly, the ladder arm's body really
818
+ // does sit between the tests — and the sub-tree then re-recovers, so the emitted C is an `if`
819
+ // nest holding a `switch` over a STRICT SUBSET of the source's case labels. Still behaviourally
820
+ // identical, and no worse than the alternative (with the reading withdrawn that same function
821
+ // comes back as one `switch` merging two arms the source wrote apart), but it is a THIRD
822
+ // spelling that neither reading would give, and nothing in the output says a dispatch was
823
+ // half-declined: "the decline is a clean ladder" holds at the OUTERMOST tree only. Pinned by
824
+ // the test "PRE5 declines a tree RECURSIVELY" on the committed fixture.
825
+ //
826
+ // WHAT THE POSITIONS ARE READ THROUGH, which is the premise that lives inside asmlift rather
827
+ // than in the compiler. `caseBlocks` holds `forwardingTarget` results (`asCase` is called on the
828
+ // resolved block), so an arm whose body is a bare jump contributes ITS TARGET's position, not
829
+ // its own. A forwarding target laid out ABOVE the dispatch would then read as a body above a
830
+ // test and decline a real `switch`.
831
+ //
832
+ // WHY THAT HAS NO INHABITANT IS MEASURED, NOT ARGUED. Two arguments that look like proofs are
833
+ // not: "the block is reached by a back edge" needs the block to DOMINATE the dispatch, which an
834
+ // `if` arm ahead of the `switch` does not, and a cross-jumped arm tail reaches it on an
835
+ // ordinary forward edge; "this flag's targets reorder nothing" is false — `MIPS_GCC` declares
836
+ // it WITH a scheduler, on the half-premise stated at `layoutIndex` above. Measured: across 28
837
+ // compiled probes on agbcc, gcc2.7.2 -O1 and gcc2.7.2kmc -O2 — arms cross-jumped onto a shared
838
+ // tail ahead of the dispatch, `goto`s out of an arm to a label above it, shared early returns,
839
+ // `continue`/`break` out of a switch inside a loop — no toolchain placed an arm's forwarding
840
+ // target above its own dispatch: every firing came back with zero bodies above the first test.
841
+ // Safe because no compiler in the corpus emits that shape, not because none can; whoever makes
842
+ // one — a new frontend, a relaxed loop recovery, a scheduler that hoists a merged tail —
843
+ // inherits this reader.
844
+ //
845
+ // `bodyPos` covers the CASE bodies only. `defaultBlk` is left out deliberately: it can be the
846
+ // dispatch's own fall-out block rather than an arm the source wrote (the W2/W4 withholdings
847
+ // above), so its position is one the source never chose. Both omissions can only make the gate
848
+ // UNDER-fire, i.e. keep a `switch`, which is the safe direction.
849
+ //
850
+ // AND THE READING REFUSES ITSELF where it has nothing to read. `layoutIndex` answers `-1` for a
851
+ // block absent from `fn.blocks`, which is not a position — taken as one it sorts below every
852
+ // real body and declines the tree holding it. That state has no inhabitant and cannot get one
853
+ // without a bug upstream: `ir/verify.ts` rejects a successor that is not a block of the fn and
854
+ // the tower runs it after the lift and after every raising pass, and `predecessorBlocks` throws
855
+ // by name on the same state near the top of `structure()`. So this is belt-and-braces over a
856
+ // VERIFIER BUG, not over an expected shape — kept because standing down costs nothing. Its test
857
+ // pins the UNREACHABILITY (both throws fire first); the `placed` branch itself stays
858
+ // unexecuted, and cannot be covered without breaking one of those two invariants.
859
+ if (switchRequiresFrontLoadedTests) {
860
+ const testPos = [...seen].map(layoutIndex);
861
+ const bodyPos = [...caseBlocks].map(layoutIndex);
862
+ const placed = [...testPos, ...bodyPos].every((i) => i >= 0);
863
+ if (placed && Math.max(...testPos) > Math.min(...bodyPos)) {
864
+ return null;
865
+ }
866
+ }
867
+
775
868
  // ARM ORDER. The case values are disjoint (PRE3), so where no arm falls through the order
776
869
  // carries no meaning and is pure matching evidence: ascending case VALUE is the neutral
777
870
  // spelling, and where a compiler has declared `switchArmsFollowLayout` the layout of the bodies
package/src/symbols.ts CHANGED
@@ -52,9 +52,30 @@ 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
+ /** ARRAY field only: the RANK — the per-dimension extents, outermost first (`u8 x[6][8]` →
56
+ * `[6, 8]`), `null` for an unbounded one. {@link SymbolInfo.dims}'s law, one indirection down,
57
+ * and for the same reason: `length` is the PRODUCT of the dimensions, so it cannot say how many
58
+ * subscripts reach an ELEMENT. `->x[i]` on a `[6][8]` member is a ROW, and against the
59
+ * project's own header that is a type error — or, where the row address flows into an integer
60
+ * context, silently the wrong address.
61
+ *
62
+ * ABSENCE READS DIFFERENTLY HERE THAN IT DOES FOR A GLOBAL, and the difference is the whole
63
+ * point of the field. {@link arrayInnerExtents} may read an absent `SymbolInfo.dims` as rank 1
64
+ * because the ELF provider's capability gate refuses a @gba-kit/debug-info that cannot report
65
+ * a global's rank. No such gate covers a MEMBER's rank — a member simply omits the key both
66
+ * when the DWARF carries no subranges and when the package is too old to look — so absence
67
+ * here means "the map could not say". A consumer that must type-check against a FOREIGN
68
+ * declaration (the indexed member spelling, structure.ts's `pointeeElement`) refuses on
69
+ * absence; a consumer synthesizing its OWN declaration may still flatten, because its access
70
+ * and its declaration then agree by construction. That is why {@link structFieldInnerExtents}
71
+ * answers `[]` for absence and the refusal lives at the access site instead. */
72
+ dims?: (number | null)[];
55
73
  /** 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
74
+ * bitfield — `size` above stays the byte SPAN its bits touch, which is why the exact
75
+ * (offset,size) scalar-field rules must exclude it. The span is NOT the access the compiler
76
+ * uses: measured with the pinned agbcc, `u32 x : 8` at bit 12 spans 2 bytes and is reached by
77
+ * a WORD, `u32 a : 20` spans 3, and `u16 a : 2` spans 1 in a 2-byte container. A consumer
78
+ * needing the access width computes it from `bitOffset`/`bitWidth` (structure/bitfields.ts). The
58
79
  * provider only emits these for LITTLE-ENDIAN ELFs: both the extract equation the access
59
80
  * recognizer solves and the `u32 name : n` layout model the synthesis verifies are LE-GCC
60
81
  * semantics, so a big-endian map carries no bitfield members at all (today's behavior). */
@@ -95,7 +116,7 @@ export interface SymbolPointee {
95
116
  * NO POINTEE, and the absence is UPSTREAM's rather than a shape asmlift dropped:
96
117
  * `@gba-kit/debug-info`'s `TypeFacts` — what a `FunctionSignature`'s params are made of — is
97
118
  * exactly these three fields. {@link SymbolInfo.pointee} exists only for a symbol AT AN ADDRESS.
98
- * Widening this is priced in docs/level-tower.md and pinned in test/param-pointee-axis.test.ts. */
119
+ * Widening this is priced in docs/level-tower.md and pinned in test/param-pointee-variation.test.ts. */
99
120
  export interface SymbolTypeFacts {
100
121
  size: number | null;
101
122
  signed: boolean | null;
@@ -197,6 +218,28 @@ export function arrayInnerExtents(info: SymbolInfo): number[] | null {
197
218
  return inner.every((d) => typeof d === 'number' && d > 0) ? (inner as number[]) : null;
198
219
  }
199
220
 
221
+ /** {@link arrayInnerExtents}, one indirection down: the INNER extents of an array MEMBER, shared
222
+ * by the access side (structure.ts's indexed member spelling) and the declaration side
223
+ * (symbolFieldType) so the two cannot disagree about a member's shape.
224
+ *
225
+ * `[]` is the rank-1 answer — one subscript, `u8 x[48];`, the spelling every map has had. It is
226
+ * ALSO what an absent `dims` answers, because the declaration synthesis that reads this renders
227
+ * the member itself and so cannot be wrong about it. A consumer type-checking against a header it
228
+ * did NOT write must test `f.dims === undefined` first and decline; see {@link
229
+ * SymbolStructField.dims} for why that asymmetry is real rather than a shortcut.
230
+ *
231
+ * Null means NO consistent pair is available (a stated rank with an unknown or non-positive inner
232
+ * extent, which neither a declaration nor a subscript can spell), and both sides honour it the
233
+ * same way: the declaration falls back to the flat member, the access to the cast form. */
234
+ export function structFieldInnerExtents(f: SymbolStructField): number[] | null {
235
+ const dims = isArrayField(f) ? f.dims : undefined;
236
+ if (dims === undefined || dims.length <= 1) {
237
+ return [];
238
+ }
239
+ const inner = dims.slice(1);
240
+ return inner.every((d) => typeof d === 'number' && d > 0) ? (inner as number[]) : null;
241
+ }
242
+
200
243
  /** address → symbols at that address; `[0]` is the provider's canonical pick. */
201
244
  export type SymbolMap = Map<number, SymbolInfo[]>;
202
245
 
@@ -225,6 +268,13 @@ export function isBitfieldField(f: SymbolStructField): boolean {
225
268
  return f.bitWidth !== undefined;
226
269
  }
227
270
 
271
+ /** Does the map declare a bitfield member anywhere, in a symbol's own layout or its pointee's? */
272
+ export function declaresBitfields(symbols: SymbolMap): boolean {
273
+ return [...symbols.values()].some((infos) =>
274
+ infos.some((i) => [...(i.layout ?? []), ...(i.pointee?.layout ?? [])].some(isBitfieldField)),
275
+ );
276
+ }
277
+
228
278
  /** A layout member that {@link declaredFields} passed: sizable, and seated at an offset no
229
279
  * earlier member already covers. */
230
280
  export type DeclaredField = SymbolStructField & { size: number };
@@ -244,6 +294,31 @@ function wellFormedField(f: unknown): f is SymbolStructField {
244
294
  ) {
245
295
  return false;
246
296
  }
297
+ // `dims`, when stated, must be an array of positive extents or nulls whose product is the
298
+ // member's own `length` — three facts that contradict each other cannot all be trusted, exactly
299
+ // as `elemSize * length !== size` declines the layout below. A member with no `elemSize` is not
300
+ // an array and may not carry a rank at all.
301
+ if (m.dims !== undefined) {
302
+ if (
303
+ !Array.isArray(m.dims) ||
304
+ m.dims.length === 0 ||
305
+ m.elemSize === undefined ||
306
+ !m.dims.every((d) => d === null || (typeof d === 'number' && Number.isInteger(d) && d > 0))
307
+ ) {
308
+ return false;
309
+ }
310
+ // The product test has to hold over the NUMERIC extents alone, not only when every extent is
311
+ // numeric. A partly-null rank still constrains `length`: `[null, 5]` says the member's
312
+ // elements come in rows of five, so a `length` of 48 contradicts it exactly as `[6, 9]` and 48
313
+ // do — and symbolFieldType, which recovers a missing outer extent as `length / prod(inner)`,
314
+ // would spell that pair `u8 grid[9.6][5];`. A non-integral quotient is not a spelling defect
315
+ // to patch downstream: it is the same three-facts-disagree malformation, declined here.
316
+ const numeric = (m.dims as (number | null)[]).filter((d): d is number => typeof d === 'number');
317
+ const stated = numeric.reduce((a, b) => a * b, 1);
318
+ if (m.length !== undefined && (numeric.length === m.dims.length ? stated !== m.length : m.length % stated !== 0)) {
319
+ return false;
320
+ }
321
+ }
247
322
  // A bitfield's two facts must be present TOGETHER and internally consistent — a bitWidth with
248
323
  // no bitOffset (or bits outside the byte span `size` claims) leaves the field unseatable, so
249
324
  // the member is malformed and the layout declines whole like any other malformed member.
@@ -341,9 +416,29 @@ export function declaredFields(layout: SymbolStructField[] | undefined): Declare
341
416
  export function symbolFieldType(f: DeclaredField): IrType {
342
417
  if (isArrayField(f)) {
343
418
  const scalarElem = f.elemSigned !== undefined && (f.elemSize === 1 || f.elemSize === 2 || f.elemSize === 4);
344
- return scalarElem && f.length !== undefined && f.elemSize! * f.length === f.size
345
- ? T.array(T.int(f.elemSize! * 8, f.elemSigned!), f.length)
346
- : T.array(T.u(8), f.size);
419
+ if (!(scalarElem && f.length !== undefined && f.elemSize! * f.length === f.size)) {
420
+ return T.array(T.u(8), f.size);
421
+ }
422
+ // The declared RANK, when the map states one: `u8 x[6][8]` rather than `u8 x[48]`. Same
423
+ // object and same size either way — but a foreign header declares the member ONE of those two
424
+ // ways, and an access spelled against the other does not type-check (see
425
+ // SymbolStructField.dims). An unspellable rank (null) keeps the flat member.
426
+ //
427
+ // The outer extent is RECOVERED as `length / prod(inner)` — the map may leave the outermost
428
+ // subrange null where it knows the total. wellFormedField already declines a `length` the
429
+ // inner extents do not divide, so the quotient is a whole number for any layout that reached
430
+ // here; it is re-checked rather than assumed because this function is exported and a
431
+ // fractional extent prints `u8 grid[9.6][5];`, which is not C. The flat member is the honest
432
+ // fallback, the same one an unspellable inner extent takes.
433
+ const inner = structFieldInnerExtents(f);
434
+ const elem = T.int(f.elemSize! * 8, f.elemSigned!);
435
+ if (inner === null || inner.length === 0) {
436
+ return T.array(elem, f.length);
437
+ }
438
+ const outer = f.length / inner.reduce((a, b) => a * b, 1);
439
+ return Number.isInteger(outer) && outer > 0
440
+ ? [outer, ...inner].reverse().reduce<IrType>((t, n) => T.array(t, n), elem)
441
+ : T.array(elem, f.length);
347
442
  }
348
443
  if (f.pointer && f.size === 4) {
349
444
  // The pointee width is byte-load-bearing: arithmetic on the loaded pointer scales by it, so
@@ -365,6 +460,32 @@ export function symbolFieldType(f: DeclaredField): IrType {
365
460
  }
366
461
  return T.array(T.u(8), f.size);
367
462
  }
463
+
464
+ /** The array shape the DECLARATION actually spells for a member — the extents outermost first and
465
+ * the element type left at the bottom — read back out of {@link symbolFieldType} rather than
466
+ * re-derived from the map's own facts. It answers whatever that declaration is, which for a
467
+ * member with no spellable element type is the flat `u8 name[size]`: extents `[size]` over `u8`,
468
+ * not the empty shape.
469
+ *
470
+ * Exists because "how many subscripts does this member take, and of what" was being answered
471
+ * twice: once here (which needs `length` and a base-type `elemSigned`, and declares the flat byte
472
+ * array without either) and once straight off `dims` at the access site. `dims` and `length` are
473
+ * INDEPENDENT facts — a flexible array member states a stride and no bound (see
474
+ * SymbolStructField.length), and a member whose OUTERMOST subrange is unbounded (`u8 data[][8]`,
475
+ * legal C) reaches a map as `dims: [null, 8]` with no `length` at all. The declaration then spells
476
+ * `u8 grid[48];` while an access reading the rank off `dims` spells `gPtr->grid[0][a0];`, which is
477
+ * not C (`subscripted value is not an array, pointer, or vector`). An access side that asks THIS
478
+ * question cannot diverge from the declaration whatever gate symbolFieldType grows next. */
479
+ export function declaredArrayShape(f: DeclaredField): { extents: number[]; elem: IrType } {
480
+ let t = symbolFieldType(f);
481
+ const extents: number[] = [];
482
+ while (t.kind === 'array') {
483
+ extents.push(t.count);
484
+ t = t.elem;
485
+ }
486
+ return { extents, elem: t };
487
+ }
488
+
368
489
  /** A 4-byte member/scalar with NO base-type signedness is the enum idiom — C89 says int. */
369
490
  export const ENUM_IS_SIGNED = true;
370
491