tables: the id-table wire (#435) - #507
Merged
Merged
Conversation
…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>
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>
…seline 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>
#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>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
Rulings on the three open points:
|
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>
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>
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>
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>
…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>
) 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>
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>
…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>
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>
This was referenced Sep 4, 2026
…d 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>
…ces (#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>
gafferongames
marked this pull request as ready for review
September 4, 2026 12:54
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Cold read of #510, changes named by the review. Table closures (land-and-expand): the id-table wire assigns wide text no kind, so SPEC-TABLES.md §11 refuses by name a type holding a wstring(N) field inside a table closure, and §2.8 says wstring(N) is not a map key. §4.12 carries the cross-reference and states that the packet wire is done while the table wire is the deferred half, with no ProjectionVersion bump for either. Expand half: #522, after #507 stops re-pinning table goldens. The write side is now precise: the used length within [0, N] and a zero code unit among the used units are refused on write in every target's own idiom, symmetric with the reader's zero-group refusal and with §4.7's interior null. Surrogate pairing is not checked on write and the page says whose job it is. Elixir additionally requires an even byte_size and raises ArgumentError otherwise. Also: exhaustion is stated at any group rather than as a positional list entry; the storage table names its departure from the reference's destination rule and why (§6.1 forbids the per-value allocation); the allocation claim carries the estate's standing Elixir exception (PORTING.md M1); the corpus refusals ride gate 7 rather than gate 5 and the two length refusals ride at wstring(4); and the interop field, the schema-side golden pins and the examples/ declaration are named as owed by the first implementation PR.
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against bin/schema built from b74a7d4 (#507). What moved: a saved ShipConfig is 89 bytes where it was 42, an all-default one is 10 where it was 2, the report carries a sixth member (the refusal verdict), and a truncated file now yields nothing at all rather than a good prefix — the id table is the trailer, located from the end, so a cut file has no vocabulary to name its fields with. The page now teaches the form byte and the refusal verdict beside the five counters. What did not move: every drift result and every counter value, in both directions, and all nine targets still generate the surface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). Every Measure moved: the fighter is 10 and 22 where it was 2 and 11, GameConfig is 142 where it was 99, and the three tool messages are 79, 49 and 45 where they were 42, 22 and 18. What did not move: the optional storage layout, all four ? refusals word for word, the None-key abort in a release build, the keyed array's survival of a mid-enum insertion with unknown=0, and the message union's tag enum and arms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: the route wire is 163 bytes where it was 172, the fleet is 145 where it was 165, and the four-thousand-node parallel build is 51,904 where it was 111,989 — the id table in the trailer carries each field id once, so a pointer-heavy graph is where the new form is cheapest. The page now says so beside the number. What did not move: the region size, sharing and null preserved through the round trip, the builder's 8,264 bytes, the map's ascending-key iteration and Find, and all three pointer refusals. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
…#447) Re-run against b74a7d4 (#507). Nothing in part 9 moved: the JSON a table writes and reads is text, and the wire's framing does not reach it. Every document, every report line, the &node graph form with sharing preserved, the unpacked tree's file list, --one-file, and the strict-mode refusal are all byte-identical to what the page carries. The packed file is larger for the reason part 6 now explains, and the page never printed its size. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: the baseline projection is version 7 where it was 6, and its field ids are the wire's new 64-bit hashes where they were 16-bit. What did not move: every one of the three verdicts word for word, both was guard rails, the --update refusal without a reason, the history block the override appends, and the was round trip reading a v1 file into the renamed field with unknown=0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: both build versions, and the facts projection is form 3 with 64-bit field ids where it was form 2 with 16-bit ones. What did not move: the cook's own sizes — 408 bytes, 328 data, 16 attribution — because a cook is a laid-out region and not the wire, so the id-table form does not reach it. Nor did Open, cook-check, the uncook byte-for-byte proof, the big-endian refusal on a little-endian build, the target-neutral build version across byte orders, or the protocol id's independence from a table edit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: the build version stamped in the block prologue, in both the C++ and the C halves. What did not move: the block is 56,064 bytes of a 196,672 maximum, exactly as before — the block form is a laid-out extent and the id-table wire does not reach it — and the count refusal, the C read through the aligned base, and the same-build refusal after a one-sided rebuild are all unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). The packet-wire handoff is unchanged: C reads the C++ packet bit for bit. The table-wire handoff is NOT, and the page now says so rather than claiming a crossing that does not happen today. C++ reads its own 89-byte save cleanly and both C and Go answer malformed on the same bytes, because the reference is on the new form and the eight ports have not moved to it. The page shows all three outputs, names what the ports do with bytes they cannot read (report malformed, misdecode nothing), notes that only C++ carries the refusal verdict, and points a reader who needs another language today at the packet wire, the cook or the block. Filed as G-15 in #521. Also moved: the reflection walk's ids are 64-bit and the enum field's kind is 30 where it was 7. The embedding demo is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
The next red behind the zero-cost gate, and the control caught itself: #507 hardened the map's N-against-L check to compare unsigned, the way §3 requires every length, count and index on this wire to be compared, and the control's sed still named the signed spelling. Its own "the sabotage patched nothing" guard is what said so, which is the guard doing its job. The sabotage now names the current line and the gate goes red on three cases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
* certify: the oracle never panics, and the release legs after #507 THE ORACLE PANICKED, so it was not an oracle. An array, map or keyed body is framed by its own `L`, and the three readers compute `end := r.off + bodyLen` and later span the elements with `r.sub(end - r.off)`. Between those two lines they read the body's header, whose count is a canonical LEB128 read against the PARENT buffer rather than against `end`. A body's `L` of 2 is the shortest that carries a header at all, but a LEB128 count is up to ten bytes, so a count spelled wide leaves the cursor past the end its own `L` set and the span goes negative. The C++ reference has the same shape at every one of its sites, but a negative span there is a reader whose `has()` refuses, so it decodes no elements and reports malformed on the first one asked for. `subTo(end)` gives the oracle that same answer by clamping the span at the cursor, and all three body readers go through it, so the invariant is stated once. The reference agrees on the mutant, replayed alone and across the whole run: 0 divergences. THE MUTANT IS PINNED. testdata/wire/tables/fuzz-vectors/ is a corpus of mutants the fuzzer has already gone red on, seeded into every run by name and fed exactly as it is, before the random pass. Vectors are never mutated and the random pass never draws from them, so the mutant sequence stays a function of the corpus and the seed alone. A RED IS A SEEK. The run seed was already deterministic, but the failure message printed neither it nor the mutant. It prints both now, the bytes as hex up to 4 KB and the SHA-256 above that. The negative control is tables-wire-fuzz-oracle-negative-control, which joins tables-wire-fuzz-negative-control and so runs in `make test`: it removes the clamp through a Go build overlay and requires the run to go red on the panic AND on the pinned vector by name. THE PORT GATES THAT READ THE ID-TABLE CORPUS. Four release legs failed on one cause: they hold a port's codec to testdata/wire/tables, which is the id-table form now, while the port still writes the wire's previous form. #507 already ruled on that shape and spelled it as dormancy; what it missed is every gate that calls a port's wire codec directly rather than through the conformance harness. The same rule now covers those, by name, citing the same issue per port: #511 Go, #512 C, #513 C#, #514 Dart, #515 Elixir, #516 JavaScript, #517 Java. No Rust gate reads the corpus. It reaches past the release legs: `make test` runs the port legs after the wire fuzzer, so the panic masked them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the zero-cost gate scans by symbol, and sanctions the reserved id The gate is the next red on main behind the wire fuzzer's panic, and it is #507's too. The id-table form's reserved node-table id is `kTableNodeTableFieldId` in every generated unit, because every reader of the form owes §3.1's refusal of that id inside a NESTED body whether or not its own closure carries a pointer: the body it is handed may have been written by a unit that does (§4). The gate's `TableNode` token matched the constant and called it pointer machinery. What a pointer-free unit pays for the rule is one `static const uint64_t` and one comparison. No arena, no builder, no handle, no lifecycle surface and no extra descriptor column, which is the claim this gate holds. So the scan is BY SYMBOL now rather than by line, `TableNode` is matched with its whole spelling so a node symbol nobody has written yet is still refused, and exactly one spelling is sanctioned by name. tables-zero-cost-negative-control plants TableNodeMap in a copy of a scanned header and requires the same scan to refuse it, so the one sanctioned symbol is shown to be the only one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * certify: drop the variables the dormant recipes were the only readers of The four Elixir sabotage variables and the three Dart GC-count variables existed for recipes that are now dormant echoes, so they are scaffolding with nothing behind it. The tunables stay: DART_SOAK_SECONDS, JAVA_SOAK_SECONDS and their kind are the knobs each gate wakes with. The failure-message const also moves above wireFailure's own doc comment, which it had split. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the map fit control's sabotage matches the line #507 left The next red behind the zero-cost gate, and the control caught itself: #507 hardened the map's N-against-L check to compare unsigned, the way §3 requires every length, count and index on this wire to be compared, and the control's sed still named the signed spelling. Its own "the sabotage patched nothing" guard is what said so, which is the guard doing its job. The sabotage now names the current line and the gate goes red on three cases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against bin/schema built from b74a7d4 (#507). What moved: a saved ShipConfig is 89 bytes where it was 42, an all-default one is 10 where it was 2, the report carries a sixth member (the refusal verdict), and a truncated file now yields nothing at all rather than a good prefix — the id table is the trailer, located from the end, so a cut file has no vocabulary to name its fields with. The page now teaches the form byte and the refusal verdict beside the five counters. What did not move: every drift result and every counter value, in both directions, and all nine targets still generate the surface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). Every Measure moved: the fighter is 10 and 22 where it was 2 and 11, GameConfig is 142 where it was 99, and the three tool messages are 79, 49 and 45 where they were 42, 22 and 18. What did not move: the optional storage layout, all four ? refusals word for word, the None-key abort in a release build, the keyed array's survival of a mid-enum insertion with unknown=0, and the message union's tag enum and arms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: the route wire is 163 bytes where it was 172, the fleet is 145 where it was 165, and the four-thousand-node parallel build is 51,904 where it was 111,989 — the id table in the trailer carries each field id once, so a pointer-heavy graph is where the new form is cheapest. The page now says so beside the number. What did not move: the region size, sharing and null preserved through the round trip, the builder's 8,264 bytes, the map's ascending-key iteration and Find, and all three pointer refusals. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
…#447) Re-run against b74a7d4 (#507). Nothing in part 9 moved: the JSON a table writes and reads is text, and the wire's framing does not reach it. Every document, every report line, the &node graph form with sharing preserved, the unpacked tree's file list, --one-file, and the strict-mode refusal are all byte-identical to what the page carries. The packed file is larger for the reason part 6 now explains, and the page never printed its size. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: the baseline projection is version 7 where it was 6, and its field ids are the wire's new 64-bit hashes where they were 16-bit. What did not move: every one of the three verdicts word for word, both was guard rails, the --update refusal without a reason, the history block the override appends, and the was round trip reading a v1 file into the renamed field with unknown=0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: both build versions, and the facts projection is form 3 with 64-bit field ids where it was form 2 with 16-bit ones. What did not move: the cook's own sizes — 408 bytes, 328 data, 16 attribution — because a cook is a laid-out region and not the wire, so the id-table form does not reach it. Nor did Open, cook-check, the uncook byte-for-byte proof, the big-endian refusal on a little-endian build, the target-neutral build version across byte orders, or the protocol id's independence from a table edit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). What moved: the build version stamped in the block prologue, in both the C++ and the C halves. What did not move: the block is 56,064 bytes of a 196,672 maximum, exactly as before — the block form is a laid-out extent and the id-table wire does not reach it — and the count refusal, the C read through the aligned base, and the same-build refusal after a one-sided rebuild are all unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Re-run against b74a7d4 (#507). The packet-wire handoff is unchanged: C reads the C++ packet bit for bit. The table-wire handoff is NOT, and the page now says so rather than claiming a crossing that does not happen today. C++ reads its own 89-byte save cleanly and both C and Go answer malformed on the same bytes, because the reference is on the new form and the eight ports have not moved to it. The page shows all three outputs, names what the ports do with bytes they cannot read (report malformed, misdecode nothing), notes that only C++ carries the refusal verdict, and points a reader who needs another language today at the packet wire, the cook or the block. Filed as G-15 in #521. Also moved: the reflection walk's ids are 64-bit and the enum field's kind is 30 where it was 7. The embedding demo is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
* docs: the tutorial, part 1 — a file, a package, and your first constants (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 2 — enums and flags (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 3 — the first packet, types and ranged integers (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 4 — the rest of the field types (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 5 — branches, unions, and your protocol (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 6 — tables, data that outlives the build (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 7 — optionals, keyed arrays, and a config format (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 8 — pointers and maps (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 9 — the text form, JSON and the tree workflow (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 10 — was and the tables baseline (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 11 — the cook and the build version (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 12 — the block form (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 13 — every language, reflection, and embedding the compiler (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 14 — the tool belt and the edges (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial in the README hub, and colon-separated part headings (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial says to put bin on your path (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, fence spacing in part 1 (#447) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial re-run on main at b74a7d4, parts 1 to 5 unmoved (#447) Every command and program in parts 1 through 5 re-run against bin/schema built from b74a7d4. The packet wire is untouched by the id-table wire: both protocol ids, every generated header excerpt, every diagnostic and every program output are byte-identical to what the page already carried. Only the build stamp in part 1's install block moved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 6 re-run on the id-table wire (#447) Re-run against bin/schema built from b74a7d4 (#507). What moved: a saved ShipConfig is 89 bytes where it was 42, an all-default one is 10 where it was 2, the report carries a sixth member (the refusal verdict), and a truncated file now yields nothing at all rather than a good prefix — the id table is the trailer, located from the end, so a cut file has no vocabulary to name its fields with. The page now teaches the form byte and the refusal verdict beside the five counters. What did not move: every drift result and every counter value, in both directions, and all nine targets still generate the surface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 7 re-run on the id-table wire (#447) Re-run against b74a7d4 (#507). Every Measure moved: the fighter is 10 and 22 where it was 2 and 11, GameConfig is 142 where it was 99, and the three tool messages are 79, 49 and 45 where they were 42, 22 and 18. What did not move: the optional storage layout, all four ? refusals word for word, the None-key abort in a release build, the keyed array's survival of a mid-enum insertion with unknown=0, and the message union's tag enum and arms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 8 re-run on the id-table wire (#447) Re-run against b74a7d4 (#507). What moved: the route wire is 163 bytes where it was 172, the fleet is 145 where it was 165, and the four-thousand-node parallel build is 51,904 where it was 111,989 — the id table in the trailer carries each field id once, so a pointer-heavy graph is where the new form is cheapest. The page now says so beside the number. What did not move: the region size, sharing and null preserved through the round trip, the builder's 8,264 bytes, the map's ascending-key iteration and Find, and all three pointer refusals. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 9 re-run on the id-table wire, nothing moved (#447) Re-run against b74a7d4 (#507). Nothing in part 9 moved: the JSON a table writes and reads is text, and the wire's framing does not reach it. Every document, every report line, the &node graph form with sharing preserved, the unpacked tree's file list, --one-file, and the strict-mode refusal are all byte-identical to what the page carries. The packed file is larger for the reason part 6 now explains, and the page never printed its size. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 10 re-run on the id-table wire (#447) Re-run against b74a7d4 (#507). What moved: the baseline projection is version 7 where it was 6, and its field ids are the wire's new 64-bit hashes where they were 16-bit. What did not move: every one of the three verdicts word for word, both was guard rails, the --update refusal without a reason, the history block the override appends, and the was round trip reading a v1 file into the renamed field with unknown=0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 11 re-run on the id-table wire (#447) Re-run against b74a7d4 (#507). What moved: both build versions, and the facts projection is form 3 with 64-bit field ids where it was form 2 with 16-bit ones. What did not move: the cook's own sizes — 408 bytes, 328 data, 16 attribution — because a cook is a laid-out region and not the wire, so the id-table form does not reach it. Nor did Open, cook-check, the uncook byte-for-byte proof, the big-endian refusal on a little-endian build, the target-neutral build version across byte orders, or the protocol id's independence from a table edit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 12 re-run on the id-table wire (#447) Re-run against b74a7d4 (#507). What moved: the build version stamped in the block prologue, in both the C++ and the C halves. What did not move: the block is 56,064 bytes of a 196,672 maximum, exactly as before — the block form is a laid-out extent and the id-table wire does not reach it — and the count refusal, the C read through the aligned base, and the same-build refusal after a one-sided rebuild are all unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 13 re-run on the id-table wire (#447) Re-run against b74a7d4 (#507). The packet-wire handoff is unchanged: C reads the C++ packet bit for bit. The table-wire handoff is NOT, and the page now says so rather than claiming a crossing that does not happen today. C++ reads its own 89-byte save cleanly and both C and Go answer malformed on the same bytes, because the reference is on the new form and the eight ports have not moved to it. The page shows all three outputs, names what the ports do with bytes they cannot read (report malformed, misdecode nothing), notes that only C++ carries the refusal verdict, and points a reader who needs another language today at the packet wire, the cook or the block. Filed as G-15 in #521. Also moved: the reflection walk's ids are 64-bit and the enum field's kind is 30 where it was 7. The embedding demo is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 14 re-run on the id-table wire, nothing moved (#447) Re-run against b74a7d4 (#507). The command map, the misplaced-flag failure, the baseline notice, all four edge refusals and the absent UnitView are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 1 rewritten for the cold read (#447) One cumulative unit named starlight, and the prerequisites a stranger needs before parts 3 and 13: the Go toolchain, the clone, make, the PATH step, and the two sibling serialize checkouts at the tags this build pins (v1.16.2 and v1.9.2). Every diagnostic now shows its Bad.schema before its output, with the error trailer. New: a paragraph saying once that every id and byte count on the page belongs to the unit as it stands at that point, and naming schema id and schema projection as the way to compare. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 2 rewritten for the cold read (#447) Enums and flags land in the cumulative starlight unit, with the whole file shown after the edit. Pending is declared where it is discussed. Every diagnostic shows its Bad.schema and its error trailer. SystemFlagsNamesMax is shown as generated rather than asserted, the ADL note now points at the program's using-directive, and the program is shown entire and runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 3 rewritten for the cold read (#447) ShipState joins the cumulative unit with the constant it needs shown in place. The program is shown entire, with the forged-packet half in it rather than as a loose fragment, and the compile line names -I gen -I ../serialize, the checkout part 1 now makes. The five range refusals show the Bad.schema shape they share and each carries its error trailer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 4 rewritten for the cold read (#447) Vector2/GameVector2 replaces the Vec2/Vec3/GameVec2 muddle: one declaration in its own file of the same unit, one native header shown in full, one field on ShipState, and the program exercises the operator. The retired [<= N] sentence is gone and the [2..8] count hazard is demonstrated with the generated write rather than asserted. Game.schema is shown entire at the end of the part, and the program compiles as shown with -I gen -I . -I ../serialize. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 5 rewritten for the cold read (#447) ChatLine, Snapshot and MaxPayloadsPerPacket are declared where the page uses them, in Net.schema of the same unit, and the program builds and reads a real two-payload Packet rather than a loose FireCommand. The scalar-arm refusal is shown here, where a reader first wants it, and it names the table closure part 7 builds. The id demonstration adds an arm and takes it back out, showing the id return. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 6 rewritten for the cold read (#447) Ruling 1's rule lands here, where tables first appear: the table wire is C++'s within one build, the eight ports are named with their issues #511 to #518, and the packet wire, cook and block cross all nine. The check notice is explained with this unit's own output and its exit status. The evolution demo copies the whole unit to starlight-2.0 and starlight-3.0, so both programs are shown entire and every file they read is created on the page. TableReport is shown and printed with all six members, duplicate included, and the truncation claim is demonstrated at three cut points rather than asserted. The flags law now uses this unit's own SystemFlags. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 7 rewritten for the cold read (#447) GunnerSettings, WeaponConfig and MaxWeapons are declared where the page uses them. The second unit lands here as the ruling asks: starlight/tools, its own package, holding the C++-only table forms (a union a table closure holds, with table, scalar and payload-free arms; an array of unions; *string and *bytes), introduced by the refusal that names the move. The main unit is shown still generating for all nine. config.cpp, none.cpp, keyed.cpp and tools.cpp are each shown entire with their compile lines, and config.bin is created here for parts 9 and 11. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 8 rewritten for the cold read (#447) Pointers stay in the starlight unit, which still generates for all nine, and maps move to the tools unit where the C++-only forms live. SetName and heads are declared in the programs that use them, and route.cpp, damage.cpp, wide.cpp and fleet.cpp are each shown entire with their compile lines. The 'a forged wire with a reference loop is refused' claim is replaced by what the loader actually does: an exhaustive single-bit sweep of an 80-byte pointered wire, showing 295 stopped, 345 opened, and one that hands back a self-referential node under an all-zero report. The page tells the reader to bound their own walks. Filed as G-19 in #521. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 9 rewritten for the cold read (#447) json.cpp and loadpacked.cpp are shown entire with their compile lines, and the graph half is in the same program rather than a loose fragment. The level_name edit is shown before the load prints it, the Corvette edit is shown as the file the designer saves, and the 'very strong' edit is reverted and repacked on the page before part 11 cooks the tree. Absent optionals not appearing in ToJson is stated where the output shows it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 10 rewritten for the cold read (#447) The baseline is created over the real cumulative unit, so the notice part 6 introduced is answered here and check goes silent again on the page. ShipConfig gains a systems SystemFlags field so the flags law is demonstrated on this unit's own flags rather than an invented one. The was diagnostic is shown beside the declaration that produced it, and the was round trip reads ship.bin, written in part 6 under the old field name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 11 rewritten for the cold read (#447) cook.cpp is shown entire with its compile line, and Mine.cook is produced by the program on the page before cmp compares it. The build-version demonstration adds a real field to this unit's GameConfig, shows the baseline passing it, the protocol id holding and the build version moving in one place, and takes it back out. Every cook command runs against the tree part 9 packed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 12 rewritten for the cold read (#447) RenderShip, RenderLaser and RenderFrame join the cumulative unit with the two constants they need, and the unit is shown still generating for all nine. produce.cpp and consume.c are shown entire with their compile lines, frame.block is created by the program on the page before the consumer reads it, and TableBlockRefusal is shown as generated so the field names in the program match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 13 rewritten to present state (#447) Ruling 1: the part now shows the three crossings that work — the packet wire, the cook and the block, each from the starlight unit into C, each run — instead of staging a failed table handoff. No rewrite narrative and no in-flight vocabulary in the page's own voice; the C++-only rule is stated once in part 6 where the promise is made. writepacket.cpp, readpacket.c, opencook.c and reflect.cpp are shown entire with their compile lines. The reflection walk now distinguishes an enum's vocabulary from a flags field's by the variant_id tell, so Shields is no longer dropped, and the Go embedding example has its go.mod, its imports and a sorted iteration order with the reason that determinism is a generator's contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, part 14 rewritten for the cold read (#447) The 'silent on success' rule is reconciled with the notice part 6 introduced and part 10 answers, with the rule stated as success is silent except while a table format is unguarded. Every edge refusal shows its Bad.schema and its error trailer, the bare-bytes case shows all four errors it really prints, and the UnitView gap is shown against this unit's own generate listing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, the four programs the page ran without showing (#447) save2.cpp, load3.cpp, keyed.cpp and j.cpp now appear with their sources and their compile lines, so every ./program the page runs is a program the page built. Verified against the real files: load3.cpp was rewritten in the working tree to match the printf the page shows, and re-run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the tutorial, the baseline notice comes from check alone (#447) The cold reader's last item. Only the check branch calls the unguarded-format nudge; generate prints nothing on success, verified: $ schema check . notice: nt declares 1 table and . holds no tables.baseline — ... $ schema generate --lang cpp --out gen . $ Corrected in part 14 and in the identical sentence part 6 carried. Part 10's separate claim, that generate diffs the closure against a committed baseline, stands: generate prints the same warnings and the same refusal as check and exits 1 on a refused edit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
pushed a commit
that referenced
this pull request
Sep 4, 2026
Corrected against the id-table wire (#507), the reachability-scoped projection (#527), the refused positional keyed spelling, and the per-language layout-contract mechanisms. Added the features a stranger could not use from this page: wstring(N), the /// doc comment and tags, maps, and unbounded arrays, each with its own status note where no backend emits it yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
pushed a commit
that referenced
this pull request
Sep 4, 2026
Corrected against the id-table wire (#507), the reachability-scoped projection (#527), the refused positional keyed spelling, and the per-language layout-contract mechanisms. Added the features a stranger could not use from this page: wstring(N), the /// doc comment and tags, maps, and unbounded arrays, each with its own status note where no backend emits it yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
* docs: the two READMEs against the finished specification README-NEW.md: the field header is a reference, a kind byte and a payload with the ids in a trailer, and only the skip and the clamp are counted; the silent class is four edits and not two; the cook's Open checks the reserved words too and names its refusal beside the null; the block form's layout is held by a static_assert in C++ and a type-initialization check in C#; descriptors carry the text key, the doc comment and the tags. Both READMEs link COMPETITION.md, and README-NEW links PORTING.md and COMPARISON-TABLES.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: USAGE against the finished specification Corrected against the id-table wire (#507), the reachability-scoped projection (#527), the refused positional keyed spelling, and the per-language layout-contract mechanisms. Added the features a stranger could not use from this page: wstring(N), the /// doc comment and tags, maps, and unbounded arrays, each with its own status note where no backend emits it yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: VERSIONING against the finished specification The field-header sentence now matches the id-table wire; the projection's exclusion list names doc comments, tags and const declarations; the evolution table gains rows for the unbounded array and for annotations; the tiny-message edge names the message form beside the file form's cost. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: PORTING M6 and M18 against the id-table wire M6 named the node-table reserved id as 0xFFFF and the node index as a u32; both are M20's own facts and are now spelled from it. M18 said no per-arm kind byte rides and called a bad arm length a kind mismatch; an arm header is a field header, so a retyped arm is the kind mismatch and a bad length is framing damage. The arm terminator control names the one-byte zero reference. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: FAQ and SECURITY against the finished specification FAQ: nothing on the table wire is fatal, and the widening event joins the list; the independent-versioning answer is the table wire and its message form rather than another library, with RPC named as what schema still declines; maps have landed; the packet wire's refusal of unbounded collections is stated; Elixir's write-side raise joins the idiom list; wstring is named as the one type not yet emitted. SECURITY: the wire fuzzer's mutator list names the id-table wire's own numbers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: COMPARISON-TABLES and COMPARISON against the finished specification The read report is six counters, not five; the silent class is four, not five; a kind that grew decodes exactly and counts widened; the wire has no size ceiling now that every length is a canonical LEB128. Maps, sorted lookup, arrays of pointers, arrays of unions and optional bounded arrays are closed rather than pending; doc comments are specified rather than deferred; the unbounded array joins the evolution table. COMPARISON names the table wire as the answer for the independently-versioned case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: COMPETITION against the finished specification and the owner's ruling The when-to-use-theirs line no longer declines the independently-versioned service: that is what a table is for, the message form is what makes it competitive by the byte, and the remaining gaps are cited as issues. Maps and general union arms move off the not-built list; the widening rule, the doc comment and retain-unknown are specified rather than declined or deferred; the id-table wire's own facts replace the u32 lengths, the 16-bit hash and the 4 GiB body, and the eight dormant port legs are named in the scoring rule and in every footnote that turned on them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the corpus and harness READMEs against the finished specification bench/tables: the framing split the performance ladder cites from this page. test/conformance: the id-table wire is the reference's and the tool's, the eight ports write its previous form, and their negative controls print dormant until they carry it. USAGE says the same where it introduces the form byte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: COMPARISON-TABLES declaration and wire rows against the id-table form Field identity is fnv1a64 with no fold, not a folded 32-bit hash; the encoding model is the form byte, the reference-and-kind header, canonical LEB128 and the trailing id table; a union arm carries its own kind byte; the node index is a LEB128 under kind 17 and the reserved id is the full 64 bits. Maps, sorted lookup, blob buffers and optional bounded arrays are landed rather than pending; the unbounded array and wstring join the rows they belong to; doc comments are specified rather than deferred. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the cold read's named changes Status notes on every present-tense claim the tree lacks: the protocol id's reachability closure (#524), the [E.Max]T refusal (#540), wstring and *wstring (#522), TableRefuseReason and the widened counter (#523), the doc and tag descriptor columns, and the union of table arms (#392). Maps and unbounded arrays state their own backend status per section rather than in one trailing sentence, and the list is owed rather than carried. FAQ scopes the nine-language answer to the packet wire with the table wire's state beside it, and gives the whole-wire malformed case and the form-byte refusal one clause, since both decode nothing. The conformance README says what the harness prints, N seeds absent, rather than what a make target echoes. Three additions the sections owed: Insert answers NULL for an oversized key, Add answers NULL at the arena, and a list's indices are not stable across an erase while its pointer is, beside the three names it claims. Em dashes are out of the new prose: the four USAGE sections, COMPETITION's new paragraphs, the conformance and bench additions and VERSIONING's new rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: COMPARISON-TABLES rows carry the same status the other pages do The two unbounded-array rows and the maps row said what the construct is without saying whether anything emits it, where every other page now does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Rowan <rowan@mas-bandwidth.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
…s that are not failures (#542) * bench: the tables corpus pins its enum by slot, and re-pins to the id-table wire The next red behind the map fit control, and the producer's own refusal named it: "record 2 is 2147 bytes, record 0 is 2139 — the table wire elides a field at its default, so a varied value landed on one. Fix the vary mapping, not this check." The diagnosis is one step past what that message says. Nothing landed on a default. A record's TRAILER carries one eight-byte entry per distinct id the record uses, and #507 made an enum ride as its VARIANT's id, so the eight entity slots drawing a weapon at random gave each record its own set of distinct variants and its own trailer length. Record lengths came out in eight-byte steps: 2123, 2131, 2139, 2147, one step per entry the draw added or dropped. The weapon is structure under this wire, so the slot's own index picks it: the eight slots take Fists through Grenade in every record, the set of ids is fixed, and the value still differs from slot to slot. It is the rule the pinned union arm already followed. A flags needs no pin, because TableDamage rides as raw bits and names no id. The goldens re-pin with it. They were the previous form and 2391 bytes, which is a second thing #507 left behind: the producer refused on the vary mapping before it ever reached the byte compare. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * elixir: the bench gate is dormant with the audit and the soak (#515) Re-pinning the tables bench corpus to the id-table wire exposed it. The gate was green against a corpus still in the previous form, which is the form this port writes, so it was holding the codec to bytes it happened to agree with rather than to the wire the reference now writes. With the corpus current it refuses in its own words: "refusing to bench a codec that does not reproduce the corpus." That is the same rule as tables-elixir-alloc-audit and tables-elixir-soak, so it takes the same dormancy and the same issue. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Reproduction:
$ printf 'package bad\n\ntable T { const(0xC7, 8)\n x int32 }\n' > Bad.schema && bin/schema check .
Bad.schema:3:11: const(value, bits) is a packet-wire construct — a table's wire is field-tagged TLV with no bit positions; remove it from table T (docs/SPEC-TABLES.md)
$ printf 'package bad\n\ntable T { x int32\n align\n y int32 }\n' > Bad.schema && bin/schema check .
Bad.schema:4:2: align is a packet-wire construct — a table's wire is field-tagged TLV with no bit positions; remove it from table T (docs/SPEC-TABLES.md)
The refusals are right and their reason survives the change, but
"field-tagged TLV" is the framing #507 replaced. A field is now
`id reference, kind, payload` against a trailing id table, and the tag is a
position into that table rather than the id itself. These were the only
place in the tool still naming the old shape.
The message now names the framing SPEC-TABLES §3 states, and the cite
carries the section:
align is a packet-wire construct — a table's wire is `id reference, kind, payload` against the id table, with no bit positions; remove it from table T (docs/SPEC-TABLES.md §3)
`reserved(bits)` carried the same sentence and moves with the pair the issue
names.
Negative control: restoring "field-tagged TLV" turns
TestTableRefusalsNameTheIdTableWire red on all three.
No generated output moves and no golden is re-pinned.
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
* docs: the payload-free arm is a packet-wire form, not a table-closure one (#521 G-07) Reproduction, from a checkout of the tool: $ cat U.schema package pfree type LaserFire { target_id uint16 } union WeaponFire { laser LaserFire ram } type FireCommand { fire WeaponFire } $ bin/schema check . # silent, exit 0 $ bin/schema generate --lang go --out gg . && echo ok # ok $ bin/schema generate --lang cpp --out gc . schema: unit declares a union with a payload-free arm (WeaponFire) — ... SPEC §4.8 said such a unit is refused by `check`, by name, in every target. The tool does not refuse it, and the tool is right: an arm with no payload has nothing for a port to guess, it rides the packet wire as its tag alone, and seven backends carry it. C and C++ refuse it at generate time through compiler/packetvoidarms.go's carrier registry, naming the union, the carriers and the follow-on, because their tagged-union storage has no member for an arm that has none. The stale sentence grouped it with the arms that ARE a front-end refusal: a `table` arm, a pointer arm, a scalar arm. The page moves, in all three places that carried the grouping: SPEC §4.8, SPEC-TABLES §11's refusal list, and USAGE's union section, which also now says which targets carry the arm and that `check` is target-neutral so the refusal arrives one command later. The test pins both halves: the tool's behavior across all nine targets, and the pages' own sentences. Reverting the three page edits turns TestPagesPlaceTheVoidArmOutsideTheTableClosureClass red on four lines. No generated output moves and no golden is re-pinned. * check: one reserved enum variant draws one diagnostic (#521 G-02, #447 F-01) Reproduction: $ printf 'package bad\n\nenum ShipType { None, Fighter }\n' > Bad.schema $ bin/schema check . Bad.schema:3:17: variant None is a compile error — every enum has None = 0 implicitly (SPEC §4.2) Bad.schema:3:17: enum ShipType's generated variant constant collides with enum ShipType's generated None constant — both generate the symbol ShipTypeNone; rename at the source (SPEC §4.6) Bad.schema:3:17: variant None collides with the implicit None variant inside enum ShipType (both become the associated constant NONE in Rust) — rename at the source (SPEC §4.6) Bad.schema:3:17: enum ShipType's generated variant constants (C form) collides with enum ShipType's generated variant constants (C form) — both generate the symbol SHIP_TYPE_NONE; rename at the source (SPEC §4.6) schema: 4 error(s) Four errors for one mistake, and it is a beginner's first enum mistake. The last three restate the first per target: the Go symbol, the Rust associated const, the C #define. checkClaimedNames now registers no generated symbol for a variant that IS one of the three names an enum generates for itself, so the reserved-word rule stands alone. The predicate is reservedEnumVariant, read by both sites, so the two lists cannot drift. $ bin/schema check . Bad.schema:3:17: variant None is a compile error — every enum has None = 0 implicitly (SPEC §4.2) schema: 1 error(s) The collision checks stay live for a name that merely COLLIDES, such as `none`, `NONE` and `max`, where the per-target wording is the whole explanation. The C-form label also stops naming the same side twice. One shared `whyC` covered the sentinels and every variant, so the C line read "enum E's generated variant constants (C form) collides with enum E's generated variant constants (C form)" and could not tell you which two declarations collided. Each registration now carries its own label. Negative control: dropping either reservedEnumVariant guard turns TestReservedEnumVariantDrawsOneDiagnostic red at four errors. Restoring the shared whyC turns TestCollidingEnumVariantKeepsItsPerTargetDiagnostics red. No generated output moves and no golden is re-pinned. * parser: a line-separated enum body names the rule and recovers (#521 G-03, #447 F-02) Reproduction: $ printf 'package bad\n\nenum Big\n{\n A\n B\n}\n' > Bad.schema $ bin/schema check . Bad.schema:5:6: expected }, found "newline" Bad.schema:6:5: unexpected "B" at file scope (declarations begin with package, const, enum, flags, type, table or union) schema: 2 error(s) An `enum` or `flags` body is comma-separated variant names. A `type` or `table` body is line-separated fields. Writing the first the way the second is written is the most likely early mistake in the language, and neither line named it. The second was worse than a silence: the newline ended the variant list, the closing `}` was expected on it, the declaration closed there, and the next variant reached the top-level loop, so the reader was told their mistake was at FILE SCOPE and handed the declaration keywords, two lines below where they actually typed it. parseVariantList now reads a newline followed by another name as the separator it was meant to be: it says the rule ONCE per body, then reads the rest of the body, so the declaration closes where the author closed it. $ bin/schema check . Bad.schema:5:6: enum variants are COMMA-separated, so A needs a comma after it — a `type` or `table` body separates its FIELDS by newline, and the two body grammars differ (SPEC §4.2) schema: 1 error(s) One fix covers both bodies: `flags` reads the same list. Negative control: reverting the missing-comma arm turns TestLineSeparatedVariantsNameTheRuleAndRecover red at two diagnostics neither of which names comma separation, and TestLineSeparatedVariantsStillYieldTheirNames red at one declaration. No generated output moves and no golden is re-pinned. * check: `?` on a union no longer draws a second, false error (#521 G-09) Reproduction: $ printf 'package bad\n\ntable A { x int32 }\ntable B { y int32 }\nunion U { a A\n b B }\ntable T { u ?U }\n' > Bad.schema $ bin/schema check . Bad.schema:7:14: field u: ?U marks a union optional, and a union is ALREADY optional — ... drop the ? (docs/SPEC-TABLES.md §2.3) Bad.schema:5:1: union U: the arm a A is not a declared type, and no table reaches U — ... hold the union in a table body, or make the arm a type (docs/SPEC-TABLES.md §2.6, §11, §15) schema: 2 error(s) The second message is false on its own terms: `table T` reaches `U` in the very line the first error is about, and the fix it prescribes is what the schema already does. The `?` refusal dropped the field, and checkTableArmsReached walked the surviving IR fields alone, so a single-token mistake sent the reader off to rewrite a correct declaration. resolveField now records the union a body's field NAMED at the point its type resolves, before any later rule can refuse the field, and the reachability walk reads that beside the surviving fields. Arms are excluded (c.arm), so a union arm inside another union does not fabricate reach. $ bin/schema check . Bad.schema:7:14: field u: ?U marks a union optional, and a union is ALREADY optional — ... drop the ? (docs/SPEC-TABLES.md §2.3) schema: 1 error(s) Negative control: dropping the unionNamedBy record turns TestOptionalUnionDrawsOneDiagnostic red at two diagnostics. TestUnreachedTableArmUnionIsStillRefused holds the refusal live where it is true, over a union no closure names and one a `type` body holds. No generated output moves and no golden is re-pinned. * cli: cook --verbose names each of its two reads (#521 G-12) Reproduction: $ bin/schema cook --root PackConfig --in tables/pack/pinned/PackConfig \ --out tree.cook --verbose tables/examples report: silent — the data matched the schema exactly report: silent — the data matched the schema exactly cooked tree.cook: 824 bytes, build version 0x913551e66dc67157, ... One command, one input, two identical report lines. `cook --in <dir>` packs the tree and then cooks the wire, so it genuinely reads twice. But two identical unlabelled lines read as the cook having read one input twice, and nothing in the output said which read produced which. reportLineStage names the read. cookInput answers whether it packed, so the label appears only where there are two reads to tell apart: report (pack): silent — the data matched the schema exactly report (cook): silent — the data matched the schema exactly `--in` a wire file reads once and keeps the unlabelled "report:" line. Without --verbose a silent report still prints nothing. The cooked bytes are identical before and after. Negative control: restoring the unlabelled reportLine at both sites turns TestCookVerboseNamesEachRead red on the indistinguishable pair. No generated output moves and no golden is re-pinned. * compiler: a misplaced flag is refused by name, not as a stat error (#521 G-01) Reproduction: $ bin/schema generate --lang cpp --out gen . --verbose schema: stat --verbose: no such file or directory Flag parsing stops at the first positional argument, so everything after it is another input path and a misplaced flag became a filename. The failure then surfaced as a syscall error on a string beginning with two dashes. The reader's mistake is legible to the tool, because nothing that begins with a dash is a schema path, and every other refusal in the tool names the rule. $ bin/schema generate --lang cpp --out gen . --verbose schema: --verbose looks like a flag, and flags come BEFORE the first path — everything after the first path is another input path; move it ahead of the paths, or spell a file that really begins with a dash as ./--verbose (SPEC §7) The guard is in GatherPaths, so every command that takes paths gets it. Negative control: reverting the guard turns TestGatherPathsRefusesAMisplacedFlagByName red, back on the stat error. No generated output moves and no golden is re-pinned. * check: the table-body refusals name the id-table wire (#521 G-17) Reproduction: $ printf 'package bad\n\ntable T { const(0xC7, 8)\n x int32 }\n' > Bad.schema && bin/schema check . Bad.schema:3:11: const(value, bits) is a packet-wire construct — a table's wire is field-tagged TLV with no bit positions; remove it from table T (docs/SPEC-TABLES.md) $ printf 'package bad\n\ntable T { x int32\n align\n y int32 }\n' > Bad.schema && bin/schema check . Bad.schema:4:2: align is a packet-wire construct — a table's wire is field-tagged TLV with no bit positions; remove it from table T (docs/SPEC-TABLES.md) The refusals are right and their reason survives the change, but "field-tagged TLV" is the framing #507 replaced. A field is now `id reference, kind, payload` against a trailing id table, and the tag is a position into that table rather than the id itself. These were the only place in the tool still naming the old shape. The message now names the framing SPEC-TABLES §3 states, and the cite carries the section: align is a packet-wire construct — a table's wire is `id reference, kind, payload` against the id table, with no bit positions; remove it from table T (docs/SPEC-TABLES.md §3) `reserved(bits)` carried the same sentence and moves with the pair the issue names. Negative control: restoring "field-tagged TLV" turns TestTableRefusalsNameTheIdTableWire red on all three. No generated output moves and no golden is re-pinned. * baseline: the history date says it is UTC (#521 G-18, #447 F-16) Reproduction, from a machine east of Greenwich: $ date && date -u Sat Sep 5 01:05:03 AEST 2026 Fri Sep 4 15:05:03 UTC 2026 $ bin/schema tables-baseline --update --reason "first baseline before 1.0 ships" . $ sed -n '/## history/,$p' tables.baseline ## history ### 2026-09-04 — first baseline before 1.0 ships The stamp is UTC, which is the right clock for an artifact read on other machines in other zones. It did not say so, so an author writes a baseline in the evening and reads yesterday's date on the file they just wrote, then doubts their tooling. The entry now reads `### 2026-09-04 (UTC) — <reason>`. SPEC-TABLES §18.4 stated no timezone at all, so the page moves with the tool and now says which clock and why. §18.1's and USAGE's example history blocks carry the label. Entries already committed keep the spelling they have: the history is prose, salvaged verbatim by every rewrite, and nothing reads a date back. The four committed tables.baseline files are unchanged. Negative control: reverting the stamp in Update turns TestBaselineHistoryStampsUTCAndLabelsIt red on the bare date. No generated output moves and no golden is re-pinned. * check: a constant's value diagnostics point at the value (#447 F-05) Reproduction: $ printf 'package bad\n\nconst C int32 = 1.5\n' > Bad.schema && bin/schema check . Bad.schema:3:1: constant C has integer type int32 but a float expression Column 1 is the declaration. The refusal is about the expression, and a range refusal on the same kind of line points at the offending value: Bad.schema:5:5: field a: its range [10, 20] excludes zero, ... The three constant refusals that say something about the EXPRESSION rather than the name now carry the expression's own position: the float expression under an integer type, the float32 overflow, and the non-finite float. A constant with no expression at all keeps the declaration's position, because there is nothing else to point at. Bad.schema:3:17: constant C has integer type int32 but a float expression Negative control: reverting valuePos turns TestConstValueDiagnosticsPointAtTheExpression red at column 1. No generated output moves and no golden is re-pinned. * docs: USAGE states the two cpp_native facts a first trial meets (#521 G-06, #447 F-21) Reproduction: $ cat A.schema package nat type Vec2 | cpp_native = GameVec2, cpp_include = "vec2.h" { x float32 y float32 } type Body { p Vec2 } $ bin/schema generate --lang cpp --out g . && grep -n 'GameVec2\|vec2.h' g/*.h $ No mapping, no include, no diagnostic. Split the two declarations across two files in the same unit and the feature works as documented. The behavior is deliberate and right: your native header derives from the generated basis struct, so it includes the header the basis is in, and a mapped reference inside that same generated header would be circular. SPEC §4.2 states it: "Inside the basis type's own generated header the mapping is off ... Sibling types declared in the same schema file therefore store the basis type", and USAGE's per-language note did not, so a reader who starts there meets a single-file unit that can never show the feature work. USAGE's C++ note now carries both facts SPEC states and the attribute's own spelling hides: the mapping is off inside the declaring file's generated header, with the worked repro and its two-file fix, and `cpp_native` names a GLOBAL type, so a namespaced engine type is reached through an alias. The tool is unchanged: the attribute is a mapping, not a request, and a file whose own siblings keep the basis type is correct. No generated output moves and no golden is re-pinned. * tables: pin the forged pointer cycle, and pin what it is (#535, #521 G-19) Reproduction, an exhaustive single-bit sweep over a `Scene` whose head is a three-node `ListNode` chain: one mutant at byte 26 turns the first node's `next` index from 3 into 2, which is that node's own index. The wire loads, the report is all zero, and the loaded node points at itself. #535 asks the reference and the Go oracle to refuse or count that. Reading the page through, they should not, and they already do exactly what it specifies, in four separate clauses: SPEC-TABLES §3.1: "A pointer field's payload is a NUMBER: it is bounds-checked and stored, never followed. There is no traversal on the load path, and therefore no traversal bound — no depth cap, no visited set, no ordering rule on the indices." The index names a node that exists and whose type matches, so no §4 event happened and no counter may move. §3.1 again: "Load itself is safe on any input: it scans, it terminates, it fabricates nothing. What a cyclic structure costs is paid by whatever WALKS it, and a consumer walking untrusted table data — a reflection dump, a text export (§16) — carries its own visit bound." Both walking consumers do, measured: the text export and a re-encode each refuse this instance by name with the §3.1 cite, and each returns rather than recursing away. §6.5's `data_cycle` is not the missing check either: its own row says it is "a data cycle reached from a builder, which is the AUTHORING side's `-1` and the one value here that is not about a wire", and §6.5's status line says TableRefuseReason is spelled in no target yet (owed as schema#523). The reader-side check #535 asks for is §14 note 9, "A reader-side acyclicity check — PRICED, NOT TAKEN ... for a property the writer already guarantees (§3.1). It is the shape the check would take if untrusted-input hardening ever demands one." Taking it is that ruling, not a fix wave item, so this lands the gate instead and leaves the ruling on the issue. What lands: - testdata/wire/tables/fuzz-vectors/pointer_self_cycle.bin, the mutant, rooted at Scene because that is the root the C++ leg carries a codec for. Rooted at ListNode it would have gone into the harness's "absent" bucket and never reached the reference. Verified: the run reports 91 seeds and 93653 mutants with it, 90 and 93652 without. - The wire fuzzer therefore seeks it rather than searching, and the leg and the oracle are held to one answer on it: 0 divergences under both the plain and the ASan legs. - internal/tablewire's TestForgedPointerCycleLoadsSilentlyAndRefusesEveryWalk pins all four clauses in Go. Negative controls, all three red: - reverting the text writer's cycle guard: "the text export walked a cyclic graph and produced 134 bytes". - reverting the wire encoder's: "a save reproduced a data cycle as 83 bytes". - adding a counter on the load path: "a counter moved for a pointer index that names a node of the right type". No generated output moves and no golden is re-pinned. * ctable: the C block accessors are one name with two verbs (#521 G-14) Reproduction: $ bin/schema generate --lang c --out gc tables/block $ grep -n "TableBlockRows Render\|_span(" gc/RenderBlock.h static SCHEMA_UNUSED TableBlockRows RenderFrameShips( const RenderFrameBlock * block ) static SCHEMA_UNUSED RenderShip * RenderFrameships_span( const RenderFrameBlock * block ) ... static SCHEMA_UNUSED TableBlockRows RenderFrameDynamicProps( const RenderFrameBlock * block ) static SCHEMA_UNUSED RenderDynamicProp * RenderFramedynamic_props_span( const RenderFrameBlock * block ) Two accessors for one field, one exported name PascalCased and the other gluing the same PascalCase table prefix onto the field's own lowercase spelling. `RenderFramedynamic_props_span` is what that produces for a multi-word field. Neither is the snake_case the rest of this backend spells its name-first surface in, namely `render_frame_block_open`, `enum_name_ship_type` and `ship_config_load`, and the two could not both be right. Both now take the C target's own spelling through `api`, as one name with two verbs: static SCHEMA_UNUSED TableBlockRows render_frame_ships_rows( const RenderFrameBlock * block ) static SCHEMA_UNUSED RenderShip * render_frame_ships_span( const RenderFrameBlock * block ) static SCHEMA_UNUSED TableBlockRows render_frame_dynamic_props_rows( const RenderFrameBlock * block ) static SCHEMA_UNUSED RenderDynamicProp * render_frame_dynamic_props_span( const RenderFrameBlock * block ) The accessors are `static SCHEMA_UNUSED` and nothing in the tree called them, so no C source moves. docs/TUTORIAL.md's part 12 C consumer did print the old pair, two lines under its own `render_frame_block_open`, which is where the asymmetry was most visible to a reader. It prints the new pair. The C++ names are untouched and were never mixed: that backend spells the pair `RenderFrameShips` / `RenderFrameShipsSpan`, PascalCase throughout, which is its own convention. The emitter is internal/codegen/ctable, outside bench/LOCK's prefixes (verified against the cpp-lock job's own matcher). No locked emitter and no locked generated tree is touched. `make test-c` passes: the conformance surfaces, the forgery fuzzers and the wire byte counts are all unchanged. Negative control: restoring either old spelling turns TestCBlockAccessorsAreSnakeCasePairs red, on the missing pair and on the mixed name still being present. No wire golden is re-pinned. No source-copy golden moved either, because the C table tree is generated into build/ and not committed.
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 table wire's id-table form (docs/SPEC-TABLES.md §3, merged as #501), in both implementations, with the corpus, the gates and the fuzzer moved to it.
The form
A saved table is three parts: the form byte, the root body, and the id table a reader finds from the END. A body is
id reference, kind, payloadterminated by the zero reference; every length, count, index and reference is one canonical LEB128; identity isfnv1a64(name)at sixty-four bits for a field, an enum variant, a union arm and a table's own name alike. The table holds every id the body used, once each, in first-use order, and the body names them by position — so a file naming forty ids across ten thousand fields carries forty ids and spends one byte a header.Three kinds ride with it:
30an enum, carrying the reference to its variant name's id;31the escape;32the payload-free arm. An arm header is a field header and carries the arm's kind. The node table rides in one field under the reserved id0xFFFFFFFFFFFFFFFF.What holds it
0xa9df771f6051391fand0xc4adaacfe6cb2809.getlebthat moved the cursor on a number it refused, a damaged count that dropped an optional's presence, the reserved id in a nested body, and a repeated node-table field.make conformanceis green — the reference leg passes every registered surface — and so arego test ./..., the tables, maps, block, cook, pack, hostile and fuzz batteries.Also in
flagsblock, and the measured paragraph re-pinned from this tree (0.98x without the 210 KB blob, 1.80x over the tiny class, 0.99x over the whole corpus).What the ports lose, and what is still open
The corpus is pinned in the new form and the eight ports write the previous one, so their five wire-carrying surfaces are ABSENT —
wire,report,json-read,json-write,json-hostile— each driver saying so and naming its issue (#511–#518). The seven per-port harness negative controls go dormant with them.Open: each port's own allocation gate, soak and forgery fuzzer reads the pinned corpus too, so
make test's port legs still fail. Keeping them dormant loses those gates until the ports move; giving the ports a corpus in the previous form keeps them running. That is a decision, not an oversight.Also open: the tool's cook half for maps; the dispatch nanosecond figures, which need a bench sitting rather than a laptop under load.
🤖 Generated with Claude Code
The tree brought current (25f9f06)
Three checks were red on
9a036cc, and none of them was a defect in the new wire code.generated. The bench table units for nine languages still carried the previous build version. The Go one was committed and the other nine were not. The C++ bench table unit also had to take the whole id-table wire, which is what this branch emits. Regenerating everygenerated/*/.stampfrom this branch's compiler moves 11 files and nothing else.The byte-identity half of the block zero-cost gate, which the
generatedjob runs after the staleness check and so never reached. 22 C++ Table sources undertestdata/golden/tables/move with the emitter this branch changes. 12 C Table sources move on one line each, the build version constant, and the new values are the onestestdata/golden/build-version/already pins on this branch. Nothing undertestdata/wire/moved: the wire pins were already re-cut here. 82 Table sources are byte-identical to their pins again.lint. Eight golangci-lint findings and eight modernize findings. Three dead functions go, seven untagged switches over a kind take their tag, one if-else chain becomes a switch, fourerrors.Asrefusal probes becomeerrors.AsType, and two backward index loops becomeslices.Backward. The emitters' output is byte-unchanged across all of it.big-endianwas not a byte-order verdict. The job died onThe same binary built for the HOST fails on the same case with the same message.
./build/schema_test_c_soak 0on a little-endian laptop:So this is the port-leg case the section above already names, and not an endianness defect: the soak's golden gate reads
testdata/wire/tables/, which is the id-table form now, and the C port writes the previous one.tables-c-big-endiangoes dormant withschema#512behind it, on the rule the eight per-port controls on this branch already follow. The cross-compile rule stays, because that is what the leg wakes with. The C++ half of the big-endian job is untouched and green: the tables battery, maps, the block driver both ways and the cook reader both ways all still run on s390x under qemu.All 20 checks pass on
25f9f06.