tables: map values that are arrays of pointers in the C++ reference (#666) - #668
Merged
Conversation
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>
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #666
mapValueIsPointer(internal/codegen/cpptable/maps.go) testedType.Pointeralone, so[N]*T,[..N]*Tand[]*Tunder a map key took themap[K]*Tarm and spelled<T>Aton an array. That is the last of #628's class, and the whole remaining set:[E]*T,[N]*stringand[]*bytesare 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]*ItemandTrails 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: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
valueis an ordinary field, soTableRef value[2], that array besidevalue_count, andTableList<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
TableRefelement:[N]*Ttakes the fixed-array arm, handle<Entry>Value *overTableRef[N], so the extent survives the handoff;[..N]*Ttakes 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;[]*Ttakes the list arm, whose storage the list emitter's own type argument already answered asTableList<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]*Tand[..N]*Ttake at any field,emitListEdgeis the one[]*Ttakes, and #662's by-value map edge already descends an entry.tables-maps-entry-node-negative-controlis 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.cppgains three rows, each pinning its wire, holdingmeasure == 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&nodelabel and itsnullrow both ride through a map value.test/conformance/harness/maps_test.gogainsmap_pairs,map_crewsandmap_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:tables-maps-value-reset-negative-control, 10 failures to 14: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 renderingkind=14 elem=17, and the ten existing*Table.hon four named constants and the announcement they describe (kTableMessageEntriesHere66 to 75,kTableAnnounceBytes786 to 901,kTableNodeTableFieldSlot48 to 54,kTableRetainKnownIds67 to 76), plusFleetTable.hgainingItem's pointer-target surface and movingShipConfig's node slot 61 to 69 and itsBuildVersion. 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
?Tand?[N]Tunder a map key are still owed (§15's named follow-on, added by tables: map values of array, enum-extent, unbounded and blob kinds in the C++ reference (#628) #662). Their presence companion makes two members, so §2.8's rule answers the ENTRY and the reference answers the value member. Untouched here: it is a different predicate on a different companion, and 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) #666 named exactly three shapes.Itembecomes a pointer target of the maps unit, and the surface lands in the header that DECLARES it.FleetTable.hgainsItemAt/ItemEmplacebecausePairs,CrewsandTrailspoint atItem, thoughFleetitself points at noItem. That is the emitter's existing unit-scopedtargetsrule, not something this change introduced, and §2.2's zero-cost property is about a map-free unit rather than a per-closure pointer surface. Flagged rather than changed.A corpus addition moves the announcement for every header in the unit. The vocabulary is one set per unit, so three new roots moved four named constants and 786 bytes of announcement in ten headers that have nothing to do with pointer arrays. That is the design (§3.3); it is worth knowing that the blast radius of a corpus unit is the whole unit's goldens.
The compile-time half has no runtime control by construction, as tables: map values of array, enum-extent, unbounded and blob kinds in the C++ reference (#628) #662 recorded for the same class: a sabotage of the handle line produces no binary to run, so its evidence is the red-first quote above and not a
maketarget.tables/maps/tables.baseline's new history line carries the tool's own em dash, whichschema tables-baseline --updatewrites as the separator between the date and the reason. Left as the tool spells it.make tables-json-list-walkis RED on main, before this branch, and it is the targetmake teststops at here. The gate asserts the list half reaches nobuild/tables-generated/maps/*Table.cpp, and tables: map values of array, enum-extent, unbounded and blob kinds in the C++ reference (#628) #662'sSpans map[uint8][]Itemmade the maps unit list-bearing, so every one of them carries it. Reproduced on a clean clone ofdcdd5cdd:LIST-WALK GATE FAILED: the list half reached the list-free unit build/tables-generated/maps/CellsTable.cpp. This branch adds three more.cppbehind that same first failure and changes nothing about it. Left alone: which side of the gate moves, the list-free set or the emitter's gating, is tables: map values of array, enum-extent, unbounded and blob kinds in the C++ reference (#628) #662's lane and a judgment call, not 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) #666's class.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]*Tpins §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.