tables: the table-closure union tag carries Count, and the reservation reaches it (#601) - #661
Merged
Conversation
RED FIRST, committed before the emitter moves so the red is a fact in the history rather than a claim in a report. Three gates and one corpus rename go in together, and all three gates fail at this commit: - `TestTableClosureUnionTagEnumExportsCountAndItsDebugName` asserts the table-closure tag enum carries `Count` and the debug-name function, the way `TestUnionTagEnumExportsCountAndItsDebugName` asserts it for a packet union. The probe unit declares an enum of THREE variants beside a union of two, the trap PR #597 found running the packet control: with both at two, the declared enum's own `Count` line satisfies a claim meant for the tag enum. - `TestTableClosureTagEnumDebugNameCoversNoneVariantsAndOutOfSet` anchors its claim to the function's BODY, from the opening line to the out-of-set `"???"`. `None`, `Laser` and `Missile` all appear elsewhere in the same header, so a whole-output claim stays green with the function deleted. - `TestATableClosureUnionArmNamedCountIsRefusedByName` replaces `TestATableClosureUnionArmNamedCountIsAccepted`, which pinned the present split as law. The reservation reaches every union once the member exists on every union. `internal/check`'s diagnostics table gains the packet and table-closure refusals as a pair, so one rule is read from both sides. The fixture arms that spelled themselves `count` for unrelated cases move to `tally` and `size`: those cases test a default on an arm, an optional arm, a json arm and a range excluding zero, and none of them is about the name. The red, at this commit: --- FAIL: TestATableClosureUnionArmNamedCountIsRefusedByName (0.00s) tagenumsurface_test.go:192: a table-closure union's arm named count passed check, and its tag enum defines Count twice --- FAIL: TestTableClosureUnionTagEnumExportsCountAndItsDebugName (0.00s) tagenumsurface_test.go:257: cpp: the table-closure tag enum's surface is short, " Count = 2, // the declared variant count (SPEC §4.2)" not emitted tagenumsurface_test.go:257: cpp: the table-closure tag enum's surface is short, "inline const char * EnumName( WeaponFireType value )" not emitted --- FAIL: TestTableClosureTagEnumDebugNameCoversNoneVariantsAndOutOfSet (0.00s) tagenumsurface_test.go:272: cpp: the table-closure tag enum has no name function, "inline const char * EnumName( WeaponFireType value )" not emitted --- FAIL: TestDiagnostics/union_variant_named_count_on_a_table-closure_union_(exported_spelling) (0.00s) diagnostics_test.go:592: compiled clean — the language broke silently (want "carries its declared variant count as the member Count") Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FOUND WHILE MOVING THE CORPUS FOR #601, and landed ahead of it because the move depends on it. `tables/messages`'s `EditBody` arm `count` has to move under the reservation, and it moves under `was` so its id stays the old name's hash and no wire pin moves. That only holds if every id derivation reads the alias, which docs/SPEC-TABLES.md §5 states outright: "Every id derivation reads the alias: the node record, the cooked node directory, the connection's announced vocabulary (§3.3), the baseline and the build version." THREE ARM-DISPATCH SITES DID NOT. The announcement's entry is `ir.TableArmEntry`, whose own comment says it hashes the WIRE name, and the encoder spends that id. The message form's readers matched on the DECLARED name's hash instead: - `internal/codegen/cpptable/messageload.go`, `<Table>LoadMessageBody` - `internal/codegen/cpptable/messagecodec.go`, the union-arm extent walk - `internal/tablewire/messagedecode.go`, the Go engine's arm dispatch So a `was`-renamed arm was written under the old name's hash and read under the new one: LOST BY ITS OWN BUILD, counted `unknown`, in the C++ reference and in the engine alike. The file form was always right — `decode.go` and every other cpptable site call `WireName()` — and no corpus carried a message-form `was` arm, so nothing was pinned that could catch it. The red, before the fix: --- FAIL: TestAWasRenamedArmSurvivesTheMessageForm (0.00s) wasarmmessage_test.go:116: the was-renamed arm was lost: the union reads back None, report {Unknown:1 KindMismatch:0 Widened:0 Clamped:0 Duplicate:0 Malformed:false Refused:false} The C++ gate is anchored to `NoteLoadMessageBody`'s own body, not to the output: the old name's hash is already in the header twice over, in the file form's dispatch and in the reflection descriptor, so a whole-output claim stays green with the message form still wrong. The negative control, the one line put back: --- FAIL: TestTheMessageFormsArmDispatchReadsTheWasAlias (0.00s) wasarmmessageid_test.go:57: cpp: the message form does not dispatch the renamed arm on its old name's hash, "case 0xb1e5e28e4479a274ull: // tally" absent from NoteLoadMessageBody wasarmmessageid_test.go:60: cpp: the message form dispatches the renamed arm on its DECLARED name's hash, "case 0xaa3f4d29d02d54ddull: // tally" — the announcement writes the alias's No committed golden moves at this commit, because no corpus declares a `was` arm that reaches a message form yet. `test/tables/R2.schema` declares two, and R2 pins no golden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n reaches it (#601) A PACKET union's tag enum carries `None`, the variants, `Count` and `Max` plus a debug-name function in all nine targets (#597, F-15). A TABLE-CLOSURE union's tag shape is a different emitter — `internal/codegen/cpptable`'s `emitTableUnion`, written beside the tables rather than among the packet declarations — and it carried `None`, the variants and `Max` alone. One construct, two surfaces, and `Count` free as an arm name on one side only because the member did not exist there to collide with. THE MEASUREMENT, taken before anything moved. Thirteen tag enums across seven committed C++ table headers carried `Max` with no `Count` beside it: `arms/{Carry,Gate,Nest,Ring}Table.h` (six between them), `lists/SaveTable.h`, `maps/DepthTable.h`, `messages/MessagesTable.h` (three), `stream/StreamTable.h` and `wide/CaptionTable.h`. Against them, `testdata/golden/cpp/`'s packet tag enums each carry `Count = N, // the declared variant count (SPEC §4.2)`. `internal/codegen/cpptable/unions.go:50` is the ONE emitter of that comment outside the nine packet backends, which is what makes this a one-target change where F-15 was a nine-target one: the table layer is the C++ reference's (docs/SPEC-TABLES.md §11, §15). The Go table backend emits no tag enum at all, so there is no oracle view to move. THE EMITTER. `emitTableUnion` now writes `Count` beside `Max` and the `EnumName` overload beneath the enum, to the character the packet emitter writes them, so a table arm and a type arm read as one family. Nothing on the read or write path calls the function and no generated code does. THE RESERVATION. `Count` moves out of `resolveUnion`'s post-loop block and into the switch beside `None`, `Max` and `Type`, which is where it belongs once the member is on every union: the kind no longer has to be known, so the arm errors and stops rather than resolving on. `ir.Union.TableClosureOnly` keeps its three other callers. EVERY ID STAYS PUT. `Count` is a generated member, not a wire fact: `git diff origin/main -- testdata/wire` is empty and `generated/` is untouched (the packet corpus declares no table-closure union). The CORPUS MOVE the reservation forces is a `was` rename, so it moves no id either — `tables/messages`'s `EditBody` arm `count` becomes `tally int32 | min = 0, max = 100, was = "count"`, and every one of its ids in `MessagesTable.h` stays `0xb1e5e28e4479a274`, in the file form's dispatch, the message form's, the reflection descriptor and the arms table. `examples-wide/Caption.schema`'s `Body.count` moves the same way. The baseline records it and says so: `arm tally id=0xb1e5e28e4479a274 kind=4 min=0 max=100 was=count`, "no compatibility-affecting edits". Four fixtures carry no stored data, so they move by plain rename with no alias: `test/tables/R1.schema` and `R2.schema` (`count` -> `amount`, and `tally` was taken there by a keyed array), `test/tables/RT2.schema`, `ir/buildversion_test.go`, `internal/baseline/baseline_test.go`, `compiler/wasrows_test.go` and the harness's `rowdemo` unit. No pinned instance selects any of them. THE GOLDENS, every moved file and why: - `tables/arms/{Carry,Gate,Nest,Ring}Table.h`, `tables/lists/SaveTable.h`, `tables/maps/DepthTable.h`, `tables/stream/StreamTable.h`: the added `Count` line and the added `EnumName` overload, nothing else. - `tables/messages/MessagesTable.h`: the same two additions on three tag enums, plus the arm's rename — `Count` -> `Tally` as a tag member, `count` -> `tally` as storage and as the text key, with every id unmoved. - `tables/wide/CaptionTable.h` and `wide/cpp/CaptionTable.h`: the two additions plus the `Body` arm's rename. - `wide/cpp/WideView.cpp`: the view's arm name and its spelling, twice. The id beside each is unchanged. - `tables/messages/tables.baseline`: the `was` row and a dated history line. - `testdata/conformance/tables/json/message_arm_edits.json` and the two `json-hostile/arm-scalar-*` texts: an arm's name IS its key in the text form (§16.2), so the key follows the rename. The wire pins those texts encode to are byte-identical. THE PAGES, in present tense. SPEC §4.2's tag-enum paragraph, §4.8's reserved-names bullet and its table-closure bullet, and §4.11's `Count` bullet each stated the split and now state the shared surface; the four `#601` links are gone. SPEC-TABLES §2.6 gains a paragraph naming the tag shape's members and its debug-name function outright, since a different emitter writes it, and §11 gains the arm-name refusal beside the other arm refusals. USAGE and TUTORIAL part 5 said "on a packet union" and now say "on every union". `docs/VERSIONING.md` lists no reserved names, so nothing there moved. THE CONTROLS, each turning exactly its own claim red. The `Count` line deleted from `emitTableUnion`: --- FAIL: TestTableClosureUnionTagEnumExportsCountAndItsDebugName (0.00s) tagenumsurface_test.go:257: cpp: the table-closure tag enum's surface is short, " Count = 2, // the declared variant count (SPEC §4.2)" not emitted The name function deleted, against the body-anchored gate: --- FAIL: TestTableClosureTagEnumDebugNameCoversNoneVariantsAndOutOfSet (0.00s) tagenumsurface_test.go:272: cpp: the table-closure tag enum has no name function, "inline const char * EnumName( WeaponFireType value )" not emitted The reservation scoped back to `!un.TableClosureOnly()`: --- FAIL: TestATableClosureUnionArmNamedCountIsRefusedByName (0.00s) tagenumsurface_test.go:192: a table-closure union's arm named count passed check, and its tag enum defines Count twice --- FAIL: TestDiagnostics/union_variant_named_count_on_a_table-closure_union_(exported_spelling) (0.00s) diagnostics_test.go:592: compiled clean — the language broke silently (want "carries its declared variant count as the member Count") Closes #601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`schema fmt` is the one command that writes a schema file (#473) and a fmt-drift gate holds the corpus canonical. The `was` attribute on `examples-wide/Caption.schema`'s renamed arm sits at the column the formatter picks, which `make fmt` supplied and this commit records. No generated byte moves. Co-Authored-By: Claude Opus 5 <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.
A PACKET union's tag enum carries
None, the variants,CountandMaxplus a debug-name function in all nine targets (#597, F-15). A TABLE-CLOSURE
union's tag shape is a different emitter —
internal/codegen/cpptable'semitTableUnion, written beside the tables rather than among the packetdeclarations — and it carried
None, the variants andMaxalone. Oneconstruct, two surfaces, and
Countfree as an arm name on one side only,because the member did not exist there to collide with.
This closes that.
Countand theEnumNameoverload land besideMax, andthe reservation moves from "where the member exists" to every union, because
the member is now on every union.
Merge, not rebase:
origin/mainatbb5f3bd4fast-forwarded in before thefirst commit.
The measurement, before anything moved
Thirteen tag enums across seven committed C++ table headers carried
Maxwith no
Countbeside it:tables/arms/{Carry,Gate,Nest,Ring}Table.htables/messages/MessagesTable.htables/lists/SaveTable.h,tables/maps/DepthTable.h,tables/stream/StreamTable.h,tables/wide/CaptionTable.hAgainst them, every packet tag enum in
testdata/golden/cpp/alreadycarried
Count = N, // the declared variant count (SPEC §4.2).internal/codegen/cpptable/unions.go:50is the ONE emitter of that commentoutside the nine packet backends, which is what makes this a one-target
change where F-15 was a nine-target one: the table layer is the C++
reference's (docs/SPEC-TABLES.md §11, §15). The Go table backend emits no
tag enum at all —
gotablereferences<Union>TypeNonefrom the codecsbut declares nothing — so there is no oracle view of the enum to move.
Red first
1211337acommits the gates alone, so the red is a fact in the historyrather than a claim in a report:
The probe unit declares an enum of THREE variants beside a union of two,
the trap #597 found running the packet control: with both at two, the
declared enum's own
Countline satisfies a claim meant for the tag enum.The debug-name gate is anchored to the function's BODY, because
None,LaserandMissileall appear elsewhere in the same header.TestATableClosureUnionArmNamedCountIsAcceptedis deleted. It pinned thesplit as law and the split is gone.
The change
The emitter.
emitTableUnionwritesCountbesideMaxand theEnumNameoverload beneath the enum, to the character the packet emitterwrites them, so a table arm and a type arm read as one family. Nothing on
the read or write path calls the function and no generated code does.
The reservation.
Countmoves out ofresolveUnion's post-loop blockand into the switch beside
None,MaxandType, which is where itbelongs once the kind no longer has to be known: the arm errors and stops
rather than resolving on.
ir.Union.TableClosureOnlykeeps its three othercallers.
Every id stays put
Countis a generated member, not a wire fact:git diff origin/main -- testdata/wireis empty.git diff origin/main -- generated/is empty — the packet corpusdeclares no table-closure union.
testdata/golden/id.txtandtestdata/golden/build-version/areuntouched.
The CORPUS MOVE the reservation forces moves no id either, because it is a
wasrename.tables/messages'sEditBodyarm becomestally int32 | min = 0, max = 100, was = "count", and every one of its idsin
MessagesTable.hstays0xb1e5e28e4479a274— the file form's dispatch,the message form's, the reflection descriptor and the arms table.
examples-wide/Caption.schema'sBody.countmoves the same way. Thebaseline records it and says so:
Fixtures that carry no stored data move by plain rename with no alias:
test/tables/R1.schemaandR2.schema(toamount, sincetallyistaken there by a keyed array),
test/tables/RT2.schema,ir/buildversion_test.go,internal/baseline/baseline_test.go,compiler/wasrows_test.goand the harness'srowdemounit. No pinnedinstance selects any of them.
The finding this turned up, and why it lands first
688acf77is not the pass. It is what the pass depends on.The
wasrename only keeps the id if every id derivation reads thealias, which docs/SPEC-TABLES.md §5 states outright: "Every id derivation
reads the alias: the node record, the cooked node directory, the
connection's announced vocabulary (§3.3), the baseline and the build
version."
Three arm-dispatch sites did not. The announcement's entry is
ir.TableArmEntry, whose own comment says it hashes the WIRE name, and theencoder spends that id. The message form's readers matched on the DECLARED
name's hash instead:
internal/codegen/cpptable/messageload.go,<Table>LoadMessageBodyinternal/codegen/cpptable/messagecodec.go, the union-arm extent walkinternal/tablewire/messagedecode.go, the Go engine's arm dispatchSo a
was-renamed arm was written under the old name's hash and read underthe new one: lost by its own build, counted
unknown, in the C++reference and in the engine alike. The file form was always right —
decode.goand every other cpptable site callWireName()— and no corpuscarried a message-form
wasarm, so nothing pinned could catch it.The C++ gate slices
NoteLoadMessageBody's own body rather than the output,because the old name's hash is already in the header twice over, in the file
form's dispatch and in the reflection descriptor. Its control, the one line
put back:
No committed golden moves at that commit: no corpus declared a
wasarmreaching a message form yet, and
R2.schema's two pin no golden.The goldens, every moved file and why
tables/arms/{Carry,Gate,Nest,Ring}Table.h,tables/lists/SaveTable.h,tables/maps/DepthTable.h,tables/stream/StreamTable.hCountline and the addedEnumNameoverload, nothing elsetables/messages/MessagesTable.hCount->Tallyas a tag member,count->tallyas storage and as the text key, every id unmovedtables/wide/CaptionTable.h,wide/cpp/CaptionTable.hBodyarm's renamewide/cpp/WideView.cpptables/messages/tables.baselinewasrow and a dated history linetestdata/conformance/tables/json/message_arm_edits.json, the twojson-hostile/arm-scalar-*textsThe controls, each turning exactly its own claim red
(1) the
Countline deleted fromemitTableUnion. The coverage gatebeside it stays green, which is right: it gates the name function.
(2) the name function deleted. Both claims fire, and the second is the
anchored one:
(3) the reservation scoped back off the table-closure shape, the one
line
if un.TableClosureOnly() { break }put back in the switch:(4) the
wasalias in the message form. Quoted above.The pages, for the cold read
Present tense, no history. Read them on their own with
git diff bb5f3bd4..HEAD -- docs/.table-closure union's tag shape carries
Maxalone (tool: the table-closure union tag shape gets its own Count, and the reservation reaches it #601)" becomes aunion's tag enum carrying
Count, with the table-closure shape named as adifferent emitter carrying the same members.
Countis refused on everyunion, the packet shape and the table-closure shape alike, and the
scoping sentence is replaced by "the reservation reaches where the member
exists, and the member exists on every union".
follow-on. It now states the shared surface, names the emitter's location,
and says the shape has ONE emitter where the packet shape has nine.
Countbullet: "on a PACKET union" becomes "on anyunion", and the legal-there sentence becomes the reservation reaching it.
ENUM'S, MEMBER FOR MEMBER, naming every member and the debug-name
function outright, since a different emitter writes it; and the
illustrative
union Value'scountarm moves totally, in the schemablock, the C++ overlay block and the selection paragraph that names it.
Countbeside the other armrefusals.
union".
All four
#601links are gone from SPEC.md.Closes #601