Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/arm-emulator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# @gba-kit/arm-emulator

## 0.5.0

## 0.4.0

## 0.3.0
Expand Down
2 changes: 1 addition & 1 deletion packages/arm-emulator/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gba-kit/arm-emulator",
"version": "0.4.0",
"version": "0.5.0",
"description": "ARM7TDMI CPU emulator supporting Thumb and ARM instruction sets",
"author": "macabeus",
"type": "module",
Expand Down
22 changes: 22 additions & 0 deletions packages/debug-info/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# @gba-kit/debug-info

## 0.5.0

### Minor Changes

- Report an array's RANK, not just its flattened element count.

`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: it stays the product, which is what sizes the object.

The two readings answer different questions, and only `dims` answers the one a consumer
spelling C needs: `g[i]` on a rank-2 array is a **row**, not an element. A consumer that
knows only the flat count writes a single subscript, which against the project's own header
is either a type error or — where the row address flows into an integer context — silently
the wrong address.

`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. A rank-1 array reports a one-entry `dims`, so an absent key
always means "not an array", never "rank unknown".

## 0.4.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/debug-info/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gba-kit/debug-info",
"version": "0.4.0",
"version": "0.5.0",
"description": "Parse ELF symbols and DWARF debug info (line tables) for GBA decomp debugging",
"author": "macabeus",
"type": "module",
Expand Down
44 changes: 43 additions & 1 deletion packages/debug-info/src/__tests__/producer-quirks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe('3. the 0xffffffff upper bound means zero-length, never 2^32 elements',
elemSize: 1,
elemSigned: false,
length: null,
dims: [null], // rank 1, the single extent unbounded
volatile: false,
const: false,
});
Expand All @@ -96,7 +97,15 @@ describe('3. the 0xffffffff upper bound means zero-length, never 2^32 elements',
it('a zero-length trailing member reads exactly like modern flexible arrays', () => {
// Mirrors the devkitarm-min `Blob.data` pin: stride reported, size and length not.
const data = agbcc.struct('Flex')!.members.find((m) => m.name === 'data')!;
expect(data).toEqual({ name: 'data', offset: 4, size: null, signed: null, elemSize: 1, elemSigned: false });
expect(data).toEqual({
name: 'data',
offset: 4,
size: null,
signed: null,
elemSize: 1,
elemSigned: false,
dims: [null],
});
expect(agbcc.resolveVariable('g_flex.data')).toBeNull();
});

Expand All @@ -117,6 +126,7 @@ describe('4. an unsized extern array is not [1]', () => {
elemSize: 2,
elemSigned: true,
length: null,
dims: [null], // the rank IS known (1); only its extent is not
volatile: false,
const: true,
});
Expand All @@ -128,8 +138,40 @@ describe('4. an unsized extern array is not [1]', () => {
elemSize: 2,
elemSigned: true,
length: 1,
dims: [1],
volatile: false,
const: false,
});
});
});

describe('5. an array RANK is not its element count', () => {
// `length` is the PRODUCT of the subranges, so it cannot say how many subscripts an
// element access takes — and `g[i]` on a `[2][3]` is a ROW, not an element. A consumer
// that only knows the flat count cannot spell an access that type-checks against the
// project's own header, which is exactly how a flattened rank surfaces: as a compile
// error in the world the header lives in, never as a wrong number.
it('reports every dimension of a fully-bounded array, outermost first', () => {
expect(agbcc.types.variableShape('g_grid3')).toMatchObject({ dims: [2, 3, 4], length: 24 });
expect(agbcc.types.variableShape('g_init_table')).toMatchObject({ dims: [2, 2], length: 4 });
expect(agbcc.types.variableShape('g_fwd_sized_table')).toMatchObject({ dims: [3, 2], length: 6 });
});

it('a DECLARATION keeps its INNER extents and loses only the unsized outer one', () => {
// `extern const short g_ext_grid[][4];` — agbcc spells the outer bound as upper_bound 0
// (indistinguishable from a real [1]), but the inner 4 IS written down, and the inner
// extents are the ones an element access needs.
expect(agbcc.types.variableShape('g_ext_grid')).toMatchObject({ dims: [null, 4] });
});

it('reports the rank of an array MEMBER too', () => {
const cells = agbcc.struct('Grid')!.members.find((m) => m.name === 'cells')!;
expect(cells).toMatchObject({ offset: 4, size: 12, elemSize: 2, length: 6, dims: [2, 3] });
});

it('control: a rank-1 array reports a one-entry rank, not none', () => {
// Absence of `dims` must mean "this is not an array", never "rank unknown" — a consumer
// gating on key presence would otherwise read every plain table as unranked.
expect(agbcc.types.variableShape('g_rom_table')).toMatchObject({ dims: [3] });
});
});
52 changes: 45 additions & 7 deletions packages/debug-info/src/__tests__/real-projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe.each(ARM_PROJECTS)('DebugInfo vs binutils oracle on $label', (project)
{ name: 'flags', offset: 8, size: 2, signed: true },
// char[6] → `size` is the WHOLE member (element size × length); the element facts are
// what an indexed read into it needs, and `signed` stays null (an array is not a base type)
{ name: 'name', offset: 10, size: 6, signed: null, elemSize: 1, elemSigned: false, length: 6 },
{ name: 'name', offset: 10, size: 6, signed: null, elemSize: 1, elemSigned: false, length: 6, dims: [6] },
// pointer → 4 bytes, and the pointee facts pointer arithmetic scales by
{ name: 'ptr', offset: 16, size: 4, signed: null, pointer: true, pointeeSize: 4, pointeeSigned: true },
{ name: 'inner', offset: 20, size: 8, signed: null }, // nested struct
Expand Down Expand Up @@ -221,6 +221,7 @@ describe.each(ARM_PROJECTS)('DebugInfo vs binutils oracle on $label', (project)
elemSize: 2,
elemSigned: true,
length: 3,
dims: [3],
volatile: false,
const: true,
});
Expand Down Expand Up @@ -385,7 +386,15 @@ describe('DebugInfo on devkitarm-min-only shapes', () => {
// The member still declares an element STRIDE — what it has no bound. So `elemSize` is
// reported and `length` is absent, the two facts being independent.
const data = di.struct('Blob')!.members.find((m) => m.name === 'data')!;
expect(data).toEqual({ name: 'data', offset: 4, size: null, signed: null, elemSize: 1, elemSigned: false });
expect(data).toEqual({
name: 'data',
offset: 4,
size: null,
signed: null,
elemSize: 1,
elemSigned: false,
dims: [null],
});
});

it('names an UNNAMED pointee by the typedef that aliases it', () => {
Expand Down Expand Up @@ -517,7 +526,7 @@ describe.each(BE_PROJECTS)('DebugInfo vs binutils oracle on $label', (project) =
{ name: 'tag', offset: 0, size: 1, signed: false },
{ name: 'count', offset: 4, size: 4, signed: true },
{ name: 'flags', offset: 8, size: 2, signed: true },
{ name: 'name', offset: 10, size: 6, signed: null, elemSize: 1, elemSigned: false, length: 6 },
{ name: 'name', offset: 10, size: 6, signed: null, elemSize: 1, elemSigned: false, length: 6, dims: [6] },
{ name: 'ptr', offset: 16, size: 4, signed: null, pointer: true, pointeeSize: 4, pointeeSigned: true },
{ name: 'inner', offset: 20, size: 8, signed: null }, // nested struct
{ name: 'tail', offset: 28, size: 4, signed: true },
Expand Down Expand Up @@ -606,6 +615,7 @@ describe.each(BE_PROJECTS)('DebugInfo vs binutils oracle on $label', (project) =
elemSize: 2,
elemSigned: true,
length: 4,
dims: [4],
volatile: false,
const: false,
});
Expand All @@ -615,6 +625,7 @@ describe.each(BE_PROJECTS)('DebugInfo vs binutils oracle on $label', (project) =
elemSize: 2,
elemSigned: true,
length: 3,
dims: [3],
volatile: false,
const: true,
});
Expand Down Expand Up @@ -685,14 +696,17 @@ describe('cross-endian equivalence (same declarations, four toolchains, both byt
// Every type/global any of the four declares. The ones not present in all four are
// enumerated by name below, so a project that genuinely lacks a shape is skipped
// EXPLICITLY rather than dropped silently.
const CANDIDATE_TYPES = ['Probe', 'Inner', 'Bits', 'Cv', 'UtilPair', 'Pair', 'Shape', 'Blob'];
const CANDIDATE_TYPES = ['Probe', 'Inner', 'Bits', 'Cv', 'UtilPair', 'Grid', 'Pair', 'Shape', 'Blob'];
const CANDIDATE_GLOBALS = [
'g_counter',
'g_probe',
'g_bits',
'g_cv',
'g_rom_table',
'g_util_pair',
'g_grid3',
'g_grid',
'g_ext_grid', // agbcc-min only — the unsized-outer-bound spelling is a GCC 2.95 quirk
'g_pair', // little-endian sources only
'g_color',
'g_mode',
Expand Down Expand Up @@ -735,8 +749,17 @@ describe('cross-endian equivalence (same declarations, four toolchains, both byt
});

it('shares exactly this declaration set — every other shape is skipped BY NAME', () => {
expect(sharedTypes).toEqual(['Probe', 'Inner', 'Bits', 'Cv', 'UtilPair']);
expect(sharedGlobals).toEqual(['g_counter', 'g_probe', 'g_bits', 'g_cv', 'g_rom_table', 'g_util_pair']);
expect(sharedTypes).toEqual(['Probe', 'Inner', 'Bits', 'Cv', 'UtilPair', 'Grid']);
expect(sharedGlobals).toEqual([
'g_counter',
'g_probe',
'g_bits',
'g_cv',
'g_rom_table',
'g_util_pair',
'g_grid3',
'g_grid',
]);
// The rest, and who lacks each. These are SOURCE facts (the big-endian projects
// declare a different set of globals; agbcc/GCC 2.95 rejects anonymous unions and
// flexible array members), not parser gaps — pinned so a shape silently vanishing
Expand All @@ -747,6 +770,7 @@ describe('cross-endian equivalence (same declarations, four toolchains, both byt
Blob: ['agbcc-min', 'mips-min', 'ppc-min'],
});
expect(skipped(CANDIDATE_GLOBALS, hasGlobal)).toEqual({
g_ext_grid: ['devkitarm-min', 'mips-min', 'ppc-min'],
g_pair: ['mips-min', 'ppc-min'],
g_color: ['mips-min', 'ppc-min'],
g_mode: ['mips-min', 'ppc-min'],
Expand All @@ -773,6 +797,7 @@ describe('cross-endian equivalence (same declarations, four toolchains, both byt
Bits: { size: 8, members: 'hearts@0:1 stars@0:1 cross@0:2 wide@1:1 after@4:4' },
Cv: { size: 4, members: 'level@0:1 gain@2:2' },
UtilPair: { size: 4, members: 'lo@0:2 hi@2:2' },
Grid: { size: 16, members: 'id@0:4 cells@4:12' },
};

it.each(sharedTypes)('every project reports the same byte layout for %s', (type) => {
Expand Down Expand Up @@ -811,8 +836,21 @@ describe('cross-endian equivalence (same declarations, four toolchains, both byt
g_probe: { kind: 'struct', structName: 'Probe', size: 32, volatile: false, const: false },
g_bits: { kind: 'struct', structName: 'Bits', size: 8, volatile: false, const: false },
g_cv: { kind: 'struct', structName: 'Cv', size: 4, volatile: true, const: false },
g_rom_table: { kind: 'array', elemSize: 2, elemSigned: true, length: 3, volatile: false, const: true },
g_rom_table: { kind: 'array', elemSize: 2, elemSigned: true, length: 3, dims: [3], volatile: false, const: true },
g_util_pair: { kind: 'struct', structName: 'UtilPair', size: 4, volatile: false, const: false },
// RANK is byte-order- and producer-independent: the dimensions come from the
// DW_TAG_subrange chain, which every producer spells in declaration order. A reader that
// multiplied them away (or read them under the wrong endianness) shows up right here.
g_grid3: {
kind: 'array',
elemSize: 1,
elemSigned: false,
length: 24,
dims: [2, 3, 4],
volatile: false,
const: false,
},
g_grid: { kind: 'struct', structName: 'Grid', size: 16, volatile: false, const: false },
};

it.each(sharedGlobals)('every project classifies %s to the same shape — TypeIndex.variableShape', (name) => {
Expand Down
Loading
Loading