tables: map values of array, enum-extent, unbounded and blob kinds in the C++ reference (#628) - #662
Merged
Merged
Conversation
) Six units under tables/maps, one per value kind §2.8 lists that the C++ reference cannot spell today, each a map and a field past it so a stop is visible: Cells map[string(8)][3]int32, Runs map[uint16][..4]Item, Slots map[int32][Slot]int32, Spans map[uint8][]Item, Docs map[string(8)]*string and Chunks map[int32]*bytes. 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: CellsTable.h:6138: cannot initialize return object of type 'const int32_t *' with an rvalue of type 'const int32_t (*)[3]' RunsTable.h:6128: cannot initialize return object of type 'const Item *' with an rvalue of type 'const Item (*)[4]' SlotsTable.h:6198: cannot initialize return object of type 'const int32_t *' with an rvalue of type 'const TableKeyed<int32_t, Slot> *' SpansTable.h:6135: cannot initialize return object of type 'const Item *' with an rvalue of type 'const TableList<Item> *' DocsTable.h:6148: a type specifier is required for all declarations inline const * TableEntryFound( const DocsPagesEntry * entry ) ChunksTable.h:6139: use of undeclared identifier 'At' The entry's own storage is right in every one of the six: §2.8 makes the entry a real table whose `value` is an ordinary field, so the array, the count companion, the keyed slot, the list slot and the buffer reference are all already emitted. What has no spelling is the HANDLE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # tables/maps/tables.baseline
`internal/tablewire`'s `visitEdges` had no case for a MAP field, so the
node-table numbering never reached the pointer slots inside an entry. The
corpus hid it: `Fleet.by_id`'s two keys name the node `Fleet.flagship`
names, so every node reachable through a map was already numbered through
a pointer field beside it.
`Docs.pages` is `map[string(8)]*string` and `Chunks.blobs` is
`map[int32]*bytes`, and each is the first slot to name a node NOTHING
ELSE names. Against this commit's parent the engine read the reference's
bytes clean and then re-encoded 64 bytes where the reference wrote 123,
dropping both blob records and both pointer fields:
map_docs: the engine re-encoded 64 bytes, the reference wrote 123
map_chunks: the engine re-encoded 61 bytes, the reference wrote 107
The fix is the page's own sentence: a map is a by-value edge of the one
declaration-order walk, reached at its field's position, its entries
visited in ascending key order, each entry descended before the next
(docs/SPEC-TABLES.md §2.8, §3.1). `MapEntryOrder` is that order, lifted
out of `encodeMap` so the numbering and the writer take it from one
place and a wire cannot disagree with its own node table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
) The handle every map surface hands back now follows the STORAGE the page gives a field of the value's kind, so the six kinds #628 names emit C++ that compiles (docs/SPEC-TABLES.md §2.8, §4.2). The entry's own storage was right in all six: §2.8 makes the entry a real table whose `value` is an ordinary field, so `int32_t value[3]`, the element array beside `value_count`, `TableKeyed<int32_t, Slot>`, `TableList<Item>` and the eight-byte `TableRef` were all already emitted. What had no spelling was the handle, because `mapValueStorageType` asked `cppFieldType` for one type name and that function answers a field's ELEMENT type, not its array, its keyed slot, its list slot or its blob. - `[N]T` is ONE member, so the handle is a pointer to the ARRAY and keeps the extent. A return type cannot spell `T (*)[N]` without wrapping the declarator around the function name, so the entry gains one alias, `<Entry>Value`, and the handle is `<Entry>Value *`. - `[..N]T` is a PAIR, the elements beside `value_count`, so the handle is the ENTRY, which is #626's rule for a text value applied to the second storage shape that has two members. `mapValueIsText` becomes `mapValueIsPair` and covers both. - `[E]T` is `TableKeyed<T, E>` and `[]T` is `TableList<T>`, one member each, so the handle is that member. - `*string` and `*bytes` name a BLOB NODE (§2.5), which has no declared type name, so the const Find resolves through `TableBlobAt` and answers `const TableBlob *` where a `map[K]*T` answers `<T>At`'s pointer. The builder's handle stays the slot, which is what an Emplace fills. RED FIRST, against this commit's parent. clang at the repo's own table flags, over a translation unit that includes one generated header and nothing else: CellsTable.h:6144: cannot initialize return object of type 'const int32_t *' with an rvalue of type 'const int32_t (*)[3]' RunsTable.h:6134: cannot initialize return object of type 'const Item *' with an rvalue of type 'const Item (*)[4]' SlotsTable.h:6204: cannot initialize return object of type 'const int32_t *' with an rvalue of type 'const TableKeyed<int32_t, Slot> *' SpansTable.h:6141: cannot initialize return object of type 'const Item *' with an rvalue of type 'const TableList<Item> *' DocsTable.h:6154: a type specifier is required for all declarations inline const * TableEntryFound( const DocsPagesEntry * entry ) ChunksTable.h:6145: use of undeclared identifier 'At' BOTH ENGINES. `test/tables/maps_main.cpp` gains five 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. `test/conformance/harness/maps_test.go` gains the six 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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tables-maps-entry-node-negative-control` short-circuits the per-entry
`Number` call in the map's by-value edge, so a node named ONLY by a map
entry takes no index. The `Docs` and `Chunks` rows are what meet it:
their `*string` and `*bytes` values are the only slots in this corpus
that name a node nothing else names, and `Fleet.by_id` cannot meet it
because `Fleet.flagship` names the same node.
RED under the sabotage, all six failures in the new rows, and the answer
is the -1 §7.6 gives an unreached pointer rather than a wrong wire:
FAIL table wire golden map_docs: -1 bytes written, 123 pinned
FAIL test/tables/maps_main.cpp:1873: need > 0
FAIL test/tables/maps_main.cpp:1874: need = -1, past the 268435456 byte measure ceiling
FAIL table wire golden map_chunks: -1 bytes written, 107 pinned
FAIL test/tables/maps_main.cpp:1959: chunk_need > 0
FAIL test/tables/maps_main.cpp:1960: chunk_need = -1, past the 268435456 byte measure ceiling
Its sabotage carries an unbalanced parenthesis a `$(call)` argument
cannot, so the recipe is spelled out as the keylength one is.
Two rows moved so the instrument survives the sabotages beside it. The
`Chunks` half left the tail of `test_blob_values` for its own function,
because a sabotage that refuses one unit's save must leave the other unit
reporting. And the `Spans` element reads sit under their count, because a
list whose elements were never placed has a NULL element pointer: under
the `depth` sabotage that read faulted, and a fault takes the buffered
output of every row with it, which is what "the gate went red, but not on
a CHECK" was saying.
The controls now red on the new rows by name, and each is the sabotage
that owns that kind's runtime behaviour:
- `valuereset` 4 to 10 failures, one per value kind's duplicate row: the
fixed array reset whole, the counted array's elements AND its count,
every keyed slot, the list slot back to empty, the blob slot to null.
- `depth` names the `Spans` rows: an entry's list elements are a term of
the map's extent at the entry's depth, so a measure summed at one depth
only leaves them unplaced.
- The compile-time half of this PR has NO runtime control by
construction: a sabotage of the storage line produces no binary to run,
and its evidence is the red-first quote in 4a2d885.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§2.8 gains two paragraphs. THE VALUE'S STORAGE IS THE ROW ITS KIND TAKES AT A FIELD, kind by kind, because the entry is a real table and `value` is an ordinary field of it. THE HANDLE FOLLOWS THE STORAGE: a pointer to the `value` member where the storage is one member, the ENTRY where it is two, the ARRAY rather than its first element for a `[N]T`, and for a pointer value the SLOT on the builder against the RESOLVED node on the const `Find`. §4.2 gains one: a map's value is a FIELD POSITION, so the array element kind, the kind 17 node index and the kind 12 and kind 33 payloads the mutator list already names all land inside an entry without a strategy of their own, and what stays the map's is the KEY. §15 gains a named follow-on. `?T` and `?[N]T` store the value beside a `bool` presence companion, which is two members, so §2.8's rule answers the entry; the C++ reference answers the value member, so a `map[K]?T`'s presence cannot be set and the value is elided on every wire. `?[..N]T` is the exception by accident, its count companion already making it a pair. Found by probing every value kind §2.8 lists; left for its own issue rather than widened into this one. The one value kind refused BY NAME gains a diagnostics case: `map[uint32]*wstring`. The entry is where the refusal has to reach a map's value, because the value is a field of a table nobody wrote. Every other kind §2.8 lists compiles as the ordinary field it is, each confirmed by generating a unit and compiling the header alone: a scalar, an enum, a `flags` mask, a declared `type`, `?T`, `?[N]T`, `?[..N]T` and a union. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…628) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
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>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
… main left stale (#632) FOUR LINES ARE THIS BRANCH'S, one per emitted message reader whose root's numbering reaches a *string blob: blobs/AssetsTable.h twice, for Asset and for Catalog; maps/DocsTable.h once, for a map whose VALUE is a text buffer; and arms/GateTable.h once, for a blob reached only through a union arm. Each is the same line, the record's bytes handed to TableUtf8Valid. NO WIRE GOLDEN MOVED: the write side is untouched, and a read that refuses damage changes no byte a writer produces. THE REST WAS ALREADY STALE ON MAIN and `make tables-block-zero-cost` was red before this branch: e77093c (#658) added the retain walk's `case 15: case 30:` and e123f1b (#662) moved the map entry readers' text path, and neither re-pinned tables/maps. maps/ChunksTable.h, RunsTable.h, SlotsTable.h and SpansTable.h carry nothing of this branch's at all, and CellsTable.h carries none of its four lines either. The gate now compares 117 Table sources byte-identical to their pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 7, 2026
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
…666) (#668) * tables: the map corpus for the three pointer-array value kinds (#666) 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> * tables: map values that are arrays of pointers (#666) 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> * docs: an array of pointers under a map key, and its handle (#666) §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> * tables: regenerate the maps unit's goldens over the three new roots (#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> * tables: the predicate's comment carries no em dash (#666) 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> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
`make conformance-pin update-goldens` on a clean checkout of main rewrites
six files and nothing else: testdata/golden/tables/maps/{Cells,Chunks,Docs,
Runs,Slots,Spans}Table.h. The pins carry neither the retain walk's arm and
enum-reference case (kind 15 and kind 30 resolving as framed content) nor the
map entry loader's present clamp, both of which the C++ table emitter writes
today.
This is the shape #461 describes. The pins were written on the branch that
became #662, which was cut before #658's retain-unknown walk landed and
merged after it; the two diffs do not touch the same lines, so the merge is
clean and nothing in CI reads these files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
… (#671) * tables: a *string blob on a message body carries no content rule, red first (#632) docs/SPEC-TABLES.md §3.1 states kinds 12 and 33's content rule MET AT A NODE: "A TEXT blob's CONTENT is refused on the same terms", so a *string blob whose bytes are not well-formed UTF-8, or which carries a zero byte, is damage. §3.3 says a form-2 body's content rules are §3's, unchanged in what they reject, and that what differs is only the recovery, which a bit stream does not have. Neither engine carries it at the message site, where both carry it at the file site (decodenodes.go and pointers.go). The two gates are the page's rows over blobdemo's Catalog, whose numbering reaches a *string blob through note and a *bytes blob through thumb. test/tables/message_blob_main.cpp is the C++ reference's and TestAStringBlobRecordOnAMessageBodyCarriesTheContentRule is the oracle's. Both are RED at this commit: the truncated sequence, the zero byte, the overlong encoding and the lead byte 0xFF all load with a silent report. The last row of each is a *bytes blob carrying the same bytes, which must stay silent: a *bytes blob is bytes and never text, so the rule is the string id's alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tables: a *string blob record on a message body carries the content rule (#632) Both engines read a form-2 blob record's bytes through the SAME function the FILE form reads one with, so there is one rule and no second copy. The C++ emitter: messagevariable.go's PASS TWO, where the record's bytes are already in hand. The align a blob record spends before its bytes leaves the span on a byte boundary, so the same pointer goes to the runtime's TableUtf8Valid and to the memcpy below it, and the line is emitted under rootReachesStringBlob, the same guard pointers.go emits the file form's under. The check does NOT ride in MessageRecordScan, which is the framing walk LoadMeasure shares: ill-formed content is sizeable, so a measure still answers the region the framing commands and only the decode refuses. The Go oracle: messagedecode.go's placement loop calls textValid on the record's bytes under the TString arm alone, which is decodenodes.go's line for the file form. What differs from the file form is only the recovery, which a bit stream does not have: where a file counts the record malformed and reads on with every slot naming it null, a batch ends there. One malformed counts, the bodies before it stand, and nothing after is read (§3.3). A *bytes blob keeps no rule, because it is bytes and never text (§3.1), and both gates carry the row that says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tables: re-pin the zero-cost goldens the blob rule moves, and the six main left stale (#632) FOUR LINES ARE THIS BRANCH'S, one per emitted message reader whose root's numbering reaches a *string blob: blobs/AssetsTable.h twice, for Asset and for Catalog; maps/DocsTable.h once, for a map whose VALUE is a text buffer; and arms/GateTable.h once, for a blob reached only through a union arm. Each is the same line, the record's bytes handed to TableUtf8Valid. NO WIRE GOLDEN MOVED: the write side is untouched, and a read that refuses damage changes no byte a writer produces. THE REST WAS ALREADY STALE ON MAIN and `make tables-block-zero-cost` was red before this branch: e77093c (#658) added the retain walk's `case 15: case 30:` and e123f1b (#662) moved the map entry readers' text path, and neither re-pinned tables/maps. maps/ChunksTable.h, RunsTable.h, SlotsTable.h and SpansTable.h carry nothing of this branch's at all, and CellsTable.h carries none of its four lines either. The gate now compares 117 Table sources byte-identical to their pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tables: two blades and two pinned vectors under the blob content rule (#632) ONE BLADE AN ENGINE, each removing exactly one line and each naming what it turns red. message-blob-accepts-ill-formed drops textValid from the oracle's message placement loop; message-emitter-blob-accepts-ill-formed drops the emitted TableUtf8Valid call from messagevariable.go's PASS TWO. Each blade is drawn twice. Against the PAGE: the oracle's rides in MESSAGE_FORM_CONTROLS against TestAStringBlobRecordOnAMessageBodyCarriesTheContentRule, and the emitter's in the new tables-message-form-blob-negative-control, which builds test/tables/message_blob_main.cpp against a regenerated tables/blobs. The true run of that program is the target's own first step, so the gate is green before the blade is drawn, and the sabotage reddens four rows and leaves the two silent-report rows standing. Against EACH OTHER: message_blob_ill_formed_text is blob_str8 re-encoded as a batch of one with the fourth byte of note's eight byte payload replaced by 0xFF, and message_blob_zero_byte is the same wire with 0x00 there instead, so the two separate the two halves of one check. Nothing else moves, so what the two readers answer differently is the content rule and nothing else. The *bytes record beside it carries bytes no UTF-8 rule would accept and is never checked, which is the vector's own control. tables-wire-fuzz-message-blob-oracle-negative-control and its leg twin replay both vectors alone and require red on each, with the reports mirrored: the leg says 0,0,0,0,0,true,read, the oracle says 0,0,0,0,0,false,read the leg says 0,0,0,0,0,false,read, the oracle says 0,0,0,0,0,true,read AND THE LEG GREW THE ROOT THE VECTORS NEED. blobdemo's Catalog is the only root on the fuzzer's roster whose numbering can PLACE a text blob record, so it is the only one whose wire can carry §3.1's rule at a node. Its AssetsTable.cpp was already in CONFORMANCE_SOURCES, so the leg costs one include and one MESSAGE_VARIABLE row, and the corpus pass now runs 45 roots rather than 44. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: §3.3's test ledger names the content rule it now holds at a node (#632) No sentence of §3 or §3.1 moves and none is added: §3.1 already says a text blob's CONTENT is refused on the same terms as a kind 12 payload, and §3.3 already says a form-2 body's content rules are §3's. What was stale is HELD BY TEST, which carries one bullet per rule with its red clause and carried none for this one. The bullet names the six rows the two engines' gates run, the *bytes row among them, and the three ways a leg goes red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 7, 2026
Merged
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
…#674) The gate asserts §13.5's zero-cost promise for the text form: the JSON walker's list half is a body of non-template code, so it rides only in a unit that declares an unbounded array and a list-free unit pays nothing for it. The premise is right. The instrument named the list-free set by directory, which is a copy of a fact the schemas own, and #662 changed that fact: `Spans tracks map[uint8][]Item` makes the maps unit list-bearing, the emitter emits the half into it correctly, and the gate reports correct emission as a leak. LIST-WALK GATE FAILED: the list half reached the list-free unit build/tables-generated/maps/CellsTable.cpp internal/listwalk asks the compiler's own IR instead. A unit is list-free when no table in its closure carries an unbounded array, and a map's generated entry is a table of that closure (§2.8), so `map[K][]T` reaches the answer with no clause of its own. The corpus is read out of the Makefile's `tables_generate` define, which is the list that generates the tree the gate scans, so a unit added to the build is under the gate the same day. The derivation never asks the C++ emitter, whose own `unitHasList` the controls sabotage. The scan widened with the fix: 21 list-bearing .cpp files across five units where it held five in one directory, and none of the half in the 42 .cpp files of the 32 list-free units where it scanned three directories. arms, rt1 and rt2 were list-bearing and unheld. Three negative controls, each red by name. PLANTED puts the half into a list-free unit's emitted .cpp and holds the scan alone. UNGATED removes the emitter's `if anyList` so every unit carries the real half, which is what §13.5's ruling costs a list-free consumer the day the gating goes. DROPPED is the other half of that switch: a list-bearing unit left with the stub, which a gate that only refused leaks would pass. The map-walk gate keeps the same hand-kept shape and is narrow rather than red: its byte compare covers ten .cpp files of the seventeen that carry a map half, and its absence scan covers two directories. Owed separately. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
…e, six baselines committed (#574, #461, #484) (#677) * CLAUDE.md: the horizon section states the present tree (#574) The table layer is in the language: `table` declares a data type on the evolution-tolerant table wire and docs/SPEC-TABLES.md is its normative page beside docs/SPEC.md, so the source-of-truth bullet names both. The parser carries `table` as a declaration (internal/parser/parser.go, scanner.KwTable); the file said it was refused by name. The protocol layer stays out, as the tree has it: `message` and `object` are reserved words the parser refuses by name and `contexts` is refused at file scope. The frozen projection tokens are stated for what they do today, which is holding every existing unit's id stable. Glenn's scope sentence of 2026-08-25 is kept verbatim, in both places it appears. Closes #574 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs, Makefile: the pages state what the tree carries docs/VERSIONING.md's owed list, in the form its #522 and #525 rows already use. #435 and #434: the C++ reference and internal/tablewire carry the id-table form, the enum kind and the escape kind, and the eight ports are what is owed (#511 to #518). #523's message form: the C++ reference carries it and internal/tablenames claims §11's names, so what is owed is the eight ports and the form's LoadRetain. #523's unbounded array: the C++ reference carries the construct and testdata/wire/tables/list_migrates.bin is pinned. The #523 doc-comment row leaves the list: the language carries the `///` comment and a tag at every line kind, and the doc and tags descriptor columns are done in all nine ports on ROADMAP.md. docs/SPEC-TABLES.md §6.1's block-identity backend status: schema#301 is closed and no open issue carries the row, so the page says so and states why the identity is taken over the block's own text and never over a compiler version. docs/SPEC-TABLES.md §6.6: TableRetain and the three verbs are IN §11's claimed set. internal/tablenames/cpp.go:13 registers TableRetain, internal/check/check.go:3646 carries LoadRetain, MeasureRetain and SaveRetain in tableGeneratedVerbs, and :3692 carries Dart's three member spellings. What still lands with the Dart port is TableRetain in that backend's library-scope registry. docs/SPEC-TABLES.md §6.1's backend-status paragraph gains the form the eight ports actually write, which is the one that preceded §3. docs/SPEC-TABLES.md §15's map and list rows state the tool's honest cook status: compiler/cook.go refuses a map or a []T at Cook and Uncook by name, and cook-check refuses a map slot by name while carrying §7.4's element-array clause. Makefile: the zero-cost gate's parenthetical no longer cites ROUND-LOG.md, which no longer exists in the tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * testdata: six stale map goldens re-pinned from the reference (#461) `make conformance-pin update-goldens` on a clean checkout of main rewrites six files and nothing else: testdata/golden/tables/maps/{Cells,Chunks,Docs, Runs,Slots,Spans}Table.h. The pins carry neither the retain walk's arm and enum-reference case (kind 15 and kind 30 resolving as framed content) nor the map entry loader's present clamp, both of which the C++ table emitter writes today. This is the shape #461 describes. The pins were written on the branch that became #662, which was cut before #658's retain-unknown walk landed and merged after it; the two diffs do not touch the same lines, so the merge is clean and nothing in CI reads these files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: a freshness gate for the pins under testdata/ (#461) ci.yml checks freshness for generated/ and for nothing under testdata/, so a conformance pin or a table golden regenerated against an older main auto-merges clean and every job stays green against a pin nobody wrote. The new `pins` job is the `generated` job's shape one door over: check out, clone ../serialize at the shared SERIALIZE_TAG, run the tree's own re-pinning targets (`conformance-pin` and `update-goldens`) in place, and fail on a dirty testdata/, tracked or untracked. Naming the targets rather than the files means a rule that starts writing a new pin is covered the moment it joins one of them. Its first run is the negative control the issue asks for, taken from live material rather than a planted byte: on main the job goes red and names the six map goldens the previous commit re-pins. With those committed it is green. Closes #461 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tables: a baseline for the six corpora that hold none (#484) `for d in tables/*/; do ./bin/schema check $d; done` names six units with no tables.baseline: blobdemo, blockdemo, blockhome, scalardemo, streamdemo and vocab9demo. Each is one unit in one directory, which is what the notice's advice needs, so each gets the file the tool writes: ./bin/schema tables-baseline --update --reason "first baseline" tables/<d> The files are generated and never hand-written. Every one carries the history entry the reason opens, so the corpus itself carries the coverage clock (docs/SPEC-TABLES.md §18.4). The loop now prints the notice zero times. What still prints it is the multi-unit case #484 carves out: test/tables holds seven units of different packages in one directory, where "commit one in this directory" is advice a caller cannot take, and bench/corpus holds one more. Neither is a baseline this commit can write. Closes #484 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: record the pins job's measured wall (#461) 1m44s on run 34113070433, against 4m27s for big-endian in the same run: the job is inside this file's two-minute rule and is not the binding constraint. The comment names where its budget goes, so the next reader who needs the time back knows the first place to look. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
map[K][N]T,map[K][..N]T,map[K][E]T,map[K][]T,map[K]*stringandmap[K]*bytesnow emit C++ that compiles, and the corpus, the goldens, theoracle and the controls say so.
Closes #628.
What lands
The emitter.
internal/codegen/cpptable/maps.go. The entry's own storagewas right in all six: §2.8 makes the entry a real table whose
valueis anordinary field, so
int32_t value[3], the element array besidevalue_count,TableKeyed<int32_t, Slot>,TableList<Item>and the eight-byteTableRefwere all already emitted. What had no spelling was the HANDLE, because
mapValueStorageTypeaskedcppFieldTypefor one type name and that functionanswers a field's ELEMENT type, not its array, its keyed slot, its list slot or
its blob.
The rule the fix states is one sentence: the handle follows the storage.
[N]TT value[N], one member<Entry>Value *, a pointer to the ARRAY[..N]TT value[N]+int32 value_count, a PAIR[E]TTableKeyed<T, E>, one memberTableKeyed<T, E> *[]TTableList<T>, one memberTableList<T> **string,*bytesTableRef, one memberconst TableBlob *on the constFindmapValueIsTextbecomesmapValueIsPairand now covers[..N]Tas well asthe three text kinds, which is #626's rule applied to the second storage shape
that has two members.
[N]Tgains one alias per entry,<Entry>Value, becausea return type cannot spell
T (*)[N]without wrapping the declarator aroundthe function name. A blob takes its own arm in the pointer branch because it
has no declared type name, which is exactly what the two
At(errors belowwere.
The corpus. Six units under
tables/maps, landed on their own in 588af57before any emitter change, each a map and a field past it so a stop is visible:
Red first
Against 1f62d47, the merge with
origin/mainat bb5f3bd and before anyemitter change. clang at the repo's own table flags, over a translation unit
that includes one generated header and nothing else:
The pointer types in the first four are the whole diagnosis: the storage was
already the array, the keyed slot and the list slot, and only the return type
was unspellable.
The second defect, found by the corpus
internal/tablewire'svisitEdgeshad no case for a MAP field, so the tool'snode-table numbering never reached the pointer slots inside an entry. The old
corpus hid it:
Fleet.by_id's two keys name the nodeFleet.flagshipnames,so every node reachable through a map was already numbered through a pointer
field beside it.
Docs.pagesandChunks.blobsare the first slots to name anode NOTHING ELSE names, and the engine read the reference's bytes clean and
then re-encoded short:
Fixed in fcf707f with the page's own sentence: a map is a by-value edge of the
one declaration-order walk, reached at its field's position, entries in
ascending key order, each descended before the next (§2.8, §3.1).
MapEntryOrderis that order, lifted out ofencodeMapso the numbering andthe writer take it from one place.
Both engines
test/tables/maps_main.cppgains six rows, one per unit, each pinning its wire,holding
measure == save, loading the region, re-saving it byte for byte, andreading the text form back to the same wire.
test/conformance/harness/maps_test.gogains the six names, so the compiler'sown engine reads the reference's bytes, writes them back byte for byte, renders
the text and reads that text to the same bytes. The two engines' TEXTS agree:
{ "rows": { "alpha": [ 10, 11, 12 ], "beta": [ 0, 1, 2 ] } } { "spans": { "4": [ { "count": 40 }, { "count": 41 } ], "9": [ { "count": 90 } ] } } { "seats": { "-2": { "Alpha": 2, "Beta": 101 }, "6": { "Alpha": 1, "Beta": 100 } } } { "tracks": { "1": [ ..., 3 items ], "8": [ ..., 2 items ] } } { "pages": { "alpha": "first page", "beta": "second page" } } { "blobs": { "-3": "3q2+7w==", "2": "AAGA/w==" } }Every map is built OUT OF KEY ORDER, so the pinned wire says the four writing
walks sorted.
Spanscarries three elements under key 1 and two under key 8,the SECOND-inserted key holding the longer list, so a walk that laid the
entries' lists in insertion order would read one list's elements as another's.
SlotsandChunksuse signed keys, so-3and-2sort first.The controls
New:
tables-maps-entry-node-negative-control. It short-circuits theper-entry
Numbercall in the map's by-value edge, so a node named ONLY by amap entry takes no index.
DocsandChunksare what meet it, and the answeris the
-1§7.6 gives an unreached pointer rather than a wrong wire:Fleet.by_idcannot meet it, becauseFleet.flagshipnames the same node.Its sabotage carries an unbalanced parenthesis a
$(call)argument cannot, sothe recipe is spelled out as the
keylengthone is.tables-maps-value-reset-negative-controlgoes 4 to 10 failures, one pervalue kind's duplicate row, which is the runtime behaviour each kind adds:
tables-maps-depth-negative-controlnow names theSpansrows. An entry'slist elements are a term of the map's extent at the entry's depth, so a measure
summed at one depth only leaves them unplaced:
The compile-time half has NO runtime control, by construction: a sabotage of
the storage line produces no binary to run. Its evidence is the red-first quote
above.
Seventeen map controls, all red on purpose:
sort 78, dead 6, ascending 4, duplicate 2, keykind 3, clamp 2, fit 2, cap 1, depth 38, textorder 6, keylength 4, keyidentity 2, keydomain 3, placefail 1, valuereset 10, entrynode 6, unreached 2.Two rows moved so the instrument survives the sabotages beside it, and each
was a real hole in the gate rather than a convenience. The
Chunkshalf leftthe tail of
test_blob_valuesfor its own function, because a sabotage thatrefuses one unit's save must leave the other unit reporting. And the
Spanselement reads sit under their count, because a list whose elements were never
placed has a NULL element pointer: under the
depthsabotage that readfaulted, and a fault takes the buffered output of every row with it, which is
what "the gate went red, but not on a CHECK" was saying.
The refusal
map[K]*wstringis the one value kind §2.8 lists that is refused BY NAME, andinternal/check/tables_test.gogains the case. The entry is where the refusalhas to reach a map's value, because the value is a field of a table nobody
wrote:
No kind is refused under a map KEY that is not already refused there: the key
refusals are unchanged and their fourteen diagnostics cases stand.
The page
§2.8 gains two paragraphs. THE VALUE'S STORAGE IS THE ROW ITS KIND TAKES AT
A FIELD, kind by kind. THE HANDLE FOLLOWS THE STORAGE: a pointer to the
valuemember where the storage is one member, the ENTRY where it is two, the ARRAY
rather than its first element for a
[N]T, and for a pointer value the SLOT onthe builder against the RESOLVED node on the const
Find.§4.2 gains one: a map's value is a FIELD POSITION, so the array element
kind, the kind 17 node index and the kind 12 and kind 33 payloads the mutator
list already names all land inside an entry without a strategy of their own,
and what stays the map's is the KEY.
§15 gains a named follow-on, below.
The goldens
NEW:
testdata/golden/tables/maps/{Cells,Runs,Slots,Spans,Docs,Chunks}Table.{h,cpp}and six wires,
testdata/wire/tables/map_{cells,runs,slots,spans,docs,chunks}.bin.MOVED, and every one has a named reason.
The unit's SHARED VOCABULARY (§3.3) grew, because
mapdemogained twelvetables: the six holders and the six entries their maps generate. The vocabulary
is emitted identically into every header of a unit, which is why three
unrelated headers move and why no per-type codec does:
TableIds::kCapacitykTableRetainKnownIdskTableMessageEntriesHerekTableNodeTableFieldSlotkTableAnnounceBytesBuildVersion0x28abc7f5927a75390xe4ae2b700e9c3e19plus every
w.put( <slot>, kTableMessageRefBitsHere )andnode.type_slot,because the vocabulary is sorted and new types shift the slots after them. The
two message wires follow from the same fact:
map_connIS the announcement,615 to 786 bytes, and
map_full_messageis 172 to 179 bytes and repacks,because a body's type reference is an index into that vocabulary.
And the four
<Base>Table.cppfiles gained the JSON LIST WALK, which is theone movement
mapdemohas never had before:Spans.tracks's[]Itemvalue isthe unit's first unbounded array, so
TableJsonIsList,TableJsonWriteListandTableJsonReadListstop being the three unreachable stubs a list-free unitemits and become the walk. It is the unit's shared vocabulary too, emitted into
every map-bearing
.cppof the unit, which is why all four move identically.make tables-json-map-walkstill reports one map half, byte-identical in tenmap-bearing
.cppfiles and in no map-free one.tables/maps/tables.baselineis ADDITIVE, regenerated with its reason throughschema tables-baseline --updateover the merge rather than hand-merged. Thevalue lines record
kind=14 elem=4 array=fixed bound=3,kind=14 elem=13 type=Item array=bounded bound=4,kind=16 elem=4 array=keyed bound=2 key=Slot,kind=14 elem=13 type=Item array=unbounded,kind=17 type=stringandkind=17 type=bytes.Page silences decided, for the owner rather than quietly
[N]Tvalue's HANDLE is. §2.8 said nothing. Decided: the ARRAY,<Entry>Value *, which keeps the extent. The alternative, the decayedT *, loses the bound a caller would need to write the last slot, and theentry, tables: text map values, string(N), wstring(N) and bytes(N), in the C++ reference (#619) #626's answer for a pair, is not warranted here because the storage
is one member and reaching it needs no second name. The cost, stated: one
typedefper such entry in the header.Findanswers. §2.8 says a*Tvalue'sFindanswers the resolved pointer. A blob has no declared type and no<T>At. Decided:const TableBlob *, throughTableBlobAt, which is whatthe same slot answers at a field (§2.5, §6.3), with
TableStringViewOfandTableBytesViewOfbeside it as at a field. The builder handle stays theSLOT, which is what
TableStringEmplaceandTableBytesEmplacefill.§2.8's memory layout states the pre-order placement for a map inside an
entry's value and says nothing about a list there. Decided: the same rule,
because §2.9 makes a list's elements the holder's node extent and an entry
is by value inside that extent. Both engines and
LoadMeasureagree, andtables-maps-depth-negative-controlis red on it, so it is pinned eitherway and worth a ruling if it should read otherwise.
Named follow-on, found by probing every value kind §2.8 lists
An optional map value's presence companion has no handle (§15, new row).
?Tand?[N]Tstore the value beside aboolpresence companion, which istwo members, so §2.8's handle rule answers the ENTRY. The reference answers the
value member instead, so a caller can fill a
map[K]?T's value and cannot setits presence, and the value is elided on every wire.
?[..N]Tis the exceptionby accident: its count companion already makes it a pair. Same root cause, one
predicate away, and left for its own issue rather than widened into this one.
Everything else §2.8 lists compiles as the ordinary field it is, each confirmed
by generating a unit and compiling the header and its walk alone: a scalar, an
enum, a
flagsmask, a declaredtype, a table by value,*T, a map, and aunion.
Merged, not rebased
origin/mainwas at bb5f3bd (#626) when this branch resumed with one commiton it. Integrated with
git merge origin/main; the one conflict wastables/maps/tables.baseline, resolved by taking main's file and regeneratingwith
schema tables-baseline --update --reason. No golden was hand-merged.Test
make tables-maps,make tables-json-map-walkandmake tables-maps-negative-controlsgreen.go test ./...green across 30packages.
make testwhole; the run is still going as this opens and its result,with the absent-toolchain failures classified apart, lands in this body.
🤖 Generated with Claude Code
Correction from the cold read
The sentence above claiming everything else §2.8 lists compiles was wrong for one set: arrays of pointers under a map key,
[N]*T,[..N]*Tand[]*T, take themap[K]*Tarm throughmapValueIsPointer(maps.go:87 testsType.Pointeralone) and emitItemAton an array; red on main too, so inherited rather than introduced here. Owed as #666, the last of #628's class; this PR lands the six kinds it names.