Skip to content

tables: was on enum variants, union arms and the fields of a type a table reaches (#442, #478) - #577

Closed
gafferongames wants to merge 7 commits into
was-rows-1from
was-rows-2
Closed

tables: was on enum variants, union arms and the fields of a type a table reaches (#442, #478)#577
gafferongames wants to merge 7 commits into
was-rows-1from
was-rows-2

Conversation

@gafferongames

@gafferongames gafferongames commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

The second was row: was on an enum variant and a union arm (#442) and on a field of a type a table closure reaches (#478), in the C++ reference, the tool, the baseline and the pages. Stacked on #576 (base was-rows-1); it retargets to main when that merges.

Red first

Commit 9b21580 holds the tests alone, and the shipped compiler at 45888e55 refuses the corpus unit they are written against:

$ ./bin/schema check test/tables/R2.schema
test/tables/R2.schema:11:12: expected }, found "|"
test/tables/R2.schema:11:12: expected newline after enum declaration, found "|"
test/tables/R2.schema:12:5: unexpected "Gold" at file scope (declarations begin with package, const, enum, flags, type, table or union)
test/tables/R2.schema:34:18: expected a field type, found "|"
schema: 4 error(s)

Behind the parser, main's checker refused was on an arm as "a named follow-on" and on a type's field as "a table-wire concept ... a type's wire is positional, so a rename there moves no bit", which #478 found to be wrong for a closure-reached type: its fields have table-wire ids, and the bare rename orphaned stored data in silence.

What rides

The grammar. An enum variant takes a qualification section (Argent | was = "Silver"), and a qualified variant ends its line, so the newline is its separator. A payload-free arm takes one too (pong | was = "ping"), and an arm with a payload carries was on its field line. schemafmt lines the pipes up and round-trips both.

The checker. was on a variant or an arm is held to the field rule: a quoted string, not empty, not its own name, and an alias colliding with a live id is refused naming both, with the alias shown where the id comes from. A flags variant refuses it by name (a mask is positional, its identity is its bit). was on a variant, an arm or a type's field that NO table closure reaches is refused naming the declaration, the rule a table field's was already follows, and a field of a type a closure reaches takes it on the same terms as a table field, json = pairing included.

The ids. Enum.VariantWireName, UnionVariant.WireName and ir.TableFieldWireName are the one place each name is read, and every derivation reads them: the enum identity tables (TableEnumRef, TableEnumId, TableEnumValue), the arm switches on load and the arm refs on measure and save, the keyed slots of an enum-keyed array, the wire extent walk, the descriptors, the announced vocabulary and the id capacity, the tool's encode and decode, the baseline, the wire projection and the cook projection. A rename under was moves neither id (TestVocabularyRenamesUnderWasMoveNeitherId, with the discrimination control: the same rename bare, with a type reaching every declaration, moves both).

The baseline matches variants and arms by wire id, as fields are matched, renders was= beside a renamed variant or arm (only when declared, so the rendering version is unmoved), and refuses a second rename aimed at the intermediate spelling naming the first (TestVocabularyWasChainsAreRefused, with the discrimination control carrying the first wire names forward in silence and the attribution control removing the was-chain rule). A bare rename of a variant or an arm warns that the old name was removed, and a bare rename of a type's field warns as the rename pair it is.

The cross-target refusal. compiler/wasrows.go: every other backend refuses a unit whose closure carries a variant, arm or type-field was, naming the rows and --lang cpp (TestWasRowsAreCppOnly).

The corpus. test/tables/R1.schema / R2.schema are the vocabulary rename pair: Silver to Argent, ward to shield, ping to pong, Buff.multiplier to mult, with an enum-keyed [Grade]int32 whose Silver slot rides under the same id. Three pinned instances (r1_cfg, r1_ping, r2_cfg, the last byte-identical to the first) ride every conformance surface, and three report rows pin the silent cross reads:

r1_cfg_as_r2       0,0,0,0,false,read
r1_ping_as_r2      0,0,0,0,false,read
r2_cfg_as_r1       0,0,0,0,false,read

The tool's text of r2_cfg keeps the new names ("grade": "Argent", "effect": {"shield": ...}, "buff": {"mult": 1.5}, "tally": {"Argent": 7}) while its bytes are r1_cfg's, which is the json = rule applied to a vocabulary: was keeps the WIRE id and the text key is the declared name.

Gates and their negative controls

make tables-wasrows-negative-control compiles test/tables/wasrows_control_main.cpp twice, against the shipped R2 and against an R2 with the four attributes stripped and regenerated by the shipped compiler, and reads the same r1_cfg golden through both:

unknown=0 kind_mismatch=0 malformed=0 grade=Argent effect=shield charge=2.5 mult=1.5 tally_argent=7
unknown=5 kind_mismatch=0 malformed=0 grade=None effect=None charge=0 mult=1 tally_argent=0
negative control: stripping was from the variant, the arms and the type's field turns the cross read RED (unknown counted, the value at its default)

Five unknowns, because the golden carries Silver twice (the grade value and grades[0]) beside the keyed slot, the arm and the type's field.

The Go gates, each with its own controls:

  • internal/check/wasrows_test.go: TestWasRowRefusals, fourteen refusals by name (a variant's was naming itself, empty, unquoted, bare, a valued key a variant does not take, a collision with a live variant, a variant outside a closure, a flags variant, an arm's was naming itself with and without a payload, an arm collision, an arm outside a closure, a type field outside a closure, and a type-field collision), and TestWasRowsResolve for what each accepted spelling lands in the IR.
  • internal/baseline/wasrows_test.go: TestVocabularyRenamesUnderWasMoveNothing (silent, rendered, round-tripped, and the bare renames say variant Silver removed, arm ward removed, arm ping removed, multiplier removed and mult added) and TestVocabularyWasChainsAreRefused.
  • ir/wasrows_test.go, internal/format/wasrows_test.go, compiler/wasrows_test.go.
  • test/tables/main.cpp test_wasrows_vocabulary: R2 writes R1's bytes, the cross read in both directions lands every renamed name with a byte-identical re-save, and the payload-free arm renamed reads as Pong.

The zero-cost law (2026-09-05)

This PR adds no counter, reason or descriptor, and no read or write path gains an instruction: every emitter change substitutes the wire name for the declared name in a constant the generated code already carried (0x...ull in a case, a ref, a descriptor column). The committed generated/ tree, the bench unit included, regenerates byte-identically under this compiler, and the bench leg's binary is the same machine code #576 measured (the sitting there: write and round_trip unchanged within noise, binaries cmp clean).

Pages

docs/SPEC.md §4.2 (the attribute vocabulary, the was paragraph, the Variant and UnionVariant productions), §4.8 (an arm's qualification), docs/SPEC-TABLES.md §2.6 (an arm takes was, and json alone waits), §4.1 (five rows: a type's field, a variant or an arm, each under was and bare, and the second rename), §5 (variants and arms carry the same attribute, and a closure-reached type's fields), §15 (the compile-time refusals), §18.1 and §18.2 (the rendering and the chain rule), §20.4 (a rename under was moves nothing), docs/VERSIONING.md promise 3 (every name the table wire carries), the edits table and the group-3 notes, the 3.0.0 list entry gone, docs/USAGE.md (the vocabulary rename, and an arm's was), docs/COMPETITION.md (the two rename rows move to ✅).

#366

The row variant and arm was (#442), type-field was (#478) moves to ✅ in the cpp column. Every other column stays ❌ behind the named refusal.

Silences the page left

  • json = on an arm stays a named follow-on (§15): an arm's key is its name.
  • A flags variant takes no was by ruling, not by omission: the page says why.
  • The eight ports carry the three when the sweep copies the grammar.
  • A bare tag on an enum variant, a flags variant or a payload-free arm parses and is carried nowhere, which SPEC.md's front-end status line now says; the rule for it is schema#523 ruling 4's.

Gate state

make test whole on this branch, green: the C++ table tests plain and under ASan/UBSan, make tables-wire-fuzz N=20000 on both builds with tblr1.Cfg and tblr2.Cfg registered in the driver, so the six R seeds are live (wire-fuzz: seed 24845619678, 127 seeds over 38 roots, 10 seeds absent (roots the leg has no codec for), 111515 enumerated + 20000 random = 131515 mutants, 0 divergences, and each R golden replayed through both units, 0 divergences), every wire-fuzz negative control red, the three projection controls (the variant-order and union-arm-order sabotages re-aimed at the wire-name loops, each still red on its own gate and green on the other's), tables-was-negative-control and tables-wasrows-negative-control as quoted, the conformance leg (wire 66/66, message 11/11, report 56/56, json-read 62/62, json-write 62/62, json-hostile 125/125, cook 6/6, cook-write 92/92, cook-foreign 6/6, block 2/2, block-foreign 2/2, block-dump 2/2, forgery 11/11, cook-forgery 111/111), go vet clean, gofmt clean, and go test ./.... The committed generated/ tree regenerates byte-identically. MacBook Air, sequential, one-minute load under 3 at the start.

🤖 Generated with Claude Code

gafferongames and others added 7 commits September 5, 2026 13:23
R1/R2 are the vocabulary rename pair: an enum variant, a union arm with a
payload, a payload-free arm and a field of a type a table reaches by value,
each renamed in R2 under was. The checker, baseline, projection, formatter
and cross-target tests state what the tree does not do yet: main's parser
has no variant qualification, its checker refuses was on an arm as a named
follow-on and on a type's field as a packet-wire concept, and its baseline
matches variants and arms by name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…able reaches (#442, #478)

A variant, an arm and a field of a type a table closure reaches ride the
table wire under the hash of their name, so a bare rename orphans every
stored value, body and field. Each now takes was, and its wire id is the
old name's hash: the enum identity tables, the arm switches, the keyed
slots, the vocabulary, the tool's encode and decode, the baseline, the
projection and the build version all read the wire name, so a rename under
was moves neither id. A flags variant refuses it, because a mask is
positional. A was outside a table closure is refused naming the
declaration, on the rule a table field's was already follows.

The baseline matches variants and arms by wire id, renders was= beside a
renamed variant or arm, and refuses a second rename aimed at the
intermediate spelling. Every other target refuses a unit that declares one
of the three, naming the follow-on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
)

SPEC §4.2 and §4.8 carry the attribute on a variant, an arm and a field of
a type a table reaches, SPEC-TABLES §2.6, §4.1, §5, §15, §18 and §20 say
what each keeps and what a bare rename costs, the versioning page's promise
3 covers every name the table wire carries, and the competition rows flip.
R1/R2 join the conformance leg with three pinned instances and three silent
report rows, and the C++ test writes the goldens and reads them across.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…#478)

r1_cfg, r1_ping and r2_cfg from the reference, r2_cfg byte-identical to
r1_cfg, the text of each from the tool with the new names, and the three
silent report rows. The union gains a scalar arm so it stays a
table-closure construct, which is where a payload-free arm is C++'s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The enum variant loop binds the wire name to v so the variant-order
sabotage still removes the list, and the union-arm-order sabotage names
v.WireName() where it named v.Name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nts five

A qualified enum variant ends its line, so the stripped R2 gives Argent
the comma a bare variant needs. Without was the reader cannot name five
things in the golden: the value, the array element holding the same
value, the keyed slot, the arm and the type's field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… state of was

The wire-fuzz driver registers tblr1.Cfg and tblr2.Cfg, so the six R seeds
are live (127 seeds over 38 roots, 10 absent, 0 divergences), and the cook
generator derives its node type ids from the wire name. TUTORIAL.md quotes
the diagnostic the checker emits for a type's field outside a closure and
says variants and arms take was, COMPETITION.md s9b no longer lists was
among what an arm may not take, and SPEC.md's front-end status line says
what each line kind does with a tag today: a type's tag is emitted, a
table's is gathered and emitted nowhere yet, and an enum variant, a flags
variant and a payload-free arm accept a bare tag in silence.

Also from the cold read: the unused `_ = inTable` is gone, the format test
uses strings.Contains, and the added prose in SPEC.md, USAGE.md,
SPEC-TABLES.md, wasrows.go, ir.go, R1.schema and R2.schema carries no em
dash.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant