tables: the map codecs — the C++ reference, the corpus and the gate (#380) - #504
Merged
Conversation
…o wires and the node extent (#380) The construct's reference half. A map is an array of one generated entry table held in ascending key order, so nothing here is a new wire rule: what is the map's own is the SORT on the write side and the key scan, the ascending check and the duplicate rule on the read side. - internal/codegen/cpptable/maps.go: the runtime (TableMap's sixteen bytes and its binary search, TableKeyOrder, the builder's head and segments, the ordered cursor the four writing walks read, the load-side fill, the optional side index) and every emitter that hangs off it. - the NODE EXTENT (§2.8, §6.3): a map's entries are by-value records inside the holder's node extent, PRE-ORDER. One walk, three emitters — the extent measure, the pack, and the framing scan LoadMeasure answers from. Pack splits into the extent and the pointer descent, because a node's extent runs to the next directory entry and a pointee laid between two of its arrays would break that. - the READER (§2.8): the key is read before the slot is chosen, by a scan of the entry's field headers; ascending against the key of the last entry that landed; a duplicate resets the slot it took; a descending key stops the map and the parent reads on; a key past the bound is skipped by its L and counted clamped; a key kind the reader does not declare empties the map for one kind_mismatch. - the walks (§3.1): a map is a by-value edge at its field's position, its entries visited in ascending key order, so a map declared before a pointer field reaches a shared node first. - the COOK's write side (§7.6): the entry array in the node's extent, the slot written by the extent writer, and the layout that sizes a node by its value. A MAP-FREE UNIT IS UNTOUCHED, to the byte: every emitter above is gated on the unit declaring one, so a pointered unit with no map emits exactly the arena runtime, Pack and node dispatch it emitted before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…se by name (#380) target_cpp registers through registerMapCarrier and drops its refuseMaps call. A map is a VARIABLE-CLASS construct and the variable class is the reference's alone (docs/SPEC-TABLES.md §2.8, §15), so every port's refusal now names the carrier and the flag that generates, and refuseMaps loses the no-carrier shape it carried between the two PRs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e controls (#380) The corpus unit is what §2.8's rules are held against: a map of values, a map of pointers whose two keys name ONE node, a map of maps at three depths, a SIGNED key, and the walk-order pin — the map of *T is DECLARED BEFORE a pointer field, so a walk that grouped maps after the pointers would number the shared node the other way round and the pinned wire says so. tables/maps/Rows.schema carries the report rows' two roots, which differ only in the KEY'S KIND. test/tables/maps_main.cpp is the gate, plain and under ASan/UBSan: the builder's five, measure == save over two sorts that never met, one image from two insertion orders, an erase that costs nothing on any wire, the const form over a locked region and a loaded one and an opened cook, LoadBuilder producing exactly Load's report, the optional index answering what Find answers, and every reader rule on a hand-made body. Eight of §2.8's named negative controls are Makefile targets, each patching the GENERATOR through a Go overlay and requiring the gate to go RED — and a sabotage that patches nothing is itself a failure, so a control cannot rot into a no-op: sort 22 failures the writer emits insertion order dead 6 failures Save emits a dead entry ascending 4 failures the reader's ascending check is dropped duplicate 2 failures the duplicate rule is dropped keykind 3 failures the key-kind rule decodes anyway clamp 2 failures the reader clamps a key instead of dropping its entry fit 3 failures the N-against-L check is dropped depth 33 failures LoadMeasure sums the extent at one depth only Also here: the key kind is checked BEFORE the framing damage that follows from it, because a key read under another kind desynchronizes the rest of the scan and the honest answer to such a body is the KIND; and a variable root whose maps reach no node no longer emits a node dispatch with nothing to switch on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…de extent (#380) The register's rule is an issue behind every gap, so the eight ports' cells cite schema#502 — the map construct's porting home — and the reference's cell names the gate and its eight negative controls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
§2.8: the build version and the baseline see the entry and neither sees its NAME. Its line is keyed by the holder's wire id and the map field's, joined by a dot and chaining through a nested map — never the generated name — so a `was` rename of the holder or of the map field moves no line and invalidates no cooked file. tables/maps/tables.baseline is where that key is now pinned on a real corpus, at three depths and over both key kinds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
) §2.8's text: a string key is the string, an integer key is its decimal spelling QUOTED, ToJson writes entries in ASCENDING key order so unpack then pack is byte-stable, and FromJson reads keys in whatever order the text gives them. A `*T` value is shared exactly as a pointer field is, so two keys naming one node carry `&node` in the text as they carry one index on the wire. A MAP IS THE OTHER CONSTRUCT THE ONE WALK CANNOT WALK ALONE, on the pointer's own terms (§16.7): its entries live behind a TableMap<Entry> the walk has no name for and placing one needs the builder's arena. So the walk calls three adapters and does not define them — a map-free unit defines them as stubs the classifier makes unreachable, a map-bearing unit defines them in the map half that follows the walk. The generic walk stays byte-identical in all 31 generated .cpp files, the map half is byte-identical in the two that carry it, and not one byte of it reaches a map-free unit — `tables-json-map-walk`. The reader's rules, each held: a repeated key is last-wins and counted duplicate; an empty object is an empty map; a key longer than N drops its entry and counts clamped, because a clamped key is a merged entry; an integer key is read by §16.2's rule and by nothing else, so "2.0" is 2 and "1e3" is 1000; a value outside the key kind's range is kind_mismatch for that entry, dropped and never clamped; and a malformed key stops the read where §16.1's rule stops it. A ninth negative control rides with them: ToJson walking the entries in any order but ascending turns the round trip's byte compare red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…380) The text form's map half is emitted only in a unit that declares a map; the STUBS are what every other unit carries, exactly as it carries the pointer adapters' stubs, so the ONE generic walk can call three functions it does not define. A map-free unit's stub half is unreachable — the classifier is a constant false — and this is the regeneration that says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ntouched (#380) The CI generated gate found three symbols reaching a map-free unit, and each is now gated with the rest of the machinery (docs/SPEC-TABLES.md §2.2, §2.8): TableWorker::AllocRaw, kTableNodeRefused, and NodeStorage's body parameter, which a root that can name no map-bearing record does not take. A pointered map-free unit's HEADER is now byte-identical to what it was before the construct existed, which is what the gate asks. What does reach every table .cpp is the map adapters' STUB half — the three the ONE generic walk calls and does not define, exactly as it calls the pointer's three. The stub is unreachable in a map-free unit, because the classifier there is a constant false. testdata/golden/tables is re-pinned for it: 18 files, +43 lines each, insertions only, and `make update-goldens` now pins the maps unit's own sources beside them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…380) The modernize pin names it, and the loop said nothing the call does not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Its wire goldens were written by a little-endian build, so a codec that reached for host byte order anywhere in a map's framing — the array's L, its N, an entry's L, the key's length — would read them wrong and write them back differently. §3's "little-endian, byte-oriented throughout" is a property of the WIRE and not of the machine, and the sorted entry array is the same bytes on both hosts because the ORDER is over the wire's bytes and never over a machine word. This is where that costs something. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Found by the big-endian twin the previous commit added, which is what it is for: a cook is written for the build that OPENS it (docs/SPEC-TABLES.md §7), so a gate that hard-codes little-endian asks a big-endian build to open a file its magic refuses. The codec crossed the byte order whole — the wire goldens, the sorted entry array, the load and the text — and the one red was the test's own hard-coded order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nder (#380) §2.8 lays "one array per map reachable BY VALUE from the record ... in depth-first field order", and the reaches that sentence covers are four, each a DIFFERENT FRAMING the load's extent scan has to walk: a nested table's body, an array of tables, an enum-keyed body's pairs, and a union arm's payload (§2.6). The corpus reached only the first. tables/maps/Depth.schema reaches all four in one root with a field past them, and the gate holds the wire golden, the exact region (one byte short is refused), the re-save and the tool path. Held by hand first, and the union arm is where it earned its keep: an ARM IS ESTABLISHED AT SELECTION, so a caller that sets the tag and fills the map without resetting the arm hands the builder a map slot out of the overlay's old bytes. That is §2.6's contract meeting §2.8's storage, and the gate now spells it at the call site. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#380) §2.8: "An unreached non-empty map slot is refused by Cook and by Lock, the same refusal §7.6 gives a pointer in that position: a non-null slot in storage the walk did not reach names entries the region will not hold, so the write answers false and nothing partial is written." A COUNTED array's slots past its live count are that storage. The extent walk reached only the live ones while the cook's extent WRITER walked all N, so a non-empty map in one wrote its entries into bytes nothing had reserved — a heap overflow the new `Depth` corpus unit made reachable and ASan named. Both halves now walk the live count, and a non-empty map past it is the refusal the page states: the test is the extent itself, because an empty map takes no bytes and a record whose extent measures ZERO is a record whose every by-value map is empty. A tenth negative control holds it, and the WIRE is not refused — a counted array rides its live slots and the unreached one simply does not ride (§3.1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
update-goldens is the ONE deliberate act that re-pins a golden (§7.2), and the map gate's three wires were not in it — a re-pin would have left them behind and the next run would have gone red on files nothing regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…amage (#380) internal/tablewire does not carry the construct yet. Without a refusal at the surface, the engine meets a kind 14 field over element kind 13, decodes nothing into a slot it has no shape for, and answers the bytes are not a Fleet body — the framing is damaged past the point the walk could continue which sends its reader looking for a corrupt file when the file is fine and the READER is the one that is short. Every table surface the tool has — pack, unpack, cook, cook-check and uncook — now refuses by name, names the carrier (--lang cpp) and names the follow-on (schema#380). A map-free unit reaches the engines exactly as it did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
§2.8's own wording, and the point of it: a buffer that is the measure and not a byte more refuses a write that ran over rather than letting it fit by accident, and one byte short refuses. The gate measured the two against each other but always wrote into a large buffer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
…435) §2.8 gains the four sentences #504 left open: iteration on `map[K]*T` yields the resolved pointer as `Find` does; a wrong key kind that also desynchronizes the scan is that one kind mismatch and nothing more, the map reading empty; an entry whose alignment exceeds the arena's is refused at compile time by name; and `Find` on a builder-form map is a program error, as it is for a TableRef, the form saying which encoding is in force. §3's dispatch sentence becomes what it is: the table is resolved once at open and a body names an id by POSITION, and HOW a reader turns a reference into a decision is its own choice — nothing on the wire depends on it and no conformance case can see the difference. §20.2's `flags` block lands, after the enums and before the unions, and the projection emits it: each variant with its BIT POSITION, counted from zero, because the bit is what a stored mask means. The measured paragraph is re-pinned from THIS tree: 0.98x without the 210 KB blob, 1.80x over the tiny class, 0.99x over the whole corpus, over 69 pinned wires. The DISPATCH nanoseconds are not re-stated — they need a bench sitting of their own, and a number measured on a laptop under load is worse than none. ONE PAGE CONFLICT the flags block exposes, resolved in the direction §20.2's own reason points: §20.1's table said WHICH flags declaration a field names carries nothing, and the block makes that false — a field swapped to a declaration with other variants changes what every stored bit MEANS. The row now says so, and the control moves from "stays" to "moves". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
* tables: the id-table wire's identity, and the engine that writes it (#435) The wire law first, then the compiler's own engine over it (docs/SPEC-TABLES.md §3, §3.1, §3.2, §5). ir/tablewire.go carries the identity and the kinds this form adds: one hash at one width — fnv1a64, no fold and no rebound — for a field, an enum variant, a union arm and a table's own name; the reserved node-table id 0xFFFFFFFFFFFFFFFF; the map entry's two constants; kind 30 for an enum, kind 31 for the escape, kind 32 for a payload-free arm; and the form byte. internal/tablewire is the form whole: the form byte read first, the body as `id reference, kind, payload` terminated by the zero reference, canonical LEB128 for every length, count, index and reference, the trailing id table in first-use order located from the end, and the node table under the reserved id in one field rather than a chain of them. The page's worked save is a golden of its own: the 120 bytes of §3.1 reproduce byte for byte, and every id on the page — head, palette, Node, value, next, Palette, id — reproduces from the hash alone. Nine gates ride with it: the form refusal at 0, 2 and 0xFF, the reference bound in both directions, the non-canonical reference, the repeated entry, the stray byte before the table, the unnameable entry that counts nothing, the key reference of 0, and the keyed body under the positional kind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the C++ reference writes and reads the id-table wire (#435) The generated codecs move to the form whole (docs/SPEC-TABLES.md §3, §3.1, §3.2): the form byte, LEB128 for every length, count, index and id reference, 64-bit identity, the trailing id table located from the end, and the three kinds this form adds. The runtime gains what the form needs and nothing else: `putleb` and `getleb` with the canonical check, `TableIds` — the writer's table, its capacity a compile-time fact of the unit's closure so a save still allocates nothing — `TableIdTable` for the reader, `TableOpen` with its three verdicts, and `TableBodyEndsEarly` for the stray byte between the root's terminator and the trailer. A length cannot be patched in place when its own width varies, so every length is MEASURED before it rides: the array, keyed and map bodies join the nested table in computing their length first. The id table is built by the walk that writes, and an ELIDED field costs no entry — the walk interns, builds the payload that decides, and truncates when nothing rides. An arm header is a field header now: the arm reference, the arm's KIND byte, its L and its payload. A retyped arm is a kind mismatch where a retyped field is one, and an arm whose L is not its kind's width is that arm's own framing damage. The node table rides in ONE field: a 64-bit L frames a numbering of any size, so the chunking and its two ceilings are gone. The descriptors carry uint64_t ids, and the report gains the refusal verdict — five zero counters and a false flag are what a clean read prints too. Held so far by the corpus compiling under -Wall -Wextra -Werror -Wshadow in every unit, and by 36 of the 46 conformance instances agreeing with the compiler's own engine byte for byte; the other ten are pinned by hand-built old-form bytes in test/tables/main.cpp, which the next commit moves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the projections at sixty-four bits — build version form 3, baseline 7 (#435) The two texts an id is taken over move with the wire (docs/SPEC-TABLES.md §18.1, §20.2). The COOK PROJECTION is form 3: every `field` and `slot` id renders at sixteen hex digits, an enum field renders under its own kind 30, and a map entry's anonymous record key is two 64-bit ids. §20.2's worked example now reproduces character for character, and both of the page's digests reproduce from it — 0xa9df771f6051391f for the worked unit and 0xc4adaacfe6cb2809 for the same unit with no table. The TABLES BASELINE is rendering version 7: every `id=` on every line is sixteen hex digits of fnv1a64, which the page names as a rendering fact of the same shape as a new judged token, so a file written under the narrower spelling refuses and names `--update`. The four corpus baselines are regenerated with that reason recorded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the reserved id and the collision refusals, at sixty-four bits (#435) ONE ID IS HELD BACK, the node table's 0xFFFFFFFFFFFFFFFF, and no declared name may produce it — a `was` alias included, because the alias is what rides (docs/SPEC-TABLES.md §5, §11). Two TABLES of one closure whose name ids collide are refused naming both, because a node record says what it is by that id alone (§3.1). The field, enum-variant and union-arm collision refusals move to sixty-four bits with the rest of the identity. At sixty-four bits no input reaches any of them, so the controls PLANT THE COLLISION BELOW THE HASH through a compiler test hook that returns the colliding value for one named spelling. Eight rows go red under a plant and green without one, which is what says each row shows the refusal rather than the source — the two 16-bit fold pairs the old rows leaned on (`costarring`/`liquid`, `Agj`/`Atj`) cannot collide at this width and are gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: gofmt the trailer's bounds check Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the reference battery on the form, and the lock closes (#435) test/tables/main.cpp is rewritten to the grammar. Every hand-built wire in it was magic bytes of the previous framing; each is now built through a WireBuilder that speaks §3 — references interned in first-use order, canonical LEB128 lengths, and the id table the body actually named laid out behind it — so a forgery says the one thing that is wrong and the framing around it is the page's. `seed` is what a repeated-id row needs: the writer's own body minus its terminator and the table it named, so a second occurrence splices in under the same references, which is what a repeat IS (§3). The independent field-id implementation moves to fnv1a64, no fold and no rebound, and pins the compiler's hash at sixty-four bits. An empty wire is ten bytes, not two. The framing arithmetic is spelled from the grammar rather than as `3 + 4 + 5`. THE ARM EVOLUTION ROWS MOVE WITH THE FORM (§4): an arm header carries the arm's KIND now, so an int32 arm read as a float32 arm and a scalar arm read as a string arm are KIND MISMATCHES where they were §4.1's silent class, and four bytes read as a body is a kind mismatch where it was framing damage. A2.schema's own comment says what each row is. And the CROSS-IMPLEMENTATION LOCK closes: all 46 text-carrying instances agree byte for byte between the reference and the compiler's engine, held by a Go gate that encodes each instance's text and compares. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the maps battery on the form, and the builder shared (#435) The hand-built row wires modelled the previous framing with fixed offsets and u32 patches. A canonical LEB128 length cannot be patched in place, so the row is built FROM THE GRAMMAR instead: a RowSpec names the entries and one knob, and every reader control turns that knob rather than editing bytes behind the writer's back. The WireBuilder moves to test/tables/wirebuilder.h so the batteries share one implementation of the form rather than three. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the corpus re-pinned on the form (#435) Every wire golden, every cooked pair, the block images and reports.txt are the reference leg's under the id-table form, and `go test ./...` is green over them — internal/tablepack included, which reads the pinned wires through the engine. The three `parts` report rows were bytes fixed by the manifest that nothing regenerated. They are the reference's now, built from §3's grammar and pinned by the battery like every other golden, so a form change moves them with everything else instead of leaving them behind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the report format gains the refusal verdict (#435) Five zero counters and a false flag are what a clean read prints too, so a FORM BYTE this reader does not carry needs a VERDICT to be visible at all (docs/SPEC-TABLES.md §3). Every report row is now five counters and `read` or `refused`: the engine's, the C++ driver's surface, and FORMAT.md's own statement of the shape. Three report rows carry it — form 0, form 2 and form 0xFF over a wire that is otherwise a good v1 Cfg — and the battery holds each to the whole rule: the verdict refused, no counter moved, no damage reported, and the value left at its declared defaults. The form byte is read FIRST, so a file that is both a newer form and damaged is a refusal too. A TEXT read never refuses, so the json-hostile rows carry `read`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the wire fuzzer on the form, and three reader bugs it found (#435) The framing model moves to the id-table wire (docs/SPEC-TABLES.md §4.2). Every number is a variable-width LEB128 now, so a mutation is a SPLICE rather than a patch: the spot's bytes are replaced and every length framing it grows or shrinks by the same delta, innermost first, so the one thing wrong with a mutant is the thing the mutator aimed at. The passes the page names ride with it: the FORM BYTE at 0, 2 and 0xFF; the REFERENCE CLASS — every reference to 0, to the entry count (the last legal slot, which must RESOLVE), to the count plus one and to the extremes; every number in its NON-MINIMAL spellings, one redundant continuation byte, then nine, and an eleven-byte form; the ID TABLE — the entry count off by one each way and at both extremes, an entry's own bytes flipped, and the same id in two entries; and the four 64-bit extremes on every length, count and index. The refusal verdict rides the leg's protocol, so a form byte the leg does not carry is compared as a verdict rather than folded into damage. IT FOUND THREE READER BUGS, each a real divergence from the page: - `has( (int64_t) n )` on a 64-bit length: 0xFFFFFFFFFFFFFFFF cast to int64 is -1, and a negative length looked like room. The fit check is unsigned now, as `room( uint64_t )`. - `getleb` advanced the cursor on a number it REFUSED, so a body whose terminator was non-canonical still ended exactly at its L and the damage was stepped over. A refused number leaves the cursor alone. - a damaged COUNT dropped an optional array's PRESENCE: the field rode, so it is present (§2.3), and only a foreign ELEMENT KIND says the payload is not the array's at all. And two rules the C++ did not carry: the reserved id inside a NESTED body is malformed (§3.1), and a body carrying the node-table id twice keeps the LAST occurrence (§3). 91,587 enumerated mutants over 80 seeds, 0 divergences, plain and under ASan/UBSan. All four negative controls go red on their own check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the gates §3 names, and the instrument for first-use order (#435) Nine new report rows for the kinds this form added, each built from §3's grammar and pinned by the reference: kind 31 at a field the reader CANNOT name (unknown), at a field it CAN (kind mismatch), at an arm, and as an element kind; kind 32 at a named field. WHERE THE READER MEETS THEM DECIDES WHICH EVENT IT COUNTS, and each row says which. Four more for THE ARM'S KIND BYTE: an enum arm and a pointer arm read as an integer arm, a `bytes` arm read as a `string` arm — kind mismatches with the union None and the siblings intact — and an arm whose `L` is not its kind's width, which is that arm's own framing damage. K1 and K2 are the ENUM-AGAINST-INTEGER pair: the same two field names with the spellings swapped, so one wire read the other way names both directions at once. Two kind mismatches each way, both fields at their declared defaults. And the ELISION, FIRST-USE AND DISTINCTNESS instrument: over every pinned instance, the id table must hold exactly the ids the body names, once each, in the order the body first names them. It says WHICH of the three rules a drifted wire broke where the byte compare only says one moved. The fuzzer's locator gained what the instrument needed to be true: a nested-union arm's payload, an enum or pointer arm's reference, a keyed triple's element by its own kind, and a BLOB record's body left opaque. Aiming at those spots found one more divergence, and the leg was right: a damaged count inside an ARM's `L` is that arm's own framing damage, so the union reads None (§3) where a FIELD would stay present. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the tool's map half — the wire and the text (#435, #380) The compiler's engine carries maps now (docs/SPEC-TABLES.md §2.8). A map rides as kind 14 over element kind 13, an array of the generated `{ key, value }` entry, so nothing here is a second framing. What is the map's own is the ORDER and the reader's rules, and both halves have them: - the WRITE side sorts into ASCENDING KEY ORDER with no key twice, and an empty map elides under §3's by-value rule; - the READ side scans each entry's body for the KEY BEFORE choosing its slot, keeping the last occurrence §3's repeated-field rule keeps; - the KEY KIND is the reader's declaration, never the first entry's, and a key under another kind empties the map for ONE kind_mismatch; - KEYS NEVER CLAMP: a key past the bound drops its whole entry and counts clamped, one per entry; - a repeated key is `duplicate` and last wins WHOLE; a descending key stops the map with its ascending prefix and flags malformed, and the parent reads on past the field's length. The TEXT form is a plain JSON object keyed by the key, with integer keys spelled as strings of digits, written in the same ascending order. Held against the reference: the engine reads the C++ leg's three pinned map wires — a map, an empty map, and a map of maps with a keyed array and a union arm holding one — writes them back BYTE FOR BYTE, renders the text and reads that text back to the same bytes. The by-name refusal narrows to the COOK surfaces, which is what the tool still owes, and says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the page's sentences, and the flags block in the projection (#435) §2.8 gains the four sentences #504 left open: iteration on `map[K]*T` yields the resolved pointer as `Find` does; a wrong key kind that also desynchronizes the scan is that one kind mismatch and nothing more, the map reading empty; an entry whose alignment exceeds the arena's is refused at compile time by name; and `Find` on a builder-form map is a program error, as it is for a TableRef, the form saying which encoding is in force. §3's dispatch sentence becomes what it is: the table is resolved once at open and a body names an id by POSITION, and HOW a reader turns a reference into a decision is its own choice — nothing on the wire depends on it and no conformance case can see the difference. §20.2's `flags` block lands, after the enums and before the unions, and the projection emits it: each variant with its BIT POSITION, counted from zero, because the bit is what a stored mask means. The measured paragraph is re-pinned from THIS tree: 0.98x without the 210 KB blob, 1.80x over the tiny class, 0.99x over the whole corpus, over 69 pinned wires. The DISPATCH nanoseconds are not re-stated — they need a bench sitting of their own, and a number measured on a laptop under load is worse than none. ONE PAGE CONFLICT the flags block exposes, resolved in the direction §20.2's own reason points: §20.1's table said WHICH flags declaration a field names carries nothing, and the block makes that false — a field swapped to a declaration with other variants changes what every stored bit MEANS. The row now says so, and the control moves from "stays" to "moves". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: a field the writer does not write is not an edge (#440) The C++ authoring walks visited every pointer slot whatever the value said. §3.1 states otherwise: a pointer under a FALSE GUARD is not visited and its target takes no index, so a save never writes a record no written field names — and it is the same walk `Lock` lays a region out in, so the region holds no such node either. The numbering, PackMeasure and Pack are one walk here, so gating it gates all three. G1 is the corpus row and its control: one value with both pointer slots filled, saved under a false guard and again under a true one — one record against two, with the region size moving with them. A walk that visited every slot would write two both times. The OTHER half of §3.1's sentence, a pointer inside an ABSENT OPTIONAL, has no declaration to write: `?T` over a variable-length closure is a named follow-on (§2.3, §15), and its diagnostic names exactly this change as the precondition — "the authoring walks must gate on the presence companion before an optional field may hold pointer edges". The walks now do; lifting the follow-on is a separate decision and is not taken here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the eight ports' wire surfaces go absent, with an issue behind each (#435) The corpus is pinned in the id-table form and the eight ports write the previous one, so every surface whose expectation is WIRE BYTES says ABSENT rather than failing: `wire`, `report`, `json-read`, `json-write` and `json-hostile`. That is five and not two — json-read writes wire bytes from a text, json-write reads wire bytes to a text, and json-hostile packs a hostile text to wire — so all five move together on the same reasoning. Each driver's `list` says so where a reader will find it, naming the port's issue: Go #511, C #512, C# #513, Dart #514, Elixir #515, JavaScript #516, Java #517, Rust #518. Each issue states what moves and the rows the port passes to get its surfaces back. PORTING.md gains M20, the id-table wire, with the reference's cells and the eight gaps; M19's cell reads the tool's map halves too. `make conformance` is green again: the reference leg passes every registered surface, and the ports keep cook, block and the forgery batteries — none of which the wire moved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the per-port conformance controls go dormant with their surfaces (#435) Seven of the harness's negative controls sabotage a port's answer on a surface that is now ABSENT — `go`, `go-walk`, `c`, `cs`, `dart`, `elixir`, `js` and `java` all turn `wire` or `json-read` red, and there is no longer a cell there to turn. Each says so in one line naming its port's issue rather than passing silently or failing on nothing, and the sed scripts that drove them are gone rather than left to rot. What still proves the harness can go red is untouched: the reference leg's own control, the block-dump one, the absence controls and the reference-surface one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the tree brought current with its own compiler, and the gates that follow it (#435) generated/: nine languages' bench table units carried the previous build version. The Go one was committed and the other nine were not, so the `generated` job called the tree stale. The C++ bench table unit also carries the whole id-table wire now, which is what this branch emits. testdata/golden/tables/: the byte-identity half of the block zero-cost gate. 22 C++ Table sources move with the emitter this branch changes. 12 C Table sources move on one line each, the build version constant, whose new values are the ones testdata/golden/build-version already pins on this branch. lint: the eight golangci-lint findings and the eight modernize findings the job names. Three dead functions go (lebSize, tableElemKind, and the unsuffixed emitEnumElementCheck, whose suffixed form is the one every call site reaches). Seven untagged switches over a kind take their tag, one if-else chain over a map key becomes a switch, four errors.As refusal probes become errors.AsType, and two backward index loops become slices.Backward. tables-c-big-endian goes dormant, with schema#512 behind it, on the rule the eight port controls on this branch already follow. The soak's golden gate compares the C codec against testdata/wire/tables, which is the id-table form now, and the C port writes the wire's previous form. The same binary fails the same way on a LITTLE-endian host, so it is not a byte-order verdict at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <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.
The C++ reference's half of
map[K]V(docs/SPEC-TABLES.md §2.8): the storage,the builder, both wires, the node extent, the cook and the text form, held by a
gate with ten negative controls demonstrated red.
The language half is #487 and the page is #479. The eight ports carry none of
it — a map is a variable-class construct and the variable class is the
reference's alone — so every port refuses a map-bearing unit by name, and
schema#502 is that gap's home behind PORTING.md's M19 cells.
What a map is here
A LOOKUP over an array of one generated
{ key, value }ENTRY table held inascending key order. No new wire kind, no new framing, no new skip rule. The
holder's record carries SIXTEEN BYTES — a self-relative reference to the entry
array and an
int32count — and the entries are by-value records inside theHOLDER'S NODE EXTENT, laid after the record's own storage, PRE-ORDER.
1. Storage.
TableMap<Entry>: the reference, the live count, padding toeight — one member, so a port that walked two pieces would account for twelve
bytes where sixteen are written.
2. Region load.
LoadMeasure's term isN × sizeof( Entry )atalignof( Entry )AT EVERY DEPTH, summed from the FRAMING alone —Nisframing and not a value, so it reads no field and the caller can refuse a number
it did not expect.
Loadcarves the arrays pre-order out of the node's ownextent. Before an entry's body decodes the reader SCANS its field headers for
the key's id: ascending against the key of the last entry that LANDED, a
duplicate resets the slot it took so last wins WHOLE, a descending key stops the
map and the PARENT reads on, a key past the bound is skipped by its
Landcounted
clamped, and a key kind the reader does not declare empties the mapfor ONE
kind_mismatch.LoadBuilderproduces exactlyLoad's report, onevery row — the gate checks the five counters and the flag against each other
for every hostile body it makes.
3. The builder.
Insertreturns NULL for a key past the bound and for anarena that cannot carve another segment alike; a duplicate REPLACES, the value
reset and the entry's address unchanged.
Findis the linear scan on thebuilder and a binary search over the sorted array in the region.
Erasemarksone bit.
Eachis insertion order, live entries only. The sort runs in the fourwriting walks, over an array of entry pointers, because sorting the segments
would move entries whose addresses a caller holds. Nothing ever moves: the
gate holds a value handed back by the first of 200 inserts and reads it after
the last.
4. The writer. Ascending, the key first in every entry body, the two
constant ids, an empty map elided by §3's by-value rule.
5. Text form. A plain JSON object keyed by the KEY, integer keys quoted and
read by §16.2's integer rule and nothing else, ascending on the way out. A map
is the other construct the ONE walk cannot walk alone, on the pointer's own
terms (§16.7): three adapters, a stub half in a map-free unit and the real half
in a map-bearing one.
6. Cook and block. The entry array is placed by the layout rule; the slot is
the extent writer's, so a node the walk never reaches keeps an empty one. An
UNREACHED non-empty map slot — a counted array's slot past its live count — is
refused by
Cookand byLock, the same refusal §7.6 gives a pointer there;the wire is not refused, because a counted array rides its live slots and the
unreached one simply does not ride. A
block form never holds a map — §2.7's absence, already in
ir/blocklayout.go.7. The index.
IndexMeasure/Index/IndexFind, caller-owned, built atload, never stored. Its hash and its load factor are NOT a cross-port contract
and no golden names either; what it is held to is the CONTRACT of the lookup,
and the gate checks it answers exactly what
Findanswers, present and absent.11. The projections. The build version and the baseline do not move: no
golden changed,
internal/baseline.Versionstays 6, andtables/mapsjoins thebaseline corpus so the ENTRY'S ANONYMOUS KEY is pinned on real schemas —
0a53e00afba279af.d91b.9194is a nested map's entry, keyed by the holder's wireid and each map field's on the way in, never by the generated name.
A map-free unit is untouched, to the byte
Every emitter above is gated on the unit declaring a map.
go test ./...isgreen with no golden re-pin,
./build/schema_test_tablespasses, the genericJSON walk is byte-identical in all 31 generated
.cppfiles, andtables-zero-coststill says "value-only tables carry no pointer or mapmachinery".
The gates
make testgainstables-maps(plain and under ASan/UBSan),tables-json-map-walk, andtables-maps-negative-controls;tables-big-endiangains the gate's s390x twin, which reads goldens a little-endian build wrote —
§3's "little-endian, byte-oriented throughout" is a property of the WIRE and
not of the machine, and a map's framing is where that costs something. Each control
patches the GENERATOR through I2's overlay sabotage, regenerates, rebuilds and
requires the gate to go RED on a CHECK — and a sabotage that patches nothing is
itself a failure, so a control cannot rot into a no-op when the emitter it names
moves.
FAIL maps_main.cpp:183: memcmp( wire_full, wire_reversed, ... ) == 0Saveemits a DEAD entryFAIL maps_main.cpp:201: n = 378, want 424FAIL maps_main.cpp:501: v.malformedFAIL maps_main.cpp:534: v.decoded[1] = 11, want 0FAIL maps_main.cpp:626: mixed_row->entries.size() = 3, want 0FAIL maps_main.cpp:556: v.decoded[1] = 11, want 12N-against-Lfit check is droppedN = 0xFFFFFFFFunder a shortLFAIL maps_main.cpp:576: RowLoadMeasure( ... ) = 85899345944, want -1LoadMeasuresums at ONE DEPTH onlyFAIL maps_main.cpp:325: f->tiers.size() = 0, want 2ToJsonwrites any order but ascendingFAIL maps_main.cpp:709: bomber != NULL && ... && bomber < fighterLockwrites an UNREACHED non-empty mapFAIL maps_main.cpp:876: !past.Lock()The clamp row is the one worth reading twice: the
clampedcount alone cannotseparate a merged entry from a dropped one, so the DECODED VALUE is the half
that says it —
11where the rule reads12.The corpus
tables/mapsis a map of values, a map of pointers whose two keys name ONEnode, a map of maps at three depths, and a SIGNED key where an unsigned compare
would order
-3after2. The walk-order pin:by_idis amap[K]*TDECLARED BEFORE the
flagshippointer field, so the map reaches the shared nodefirst and numbers it first; a walk that grouped maps after the pointers would
number them the other way round and
map_full.binsays so.tables/maps/Rows.schemacarries the report rows' two roots, which differ onlyin the KEY'S KIND, and
tables/maps/Depth.schemareaches the other threeby-value edges a map's extent rides under — an array of tables, an enum-keyed
array of them and a UNION ARM (§2.6) — each a different framing the load's
extent scan has to walk.
What this PR does NOT carry
The Go tool half (brief item 8) is not here, and neither are the conformance
manifest rows that depend on it:
internal/tablewire,internal/tabletextandinternal/tablecookdo not yet decode a map, so there is no secondimplementation to hold the reference against and §4.2's differential fuzzer
gains no map rows. The reference's counters are pinned per row by the gate
above, which is what stands in until the oracle lands.
schema cook-check'sentry-array containment, alignment, fit and no-overlap checks (brief item 6's
second half) are the tool's too and go with it.
That is the whole of the gap, and it is one PR's worth of work on its own. So
that it is not a silent gap, every table surface the tool has —
pack,unpack,cook,cook-check,uncook— refuses a map-bearing unit BYNAME, names the carrier and names the follow-on. Without that refusal the
engine met a kind 14 field over element kind 13 and answered "the framing is
damaged past the point the walk could continue", which sends its reader
looking for a corrupt file when the file is fine and the reader is the one that
is short.
Two readings the page leaves open, and what this took
map[K]*T. §2.8 says iteration yields "the KEY beside areference to the VALUE" and, separately, that the const
Findon amap[K]*Tanswers the RESOLVED pointer. Iteration here matchesFind— areference to the pointer SLOT would hand a caller a
TableRefit has no wayto resolve from the proxy.
reader's rules but does not say which wins when a key read under another kind
leaves the rest of the entry's headers unreadable. This counts the KIND: the
framing damage follows from the kind, and calling it framing damage would
report the consequence instead of the cause.
tag and fills a map inside the arm without resetting it hands the builder a
map slot out of the overlay's old bytes. That is §2.6's contract meeting
§2.8's storage; the gate spells the reset at the call site.
refused at compile time by a
static_assertnaming it, which extends thearena's existing node rule (
alignof( T ) <= kTableAlign) rather thaninventing one. The page does not reach this case.
🤖 Generated with Claude Code