Skip to content

Report an array's RANK, not just its flat element count - #7

Merged
macabeus merged 3 commits into
mainfrom
feat/array-rank-dims
Aug 2, 2026
Merged

Report an array's RANK, not just its flat element count#7
macabeus merged 3 commits into
mainfrom
feat/array-rank-dims

Conversation

@macabeus

@macabeus macabeus commented Aug 2, 2026

Copy link
Copy Markdown
Owner

variableShape()'s array arm and struct()'s array members now carry dims — the per-dimension extents, outermost first (u16 g[4][0x400][4, 1024]). length is unchanged: still the product, which is what sizes the object.

Why

The two readings answer different questions, and only dims answers the one a consumer spelling C has to answer: g[i] on a rank-2 array is a ROW, not an element. Knowing only the flat count you write one subscript, which against the project's own header is a type error — or, where the row address flows into an integer context, merely a warning and silently the wrong address.

This is not hypothetical. It was found from the consumer side: asmlift emitted gBgTilemapBufs[i] for the Klonoa decomp's own extern u16 gBgTilemapBufs[4][0x400], which is incompatible types in assignment ×4. Across six vendored GBA decomp projects the maps carry 444 rank>1 array globals; kleod's context alone declares 576 multi-dimensional arrays.

Details

  • arrayDims() is the new primitive; arrayLength() is re-derived from it rather than kept as a parallel loop, so the flat count and the rank cannot drift.
  • null marks an unbounded dimension.
  • On a DW_AT_declaration, a leading extent of 1 is GCC 2.95's spelling of an unsized outer bound (extern T x[], extern T x[][4]) and is reported as null — mirroring the rule length already applies. The inner extents are written down and survive, and they are the ones an element access needs. A definition keeps its 1, since the definition is the witness.
  • A rank-1 array reports a one-entry dims, so an absent key always means "not an array", never "rank unknown" — which is what lets a consumer gate on key presence.

Fixtures — all four test projects

The facts dims reads are producer- and byte-order-sensitive (a DW_TAG_subrange chain whose bounds go through the endian-aware reader; DWARF 2 spells them differently from DWARF 3+), so one toolchain is not evidence. All four projects gain unsigned char g_grid3[2][3][4] and struct Grid { int id; unsigned short cells[2][3]; }, and all four report dims: [2,3,4] / [2,3] identically across GCC 2.95 (DWARF 2), GCC 14 (DWARF 3+), and both byte orders.

They ride the existing cross-project machinery rather than a separate test: Grid joins CANDIDATE_TYPES and g_grid3/g_grid join CANDIDATE_GLOBALS, so the layout, declaration-fact and variableShape comparisons all pick them up. Mutation-checked — flattening arrayDims to a single product turns the shared-shape test red on all four.

agbcc-min additionally carries extern const short g_ext_grid[][4] (defined in crt0.s, so only the inner extent is knowable). That one is deliberately not shared: the unsized-outer-bound encoding is a GCC 2.95 quirk, so it is listed in CANDIDATE_GLOBALS and recorded by skipped as agbcc-only rather than left as an invisible asymmetry.

222 tests green, tsc --noEmit and eslint clean. All four ELF/oracle artifacts rebuilt and byte-stable.

Note on StructMember.dims

Reported for symmetry with VariableShape — a member and a global are the same DWARF array type. It has no consumer in asmlift today, deliberately: a consumer synthesizing its own struct declaration may flatten the member, because its access and its declaration then agree by construction. Rank only becomes load-bearing where the declaration belongs to someone else. The docstring says so.


Consumed by macabeus/asmlift#9, which requires >= 0.5.0, so it cannot pnpm install until this is published.

🤖 Generated with Claude Code

…ount

`variableShape()`'s array arm and `struct()`'s array members now carry `dims` — the
per-dimension extents, outermost first. `length` is unchanged (still the product).

Only `dims` answers the question a consumer spelling C has to answer: `g[i]` on a
rank-2 array is a ROW, not an element. Knowing just the flat count you write one
subscript, which against the project's own header is a type error — or, where the row
address flows into an integer context, silently the wrong address.

On a DW_AT_declaration a leading extent of 1 is GCC 2.95's unsized-outer-bound spelling
(`extern T x[]`, `extern T x[][4]`) and reports null, mirroring the rule `length` already
applies; the inner extents are written down and survive. A rank-1 array reports a
one-entry `dims`, so an absent key always means "not an array", never "rank unknown".

Fixtures: agbcc-min gains g_grid3 [2][3][4], the extern g_ext_grid[][4] idiom, and a
2-D struct member Grid.cells.
…st agbcc

`dims` was only exercised by agbcc-min. The facts it reads are producer- and
byte-order-sensitive — the dimensions come from a DW_TAG_subrange chain whose bounds are
read through the endian-aware reader, and DWARF 2 spells them differently from DWARF 3+ —
so one toolchain is not evidence.

mips-min, ppc-min and devkitarm-min gain the same two shapes agbcc-min already had:
`unsigned char g_grid3[2][3][4]` (a fully-bounded rank 3) and `struct Grid { int id;
unsigned short cells[2][3]; }` (the same fact on a member). All four now report
`dims: [2,3,4]` / `[2,3]` identically across GCC 2.95 (DWARF 2), GCC 14 (DWARF 3+), and
both byte orders.

They are wired into the EXISTING cross-project machinery rather than tested separately:
`Grid` joins CANDIDATE_TYPES and `g_grid3`/`g_grid` join CANDIDATE_GLOBALS, so the layout,
declaration-fact and variableShape comparisons all pick them up, and the "shares exactly
this declaration set" pin records the new membership. Mutation-checked: flattening
arrayDims to a single product turns the shared-shape test red on all four.

`g_ext_grid` is NOT shared, and is now listed in CANDIDATE_GLOBALS so `skipped` says so out
loud rather than leaving the asymmetry invisible: the unsized-outer-bound spelling
(`extern T x[][4]` encoded as upper_bound 0) is a GCC 2.95 ENCODING quirk, so agbcc-min is
the only place it can be pinned.

devkitarm-min's block is APPENDED, not inserted — debug-macro.spec.ts pins the macro
fixtures by exact line number and the file says so; inserting mid-file moved REG_DISPSTAT
from 157 to 173 and broke three tests.

222 tests green (was 218), typecheck + lint clean, all four ELF/oracle artifacts rebuilt.
@macabeus
macabeus merged commit 935f8ee into main Aug 2, 2026
2 checks passed
@macabeus
macabeus deleted the feat/array-rank-dims branch August 2, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant