Skip to content

tables: map values that are arrays of pointers in the C++ reference (#666) - #668

Merged
rowan-claude merged 6 commits into
mainfrom
fix-666-map-pointer-arrays
Sep 7, 2026
Merged

tables: map values that are arrays of pointers in the C++ reference (#666)#668
rowan-claude merged 6 commits into
mainfrom
fix-666-map-pointer-arrays

Conversation

@rowan-claude

@rowan-claude rowan-claude commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #666

mapValueIsPointer (internal/codegen/cpptable/maps.go) tested Type.Pointer alone, so [N]*T, [..N]*T and []*T under a map key took the map[K]*T arm and spelled <T>At on an array. That is the last of #628's class, and the whole remaining set: [E]*T, [N]*string and []*bytes are refused by name at the entry's value.

Red first

Three units under tables/maps, one per shape: Pairs map[uint32][2]*Item, Crews map[uint32][..2]*Item and Trails map[uint32][]*Item, each with a field past the map so a stop is visible. Against main's emitter each generated header is refused alone, clang at the repo's own table flags over a translation unit that includes it and nothing else:

PairsTable.h:6168: no matching function for call to 'ItemAt'
  candidate not viable: no known conversion from 'const TableRef[2]'
  to 'const TableRef' for 1st argument
PairsTable.h:6169: cannot initialize return object of type 'TableRef *'
  with an rvalue of type 'TableRef (*)[2]'
CrewsTable.h:6170: no matching function for call to 'ItemAt'
  candidate not viable: no known conversion from 'const TableRef[2]'
  to 'const TableRef' for 1st argument
CrewsTable.h:6171: cannot initialize return object of type 'TableRef *'
  with an rvalue of type 'TableRef (*)[2]'
TrailsTable.h:6171: no matching function for call to 'ItemAt'
  candidate not viable: no known conversion from 'const TableList<Item *>'
  to 'const TableRef' for 1st argument
TrailsTable.h:6172: cannot initialize return object of type 'TableRef *'
  with an rvalue of type 'TableList<Item *> *'

The entry's own storage was right in all three, and so was its value reset: §2.8 makes the entry a real table whose value is an ordinary field, so TableRef value[2], that array beside value_count, and TableList<Item *> were all already emitted, and every codec, every walk and the text form are the field's own. What had no spelling was the HANDLE.

The fix

The predicate becomes pointer AND no array AND no enum key, so it reports the ONE slot it was written for, and the three array arms carry a TableRef element:

  • [N]*T takes the fixed-array arm, handle <Entry>Value * over TableRef[N], so the extent survives the handoff;
  • [..N]*T takes the pair arm, because a counted array is a pair whatever its element is, so the handle is the ENTRY, which reaches the slots and the count;
  • []*T takes the list arm, whose storage the list emitter's own type argument already answered as TableList<T *>.

No walk code was added. The reachable walk for pointers inside a map value's array is NOT a new site: emitPointerSlotsOf (internal/codegen/cpptable/pointers.go:1505) is the one slot loop [N]*T and [..N]*T take at any field, emitListEdge is the one []*T takes, and #662's by-value map edge already descends an entry. tables-maps-entry-node-negative-control is the control that owns reaching inside a map value at all, and it now reddens on all three new rows.

Both engines

test/tables/maps_main.cpp gains three rows, each pinning its wire, holding measure == save, loading the region, re-saving it byte for byte, and reading the text form back to the same wire. Each instance carries a SHARED node and a slot that names NONE, so §16.7's &node label and its null row both ride through a map value.

test/conformance/harness/maps_test.go gains map_pairs, map_crews and map_trails, so the compiler's own engine reads the reference's bytes, writes them back byte for byte, renders the text and reads that text to the same bytes. The engine needed no change.

Controls, quoted red on the new rows

tables-maps-entry-node-negative-control, 6 failures to 15:

FAIL table wire golden map_pairs: -1 bytes written, 129 pinned
FAIL test/tables/maps_main.cpp:2138: need > 0
FAIL test/tables/maps_main.cpp:2139: need = -1, past the 268435456 byte measure ceiling
FAIL table wire golden map_crews: -1 bytes written, 137 pinned
FAIL test/tables/maps_main.cpp:2243: need > 0
FAIL test/tables/maps_main.cpp:2244: need = -1, past the 268435456 byte measure ceiling
FAIL table wire golden map_trails: -1 bytes written, 129 pinned
FAIL test/tables/maps_main.cpp:2360: need > 0
FAIL test/tables/maps_main.cpp:2361: need = -1, past the 268435456 byte measure ceiling

tables-maps-value-reset-negative-control, 10 failures to 14:

FAIL test/tables/maps_main.cpp:2209: repeat != NULL && ( *repeat )[0].value == 0 && ( *repeat )[1].value == 0
FAIL test/tables/maps_main.cpp:2314: repeat != NULL && repeat->value_count == 0
FAIL test/tables/maps_main.cpp:2315: repeat != NULL && repeat->value[0].value == 0
FAIL test/tables/maps_main.cpp:2431: repeat != NULL && repeat->count == 0

All seventeen map controls stay red under their own sabotage.

The page

§2.8 gains the paragraph that states the element for these three, between the storage list and the handle rule, and the handle rule gains the sentence the reference had backwards: the ARRAY FORM decides an array of pointers' handle, never the element. §4.2's map-value paragraph names the element kind the wire fuzzer's array strategies meet here, kind 17.

Goldens

make update-goldens. Every moved file is the maps unit: six new goldens for the three roots, the baseline's three roots plus three anonymous entries rendering kind=14 elem=17, and the ten existing *Table.h on four named constants and the announcement they describe (kTableMessageEntriesHere 66 to 75, kTableAnnounceBytes 786 to 901, kTableNodeTableFieldSlot 48 to 54, kTableRetainKnownIds 67 to 76), plus FleetTable.h gaining Item's pointer-target surface and moving ShipConfig's node slot 61 to 69 and its BuildVersion. The two moved wire goldens are the announcement itself and the message body that references the vocabulary by slot. Full accounting in e1060a0.

🤖 Generated with Claude Code

Silences, for the owner

From the cold read

The shared-node and null-slot sentence above is true of the Pairs and Trails rows; the Crews row emplaces three distinct nodes inside its live count with no share and no null slot, so [..N]*T pins §16.7's two rows through a map value by the other two rows only. Everything else verified by command: the three reds on main's compiler, the predicates, the one slot loop (a shortened loop reddens the map gate on the Pairs row alone), the oracle rows, seventeen controls red with the new failures on the new rows' lines, the four constants and the wire files as tabled.

rowan-claude and others added 6 commits September 7, 2026 05:41
Three units under tables/maps, one per value kind whose element is a
POINTER, each a map and a field past it so a stop is visible: Pairs
map[uint32][2]*Item, Crews map[uint32][..2]*Item and Trails
map[uint32][]*Item. `[E]*T`, `[N]*string` and `[]*bytes` are refused by
name (§11), so this is the whole remaining set of #628's class.

RED FIRST. Against this commit's emitter each generated header is refused
alone, clang at the repo's own table flags over a translation unit that
includes it and nothing else:

    PairsTable.h:6168: no matching function for call to 'ItemAt'
      candidate not viable: no known conversion from 'const TableRef[2]'
      to 'const TableRef' for 1st argument
    PairsTable.h:6169: cannot initialize return object of type 'TableRef *'
      with an rvalue of type 'TableRef (*)[2]'
    CrewsTable.h:6170: no matching function for call to 'ItemAt'
      candidate not viable: no known conversion from 'const TableRef[2]'
      to 'const TableRef' for 1st argument
    CrewsTable.h:6171: cannot initialize return object of type 'TableRef *'
      with an rvalue of type 'TableRef (*)[2]'
    TrailsTable.h:6171: no matching function for call to 'ItemAt'
      candidate not viable: no known conversion from 'const TableList<Item *>'
      to 'const TableRef' for 1st argument
    TrailsTable.h:6172: cannot initialize return object of type 'TableRef *'
      with an rvalue of type 'TableList<Item *> *'

The entry's own storage is right in all three: §2.8 makes the entry a real
table whose `value` is an ordinary field, so `TableRef value[2]`, that
array beside `value_count`, and `TableList<Item *>` are all already
emitted, and so is the value reset that clears every slot. What has no
spelling is the HANDLE: `mapValueIsPointer` tests `Type.Pointer` alone, so
an array of pointers takes the `map[K]*T` arm and spells `<T>At` on an
array.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handle every map surface hands back now follows the ARRAY FORM and not
the element, so `[N]*T`, `[..N]*T` and `[]*T` under a map key emit C++ that
compiles (docs/SPEC-TABLES.md §2.1, §2.8, §4.2).

The entry's own storage was right in all three, and so was its value
reset: §2.8 makes the entry a real table whose `value` is an ordinary
field, so `TableRef value[2]`, that array beside `value_count`, and
`TableList<Item *>` were all already emitted, and the codec is the field's
own on every walk. What had no spelling was the HANDLE, because
`mapValueIsPointer` tested `Type.Pointer` alone and sent all three to the
`map[K]*T` arm, which spells `<T>At` on an array.

- the predicate is now pointer AND no array AND no enum key, so it reports
  the ONE slot it was written for;
- `[N]*T` takes the fixed-array arm and its element is a `TableRef`, so
  the handle is `<Entry>Value *` over `TableRef[N]`;
- `[..N]*T` takes the pair arm, because a counted array is a pair whatever
  its element is: the count beside the slots is the second member, so the
  handle is the ENTRY;
- `[]*T` takes the list arm, whose storage already answered
  `TableList<T *>` through the list emitter's own type argument.

`[E]*T`, `[N]*string` and `[]*bytes` are refused by name at the entry's
value (§2.4, §15), so this is the whole set.

RED FIRST, against this commit's parent, in 5bfd348's message: clang at
the repo's own table flags refused each of the three generated headers
alone, on `no matching function for call to 'ItemAt'` and on a `TableRef *`
that cannot be initialized from `TableRef (*)[2]` or `TableList<Item *> *`.

BOTH ENGINES. `test/tables/maps_main.cpp` gains three rows, one per unit,
each pinning its wire, holding `measure == save`, loading the region,
re-saving it byte for byte, and reading the text form back to the same
wire. Each instance carries a SHARED node and a slot that names NONE, so
the `&node` label and the `null` row of §16.7 both ride through a map
value. `test/conformance/harness/maps_test.go` gains the three names, so
the compiler's own engine reads the reference's bytes, writes them back
byte for byte, renders the text and reads that text to the same bytes; the
engine needed no change, because #662's by-value map edge already descends
an entry and the entry's value is an ordinary pointer-array field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§2.8 gains one paragraph between the storage list and the handle rule. AN
ARRAY OF POINTERS IS ITS ARRAY FORM'S ROW OVER A REFERENCE ELEMENT: the
element is the eight-byte reference every pointer field has, and the array
form decides the rest, so `[N]*T` is `N` references and one member,
`[..N]*T` is `N` references beside its `int32` used count and two members,
and `[]*T` is the sixteen-byte list slot over reference elements and one
member. Each slot names a node of the walk below, so two slots may name
one node and a slot may name none. `[E]*T`, `[N]*string` and `[]*bytes`
are refused by name, and the refusal reaches a map's value at the entry.

The handle rule gains the sentence that follows from it: THE ARRAY FORM
DECIDES AN ARRAY OF POINTERS' HANDLE, NEVER THE ELEMENT, which is the one
the C++ reference had backwards.

§4.2's map-value paragraph names the element kind the wire fuzzer's array
strategies meet at these three: kind `17`, a node index, so a `[N]*T`, a
`[..N]*T` and a `[]*T` are the array strategies over node indices rather
than a strategy of their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…666)

`make update-goldens`. Every moved file is the maps unit and nothing else,
and each movement is the three new roots joining that unit:

- SIX NEW GOLDENS, `PairsTable.{h,cpp}`, `CrewsTable.{h,cpp}` and
  `TrailsTable.{h,cpp}`: the three new units' own emission.
- `tables/maps/tables.baseline`: the projection gains the three roots, the
  three anonymous entries keyed by the holder's wire id and the map
  field's, and the three map fields. Each entry's value renders
  `kind=14 elem=17`, a kind 14 array over kind 17 NODE INDEX elements,
  which is §4.2's row for an array of pointers and the evidence the
  element is a reference and not the pointee.
- THE TEN EXISTING `*Table.h` GOLDENS, on FOUR NAMED CONSTANTS and the
  announcement blob they describe. The unit's vocabulary is one set shared
  by every header of the unit (§3.3), so three roots grow it for all of
  them: `kTableMessageEntriesHere` 66 to 75 and `kTableAnnounceBytes` 786
  to 901, the projection half accounting for six of the nine new entries,
  which moves `kTableNodeTableFieldSlot` 48 to 54, and the tail for three.
  `kTableRetainKnownIds` 67 to 76 is the same count at the retain-unknown
  known-id table (§6.6). No existing `*Table.cpp` moved: the text form's
  translation unit carries no vocabulary constant.
- `FleetTable.h` moves twice more. `Item` is a POINTER TARGET of the unit
  now, so the `ItemAt`/`ItemEmplace` surface is emitted where `Item` is
  declared, which is Fleet's file, and `ShipConfig`'s node slot moves 61
  to 69 with the vocabulary. `BuildVersion` moves with the projection, as
  it does on every projection edit.
- `testdata/wire/tables/map_conn.bin` 786 to 901 bytes: it IS the
  announcement, so it carries the nine new entries.
- `testdata/wire/tables/map_full_message.bin`, 179 bytes both ways: the
  message body references the vocabulary by slot, and `ShipConfig`'s slot
  moved 61 to 69 inside a field of unchanged width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
House rule, and the refusal citation is §2.4 and §15, which is what the
diagnostic itself names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

Map values that are arrays of pointers, [N]*T, [..N]*T and []*T, emit C++ that does not compile in the reference (the last of #628's class)

1 participant