From f978ee883b505351f3410ea90bb6907a56f070a1 Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sat, 5 Sep 2026 12:24:08 -0700 Subject: [PATCH 1/7] was rows: the tests first, red (#442, #478) 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 --- compiler/wasrows_test.go | 78 ++++++++++++++++ internal/baseline/wasrows_test.go | 124 ++++++++++++++++++++++++++ internal/check/diagnostics_test.go | 4 +- internal/check/wasrows_test.go | 128 +++++++++++++++++++++++++++ internal/format/wasrows_test.go | 37 ++++++++ ir/wasrows_test.go | 73 +++++++++++++++ test/tables/R1.schema | 40 +++++++++ test/tables/R2.schema | 44 +++++++++ test/tables/wasrows_control_main.cpp | 30 +++++++ 9 files changed, 556 insertions(+), 2 deletions(-) create mode 100644 compiler/wasrows_test.go create mode 100644 internal/baseline/wasrows_test.go create mode 100644 internal/check/wasrows_test.go create mode 100644 internal/format/wasrows_test.go create mode 100644 ir/wasrows_test.go create mode 100644 test/tables/R1.schema create mode 100644 test/tables/R2.schema create mode 100644 test/tables/wasrows_control_main.cpp diff --git a/compiler/wasrows_test.go b/compiler/wasrows_test.go new file mode 100644 index 000000000..db60c763e --- /dev/null +++ b/compiler/wasrows_test.go @@ -0,0 +1,78 @@ +// A `was` on an enum variant, a union arm or a type's field (docs/SPEC-TABLES.md +// §5) is C++'s today: the reference carries it, and every other target refuses +// the unit by name. +package compiler + +import ( + "fmt" + "strings" + "testing" + + "github.com/mas-bandwidth/schema/v2/ir" +) + +const wasRowsUnit = `package wrows + +enum Grade +{ + Bronze, + Argent | was = "Silver" + Gold +} + +type Buff +{ + mult float32 = 1.0 | was = "multiplier" +} + +type Ward +{ + charge float32 = 0.0 +} + +union Effect +{ + shield Ward | was = "ward" + pong | was = "ping" + count int32 +} + +table Cfg +{ + grade Grade + effect Effect + buff Buff +} +` + +func TestWasRowsAreCppOnly(t *testing.T) { + u := unitFromSource(t, wasRowsUnit) + c := New() + for _, target := range c.Targets() { + out, err := c.Generate(u, target, Options{}) + if target == "cpp" { + if err != nil { + t.Fatalf("cpp carries the was rows and refused: %v", err) + } + var all strings.Builder + for _, b := range out { + all.Write(b) + } + // every id the renamed things ride under is the OLD name's hash + for _, old := range []string{"Silver", "ward", "ping", "multiplier"} { + want := fmt.Sprintf("0x%016xull", ir.TableWireId(old)) + if !strings.Contains(all.String(), want) { + t.Errorf("cpp output lacks the id of %q, %s", old, want) + } + } + continue + } + if err == nil { + t.Errorf("%s emitted a unit with variant, arm and type-field was instead of refusing it", target) + continue + } + if !strings.Contains(err.Error(), "Buff.mult, Effect.pong, Effect.shield and Grade.Argent") || !strings.Contains(err.Error(), "--lang cpp") { + t.Errorf("%s refused without naming the rows and the carrier: %v", target, err) + } + } +} diff --git a/internal/baseline/wasrows_test.go b/internal/baseline/wasrows_test.go new file mode 100644 index 000000000..dd1cf7bf9 --- /dev/null +++ b/internal/baseline/wasrows_test.go @@ -0,0 +1,124 @@ +// The second `was` row in the baseline (docs/SPEC-TABLES.md §18): a variant, an +// arm and a type's field renamed under `was` keep their ids, so the diff is +// silent, and a second rename aimed at the intermediate spelling is refused +// naming the first. +package baseline_test + +import ( + "strings" + "testing" + + "github.com/mas-bandwidth/schema/v2/internal/baseline" +) + +const wasRowsBaseSrc = `package rows + +enum Grade { Bronze, Silver, Gold } + +type Buff +{ + multiplier float32 = 1.0 +} + +type Ward +{ + charge float32 = 0.0 +} + +union Effect +{ + ward Ward + ping +} + +table Cfg +{ + grade Grade + effect Effect + buff Buff + tally [Grade]int32 +} +` + +// renamedUnderWas is the base unit with every vocabulary rename declared. +func renamedUnderWas(t *testing.T) string { + t.Helper() + src := editOf(t, wasRowsBaseSrc, "enum Grade { Bronze, Silver, Gold }", "enum Grade\n{\n Bronze,\n Argent | was = \"Silver\"\n Gold\n}") + src = editOf(t, src, "multiplier float32 = 1.0", "mult float32 = 1.0 | was = \"multiplier\"") + src = editOf(t, src, " ward Ward\n ping\n", " shield Ward | was = \"ward\"\n pong | was = \"ping\"\n") + return src +} + +func TestVocabularyRenamesUnderWasMoveNothing(t *testing.T) { + base := committed(t, wasRowsBaseSrc) + live := baseline.Render(unit(t, renamedUnderWas(t))) + got := baseline.Diff(base, live, baseline.DefaultTokenPolicy) + if refusals, warnings := baseline.Split(got); len(refusals) != 0 || len(warnings) != 1 { + t.Fatalf("renames under was refuse nothing and warn once (the type field's json pairing hint), got:%s", summary(got)) + } + if !find(got, baseline.Warn, "Buff.mult", `renamed under was = "multiplier"`) { + t.Errorf("the type field's rename hints the text-key pairing, got:%s", summary(got)) + } + text := live.Text() + for _, want := range []string{"variant Argent id=", " was=Silver\n", "arm shield id=", "payload=Ward was=ward\n", "arm pong id=", "kind=none was=ping\n", "field mult id=", "was=multiplier\n"} { + if !strings.Contains(text, want) { + t.Errorf("the rendered baseline lacks %q:\n%s", want, text) + } + } + back, err := baseline.Parse("tables.baseline", []byte(text)) + if err != nil { + t.Fatalf("parse: %v", err) + } + if back.Text() != text { + t.Errorf("the variant and arm lines do not round-trip:\n--- got ---\n%s\n--- want ---\n%s", back.Text(), text) + } + + // the DISCRIMINATION control: the same renames WITHOUT was are removals + // and additions the file says out loud + bare := strings.ReplaceAll(wasRowsBaseSrc, "Silver", "Argent") + bare = strings.ReplaceAll(bare, "multiplier", "mult") + bare = editOf(t, bare, " ward Ward\n ping\n", " shield Ward\n pong\n") + got = baseline.Diff(base, baseline.Render(unit(t, bare)), baseline.DefaultTokenPolicy) + for _, want := range []struct{ where, what string }{ + {"enum Grade", "variant Silver removed"}, + {"union Effect", "arm ward removed"}, + {"union Effect", "arm ping removed"}, + {"table Buff", "multiplier removed and mult added"}, + } { + if !find(got, baseline.Warn, want.where, want.what) { + t.Errorf("a bare rename must warn %q at %s, got:%s", want.what, want.where, summary(got)) + } + } +} + +func TestVocabularyWasChainsAreRefused(t *testing.T) { + base := committed(t, renamedUnderWas(t)) + chained := editOf(t, renamedUnderWas(t), `Argent | was = "Silver"`, `Silvered | was = "Argent"`) + chained = editOf(t, chained, `shield Ward | was = "ward"`, `aegis Ward | was = "shield"`) + chained = editOf(t, chained, `pong | was = "ping"`, `pung | was = "pong"`) + chained = editOf(t, chained, `mult float32 = 1.0 | was = "multiplier"`, `factor float32 = 1.0 | was = "mult"`) + got := baseline.Diff(base, baseline.Render(unit(t, chained)), baseline.DefaultTokenPolicy) + for _, want := range []struct{ where, what string }{ + {"enum Grade.Silvered", `was = "Argent" names Argent, which itself rode under was = "Silver"`}, + {"enum Grade.Silvered", `write was = "Silver"`}, + {"union Effect.aegis", `write was = "ward"`}, + {"union Effect.pung", `write was = "ping"`}, + {"Buff.factor", `write was = "multiplier"`}, + } { + if !find(got, baseline.Refuse, want.where, want.what) { + t.Errorf("a second was aimed at the intermediate spelling must be refused at %s with %q, got:%s", want.where, want.what, summary(got)) + } + } + // the DISCRIMINATION control: the first wire names carried forward + right := editOf(t, renamedUnderWas(t), `Argent | was = "Silver"`, `Silvered | was = "Silver"`) + right = editOf(t, right, `shield Ward | was = "ward"`, `aegis Ward | was = "ward"`) + right = editOf(t, right, `pong | was = "ping"`, `pung | was = "ping"`) + right = editOf(t, right, `mult float32 = 1.0 | was = "multiplier"`, `factor float32 = 1.0 | was = "multiplier"`) + if ctrl := baseline.Diff(base, baseline.Render(unit(t, right)), baseline.DefaultTokenPolicy); len(ctrl) != 0 { + t.Errorf("carrying the first wire names forward must be silent, got:%s", summary(ctrl)) + } + // the ATTRIBUTION control + if got := baseline.Diff(base, baseline.Render(unit(t, chained)), without("was-chain")); find(got, baseline.Refuse, "", "FIRST wire name") { + t.Errorf("with the \"was-chain\" rule removed no chain refusal fires, got:%s", summary(got)) + } +} diff --git a/internal/check/diagnostics_test.go b/internal/check/diagnostics_test.go index 21445b899..46062c23b 100644 --- a/internal/check/diagnostics_test.go +++ b/internal/check/diagnostics_test.go @@ -305,8 +305,8 @@ func TestDiagnostics(t *testing.T) { src: "package t\nunion U {\n count int32 = 3\n}\ntable Root { u U }\n"}, {name: "an optional arm", want: "SELECTION IS THE ARM'S PRESENCE", src: "package t\nunion U {\n count ?int32\n}\ntable Root { u U }\n"}, - {name: "was on an arm", want: "was on an arm is a named follow-on", - src: "package t\nunion U {\n count int32 | was = \"tally\"\n}\ntable Root { u U }\n"}, + {name: "was on an arm of a union no table reaches", want: "no table reaches", + src: "package t\ntype A { x int32 }\nunion U {\n a A | was = \"z\"\n}\ntype Root { u U }\n"}, {name: "json on an arm", want: "json on an arm is a named follow-on", src: "package t\nunion U {\n count int32 | json = \"n\"\n}\ntable Root { u U }\n"}, {name: "an enum-keyed array arm", want: "an enum-keyed array is not an arm", diff --git a/internal/check/wasrows_test.go b/internal/check/wasrows_test.go new file mode 100644 index 000000000..24850841f --- /dev/null +++ b/internal/check/wasrows_test.go @@ -0,0 +1,128 @@ +// The second `was` row's front end (docs/SPEC-TABLES.md §5): `was` on an enum +// variant, on a union arm and on a field of a `type` a table reaches. Every +// refusal the checker gives, and the resolution each accepted spelling lands +// in the IR. +package check + +import ( + "strings" + "testing" + + "github.com/mas-bandwidth/schema/v2/internal/parser" + "github.com/mas-bandwidth/schema/v2/ir" +) + +func TestWasRowRefusals(t *testing.T) { + cases := []struct { + name string + want string + src string + }{ + {name: "a variant's was naming its own name", want: "names the variant's own current name", + src: "package t\nenum E\n{\n A | was = \"A\"\n}\ntable Tab { e E }\n"}, + {name: "a variant's was with an empty string", want: "names nothing", + src: "package t\nenum E\n{\n A | was = \"\"\n}\ntable Tab { e E }\n"}, + {name: "a variant's was takes a quoted string", want: "was takes the variant's old name as a quoted string", + src: "package t\nenum E\n{\n A | was = B\n}\ntable Tab { e E }\n"}, + {name: "a variant's was written bare", want: "attribute was requires a value", + src: "package t\nenum E\n{\n A | was\n}\ntable Tab { e E }\n"}, + {name: "a variant takes no other valued key", want: "is not an attribute a variant takes", + src: "package t\nenum E\n{\n A | max = 3\n}\ntable Tab { e E }\n"}, + {name: "a variant's was colliding with a live variant", want: "collide on table-wire id", + src: "package t\nenum E\n{\n A,\n B | was = \"A\"\n}\ntable Tab { e E }\n"}, + {name: "a variant's was outside a table closure", want: "no table reaches E", + src: "package t\nenum E\n{\n A | was = \"Z\"\n}\ntype P { e E }\n"}, + {name: "a flags variant takes no was", want: "takes no was", + src: "package t\nflags F\n{\n A | was = \"Z\"\n}\ntable Tab { f F }\n"}, + {name: "an arm's was naming its own name", want: "names the field's own current name", + src: "package t\ntype A { x int32 }\nunion U\n{\n a A | was = \"a\"\n}\ntable Tab { u U }\n"}, + {name: "a payload-free arm's was naming its own name", want: "names the variant's own current name", + src: "package t\nunion U\n{\n a | was = \"a\"\n}\ntable Tab { u U }\n"}, + {name: "an arm's was colliding with a live arm", want: "collide on table-wire id", + src: "package t\ntype A { x int32 }\nunion U\n{\n a A\n b A | was = \"a\"\n}\ntable Tab { u U }\n"}, + {name: "an arm's was outside a table closure", want: "no table reaches U", + src: "package t\ntype A { x int32 }\nunion U\n{\n a A | was = \"z\"\n}\ntype P { u U }\n"}, + {name: "a type field's was outside a table closure", want: "no table reaches P", + src: "package t\ntype P { speed float32 | was = \"velocity\" }\n"}, + {name: "a type field's was colliding inside the type", want: "collide on table-wire id", + src: "package t\ntype P\n{\n a int32\n b int32 | was = \"a\"\n}\ntable Tab { p P }\n"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + f, perrs := parser.Parse("T.schema", []byte(tc.src)) + if len(perrs) > 0 { + t.Fatalf("parse: %v", perrs[0]) + } + _, errs := Unit([]SourceFile{{Path: "T.schema", Name: "T.schema", Base: "T", Bytes: []byte(tc.src), AST: f}}) + if len(errs) == 0 { + t.Fatalf("compiled clean — want %q", tc.want) + } + for _, e := range errs { + if strings.Contains(e.Error(), tc.want) { + return + } + } + t.Fatalf("no diagnostic contains %q; got: %v", tc.want, errs) + }) + } +} + +const wasRowsRenamedSrc = `package t + +enum Grade +{ + Bronze, + Argent | was = "Silver" + Gold +} + +type Buff +{ + mult float32 = 1.0 | was = "multiplier" +} + +type Ward +{ + charge float32 = 0.0 +} + +union Effect +{ + shield Ward | was = "ward" + pong | was = "ping" +} + +table Cfg +{ + grade Grade + effect Effect + buff Buff +} +` + +func TestWasRowsResolve(t *testing.T) { + u := buildUnit(t, wasRowsRenamedSrc) + g := u.Enums["Grade"] + if g.VariantWireName(0) != "Bronze" || g.VariantWireName(1) != "Silver" || g.VariantWireName(2) != "Gold" { + t.Errorf("variant wire names: %v / %v", g.Variants, g.Was) + } + if g.VariantWireNameOf("Argent") != "Silver" || g.VariantWireNameOf("Gold") != "Gold" { + t.Error("VariantWireNameOf") + } + un := u.TableUnions["Effect"] + if un == nil { + un = u.Unions["Effect"] + } + if un.Variants[0].WireName() != "ward" || un.Variants[0].WasName != "ward" || un.Variants[0].F.WasName != "ward" { + t.Errorf("arm wire name: %+v", un.Variants[0]) + } + if !un.Variants[1].Void() || un.Variants[1].WireName() != "ping" { + t.Errorf("payload-free arm wire name: %+v", un.Variants[1]) + } + if f := u.Structs["Buff"].Fields[0]; ir.TableFieldWireName(f) != "multiplier" || ir.TableFieldWireId(f) != ir.TableWireId("multiplier") { + t.Errorf("type field wire name: %+v", f) + } + if got := strings.Join(ir.WasRows(u), " "); got != "Buff.mult Effect.pong Effect.shield Grade.Argent" { + t.Errorf("WasRows: %q", got) + } +} diff --git a/internal/format/wasrows_test.go b/internal/format/wasrows_test.go new file mode 100644 index 000000000..63c73c219 --- /dev/null +++ b/internal/format/wasrows_test.go @@ -0,0 +1,37 @@ +package format + +import "testing" + +// The second `was` row through schemafmt: a qualified variant ends its line, +// a payload-free arm's section follows its name, and both come back as they +// went (SPEC §7.4). +func TestFormatsVariantAndArmWas(t *testing.T) { + src := "package probe\n\nenum Grade\n{\n Bronze,\n Argent|was=\"Silver\"\n Gold\n}\n\ntype Ward\n{\n charge float32\n}\n\nunion Effect\n{\n shield Ward|was=\"ward\"\n pong|was=\"ping\"\n}\n\ntable Cfg\n{\n grade Grade\n effect Effect\n}\n" + out, err := Format("Probe.schema", []byte(src)) + if err != nil { + t.Fatalf("format: %v", err) + } + for _, want := range []string{" Argent | was = \"Silver\"\n Gold\n", "shield Ward | was = \"ward\"\n", "pong | was = \"ping\"\n"} { + if !contains(string(out), want) { + t.Errorf("formatted output lacks %q:\n%s", want, out) + } + } + again, err := Format("Probe.schema", out) + if err != nil { + t.Fatalf("format twice: %v", err) + } + if string(again) != string(out) { + t.Errorf("not idempotent:\n%s", again) + } +} + +func contains(s, sub string) bool { return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0) } + +func indexOf(s, sub string) int { + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return i + } + } + return -1 +} diff --git a/ir/wasrows_test.go b/ir/wasrows_test.go new file mode 100644 index 000000000..01b86e676 --- /dev/null +++ b/ir/wasrows_test.go @@ -0,0 +1,73 @@ +// The second `was` row against the two ids (docs/VERSIONING.md promise 4): a +// variant, an arm or a type's field renamed under `was` moves neither id, and +// the same rename bare moves both where a type reaches the declaration. +package ir_test + +import ( + "strings" + "testing" + + "github.com/mas-bandwidth/schema/v2/ir" +) + +const wasRowsBaseSource = `package demo + +enum Grade { Bronze, Silver, Gold } + +type Buff +{ + multiplier float32 = 1.0 +} + +type Ward +{ + charge float32 = 0.0 +} + +union Effect +{ + ward Ward + ping +} + +type Packet +{ + grade Grade + effect Effect + buff Buff +} + +table Cfg +{ + grade Grade + effect Effect + buff Buff +} +` + +func TestVocabularyRenamesUnderWasMoveNeitherId(t *testing.T) { + base := unitFrom(t, wasRowsBaseSource) + for _, tc := range []struct{ name, old, was, bare string }{ + {"an enum variant", "enum Grade { Bronze, Silver, Gold }", "enum Grade\n{\n Bronze,\n Argent | was = \"Silver\"\n Gold\n}", "enum Grade { Bronze, Argent, Gold }"}, + {"a union arm", " ward Ward\n", " shield Ward | was = \"ward\"\n", " shield Ward\n"}, + {"a payload-free arm", " ping\n", " pong | was = \"ping\"\n", " pong\n"}, + {"a type's field", "multiplier float32 = 1.0", "mult float32 = 1.0 | was = \"multiplier\"", "mult float32 = 1.0"}, + } { + under := unitFrom(t, strings.Replace(wasRowsBaseSource, tc.old, tc.was, 1)) + if under.ProtocolId != base.ProtocolId { + t.Errorf("%s renamed under was moved the protocol id", tc.name) + } + if ir.BuildVersion(under) != ir.BuildVersion(base) { + t.Errorf("%s renamed under was moved the build version:\n%s", tc.name, ir.CookProjection(under)) + } + // the DISCRIMINATION control: the bare rename moves both, because + // Packet reaches every declaration and names ride the projection + bare := unitFrom(t, strings.Replace(wasRowsBaseSource, tc.old, tc.bare, 1)) + if bare.ProtocolId == base.ProtocolId { + t.Errorf("%s renamed bare did not move the protocol id", tc.name) + } + if ir.BuildVersion(bare) == ir.BuildVersion(base) { + t.Errorf("%s renamed bare did not move the build version", tc.name) + } + } +} diff --git a/test/tables/R1.schema b/test/tables/R1.schema new file mode 100644 index 000000000..248379b95 --- /dev/null +++ b/test/tables/R1.schema @@ -0,0 +1,40 @@ +// R1.schema — the OLD side of the vocabulary rename pair (docs/SPEC-TABLES.md +// §5): an enum variant, two union arms and a field of a `type` a table reaches +// by value, each renamed in R2 under `was`. Every one of them rides the table +// wire under the hash of its name, so a bare rename would leave every stored +// value, body and field one the reader cannot name. Distinct packages so both +// generations compile into one test binary. +package tblr1 + +enum Grade { Bronze, Silver, Gold } + +type Buff +{ + multiplier float32 = 1.0 +} + +type Boost +{ + power int32 = 0 +} + +type Ward +{ + charge float32 = 0.0 +} + +union Effect +{ + boost Boost + ward Ward + ping +} + +table Cfg +{ + grade Grade + effect Effect + buff Buff + grades [..4]Grade + tally [Grade]int32 +} diff --git a/test/tables/R2.schema b/test/tables/R2.schema new file mode 100644 index 000000000..b74a59b43 --- /dev/null +++ b/test/tables/R2.schema @@ -0,0 +1,44 @@ +// R2.schema — the NEW side of the vocabulary rename pair: Silver is Argent, +// the ward arm is shield, the ping arm is pong, and Buff.multiplier is mult, +// each declared with `was`, so every id is the old name's hash and an R1 +// config reads in silence (docs/SPEC-TABLES.md §5). The negative control +// strips the four attributes and watches `unknown` count. +package tblr2 + +enum Grade +{ + Bronze, + Argent | was = "Silver" + Gold +} + +type Buff +{ + mult float32 = 1.0 | was = "multiplier" +} + +type Boost +{ + power int32 = 0 +} + +type Ward +{ + charge float32 = 0.0 +} + +union Effect +{ + boost Boost + shield Ward | was = "ward" + pong | was = "ping" +} + +table Cfg +{ + grade Grade + effect Effect + buff Buff + grades [..4]Grade + tally [Grade]int32 +} diff --git a/test/tables/wasrows_control_main.cpp b/test/tables/wasrows_control_main.cpp new file mode 100644 index 000000000..887d177bd --- /dev/null +++ b/test/tables/wasrows_control_main.cpp @@ -0,0 +1,30 @@ +// THE VOCABULARY `was` CONTROL (docs/SPEC-TABLES.md §5). R1's config, read +// under R2. Built against the shipped R2 every renamed name lands: the enum +// value, the union arm, the keyed slot and the nested type's field. Built +// against an R2 whose four `was` attributes were stripped, each is a name +// this reader cannot find: `unknown` counts four, the value and the union +// read None, the slot is dropped, and the field holds its declared default. +// The Makefile compiles this file twice and requires the two answers to +// differ exactly that way. +#include "R2Table.h" +#include +#include + +int main() +{ + FILE * f = fopen( "testdata/wire/tables/r1_cfg.bin", "rb" ); + if ( f == NULL ) { printf( "missing golden\n" ); return 2; } + static uint8_t wire[4096]; + const int64_t n = (int64_t) fread( wire, 1, sizeof( wire ), f ); + fclose( f ); + tblr2::Cfg cfg; + tblr2::TableReport report; + if ( !tblr2::CfgLoad( cfg, wire, n, &report ) ) { printf( "load refused\n" ); return 2; } + const char * grade = cfg.grade == tblr2::Grade::Argent ? "Argent" : cfg.grade == tblr2::Grade::None ? "None" : "other"; + const char * effect = cfg.effect.type == tblr2::EffectType::Shield ? "shield" : cfg.effect.type == tblr2::EffectType::None ? "None" : "other"; + const float charge = cfg.effect.type == tblr2::EffectType::Shield ? cfg.effect.shield.charge : 0.0f; + printf( "unknown=%d kind_mismatch=%d malformed=%d grade=%s effect=%s charge=%g mult=%g tally_argent=%d\n", + (int) report.unknown, (int) report.kind_mismatch, (int) report.malformed, + grade, effect, charge, cfg.buff.mult, cfg.tally[tblr2::Grade::Argent] ); + return 0; +} From 1af1d90042137cec13e8a4b62edcf3e1e05fde31 Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sat, 5 Sep 2026 12:24:08 -0700 Subject: [PATCH 2/7] tables: was on enum variants, union arms and the fields of a type a table 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 --- Makefile | 37 ++++++- compiler/builtin.go | 3 + compiler/wasrows.go | 30 ++++++ internal/ast/ast.go | 7 ++ internal/baseline/baseline.go | 43 ++++++++- internal/baseline/diff.go | 79 +++++++++++++-- internal/check/check.go | 144 ++++++++++++++++++++++++---- internal/codegen/cpptable/arms.go | 2 +- internal/codegen/cpptable/codecs.go | 22 ++--- internal/codegen/cpptable/extent.go | 2 +- internal/format/format.go | 11 ++- internal/parser/parser.go | 26 ++++- internal/tablewire/decode.go | 6 +- internal/tablewire/encode.go | 4 +- ir/buildversion.go | 14 +-- ir/ir.go | 102 ++++++++++++++++++-- ir/projection.go | 14 +-- ir/tablewire.go | 16 ++-- 18 files changed, 476 insertions(+), 86 deletions(-) create mode 100644 compiler/wasrows.go diff --git a/Makefile b/Makefile index 7d8a214a0..e7c1735cc 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,8 @@ define tables_generate $(1) generate --lang cpp --out $(2)/a2 test/tables/A2.schema $(1) generate --lang cpp --out $(2)/w1 test/tables/W1.schema $(1) generate --lang cpp --out $(2)/w2 test/tables/W2.schema + $(1) generate --lang cpp --out $(2)/r1 test/tables/R1.schema + $(1) generate --lang cpp --out $(2)/r2 test/tables/R2.schema $(1) generate --lang cpp --out $(2)/scalars tables/scalars $(1) generate --lang cpp --out $(2)/maps tables/maps $(1) generate --lang cpp --out $(2)/lists tables/lists @@ -146,9 +148,9 @@ endef tables_includes = -I$(1)/examples -I$(1)/pointers -I$(1)/block -I$(1)/blockhome -Itest/tables \ -I$(1)/v1 -I$(1)/v2 -I$(1)/p1 -I$(1)/p2 -I$(1)/p3 -I$(1)/jsonkeys \ - -I$(1)/messages -I$(1)/stream -I$(1)/blobs -I$(1)/m1 -I$(1)/m2 -I$(1)/a1 -I$(1)/a2 -I$(1)/g1 -I$(1)/k1 -I$(1)/k2 -I$(1)/w1 -I$(1)/w2 -I$(1)/scalars -I$(1)/scalars2 -I$(1)/maps -I$(1)/lists -I$(1)/backend -I$(1)/vocab -I$(SERIALIZE) + -I$(1)/messages -I$(1)/stream -I$(1)/blobs -I$(1)/m1 -I$(1)/m2 -I$(1)/a1 -I$(1)/a2 -I$(1)/g1 -I$(1)/k1 -I$(1)/k2 -I$(1)/w1 -I$(1)/w2 -I$(1)/r1 -I$(1)/r2 -I$(1)/scalars -I$(1)/scalars2 -I$(1)/maps -I$(1)/lists -I$(1)/backend -I$(1)/vocab -I$(SERIALIZE) -build/tables-generated/.stamp: bin/schema $(SCHEMAS_TABLES) $(SCHEMAS_TABLES_POINTERS) $(SCHEMAS_TABLES_BLOCK) $(SCHEMAS_TABLES_MESSAGES) $(SCHEMAS_TABLES_BLOBS) $(SCHEMAS_TABLES_SCALARS) $(SCHEMAS_TABLES_MAPS) $(SCHEMAS_TABLES_LISTS) $(SCHEMAS_TABLES_BACKEND) $(SCHEMAS_TABLES_VOCAB) test/tables/V1.schema test/tables/V2.schema test/tables/P1.schema test/tables/P2.schema test/tables/P3.schema test/tables/JsonKeys.schema test/tables/M1.schema test/tables/M2.schema test/tables/A1.schema test/tables/A2.schema test/tables/G1.schema test/tables/K1.schema test/tables/K2.schema test/tables/W1.schema test/tables/W2.schema test/tables/Scalars2.schema +build/tables-generated/.stamp: bin/schema $(SCHEMAS_TABLES) $(SCHEMAS_TABLES_POINTERS) $(SCHEMAS_TABLES_BLOCK) $(SCHEMAS_TABLES_MESSAGES) $(SCHEMAS_TABLES_BLOBS) $(SCHEMAS_TABLES_SCALARS) $(SCHEMAS_TABLES_MAPS) $(SCHEMAS_TABLES_LISTS) $(SCHEMAS_TABLES_BACKEND) $(SCHEMAS_TABLES_VOCAB) test/tables/V1.schema test/tables/V2.schema test/tables/P1.schema test/tables/P2.schema test/tables/P3.schema test/tables/JsonKeys.schema test/tables/M1.schema test/tables/M2.schema test/tables/A1.schema test/tables/A2.schema test/tables/G1.schema test/tables/K1.schema test/tables/K2.schema test/tables/W1.schema test/tables/W2.schema test/tables/R1.schema test/tables/R2.schema test/tables/Scalars2.schema @mkdir -p build/tables-generated $(call tables_generate,./bin/schema,build/tables-generated) @touch $@ @@ -2456,6 +2458,7 @@ test: build/schema_test build/schema_test_guard build/schema_test_tables build/s $(MAKE) tables-flat-wire $(MAKE) tables-flat-wire-negative-control $(MAKE) tables-was-negative-control + $(MAKE) tables-wasrows-negative-control $(MAKE) tables-shared-node-negative-control $(MAKE) tables-keyed-iteration-negative-control $(MAKE) tables-hooks @@ -3116,6 +3119,7 @@ CONFORMANCE_INCLUDES := -Ibuild/tables-generated/examples -Ibuild/tables-generat -Ibuild/tables-generated/m1 -Ibuild/tables-generated/m2 -Ibuild/tables-generated/a1 -Ibuild/tables-generated/a2 -Ibuild/tables-generated/g1 -Ibuild/tables-generated/k1 -Ibuild/tables-generated/k2 -Ibuild/tables-generated/w1 -Ibuild/tables-generated/w2 -Ibuild/tables-generated/blobs -Itest/tables -Ibuild/tables-generated/scalars -Ibuild/tables-generated/scalars2 -Ibuild/tables-generated/backend -Ibuild/tables-generated/vocab -I$(SERIALIZE) CONFORMANCE_SOURCES = build/tables-generated/examples/TablesTable.cpp \ build/tables-generated/w1/W1Table.cpp build/tables-generated/w2/W2Table.cpp \ + build/tables-generated/r1/R1Table.cpp build/tables-generated/r2/R2Table.cpp \ build/tables-generated/scalars/ScalarsTable.cpp build/tables-generated/scalars2/Scalars2Table.cpp \ build/tables-generated/examples/WideTable.cpp build/tables-generated/examples/NestedTable.cpp \ build/tables-generated/examples/KeyedTable.cpp build/tables-generated/examples/PackTable.cpp build/tables-generated/v1/V1Table.cpp \ @@ -3670,3 +3674,32 @@ tables-was-negative-control: build/tables-generated/.stamp test/tables/was_contr @grep -q '^unknown=2 kind_mismatch=0 malformed=0 flagship=null escorts=2 home_name=untitled$$' build/tables-was-nc/without-was.log || \ { echo "NEGATIVE CONTROL FAILED: without was, the W1 fleet did not read as unknown records under W2"; exit 1; } @echo "negative control: stripping was from the renamed table turns the cross read RED (unknown counted, the pointers null, the value at its default)" + +# THE VOCABULARY `was` CONTROL (docs/SPEC-TABLES.md §5). R1's config, whose +# enum value, union arm, keyed slot and nested type field ride under the +# hashes of Silver, ward, Silver and multiplier, read under R2, where each is +# renamed under `was`. The control strips the four attributes from R2 in a +# build copy, regenerates that unit with the SHIPPED compiler, and reads the +# same golden through the same program: the value reads None, the union +# reads None, the slot is dropped, the field holds its default, and `unknown` +# counts each. The positive half runs first, against the shipped R2. +.PHONY: tables-wasrows-negative-control +tables-wasrows-negative-control: build/tables-generated/.stamp test/tables/wasrows_control_main.cpp + @mkdir -p build/tables-wasrows-nc + $(CXX) $(TABLES_CXXFLAGS) -Ibuild/tables-generated/r2 -I$(SERIALIZE) test/tables/wasrows_control_main.cpp \ + build/tables-generated/r2/R2Table.cpp -o build/tables-wasrows-nc/with-was + @./build/tables-wasrows-nc/with-was > build/tables-wasrows-nc/with-was.log + @cat build/tables-wasrows-nc/with-was.log + @grep -q '^unknown=0 kind_mismatch=0 malformed=0 grade=Argent effect=shield charge=2.5 mult=1.5 tally_argent=7$$' build/tables-wasrows-nc/with-was.log || \ + { echo "CONTROL FAILED: with was, the R1 config did not read in silence under R2"; exit 1; } + @sed -e 's/ | was = "[a-z]*"$$//; s/ | was = "[A-Za-z]*"$$//' test/tables/R2.schema > build/tables-wasrows-nc/R2.schema + @test $$(grep -c 'was' build/tables-wasrows-nc/R2.schema) -eq $$(grep -c 'was' test/tables/R2.schema | awk '{print $$1 - 4}') || \ + { echo "NEGATIVE CONTROL: the was sabotage did not strip exactly four attributes"; exit 1; } + @rm -rf build/tables-wasrows-nc/r2 && ./bin/schema generate --lang cpp --out build/tables-wasrows-nc/r2 build/tables-wasrows-nc/R2.schema + $(CXX) $(TABLES_CXXFLAGS) -Ibuild/tables-wasrows-nc/r2 -I$(SERIALIZE) test/tables/wasrows_control_main.cpp \ + build/tables-wasrows-nc/r2/R2Table.cpp -o build/tables-wasrows-nc/without-was + @./build/tables-wasrows-nc/without-was > build/tables-wasrows-nc/without-was.log + @cat build/tables-wasrows-nc/without-was.log + @grep -q '^unknown=4 kind_mismatch=0 malformed=0 grade=None effect=None charge=0 mult=1 tally_argent=0$$' build/tables-wasrows-nc/without-was.log || \ + { echo "NEGATIVE CONTROL FAILED: without was, the R1 config did not read as unknown names under R2"; exit 1; } + @echo "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)" diff --git a/compiler/builtin.go b/compiler/builtin.go index 0ca102a91..a8069e40b 100644 --- a/compiler/builtin.go +++ b/compiler/builtin.go @@ -156,6 +156,9 @@ func refuseUnported(u *ir.Unit, target string) error { if err := refuseValueDefaults(u, target); err != nil { return err } + if err := refuseWasRows(u, target); err != nil { + return err + } if err := refuseTableArms(u, target); err != nil { return err } diff --git a/compiler/wasrows.go b/compiler/wasrows.go new file mode 100644 index 000000000..a4036bd83 --- /dev/null +++ b/compiler/wasrows.go @@ -0,0 +1,30 @@ +// The `was` ROWS' cross-target refusal (docs/SPEC-TABLES.md §5): a `was` on an +// enum variant, on a union arm, or on a field of a `type` a table reaches is +// carried by the C++ reference and the tool, and every other target names the +// follow-on rather than hashing the declared name where the wire carries the +// alias. [refuseUnported] reaches it for every port. A `was` on a TABLE's own +// field is every port's already, and a `was` on a table declaration names a +// node type id the ports' fixed class never writes. +package compiler + +import ( + "fmt" + + "github.com/mas-bandwidth/schema/v2/ir" +) + +// wasRowTargets is the canonical name of every built-in target whose backends +// carry the three; refuseWasRows names them. +var wasRowTargets = []string{"cpp"} + +// refuseWasRows is the named refusal every target without the form gives a +// unit whose table closure carries a variant, arm or type-field `was`. +func refuseWasRows(u *ir.Unit, target string) error { + names := ir.WasRows(u) + if len(names) == 0 { + return nil + } + carry, flags := carriers(wasRowTargets) + return fmt.Errorf("unit declares was on an enum variant, a union arm or a type's field (%s) — the three are %s only today, and the %s form is a named follow-on; generate with %s (docs/SPEC-TABLES.md §5)", + englishList(names), englishList(carry), target, englishList(flags)) +} diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 1548b6023..afb1971d9 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -83,6 +83,9 @@ type UnionVariant struct { Type string // the payload type name, when the arm names a bare declaration TypePos Pos Arm *Field + // Attrs is a PAYLOAD-FREE arm's qualification section: a bare name + // followed by `|`. An arm with a payload carries its section on Arm. + Attrs []Attr } func (d *ConstDecl) DeclName() string { return d.Name } @@ -102,6 +105,10 @@ func (d *UnionDecl) DeclPos() Pos { return d.Pos } type Name struct { Text string Pos Pos + // Attrs is the variant's qualification section (SPEC §4.2): tags, and on + // an enum variant the `was` rename (docs/SPEC-TABLES.md §5). The section + // runs to the end of the line, so a qualified variant ends its line. + Attrs []Attr } // Block is a { ... } body. diff --git a/internal/baseline/baseline.go b/internal/baseline/baseline.go index 0ecc6e408..3cae1d103 100644 --- a/internal/baseline/baseline.go +++ b/internal/baseline/baseline.go @@ -150,6 +150,10 @@ type Variant struct { Name string Id uint64 Payload string // union arms only + // Was is the variant's `was` alias, rendered as `was=` on its line and + // judged on nothing: the id is the identity, and this is what lets the + // check name the spelling a second rename should have used (§18.2). + Was string } // A Flags is one flags declaration in the closure. The ORDER IS THE FACT: @@ -252,8 +256,8 @@ func Render(u *ir.Unit) *Unit { func renderEnum(e *ir.Enum) Enum { out := Enum{Name: e.Name} - for _, v := range e.Variants { - out.Variants = append(out.Variants, Variant{Name: v, Id: ir.TableWireId(v)}) + for i, v := range e.Variants { + out.Variants = append(out.Variants, Variant{Name: v, Id: ir.TableWireId(e.VariantWireName(i)), Was: wasOf(e.Was, i)}) } return out } @@ -272,7 +276,8 @@ func renderUnion(un *ir.Union) Union { // else still reads; an arm with NO PAYLOAD carries `kind=none`; every // other arm carries the FIELD tokens for what it is, judged by the one // policy table a field is judged by. - arm := Field{Name: v.Name, Id: ir.TableWireId(v.Name)} + // the id is the WIRE name's hash, the `was` alias after a rename (§5) + arm := Field{Name: v.Name, Id: ir.TableWireId(v.WireName())} switch { case v.Body(): arm.Tokens = []Token{{Key: "payload", Value: v.Ref.WireName()}} // the wire name, as a field's type= is @@ -281,6 +286,13 @@ func renderUnion(un *ir.Union) Union { default: arm.Tokens = renderField(v.F).Tokens } + if v.WasName != "" && !arm.hasToken("was") { + arm.Tokens = append(arm.Tokens, Token{Key: "was", Value: v.WasName}) + } + // AN ARM'S TEXT KEY IS ITS OWN NAME and takes no `json =` (§2.6, §16.2), + // so there is no pairing for the rename hint to offer: the key is + // recorded as already answered, and a renamed arm draws no hint + arm.JsonKey = v.Name out.Arms = append(out.Arms, arm) } return out @@ -471,7 +483,11 @@ func (u *Unit) Text() string { for _, e := range u.Enums { fmt.Fprintf(&b, "\nenum %s\n", e.Name) for _, v := range e.Variants { - fmt.Fprintf(&b, " variant %s id=0x%016x\n", v.Name, v.Id) + if v.Was != "" { + fmt.Fprintf(&b, " variant %s id=0x%016x was=%s\n", v.Name, v.Id, v.Was) + } else { + fmt.Fprintf(&b, " variant %s id=0x%016x\n", v.Name, v.Id) + } } } for _, f := range u.Flags { @@ -620,6 +636,11 @@ func (u *Unit) parseMemberLine(path string, lineno int, section string, fields [ if fields[0] != "variant" || len(u.Enums) == 0 { return bad() } + for _, tok := range fields[2:] { + if k, val, ok := strings.Cut(tok, "="); ok && k == "was" { + v.Was = val + } + } e := &u.Enums[len(u.Enums)-1] e.Variants = append(e.Variants, v) return nil @@ -708,3 +729,17 @@ func collectUnion(out *Unit, un *ir.Union, seenEnum, seenFlags, seenUnion map[st } } } + +// wasOf is the i-th alias of a parallel `was` list, "" past its end. +func wasOf(was []string, i int) string { + if i < len(was) { + return was[i] + } + return "" +} + +// hasToken reports whether the field's line carries the key. +func (f Field) hasToken(key string) bool { + _, has := f.Get(key) + return has +} diff --git a/internal/baseline/diff.go b/internal/baseline/diff.go index ae7adb153..0fa08cd7c 100644 --- a/internal/baseline/diff.go +++ b/internal/baseline/diff.go @@ -1063,16 +1063,47 @@ func (d *differ) diffEnums() []Finding { if !ok { continue } - have := map[string]bool{} + // VARIANTS MATCH BY WIRE ID, as fields do: `was` keeps the id through + // a rename, so a renamed variant is the same variant here + have := map[uint64]bool{} for _, v := range le.Variants { - have[v.Name] = true + have[v.Id] = true } for _, v := range be.Variants { - if !have[v.Name] { + if !have[v.Id] { out = append(out, Finding{Warn, "enum " + le.Name, fmt.Sprintf("variant %s removed — stored values naming it read as None and count unknown", v.Name)}) } } + out = append(out, d.variantWasChain(be, le)...) + } + return out +} + +// variantWasChain is [differ.wasChain] for an enum's variants +// (docs/SPEC-TABLES.md §5): `was` names the FIRST wire name, forever, and a +// second rename aimed at the intermediate spelling hashes a name no value was +// ever written under. +func (d *differ) variantWasChain(be, le Enum) []Finding { + if d.policy["was-chain"] != RuleFixed { + return nil + } + byName := map[string]Variant{} + for _, v := range be.Variants { + byName[v.Name] = v + } + var out []Finding + for _, lv := range le.Variants { + if lv.Was == "" { + continue + } + bv, known := byName[lv.Was] + if !known || bv.Was == "" || bv.Was == lv.Was { + continue + } + out = append(out, Finding{Refuse, "enum " + le.Name + "." + lv.Name, fmt.Sprintf( + "was = %q names %s, which itself rode under was = %q — `was` names the FIRST wire name, forever, so this variant now rides under id 0x%016x, an id no value was ever written under; write was = %q", + lv.Was, lv.Was, bv.Was, lv.Id, bv.Was)}) } return out } @@ -1106,12 +1137,13 @@ func (d *differ) diffUnions() []Finding { if !ok { continue } - arms := map[string]Field{} + // ARMS MATCH BY WIRE ID, as fields do (§5) + arms := map[uint64]Field{} for _, a := range lu.Arms { - arms[a.Name] = a + arms[a.Id] = a } for _, a := range bu.Arms { - la, still := arms[a.Name] + la, still := arms[a.Id] if !still { if d.policy["union-arm"] == RuleLoss { out = append(out, Finding{Warn, "union " + lu.Name, @@ -1128,8 +1160,41 @@ func (d *differ) diffUnions() []Finding { // set, and an added or removed judged token refuses on the same // rule a changed one does: no kind byte separates an arm's type // on the wire (§4.1's fifth silent member). - out = append(out, d.diffTokens("union "+lu.Name+"."+a.Name, a, la)...) + out = append(out, d.diffTokens("union "+lu.Name+"."+la.Name, a, la)...) + } + out = append(out, d.armWasChain(bu, lu)...) + } + return out +} + +// armWasChain is [differ.wasChain] for a union's arms (docs/SPEC-TABLES.md +// §5): `was` names the FIRST wire name, forever, and a second rename aimed at +// the intermediate spelling hashes a name no body was ever written under. +func (d *differ) armWasChain(bu, lu Union) []Finding { + if d.policy["was-chain"] != RuleFixed { + return nil + } + byName := map[string]Field{} + for _, a := range bu.Arms { + byName[a.Name] = a + } + var out []Finding + for _, la := range lu.Arms { + now, has := la.Get("was") + if !has { + continue + } + ba, known := byName[now] + if !known { + continue + } + first, chained := ba.Get("was") + if !chained || first == now { + continue } + out = append(out, Finding{Refuse, "union " + lu.Name + "." + la.Name, fmt.Sprintf( + "was = %q names %s, which itself rode under was = %q — `was` names the FIRST wire name, forever, so this arm now rides under id 0x%016x, an id no body was ever written under; write was = %q", + now, now, first, la.Id, first)}) } return out } diff --git a/internal/check/check.go b/internal/check/check.go index 5918022c5..1f837635b 100644 --- a/internal/check/check.go +++ b/internal/check/check.go @@ -672,7 +672,7 @@ func (c *checker) resolveEnum(d *ast.EnumDecl) *ir.Enum { defer delete(c.resolvingEnum, d.Name) seen := map[string]bool{} - var variants []string + var variants, was []string for _, v := range d.Variants { // the three names below are reservedEnumVariant's set, and // checkClaimedNames reads the same predicate: a variant that IS the @@ -696,6 +696,7 @@ func (c *checker) resolveEnum(d *ast.EnumDecl) *ir.Enum { } seen[v.Text] = true variants = append(variants, v.Text) + was = append(was, c.variantWas("enum", d.Name, v)) } // An enum with no variants is LEGAL: it holds only the implicit None = 0, // so its wire range is the degenerate [0, 0] and it costs zero bits. That @@ -740,7 +741,7 @@ func (c *checker) resolveEnum(d *ast.EnumDecl) *ir.Enum { } max = v.Int64() } - en := &ir.Enum{Name: d.Name, Variants: variants, Max: max, StorageBits: ir.StorageBitsFor(max)} + en := &ir.Enum{Name: d.Name, Variants: variants, Was: was, Max: max, StorageBits: ir.StorageBitsFor(max)} c.enums[d.Name] = en return en } @@ -758,6 +759,17 @@ func (c *checker) resolveFlags(d *ast.FlagsDecl) *ir.Flags { } seen[v.Text] = true variants = append(variants, v.Text) + for _, a := range v.Attrs { + switch { + case a.Key == "was": + // a mask is POSITIONAL (docs/SPEC-TABLES.md §5): a flags + // variant's identity is its bit, no name rides, and a + // rename in place is the baseline's to judge (§18.2) + c.errf(a.Pos, "flags %s: variant %s takes no was — a mask is positional, a variant's identity is its bit and no name rides the wire, so a rename keeps every stored bit; the baseline judges a rename in place (docs/SPEC-TABLES.md §5, §18.2)", d.Name, v.Text) + case a.Value != nil: + c.errf(a.Pos, "flags %s: variant %s takes no valued key; a tag is a bare identifier (SPEC §4.2)", d.Name, v.Text) + } + } } if len(variants) == 0 { c.errf(d.Pos, "flags %s has no variants", d.Name) @@ -947,6 +959,14 @@ func (c *checker) resolveUnion(d *ast.UnionDecl) { // `type` or, inside a table closure, a `table` (docs/SPEC-TABLES.md // §2.6). Every other arm carries its field line and nothing else. out := ir.UnionVariant{Name: v.Name, F: arm} + // THE ARM'S RENAME (docs/SPEC-TABLES.md §5): on an arm with a payload + // the field line carries it, and on a payload-free arm the name's own + // section does + if arm != nil { + out.WasName = arm.WasName + } else { + out.WasName = c.variantWas("union", d.Name, ast.Name{Text: v.Name, Pos: v.Pos, Attrs: v.Attrs}) + } if arm != nil && arm.Type.Kind == ir.TNamed { if st, isStruct := arm.Type.Ref.(*ir.Struct); isStruct { out.Type, out.Ref = arm.Type.Name, st @@ -970,7 +990,7 @@ func (c *checker) resolveUnion(d *ast.UnionDecl) { // field and the closure, not to the arm. func (c *checker) resolveArm(union string, v ast.UnionVariant) *ir.Field { if v.Arm == nil { - return nil // a PAYLOAD-FREE arm: the name is the whole of it (SPEC §4.8) + return nil // a PAYLOAD-FREE arm: the name and its section are the whole of it (SPEC §4.8) } where := fmt.Sprintf("union %s: arm %s", union, v.Name) if v.Arm.Default != nil { @@ -989,9 +1009,6 @@ func (c *checker) resolveArm(union string, v ast.UnionVariant) *ir.Field { return nil } switch { - case f.WasName != "": - c.errf(v.Arm.Pos, "%s: was on an arm is a named follow-on — an arm's wire id is its NAME hash and arms already evolve by name, so a rename is a new arm today (docs/SPEC-TABLES.md §2.6, §5, §15)", where) - return nil case f.JsonKey != "": c.errf(v.Arm.Pos, "%s: json on an arm is a named follow-on — an arm's own name is its key in the text form (docs/SPEC-TABLES.md §2.6, §16.2, §15)", where) return nil @@ -1373,13 +1390,11 @@ func (c *checker) resolveField(owner string, f *ast.Field, inTable bool) *ir.Fie c.resolveAttrs(f, out) - if out.WasName != "" && !inTable { - c.errf(f.Pos, "field %s: was is a table-wire concept — it aliases a renamed field's wire id, and only table fields have wire ids; a `type`'s wire is positional, so a rename there moves no bit (docs/SPEC-TABLES.md)", f.Name) - return nil - } - // `json` outside a table CLOSURE is refused in checkTables, where the - // closure is known: a `type` a table reaches has a text form and may - // carry the attribute, and only membership decides it. + // `was` and `json` outside a table CLOSURE are refused in checkTables, + // where the closure is known: a `type` a table reaches has table-wire + // field ids and a text form and may carry both, and only membership + // decides it (docs/SPEC-TABLES.md §5, §16.4). + _ = inTable // the fixed and 128-bit families mirror serialize's own surface exactly // (SPEC §4.3, runtime-first): fixed(I, F) and int128 are RANGED — the @@ -2242,6 +2257,7 @@ func (c *checker) checkTables() { c.checkTableVariantIdentity(names) c.checkOptionalVariableClosures(names) c.checkJsonKeysInClosure() + c.checkWasInClosure() } // checkOptionalVariableClosures refuses an OPTIONAL whose value's closure is @@ -2600,27 +2616,53 @@ func (c *checker) checkTableVariantIdentity(closureNames []string) { name, e.Max, reachedBy[name], name) } seen := map[uint64]string{} - for _, v := range e.Variants { - id := ir.TableWireId(v) + for i, v := range e.Variants { + // THE EFFECTIVE NAME: a variant renamed under `was` rides under + // the hash of its old name (docs/SPEC-TABLES.md §5) + id := ir.TableWireId(e.VariantWireName(i)) if prev, dup := seen[id]; dup { c.errf(pos(name), "enum %s: variants %s and %s collide on table-wire id 0x%016x, and %s reaches it, putting %s in a table closure — rename one (docs/SPEC-TABLES.md §5)", - name, prev, v, id, reachedBy[name], name) + name, prev, describeVariant(v, e.Was[i]), id, reachedBy[name], name) continue } - seen[id] = v + seen[id] = describeVariant(v, e.Was[i]) } } for _, name := range sortedKeys(unions) { un := unions[name] seen := map[uint64]string{} for _, v := range un.Variants { - id := ir.TableWireId(v.Name) + id := ir.TableWireId(v.WireName()) if prev, dup := seen[id]; dup { c.errf(pos(name), "union %s: arms %s and %s collide on table-wire id 0x%016x, and %s reaches it, putting %s in a table closure — rename one (docs/SPEC-TABLES.md §5)", - name, prev, v.Name, id, reachedBy[name], name) + name, prev, describeVariant(v.Name, v.WasName), id, reachedBy[name], name) continue } - seen[id] = v.Name + seen[id] = describeVariant(v.Name, v.WasName) + } + } + // `was` IS A TABLE-WIRE CONCEPT (docs/SPEC-TABLES.md §5): a variant or an + // arm has a wire identity only where a table closure reaches its + // declaration, so an alias anywhere else preserves nothing and is refused + // naming the declaration, exactly as a field's is outside a closure. + for _, name := range sortedKeys(c.enums) { + if _, reached := enums[name]; reached { + continue + } + for i, w := range c.enums[name].Was { + if w != "" { + c.errf(pos(name), "enum %s: variant %s carries was = %q, but no table reaches %s — was is a table-wire concept, and a variant of an enum outside a table closure has no wire identity for it to keep (docs/SPEC-TABLES.md §5)", name, c.enums[name].Variants[i], w, name) + } + } + } + for _, name := range sortedKeys(c.unions) { + if _, reached := unions[name]; reached { + continue + } + for _, v := range c.unions[name].Variants { + if v.WasName != "" { + c.errf(pos(name), "union %s: arm %s carries was = %q, but no table reaches %s — was is a table-wire concept, and an arm of a union outside a table closure has no wire identity for it to keep (docs/SPEC-TABLES.md §5)", name, v.Name, v.WasName, name) + } } } } @@ -3825,3 +3867,65 @@ func (c *checker) resolveFlagsDefault(f *ast.Field, out *ir.Field, fl *ir.Flags, out.HasDefault = true out.DefInt = mask } + +// variantWas resolves one enum variant's qualification section (SPEC §4.2, +// docs/SPEC-TABLES.md §5): `was = "OldName"` is the variant's rename, held +// to the field rule (a quoted string, not empty, not its own name), a bare +// identifier is a tag, and every other valued key is refused by name. It +// returns the alias, "" when none is declared. +func (c *checker) variantWas(what, decl string, v ast.Name) string { + was := "" + for _, a := range v.Attrs { + switch { + case a.Key == "was": + lit, ok := a.Value.(*ast.StringLit) + switch { + case a.Value == nil: + c.errf(a.Pos, "attribute was requires a value, as was = ... (SPEC §4.6)") + case !ok: + c.errf(a.Pos, `was takes the variant's old name as a quoted string, e.g. was = "Argent" (docs/SPEC-TABLES.md §5)`) + case lit.Value == "": + c.errf(a.Pos, "was = \"\" names nothing — was records the variant's old name after a rename (docs/SPEC-TABLES.md §5)") + case lit.Value == v.Text: + c.errf(a.Pos, "%s %s: variant %s: was = %q names the variant's own current name — was records the OLD name after a rename; drop the attribute until one happens (docs/SPEC-TABLES.md §5)", what, decl, v.Text, lit.Value) + default: + was = lit.Value + } + case a.Value != nil: + c.errf(a.Pos, "%s %s: variant %s: %s is not an attribute a variant takes — the valued vocabulary here is was alone, and a tag is a bare identifier (SPEC §4.2, docs/SPEC-TABLES.md §5)", what, decl, v.Text, a.Key) + } + } + return was +} + +// describeVariant names a variant or an arm for the id-collision diagnostic, +// showing the was alias when that is where the colliding id comes from. +func describeVariant(name, was string) string { + if was != "" { + return fmt.Sprintf("%s (was %q)", name, was) + } + return name +} + +// checkWasInClosure is §5's closure rule for a `type`'s fields: a field of a +// type no table reaches has no table-wire id, so its `was` preserves nothing +// and is refused naming the type. A type a table reaches by value has +// table-wire field ids, and the alias is what its stored bodies ride under. +func (c *checker) checkWasInClosure() { + for _, name := range sortedKeys(c.structs) { + if c.tableClosure[name] { + continue + } + st := c.structs[name] + pos := ast.Pos{} + if d, ok := c.astDecls[name]; ok { + pos = d.DeclPos() + } + for _, f := range st.Fields { + if f.WasName != "" { + c.errf(pos, "type %s: field %s carries was = %q, but no table reaches %s — was is a table-wire concept, and a field of a type outside a table closure has no wire id for it to keep; the packet wire is positional, so a rename there orphans nothing (docs/SPEC-TABLES.md §5)", + name, f.Name, f.WasName, name) + } + } + } +} diff --git a/internal/codegen/cpptable/arms.go b/internal/codegen/cpptable/arms.go index 4e25125bf..e96175a3b 100644 --- a/internal/codegen/cpptable/arms.go +++ b/internal/codegen/cpptable/arms.go @@ -360,7 +360,7 @@ func (g *tableGen) emitArmLoad(v ir.UnionVariant, base, ind, rdr, tag, none, sfx g.pf("%s TableReader %s( %s.buffer + %s.offset, (int64_t) %s, r.report, r.ids );\n", ind, inner, rdr, rdr, length) g.pf("%s switch ( %s ) // the arm's NAME hash (§5)\n%s {\n", ind, id, ind) for _, in := range un.Variants { - g.pf("%s case 0x%016xull: // %s\n%s {\n", ind, ir.TableWireId(in.Name), in.Name, ind) + g.pf("%s case 0x%016xull: // %s\n%s {\n", ind, ir.TableWireId(in.WireName()), in.Name, ind) g.pf("%s if ( %s != %d ) { %s.type = %sType::None; r.report->kind_mismatch++; break; }\n", ind, innerKind, armWireKind(in), value, un.Name) g.pf("%s %s.type = %sType::%s;\n", ind, value, un.Name, ir.GoExportName(in.Name)) g.emitArmLoad(in, value, ind+" ", inner, value+".type", un.Name+"Type::None", sfx+"a") diff --git a/internal/codegen/cpptable/codecs.go b/internal/codegen/cpptable/codecs.go index f952b4e19..9d9acdaf7 100644 --- a/internal/codegen/cpptable/codecs.go +++ b/internal/codegen/cpptable/codecs.go @@ -562,8 +562,8 @@ func (g *tableGen) emitEnumIdentity(e *ir.Enum) { g.pf("inline bool TableEnumRef( TableIds & ids, %s value, uint64_t & ref )\n{\n", e.Name) g.pf(" switch ( value )\n {\n") g.pf(" case %s::None: ref = 0; return true;\n", e.Name) - for _, v := range e.Variants { - g.pf(" case %s::%s: ref = %s; return true;\n", e.Name, v, g.wireRef(ir.TableWireId(v))) + for i, v := range e.Variants { + g.pf(" case %s::%s: ref = %s; return true;\n", e.Name, v, g.wireRef(ir.TableWireId(e.VariantWireName(i)))) } g.pf(" default: return false; // no variant names this value: no wire identity\n") g.pf(" }\n}\n") @@ -578,15 +578,15 @@ func (g *tableGen) emitEnumIdentity(e *ir.Enum) { g.pf("inline bool TableEnumId( %s value, uint64_t & id )\n{\n", e.Name) g.pf(" switch ( value )\n {\n") g.pf(" case %s::None: id = 0; return true;\n", e.Name) - for _, v := range e.Variants { - g.pf(" case %s::%s: id = 0x%016xull; return true;\n", e.Name, v, ir.TableWireId(v)) + for i, v := range e.Variants { + g.pf(" case %s::%s: id = 0x%016xull; return true;\n", e.Name, v, ir.TableWireId(e.VariantWireName(i))) } g.pf(" default: return false; // no variant names this value: no wire identity\n") g.pf(" }\n}\n") g.pf("inline bool TableEnumValue( uint64_t id, %s & out )\n{\n", e.Name) g.pf(" switch ( id )\n {\n") - for _, v := range e.Variants { - g.pf(" case 0x%016xull: out = %s::%s; return true;\n", ir.TableWireId(v), e.Name, v) + for i, v := range e.Variants { + g.pf(" case 0x%016xull: out = %s::%s; return true;\n", ir.TableWireId(e.VariantWireName(i)), e.Name, v) } g.pf(" default: return false; // an id this build cannot name\n") g.pf(" }\n}\n") @@ -956,7 +956,7 @@ func (g *tableGen) emitUnionPayloadMeasure(f *ir.Field, expr, into, ind, sfx str g.noteRef(v.Type) g.pf("%s case %sType::%s:\n%s {\n", ind, un.Name, ir.GoExportName(v.Name), ind) g.pf("%s int64_t %s = 0;\n", ind, body) - g.pf("%s const uint64_t arm_ref%s = %s;\n", ind, sfx, g.wireRef(ir.TableWireId(v.Name))) + g.pf("%s const uint64_t arm_ref%s = %s;\n", ind, sfx, g.wireRef(ir.TableWireId(v.WireName()))) g.emitArmMeasure(v, expr, body, ind+" ", "return -1;", sfx) g.pf("%s %s += TableLebBytes( arm_ref%s ) + 1 + %s;\n", ind, into, sfx, framed(body)) g.pf("%s break;\n%s }\n", ind, ind) @@ -1373,7 +1373,7 @@ func (g *tableGen) emitUnionPayloadSave(f *ir.Field, expr, ind, onBad, sfx strin for _, v := range un.Variants { g.noteRef(v.Type) g.pf("%s case %sType::%s:\n%s {\n", ind, un.Name, ir.GoExportName(v.Name), ind) - g.pf("%s const uint64_t arm_ref%s = %s;\n", ind, sfx, g.wireRef(ir.TableWireId(v.Name))) + g.pf("%s const uint64_t arm_ref%s = %s;\n", ind, sfx, g.wireRef(ir.TableWireId(v.WireName()))) g.pf("%s int64_t %s = 0;\n", ind, body) g.emitArmMeasure(v, expr, body, ind+" ", onBad, sfx) g.pf("%s w.putleb( arm_ref%s ); w.put8( %d ); w.putleb( (uint64_t) %s ); // %s\n", ind, sfx, armWireKind(v), body, v.Name) @@ -1748,7 +1748,7 @@ func (g *tableGen) emitTableReadField(f *ir.Field, kind int) { g.pf("%s{\n%s TableReader sub( r.buffer + r.offset, (int64_t) body_len, r.report, r.ids );\n", ind, ind) g.pf("%s switch ( arm_id ) // the arm's NAME hash (docs/SPEC-TABLES.md §5)\n%s {\n", ind, ind) for _, v := range un.Variants { - g.pf("%s case 0x%016xull: // %s\n%s {\n", ind, ir.TableWireId(v.Name), v.Name, ind) + g.pf("%s case 0x%016xull: // %s\n%s {\n", ind, ir.TableWireId(v.WireName()), v.Name, ind) g.pf("%s if ( arm_kind != %d )\n%s {\n", ind, armWireKind(v), ind) g.pf("%s // A RETYPED ARM IS JUDGED BY THE FIELD RULES (§3): the\n", ind) g.pf("%s // arm skips by L, the union reads None, and the parent reads on\n", ind) @@ -1824,7 +1824,7 @@ func (g *tableGen) emitTableReadElementInto(f *ir.Field, kind int, dst, ind, rdr g.pf("%s TableReader elem_arm%s( %s.buffer + %s.offset, (int64_t) %s, r.report, r.ids );\n", ind, sfx, rdr, rdr, length) g.pf("%s switch ( %s ) // the arm's NAME hash (docs/SPEC-TABLES.md §5)\n%s {\n", ind, id, ind) for _, v := range un.Variants { - g.pf("%s case 0x%016xull: // %s\n%s {\n", ind, ir.TableWireId(v.Name), v.Name, ind) + g.pf("%s case 0x%016xull: // %s\n%s {\n", ind, ir.TableWireId(v.WireName()), v.Name, ind) g.pf("%s if ( %s != %d ) { %s.type = %sType::None; r.report->kind_mismatch++; break; }\n", ind, armKind, armWireKind(v), dst, un.Name) g.pf("%s %s.type = %sType::%s;\n", ind, dst, un.Name, ir.GoExportName(v.Name)) g.emitArmLoad(v, dst, ind+" ", "elem_arm"+sfx, dst+".type", un.Name+"Type::None", sfx+"a") @@ -2287,7 +2287,7 @@ func (g *tableGen) emitFieldInfo(f *ir.Field, sp fieldSpelling, hoisted bool) { return fmt.Sprintf("\"%s\"", v.Name) }, "\"???\"", "\"None\"") variantId = unionArmLambda(ref, "uint64_t", func(v ir.UnionVariant) string { - return fmt.Sprintf("0x%016xull", ir.TableWireId(v.Name)) + return fmt.Sprintf("0x%016xull", ir.TableWireId(v.WireName())) }, "0", "0") arms = g.unionArmsLambda(ref, hoisted) for _, v := range ref.Variants { diff --git a/internal/codegen/cpptable/extent.go b/internal/codegen/cpptable/extent.go index 95098ea97..13d2c7f3b 100644 --- a/internal/codegen/cpptable/extent.go +++ b/internal/codegen/cpptable/extent.go @@ -511,7 +511,7 @@ func (g *tableGen) emitWireExtentCases(st *ir.Struct) { continue } g.pf(" case 0x%016xull: if ( !%sWireExtent( arm_body, (int64_t) arm_len, at, ids, reason ) ) { return false; } break; // %s\n", - ir.TableWireId(v.Name), v.Type, v.Name) + ir.TableWireId(v.WireName()), v.Type, v.Name) } g.pf(" default: break; // an arm this reader cannot name reads None\n") g.pf(" }\n") diff --git a/internal/format/format.go b/internal/format/format.go index 75a669c60..bff5e8b1a 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -527,7 +527,12 @@ func splitField(s string) (name, typ, tail string, ok bool) { // run (SPEC §7.4 rule 2) typ = rest tail = "" - if k := strings.Index(rest, " |"); k >= 0 { + if strings.HasPrefix(rest, "|") { + // a PAYLOAD-FREE ARM with a qualification section (SPEC §4.8): the + // name is the whole definition, and the section is the tail, so the + // pipes of a run line up + typ, tail = "", rest + } else if k := strings.Index(rest, " |"); k >= 0 { typ, tail = rest[:k], strings.TrimLeft(rest[k:], " ") } if strings.HasSuffix(typ, "{") { @@ -583,7 +588,7 @@ func fpDecl(b *strings.Builder, d ast.Decl) { // or the formatter could move one and the safety net would not see it fmt.Fprintf(b, "union %s\n{\n", d.Name) for _, v := range d.Variants { - b.WriteString("variant ") + b.WriteString("variant " + v.Name + fpAttrs(v.Attrs) + " ") fpField(b, v.Arm) } b.WriteString("}\n") @@ -740,7 +745,7 @@ func fpAttrs(attrs []ast.Attr) string { func fpNames(names []ast.Name) string { var out []string for _, n := range names { - out = append(out, n.Text) + out = append(out, n.Text+fpAttrs(n.Attrs)) } return strings.Join(out, ",") } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index ea80e7967..5f77888f8 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -298,7 +298,20 @@ func (p *parser) parseVariantList(what string) []ast.Name { if t.Kind != scanner.Ident { break } - names = append(names, ast.Name{Text: t.Text, Pos: t.Pos}) + n := ast.Name{Text: t.Text, Pos: t.Pos} + if p.kind() == scanner.Pipe { + // A QUALIFIED VARIANT (SPEC §4.2): the section runs to the end of + // the line, so the variant ends its line and the newline is its + // separator. A trailing comma inside the section is the section's + // own, and the list goes on below it. + n.Attrs = p.parsePipeAttrs() + names = append(names, n) + for p.kind() == scanner.Newline { + p.advance() + } + continue + } + names = append(names, n) if p.kind() == scanner.Comma { p.advance() // trailing comma allowed; newlines around commas are whitespace continue @@ -357,11 +370,16 @@ func (p *parser) parseUnionBody() []ast.UnionVariant { continue } typePos := p.tok().Pos - if p.kind() == scanner.Newline || p.kind() == scanner.RBrace || p.kind() == scanner.EOF { + if p.kind() == scanner.Newline || p.kind() == scanner.RBrace || p.kind() == scanner.EOF || p.kind() == scanner.Pipe { // A PAYLOAD-FREE ARM is a bare name (SPEC §4.8): the arm has // no storage, the packet wire carries the tag alone and the - // table wire the arm id with L = 0 (docs/SPEC-TABLES.md §2.6) - variants = append(variants, ast.UnionVariant{Name: name.Text, Pos: name.Pos, TypePos: typePos}) + // table wire the arm id with L = 0 (docs/SPEC-TABLES.md §2.6). + // Its qualification section, when it has one, follows the name. + v := ast.UnionVariant{Name: name.Text, Pos: name.Pos, TypePos: typePos} + if p.kind() == scanner.Pipe { + v.Attrs = p.parsePipeAttrs() + } + variants = append(variants, v) p.expectTerminator("union arm") continue } diff --git a/internal/tablewire/decode.go b/internal/tablewire/decode.go index 0e8210bc7..46f44c865 100644 --- a/internal/tablewire/decode.go +++ b/internal/tablewire/decode.go @@ -923,7 +923,7 @@ func (r *wireReader) unionCell(cell *tabletext.Cell, f *ir.Field) bool { sub := r.sub(length) tag := 0 for i, v := range un.Variants { - if ir.TableWireId(v.Name) == armID { + if ir.TableWireId(v.WireName()) == armID { tag = i + 1 break } @@ -1155,8 +1155,8 @@ func (r *wireReader) scalar(cell *tabletext.Cell, f *ir.Field, atField bool) boo // enumValueForId is the declaration-side value an id names, -1 when no variant // does. func enumValueForId(e *ir.Enum, id uint64) int64 { - for i, v := range e.Variants { - if ir.TableWireId(v) == id { + for i := range e.Variants { + if ir.TableWireId(e.VariantWireName(i)) == id { return int64(i + 1) } } diff --git a/internal/tablewire/encode.go b/internal/tablewire/encode.go index 79ce85319..1b76228a8 100644 --- a/internal/tablewire/encode.go +++ b/internal/tablewire/encode.go @@ -475,7 +475,7 @@ func encodeElement(e *encoder, w *buf, f *ir.Field, kind int, cell *tabletext.Ce // the arm's kind byte is what makes a retyped arm an ordinary kind mismatch // instead of a value read under the wrong rule. func encodeArmHeader(e *encoder, w *buf, arm ir.UnionVariant, cell *tabletext.Cell) error { - w.leb(e.ids.ref(ir.TableWireId(arm.Name))) + w.leb(e.ids.ref(ir.TableWireId(arm.WireName()))) w.u8(uint8(armWireKind(arm))) body, err := encodeArm(e, arm, cell) if err != nil { @@ -609,7 +609,7 @@ func variantWireId(e *ir.Enum, value uint64, field string) (id uint64, none bool if name == "None" { return 0, true, nil } - return ir.TableWireId(name), false, nil + return ir.TableWireId(e.VariantWireNameOf(name)), false, nil } // cellIsDefault is the writer's elision test: a field holding its declared diff --git a/ir/buildversion.go b/ir/buildversion.go index 5c43dc60d..21725ac62 100644 --- a/ir/buildversion.go +++ b/ir/buildversion.go @@ -150,10 +150,12 @@ func CookProjection(u *Unit) string { for _, name := range sortedKeysOf(enums) { fmt.Fprintf(&b, "enum %s\n", name) - for i, v := range enums[name].Variants { + for i := range enums[name].Variants { // the STORED VALUE, not a positional index: None = 0 is implicit - // and never listed, so declared variants start at 1 - fmt.Fprintf(&b, " variant %d %s\n", i+1, v) + // and never listed, so declared variants start at 1. The name is + // the WIRE name (§5): a variant renamed under `was` keeps the id + // every stored value carries, so the rename moves nothing. + fmt.Fprintf(&b, " variant %d %s\n", i+1, enums[name].VariantWireName(i)) } } // A `flags` DECLARATION TAKES A BLOCK OF ITS OWN, and it is the enum block @@ -245,19 +247,19 @@ func cookArmLine(u *Unit, un *Union, tag int, v UnionVariant, enums map[string]* if v.Void() { // AN ARM WITH NO PAYLOAD carries `kind=none` (§20.2, §18.1): the kind // token saying there is no kind to carry, and no storage to offset - return fmt.Sprintf(" arm %d %s kind=none\n", tag, v.Name) + return fmt.Sprintf(" arm %d %s kind=none\n", tag, v.WireName()) } if v.Body() { // an arm that names a declared `type` or `table` carries `payload=` // and nothing else, exactly as it always did — so a unit whose arms // all name declared types projects exactly as it did before arms // could be anything else, and its build version does not move - return fmt.Sprintf(" arm %d %s payload=%s\n", tag, v.Name, v.Type) + return fmt.Sprintf(" arm %d %s payload=%s\n", tag, v.WireName(), v.Type) } size, align := ArmLayout(u, v) fl := FieldLayout{Field: v.F, Offset: armOffset, Size: size, Align: align} return fmt.Sprintf(" arm %d %s kind=%d offset=%d size=%d%s\n", - tag, v.Name, TableWireScalarKind(v.F), armOffset, size, cookFacts(u, fl, enums, flags, unions)) + tag, v.WireName(), TableWireScalarKind(v.F), armOffset, size, cookFacts(u, fl, enums, flags, unions)) } // cookFacts is the token tail a field line and an ARM line share: the scale, diff --git a/ir/ir.go b/ir/ir.go index bbd6b20a9..8228390eb 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -87,10 +87,15 @@ type Const struct { // Enum is an `enum` declaration: None = 0 implicit, variants dense from 1 // (SPEC §4.2). type Enum struct { - Name string - Variants []string // implicit None = 0 is not listed; variants pack from 1 - Max int64 // top wire value: variant count, or the | max = K widening - StorageBits int // 8 / 16 / 32 / 64 — smallest unsigned fitting Max + Name string + Variants []string // implicit None = 0 is not listed; variants pack from 1 + // Was is each variant's `was = "OldName"` rename alias, parallel to + // Variants and "" where none is declared (docs/SPEC-TABLES.md §5): the + // variant's table-wire id derives from it instead of the name. See + // [Enum.VariantWireName]. + Was []string + Max int64 // top wire value: variant count, or the | max = K widening + StorageBits int // 8 / 16 / 32 / 64 — smallest unsigned fitting Max } // Flags is a `flags` declaration: one bit per variant, consumed as masks @@ -116,10 +121,14 @@ type Union struct { // docs/SPEC-TABLES.md §2.6): F carries the whole of it — the resolved type, // the array shape, the bounds — and is never nil on a checked union. type UnionVariant struct { - Name string // declared, field-style lower_snake - Type string // the payload type's name; "" when the arm names no declaration - Ref *Struct // the payload: a `type`, or inside a table closure a `table`; nil otherwise - F *Field // the arm as a field line + Name string // declared, field-style lower_snake + // WasName is the arm's `was = "old_name"` rename alias, "" when none is + // declared (docs/SPEC-TABLES.md §5): the arm's table-wire id derives from + // it instead of Name. On an arm with a payload it is F.WasName too. + WasName string + Type string // the payload type's name; "" when the arm names no declaration + Ref *Struct // the payload: a `type`, or inside a table closure a `table`; nil otherwise + F *Field // the arm as a field line } // Body reports whether the arm's payload is a TABLE BODY on the wire — a @@ -544,3 +553,80 @@ func PointeeWireName(f *Field) string { } return f.Type.Name } + +// VariantWireName is the name variant i's table-wire id is the hash of +// (docs/SPEC-TABLES.md §5): its `was` alias after a rename, and its declared +// name otherwise. +func (e *Enum) VariantWireName(i int) string { + if i < len(e.Was) && e.Was[i] != "" { + return e.Was[i] + } + return e.Variants[i] +} + +// VariantWireNameOf is [Enum.VariantWireName] by declared name, and the name +// itself when the enum declares no such variant. +func (e *Enum) VariantWireNameOf(name string) string { + for i, v := range e.Variants { + if v == name { + return e.VariantWireName(i) + } + } + return name +} + +// WireName is the name the arm's table-wire id is the hash of +// (docs/SPEC-TABLES.md §5): its `was` alias after a rename, and its declared +// name otherwise. +func (v UnionVariant) WireName() string { + if v.WasName != "" { + return v.WasName + } + return v.Name +} + +// TableFieldWireName is the name a field's table-wire id is the hash of +// (docs/SPEC-TABLES.md §5): the `was` alias after a rename, else the name. +func TableFieldWireName(f *Field) string { + if f.WasName != "" { + return f.WasName + } + return f.Name +} + +// WasRows names every `was` a unit declares on an enum variant, a union arm, +// or a field of a `type` (docs/SPEC-TABLES.md §5), as `Decl.name`, sorted. A +// table's own fields and a table declaration are not listed: those two rows +// every target carries. It is what a target without the form refuses by name. +func WasRows(u *Unit) []string { + var out []string + for name, e := range u.Enums { + for i, w := range e.Was { + if w != "" { + out = append(out, name+"."+e.Variants[i]) + } + } + } + note := func(name string, un *Union) { + for _, v := range un.Variants { + if v.WasName != "" { + out = append(out, name+"."+v.Name) + } + } + } + for name, un := range u.Unions { + note(name, un) + } + for name, un := range u.TableUnions { + note(name, un) + } + for name, st := range u.Structs { + for _, f := range st.Fields { + if f.WasName != "" { + out = append(out, name+"."+f.Name) + } + } + } + sort.Strings(out) + return out +} diff --git a/ir/projection.go b/ir/projection.go index bd037360d..2cfbd918d 100644 --- a/ir/projection.go +++ b/ir/projection.go @@ -118,8 +118,8 @@ func WireProjection(u *Unit) string { // reorder is invisible without them — the spurious MATCH this // projection exists to refuse — and a rename therefore moves the id. fmt.Fprintf(&b, "enum %s max=%d storage=%d variants=%d\n", e.Name, e.Max, e.StorageBits, len(e.Variants)) - for i, v := range e.Variants { - fmt.Fprintf(&b, " variant %d name=%s\n", i+1, v) + for i := range e.Variants { + fmt.Fprintf(&b, " variant %d name=%s\n", i+1, e.VariantWireName(i)) } } @@ -186,11 +186,11 @@ func WireProjection(u *Unit) string { for i, v := range un.Variants { switch { case v.Void(): - fmt.Fprintf(&b, " variant %d name=%s kind=none\n", i+1, v.Name) + fmt.Fprintf(&b, " variant %d name=%s kind=none\n", i+1, v.WireName()) case v.Body(): - fmt.Fprintf(&b, " variant %d name=%s payload=%s\n", i+1, v.Name, v.Type) + fmt.Fprintf(&b, " variant %d name=%s payload=%s\n", i+1, v.WireName(), v.Type) default: - fmt.Fprintf(&b, " variant %d name=%s ", i+1, v.Name) + fmt.Fprintf(&b, " variant %d name=%s ", i+1, v.WireName()) projectField(&b, v.F, "") } } @@ -226,7 +226,9 @@ func projectField(b *strings.Builder, f *Field, ind string) { // existing id stable — so a rename moves the id even though the wire is // unmoved. Dropping it is a ProjectionVersion bump, taken deliberately // or not at all. - fmt.Fprintf(b, "%sfield %s kind=%d", ind, f.Name, int(f.Type.Kind)) + // A `was` RENAME PROJECTS THE WIRE NAME (docs/SPEC-TABLES.md §5), so the + // rename that keeps a table-wire identity keeps the protocol id too. + fmt.Fprintf(b, "%sfield %s kind=%d", ind, TableFieldWireName(f), int(f.Type.Kind)) if f.Type.Kind == TNamed { fmt.Fprintf(b, " type=%s", f.Type.Name) diff --git a/ir/tablewire.go b/ir/tablewire.go index 0f07c691c..8406a13e6 100644 --- a/ir/tablewire.go +++ b/ir/tablewire.go @@ -202,8 +202,8 @@ func TableWireIdCapacity(u *Unit) int { ids[MapValueWireId] = true } if f.KeyEnumRef != nil { - for _, v := range f.KeyEnumRef.Variants { - ids[TableWireId(v)] = true + for i := range f.KeyEnumRef.Variants { + ids[TableWireId(f.KeyEnumRef.VariantWireName(i))] = true } } if f.Type.Kind != TNamed { @@ -211,8 +211,8 @@ func TableWireIdCapacity(u *Unit) int { } switch ref := f.Type.Ref.(type) { case *Enum: - for _, v := range ref.Variants { - ids[TableWireId(v)] = true + for i := range ref.Variants { + ids[TableWireId(ref.VariantWireName(i))] = true } case *Union: noteUnion(ref) @@ -225,7 +225,7 @@ func TableWireIdCapacity(u *Unit) int { } seen[un] = true for _, v := range un.Variants { - ids[TableWireId(v.Name)] = true + ids[TableWireId(v.WireName())] = true if v.F != nil { noteField(v.F) } @@ -331,8 +331,8 @@ func TableVocabulary(u *Unit) []uint64 { } collectArmRefs(enums, flags, unions) for _, name := range sortedKeysOf(enums) { - for _, v := range enums[name].Variants { - place(TableWireId(v)) + for i := range enums[name].Variants { + place(TableWireId(enums[name].VariantWireName(i))) } } // A `flags` DECLARATION NAMES NOTHING ON THIS WIRE: a mask rides raw, so @@ -341,7 +341,7 @@ func TableVocabulary(u *Unit) []uint64 { // the enums and the unions. for _, name := range sortedKeysOf(unions) { for _, v := range unions[name].Variants { - place(TableWireId(v.Name)) + place(TableWireId(v.WireName())) } } From 02219520eab25b74bce0bafff6ac83e050e27f43 Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sat, 5 Sep 2026 12:39:27 -0700 Subject: [PATCH 3/7] was rows: the pages, the corpus rows and the conformance leg (#442, #478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Makefile | 2 +- docs/COMPETITION.md | 19 ++--- docs/SPEC-TABLES.md | 74 +++++++++++++------ docs/SPEC.md | 51 ++++++++----- docs/USAGE.md | 43 ++++++++++- docs/VERSIONING.md | 21 +++--- internal/format/wasrows_test.go | 4 +- test/conformance/cpp/main.cpp | 4 + test/tables/main.cpp | 94 ++++++++++++++++++++++++ testdata/conformance/tables/MANIFEST.txt | 16 ++++ 10 files changed, 263 insertions(+), 65 deletions(-) diff --git a/Makefile b/Makefile index e7c1735cc..4b7a945e2 100644 --- a/Makefile +++ b/Makefile @@ -3116,7 +3116,7 @@ CONFORMANCE_INCLUDES := -Ibuild/tables-generated/examples -Ibuild/tables-generat -Ibuild/tables-generated/v2 -Ibuild/tables-generated/p1 -Ibuild/tables-generated/p3 \ -Ibuild/tables-generated/block -Ibuild/tables-generated/pointers \ -Ibuild/tables-generated/p2 -Ibuild/tables-generated/messages -Ibuild/tables-generated/stream \ - -Ibuild/tables-generated/m1 -Ibuild/tables-generated/m2 -Ibuild/tables-generated/a1 -Ibuild/tables-generated/a2 -Ibuild/tables-generated/g1 -Ibuild/tables-generated/k1 -Ibuild/tables-generated/k2 -Ibuild/tables-generated/w1 -Ibuild/tables-generated/w2 -Ibuild/tables-generated/blobs -Itest/tables -Ibuild/tables-generated/scalars -Ibuild/tables-generated/scalars2 -Ibuild/tables-generated/backend -Ibuild/tables-generated/vocab -I$(SERIALIZE) + -Ibuild/tables-generated/m1 -Ibuild/tables-generated/m2 -Ibuild/tables-generated/a1 -Ibuild/tables-generated/a2 -Ibuild/tables-generated/g1 -Ibuild/tables-generated/k1 -Ibuild/tables-generated/k2 -Ibuild/tables-generated/w1 -Ibuild/tables-generated/w2 -Ibuild/tables-generated/r1 -Ibuild/tables-generated/r2 -Ibuild/tables-generated/blobs -Itest/tables -Ibuild/tables-generated/scalars -Ibuild/tables-generated/scalars2 -Ibuild/tables-generated/backend -Ibuild/tables-generated/vocab -I$(SERIALIZE) CONFORMANCE_SOURCES = build/tables-generated/examples/TablesTable.cpp \ build/tables-generated/w1/W1Table.cpp build/tables-generated/w2/W2Table.cpp \ build/tables-generated/r1/R1Table.cpp build/tables-generated/r2/R2Table.cpp \ diff --git a/docs/COMPETITION.md b/docs/COMPETITION.md index 52c9275ab..d5e60431e 100644 --- a/docs/COMPETITION.md +++ b/docs/COMPETITION.md @@ -82,10 +82,7 @@ fields are dropped on rewrite unless a caller opts into retention, which is specified and unbuilt ([#525](https://github.com/mas-bandwidth/schema/issues/525)); a retired name can be re-added and decode old bytes under a new meaning -([#441](https://github.com/mas-bandwidth/schema/issues/441)); `was` covers a -table's own fields and a table declaration, so a renamed variant or arm -orphans its data -([#442](https://github.com/mas-bandwidth/schema/issues/442)); and the id-table +([#441](https://github.com/mas-bandwidth/schema/issues/441)); and the id-table wire is the C++ reference's and the tool's, with the eight ports still writing the previous form ([#511](https://github.com/mas-bandwidth/schema/issues/511) to @@ -262,7 +259,7 @@ sourced across five columns. | feature | schema | Protocol Buffers | FlatBuffers | Cap'n Proto | Avro | |---|---|---|---|---|---| | Add, remove or reorder a field freely | ✅ [s19] | ✅ [83] | 🔶 [84] | 🔶 [85] | 🔶 [86] | -| Rename a field without orphaning stored data | 🔶 [s20] | ✅ [88] | ✅ [89] | ✅ [90] | 🔶 [91] | +| Rename a field without orphaning stored data | ✅ [s20] | ✅ [88] | ✅ [89] | ✅ [90] | 🔶 [91] | | Change a field's type with no silent misdecode | 🔶 [s21] | ❌ [92] | ❌ [93] | ❌ [94] | ✅ [95] | | Change a declared default without reinterpreting stored bytes | 🔶 [s22] | 🔶 [97] | ❌ [98] | ❌ [94] | ✅ [99] | | Unknown fields preserved through a read-and-rewrite | ❌ [s23] | ✅ [101] | 🔶 [102] | 🔶 [100] | ❌ [103] | @@ -304,7 +301,7 @@ sourced across five columns. | feature | schema | Protocol Buffers | FlatBuffers | Cap'n Proto | Avro | |---|---|---|---|---|---| -| Rename an enum variant, union arm or named type without orphaning stored data | 🔶 [s40] | ✅ [88] | ✅ [89] | 🔶 [163] | 🔶 [91] | +| Rename an enum variant, union arm or named type without orphaning stored data | ✅ [s40] | ✅ [88] | ✅ [89] | 🔶 [163] | 🔶 [91] | | A retired name or number cannot be silently reused | ❌ [s41] | ✅ [165] | 🔶 [166] | 🔶 [167] | ❌ [168] | | Defined widening or promotion of a field's type on read | ❌ [s42] | 🔶 [170] | 🔶 [93] | 🔶 [171] | ✅ [172] | | The writer's schema is recoverable at read time | ❌ [s43] | 🔶 [174] | 🔶 [175] | ❌ [61] | ✅ [176] | @@ -316,9 +313,9 @@ sourced across five columns. - **Renames beyond fields.** Protocol Buffers and FlatBuffers rename a variant or a named type freely because their wires carry numbers instead of names, - and Cap'n Proto does where the type pins an explicit id; schema's `was` is - a field's and a table declaration's today, not yet a variant's or an - arm's [s40]. + and Cap'n Proto does where the type pins an explicit id; schema keeps the + data through every one of those renames, but asks the author for one + attribute, `was`, because its wire carries the name's hash [s40]. - **Retired names.** Protocol Buffers' `reserved` is the right mechanism and schema lacks it, so a removed name can be re-added years later and decode old bytes under a new meaning [s41]. @@ -422,7 +419,7 @@ list at the end, with the section that establishes the claim. - s17. Table wire. Eight-byte region references and 64-bit cook part lengths, with no aggregate ceiling in the format (SPEC-TABLES §6.3, §7.1). 🔶 for two reasons, one in the format and one in the implementations. The tolerant wire's own lengths, counts, indices and references are canonical LEB128 with 64 bits of capability, so no body, count or index has a ceiling below `2^64 − 1` (§3) — but that wire is the reference's and the tool's, and the eight ports still write the previous form ([#511](https://github.com/mas-bandwidth/schema/issues/511) to [#518](https://github.com/mas-bandwidth/schema/issues/518)). And the two accelerators are read out of a `byte[]` in the managed ports, which stops at 2 GiB: SPEC-TABLES' ladder states it as the one Java divergence that costs a stated requirement, C# meets the same `int` ceiling on its span overload and answers it with the pointer form beside it, and the foreign-memory overload is a named follow-on (§15). - s18. One standard, one corpus, and byte identity is proven where nine backends produce bytes. The packet wire is bit-for-bit compatible across all nine runtimes, pinned in CI with shared golden bytes, and a compiler change that breaks a wire golden is stop-the-line (SPEC §1, §3.2, §7.2). On the table wire the `wire` surface byte-compares every registered leg's `Save` against one golden over the FIXED class, and `measure == save at exact capacity` is a hard invariant held by a mandatory battery (SPEC-TABLES §9). 🔶 because the variable, message, wide and blob classes have one writer: the eight ports produce no bytes for those cases and answer ABSENT per case, so no cell claims agreement it did not test. - s19. Table wire. Name identity: add anywhere, remove, reorder, each reported by the read instead of refused (SPEC-TABLES §4, §5). Carried by all nine, on the `wire` and `report` surfaces over the fixed class. Whether a retired name can be silently reused is [s41], not this row. -- s20. `| was = "old"` keeps the wire id, and a bare rename is a removal and an addition the compiler cannot see. The committed baseline warns on that pair: `internal/baseline/diff.go`'s `renamePair` reports a wire id removed and a wire id added in one edit and names the `was` and `json =` spellings that keep the data ([#444](https://github.com/mas-bandwidth/schema/issues/444), SPEC-TABLES §5, §18.2). It warns and never refuses, because two independent edits in one commit are legitimate. 🔶 because `was` covers a table's own fields and a table declaration today: variants and arms are [#442](https://github.com/mas-bandwidth/schema/issues/442) and the fields of a `type` a table reaches are [#478](https://github.com/mas-bandwidth/schema/issues/478). +- s20. `| was = "old"` keeps the wire id, and a bare rename is a removal and an addition the compiler cannot see. The committed baseline warns on that pair: `internal/baseline/diff.go`'s `renamePair` reports a wire id removed and a wire id added in one edit and names the `was` and `json =` spellings that keep the data ([#444](https://github.com/mas-bandwidth/schema/issues/444), SPEC-TABLES §5, §18.2). It warns and never refuses, because two independent edits in one commit are legitimate. `was` covers every name the table wire carries: a table's own fields, a table declaration, enum variants, union arms and the fields of a `type` a table reaches (SPEC-TABLES §5). - s21. A changed kind reads as the default and is counted `kind_mismatch` (SPEC-TABLES §4), in all nine over the fixed class. The respellings a shared kind once left open are closed: an enum has kind `30` and a pointer index kind `17`, so an enum-typed field respelled as its raw `uint16`, and a `*T` respelled as a `uint32`, are ordinary counted mismatches in both directions (§3, §3.1, §4.1). It is still not the whole story, and SPEC-TABLES §4.1 says so: a field's REFERENT dropped or swapped for a twin that cannot stand in for it, and a `fixed` field's `F` moved under the same storage width, each keep the kind and change what the bytes mean with no counter to fire. Both are guarded only by the committed baseline (§18). - s22. Silent on the wire, refused at compile time by the committed baseline (SPEC-TABLES §4.1, §18.2). 🔶 because the baseline is opt-in by design, "no file, no check" (§18.1). A unit that declares a table and holds no baseline draws a one-line stderr notice from `schema check` naming what is unguarded and the command that commits one ([#445](https://github.com/mas-bandwidth/schema/issues/445), §18.1). The notice never touches the exit code, so the limitation stands. - s23. Decided and not built. The DEFAULT is a drop, by decision, with the read report counting what a rewrite would lose under the never-clobber rule ([VERSIONING.md](VERSIONING.md)). Retain-unknown is the opt-in beside it, a REGION round trip whose buffer the caller sizes and owns, covering unknown FIELDS and no other class and no other counter, with `retained` and `retain_lost` on the same report struct (SPEC-TABLES §6.6). No port carries it ([#525](https://github.com/mas-bandwidth/schema/issues/525)). @@ -443,7 +440,7 @@ list at the end, with the section that establishes the claim. - s37. JSON in and out by one generic walk over the descriptors, `| json = "key"`, the read report on the way in, `&node` for a shared node (SPEC-TABLES §16). The walk itself is carried by all nine (PORTING M9), and each backend's is compared unit by unit. 🔶 because the text surfaces are where the eight ports answer ABSENT, on `json-read` and `json-write` alike: the message, wide, blob and variable classes have no port text form ([the conformance contract](../test/conformance/README.md), SPEC-TABLES §15). - s38. Decided and not built. The design is the OPT-IN `///` block: a contiguous run of `///` lines binding to the declaration, field, variant or arm below it, carried verbatim into the `doc` descriptor column beside a `tags` column and into ordinary line comments in the generated code, with `| doc = "..."` refused by name so one text has one spelling (SPEC §4.1, §4.11, SPEC-TABLES §8.1). A plain `//` above the same item stays a comment and reaches nothing, which is what keeps a tree of working notes out of every game's binary. No backend emits either column ([#523](https://github.com/mas-bandwidth/schema/issues/523)). - s39. Nine languages byte-identical on the packet wire in CI (SPEC §1). Tables are carried under [#366](https://github.com/mas-bandwidth/schema/issues/366). -- s40. A table declaration takes `was` ([#396](https://github.com/mas-bandwidth/schema/issues/396)): the node type id every stored record carries is the hash of the first name, so a renamed pointer target still reads, and the rename moves neither id (SPEC-TABLES §5). 🔶 because a variant and an arm are [#442](https://github.com/mas-bandwidth/schema/issues/442), before 3.0.0: a renamed variant is a new variant today. +- s40. A table declaration takes `was` ([#396](https://github.com/mas-bandwidth/schema/issues/396)): the node type id every stored record carries is the hash of the first name, so a renamed pointer target still reads, and the rename moves neither id (SPEC-TABLES §5). An enum variant and a union arm take it on their own line ([#442](https://github.com/mas-bandwidth/schema/issues/442)), and so does a field of a `type` a table reaches ([#478](https://github.com/mas-bandwidth/schema/issues/478)): the id is the old name's hash in every case, and a `flags` variant, whose identity is its bit, needs none. - s41. Decided and not built. The retired-names ledger in the baseline is [#441](https://github.com/mas-bandwidth/schema/issues/441), before 3.0.0; nothing today marks a removed name retired, so it can be re-added and decode old bytes under a new meaning. - s42. Decided and not built. An integer kind read into a WIDER integer kind of the same signedness, and `f32` read into `f64`, decode EXACTLY and count `widened`; the signed ladder is kinds `2`, `3`, `4`, `5`, `18` and the unsigned one `6`, `7`, `8`, `9`, `19`, and every other pair stays `kind_mismatch` because each is a value the wider kind would accept and the schema does not mean (SPEC-TABLES §4). The path runs FORWARD only — an old build meeting the wider kind narrows and reads its default — so the baseline refuses the edit like any kind change. Nothing counts `widened` in any language yet ([#523](https://github.com/mas-bandwidth/schema/issues/523)). - s43. Declined, with the reason in the table above. diff --git a/docs/SPEC-TABLES.md b/docs/SPEC-TABLES.md index c16e97fb4..65d2fb923 100644 --- a/docs/SPEC-TABLES.md +++ b/docs/SPEC-TABLES.md @@ -1328,9 +1328,10 @@ already spends what it would buy**, and each is refused by name (§11): nothing and a framing case the wire does not have (§2.3 refuses `?` on a union field for the same reason). What an optional arm's case actually wants is the payload-free arm above, which says "this arm, no value"; -- **`was` and `json`** — arms already evolve by name (§5), and the arm's - name is its key in the text form (§16.2); each is the field feature one - level down and waits for a case (§15); +- **`json`**: the arm's name is its key in the text form (§16.2), so a + key that is not the name is the field feature one level down and waits + for a case (§15). `was` an arm DOES take, a payload-free arm included: + it is the arm's rename, and the arm's id is the old name's hash (§5); - **an enum-keyed array `[E]T`** — a keyed body elides slots by name (§3.2) and its `None` slot wants its rule stated before it is wire, exactly as `[E]*T` and `[E]Body` do (§15); @@ -5444,7 +5445,7 @@ the one the committed baseline (§18) exists to refuse: Everything else is either reported or safe. Fields may be added, removed, reordered and renamed under `was`; enum variants and union arms may be -added anywhere, removed and reordered; array bounds may move; a field may +added anywhere, removed, reordered and renamed under `was`; array bounds may move; a field may change between `T` and `?T` — all of it either invisible to the wire or counted in the report. Moving a field to or from `*T` is a kind change and is counted (§3.1). @@ -5544,7 +5545,11 @@ only. | a TABLE renamed where it is a POINTER TARGET | **not silent**: a table's own name is its node's type id on the wire (§5), so every node of the old name is unnameable — skipped by its length and counted `unknown`, with every pointer to it reading null (§3.1) | as the row above | **moves** | | a TABLE renamed under `was` (§5) | silent, and nothing is lost: the type id is the old name's hash | passes, and the file records the declared name beside the wire name | no: the record line and every referent carry the wire name | | a TABLE renamed a SECOND time, the new `was` naming the INTERMEDIATE spelling | `unknown` for every stored record of the table, and every pointer to it reads null | **refuses**: `was` names the first wire name, forever (§5) | **moves** | -| a `type`'s FIELD renamed, where `was` is refused (SPEC.md §4.2) | `unknown` on the table wire, whose field id is the name's hash | passes in silence | **moves**, and through the protocol id as well (SPEC.md §3.1) | +| a `type`'s FIELD renamed under `was`, the type reached by a table closure (§5) | silent, and nothing is lost: the field id is the old name's hash | passes, and the edit that adds the `was` hints the `json =` pairing | no, and the protocol id stands still too: the projection carries the wire name | +| a `type`'s FIELD renamed BARE | `unknown` on the table wire, whose field id is the name's hash, and the field reads its default | **warns**: a removal and an addition in one body in one edit is the shape of a rename (§18.2) | **moves**, and through the protocol id as well (SPEC.md §3.1) | +| an enum VARIANT or a union ARM renamed under `was` (§5) | silent, and nothing is lost: the id is the old name's hash | passes, and the file records the alias beside the id | no | +| an enum VARIANT or a union ARM renamed BARE | `unknown`: a stored value reads `None`, a stored body reads `None`, a keyed slot is dropped | **warns** that the old name was removed | **moves** | +| a VARIANT or an ARM renamed a SECOND time, the new `was` naming the INTERMEDIATE spelling | `unknown` for every stored value or body | **refuses**: `was` names the first wire name, forever (§5) | **moves** | ### 4.2 The read is the verifier: the wire fuzzer @@ -5827,11 +5832,26 @@ values and a union's arms ride under their own name hashes (§3), so: - **Variants may be added anywhere, removed, and reordered** — the edit §4's field rule always allowed, now true of a vocabulary too. What a reader cannot name reads as `None` (enum) or empty (union), counted. -- **Renaming a variant is a wire change**, and there is no `was` for one. - Every other edit a vocabulary takes already rides by name, so a rename is - the one edit left to cover, and covering it is a named follow-on (§15). - A renamed variant is a NEW variant, and old data carrying the old name - reads as unknown. Rename a variant only when that is what you mean. +- **A rename declares the old name with `was`, as a field's does.** An + enum variant and a union arm, a payload-free arm included, take the + attribute on their own line: `Argent | was = "Silver"`, `shield Ward | was + = "ward"`, `pong | was = "ping"`. The id is the hash of the OLD name, so a + stored value or body written under it reads in silence, and every id + derivation reads the alias: the enum identity tables, the arm switches, + the keyed slots of an enum-keyed array, the announced vocabulary (§3.3), + the baseline and both ids (§20.4). The refusals are the field's: `was` + naming the variant's own name, `was = ""`, an alias colliding with a live + variant's id, and `was` on a variant of an enum or an arm of a union that + no table closure reaches, which has no wire identity for it to keep. A + `flags` variant refuses it too: a mask is positional, its identity is its + bit, and a rename keeps every stored bit. A variant renamed BARE is a NEW + variant, and old data carrying the old name reads as unknown. +- **A field of a `type` a table closure reaches takes `was` too.** Such a + type rides as a nested body whose fields carry ids (§2), so a bare rename + orphans every stored body exactly as a table field's rename does, and the + attribute keeps the id on the same terms, `json =` pairing included. A + field of a type NO table reaches refuses it naming the type: the packet + wire is positional and there is nothing to keep. - **Two variants of one enum, or two arms of one union, whose ids collide are a compile error naming both** — the field rule, applied to the vocabulary. Scoped to the TABLE CLOSURE: the packet wire identifies a @@ -8985,7 +9005,8 @@ in build version (§20.5). own name (§5). - Id collisions, hash or `was`-induced, a table's alias colliding with a live table's type id included (§5). -- `was` outside a table body, and `was` on a `type` declaration (§5). +- `was` outside a table closure, on a `flags` variant, and on a `type` + declaration (§5). - A string, bytes or flags default that is not the field's own literal: a string past the capacity, a string that is not UTF-8, a brace list on a field that is not `flags`, a name in it that is not a variant of the @@ -10658,9 +10679,9 @@ inspects everything in the schema built: would both be `L = 0`. What it would take is one payload shape the wire does not have, a presence byte under the arm's length ahead of the value, and a case the payload-free arm does not already answer. -- **`was` AND `json` ON AN ARM** (§2.6): an arm rename that keeps the arm - id, and an arm key in the text form that is not the arm's name — each is - the field feature one level down, and each waits for a case. `= default` +- **`json` ON AN ARM** (§2.6): an arm key in the text form that is not the + arm's name is the field feature one level down, and it waits for a case. + `= default` ON AN ARM waits with SPEC §5's untaken-branch question: zero at selection is the pinned rule, and a default at selection would be its first exception. @@ -11786,7 +11807,8 @@ RANGE (`min=` and `max=`), presence of an optional, a fixed field's `F` default as exact canonical text (a fixed default as the RAW integer its storage holds, a string or bytes default as `bytes:` and its bytes in hex, so a space in a default cannot split the token, and a flags default as the -mask its names spell), and the `was` alias; then each enum's variants in order with their ids, each flags' +mask its names spell), and the `was` alias; then each enum's variants in order with their ids and, +on a renamed variant, `was=` and its alias, each flags' variants in positional order, and each union's arms in order with their ids and their own wire facts. @@ -11800,8 +11822,11 @@ FIELD tokens for what it is, in the field line's own column order and judged by the field line's own rules: `kind=`, then where the fact exists `elem=`, the `enum=` / `flags=` / `union=` / `type=` that names its referent, `array=`, `bound=`, `frac=`, `size=`, `min=` and `max=`. A tightened arm -range warns exactly as a field's does, and an arm records no default and no -`was` because an arm takes neither (§2.6). The three spellings are DISJOINT, +range warns exactly as a field's does, an arm records no default because an +arm takes none (§2.6), and a renamed arm records `was=` and its alias on +every spelling, judged on nothing: the id is the identity, and the alias is +what lets the check name the spelling a second rename should have used +(§18.2). The three spellings are DISJOINT, which is what makes an arm moved between a body and anything else a REFUSAL rather than a silence (§18.2): the token SET moves, and an added or removed judged token refuses on the same rule a changed one does. @@ -11939,7 +11964,9 @@ committed file whenever one is there, and: was ever written under, and the refusal names both spellings and the one that is correct. A TABLE's `was` is held to the same rule over the declared name the file recorded beside its wire name (§18.1): a second - rename aimed at that name is refused, naming the first. + rename aimed at that name is refused, naming the first. A VARIANT's and + an ARM's `was` are held to it over the `was=` the file recorded on their + lines, and a field of a `type` a table reaches is a field here. - **WARNS** — an array bound or a string/bytes capacity shrunk, a map's KEY bound included, **and an unbounded `[]T` given a bound** (§2.9), which is a capacity shrunk from every count to N; a field changed between a map and the @@ -11962,7 +11989,7 @@ committed file whenever one is there, and: and a field that already pairs its key is told nothing. - **PASSES, in silence** — everything the wire absorbs: fields added, removed, reordered or renamed under `was`; enum variants and union arms - added anywhere; flags variants APPENDED at the end; bounds, capacities and + added anywhere or renamed under `was`; flags variants APPENDED at the end; bounds, capacities and ranges grown, **a bounded array's bound REMOVED for `[]T` included** (§2.9), which is the largest growth there is; a bounded array made fixed or the reverse; a field moved @@ -13459,8 +13486,10 @@ wrong fails to build instead of degrading. - **a specified default changed, added or removed**, and **a declared range tightened, loosened, added or removed** — group 3, and the reason group 3 exists; -- **an `enum` variant inserted, removed, reordered or renamed**; a `union` arm - inserted, removed, reordered or renamed; **a `flags` variant inserted, +- **an `enum` variant inserted, removed, reordered or renamed bare**; a + `union` arm inserted, removed, reordered or renamed bare, where a rename + under `was` moves nothing because the projection carries the wire name + (§5); **a `flags` variant inserted, removed, reordered or renamed in place**, because a variant's BIT POSITION is what a stored and a cooked mask mean and nothing on the wire can report a move (§20.1, §4.1). It is the one group-3 fact the read report cannot see, @@ -13589,7 +13618,8 @@ a second digest. line sees: a specified default changed; **a declared range tightened**; **a `bits(N)` narrowed within one storage width** — the case where the implied range moves and the storage kind, the size and the wire id do not; an `enum` - variant renamed; two `enum` variants swapped; **a `union` arm RENAMED**; + variant renamed bare; two `enum` variants swapped; **a `union` arm RENAMED + bare**, a rename under `was` being the control that moves neither id; **a `flags` variant REORDERED, and one RENAMED**. **Whether a row also rides group 1 now depends on REACHABILITY, and that is the point of the scoping** (SPEC.md §3.1): a vocabulary a `type` reaches rides group 1 as diff --git a/docs/SPEC.md b/docs/SPEC.md index d5b5320ce..75e680f9b 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -552,10 +552,11 @@ UnionVariant = ident [ ArmType ] [ AttrSection ] NL . // AN ARM IS // that type takes. A BARE NAME is an // arm with NO PAYLOAD, and it takes a // qualification of its own — tags - // only, since it shapes no value. - // No "= Default", no "?", no - // was/json: each is refused at the - // arm (§4.8, SPEC-TABLES.md §2.6) + // and `was`, since it shapes no + // value. No "= Default", no "?", no + // json: each is refused at the arm + // (§4.8, SPEC-TABLES.md §2.6). `was` + // is the arm's rename (§4.2) ArmType = [ "[" Bound "]" ] Scalar . // the field Type without its "?". // An arm that names neither a // declared `type` nor nothing at all @@ -568,8 +569,10 @@ ConstExpr = IntExpr | FloatExpr . Enum = "enum" ident ( VariantList | AttrSection NL VariantList ) NL . VariantList = "{" [ Variant { VariantSep Variant } [ VariantSep ] ] "}" . -Variant = ident [ AttrSection ] . // the qualification carries TAGS - // and nothing else (§4.2) +Variant = ident [ AttrSection ] . // the qualification carries TAGS, + // and on an enum variant `was`, + // the rename (§4.2); a flags + // variant takes tags alone VariantSep = "," | NL . // a comma, or the newline that // ends a qualified variant's line; // a trailing separator is OK @@ -895,9 +898,10 @@ sequence uint16 (required — §4.3); enum and flags declarations take `max`; type declarations take the `cpp_native`/`cpp_include` pair (below); a field of a **table** body takes `was` (below) and `json` (SPEC-TABLES.md §16.4), a - **table** declaration takes `was` (below), and a **union** declaration, a - **constant**, an enum or flags **variant** and a union **arm** take no - valued key at all. **The + field of a **type** that a table closure reaches takes `was` and `json` + on the same terms, a **table** declaration, an **enum variant** and a + union **arm** take `was` (below), and a **union** declaration, a + **constant** and a **flags variant** take no valued key at all. **The VALUELESS half is open at every one of them** — that is the tag, below. - **A bare identifier that spells a known valued key is refused by name**, never taken as a tag: `| min` draws "min takes a value: write min = 0". The @@ -913,15 +917,27 @@ sequence uint16 word named, so `| table` draws "table is a reserved word" rather than becoming a tag. **A repeated tag on one line is refused by name** too, and so is a tag that repeats a valued key already on the line. -- **`was = "old_name"` — the rename attribute, table bodies only** +- **`was = "old_name"` — the rename attribute, table closures only** (SPEC-TABLES.md §5). A table field's wire id is the hash of its name, so a bare rename would orphan every byte ever written under the old one; `was` keeps the old identity through the rename: `speed float32 | was = - "velocity"`. It takes the old name as a QUOTED STRING. On a `type` field it - is refused by name: the packet wire is positional, so a rename orphans no - stored value and there is no identity for `was` to carry. It is not a free - edit — field NAMES ride in the projection (§3.1), so renaming a `type` - field moves the protocol id and both sides redeploy together. **A `table` + "velocity"`. It takes the old name as a QUOTED STRING. **A field of a + `type` that a table closure reaches takes it on the same terms**: such a + type rides the table wire as a nested body whose fields carry ids + (SPEC-TABLES.md §2), so a rename there orphans stored data exactly as a + table field's does, and `was` keeps the id. On a field of a type NO table + reaches it is refused naming the type: the packet wire is positional, so + a rename there orphans no stored value and there is no identity for `was` + to carry. A bare rename is not a free edit on either wire — field NAMES + ride in the projection (§3.1), so renaming a `type` field bare moves the + protocol id and both sides redeploy together, where a rename under `was` + projects the wire name and moves nothing. **An enum variant and a union + arm take the same attribute**, `Argent | was = "Silver"` and `shield Ward + | was = "ward"`, a payload-free arm included, `pong | was = "ping"`: a + variant and an arm ride the table wire under the hash of their name + (SPEC-TABLES.md §3), and the alias keeps that id. A `flags` variant + refuses it by name: a mask is positional, a variant's identity is its + bit, and a rename there keeps every stored bit. **A `table` declaration takes the same attribute**, `table Ship | was = "Vessel"`: a table's name is its node type id on the table wire (SPEC-TABLES.md §3.1), and the alias keeps that id through the rename, so every stored record of @@ -1497,8 +1513,9 @@ union Value lower_snake, unique within the union), then the arm's type, then the value-shaping attributes that type takes: `| min`, `| max`, a compressed float's range and resolution — plus TAGS, which every declared item takes - (§4.2), including a bare-name arm, whose qualification section can hold - nothing else. **A row that is a BARE NAME is an arm with no payload** + (§4.2), and `was`, the arm's rename (§4.2), including a bare-name arm, + whose qualification section can hold nothing else. **A row that is a BARE + NAME is an arm with no payload** (below). **What a row may not take is SPEC-TABLES.md §2.6's list**, each refused by name. A union FIELD likewise takes no valued attribute and no `= default` (it zero-initializes to None, joining diff --git a/docs/USAGE.md b/docs/USAGE.md index 4a830174d..b049425cb 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -899,8 +899,10 @@ enum Grade { Bronze, Silver, Gold } // v2 — every stored Gold still loads Gol ``` A variant a reader has no name for loads as `None` (enum) or empty (union) -and counts as `unknown` — never as its neighbour. There is no `was` for a -variant: renaming one is a new variant, and old data reads as unknown. +and counts as `unknown` — never as its neighbour. A rename declares the old +name with `was` on the variant's own line, `Argent | was = "Silver"`, and +the stored values keep reading (see renaming below). Renaming one bare is a +new variant, and old data reads as unknown. **`flags` is the exception: append at the END.** A mask rides as its raw bits, so a variant's identity is its BIT POSITION. Inserting or reordering @@ -1988,8 +1990,9 @@ union Value `ping` selects and carries nothing, which is not the union's `None`: `None` says no arm was selected. What an arm may NOT take is a specified default, a -`?`, a `was`, a `json`, an enum-keyed `[E]T`, an `if` guard, a `map` or an -unbounded `[]T` — each refused by name (SPEC-TABLES.md §2.6). +`?`, a `json`, an enum-keyed `[E]T`, an `if` guard, a `map` or an +unbounded `[]T` — each refused by name (SPEC-TABLES.md §2.6). `was` it takes, +`pong | was = "ping"`: the arm's rename (see renaming below). ### Pointers: `next *Node` @@ -3034,6 +3037,38 @@ writing the old id, and neither the protocol id nor the build version moves. `was` on a `type` declaration is refused: a type rides by value and has no node type id to keep. +Every other name the table wire carries renames the same way. An enum +variant and a union arm ride under the hash of their name, and so does each +field of a `type` a table holds by value, so each takes `was` on its own +line: + +``` +enum Grade +{ + Bronze, + Argent | was = "Silver" + Gold +} + +union Effect +{ + shield Ward | was = "ward" + pong | was = "ping" +} + +type Buff +{ + mult float32 = 1.0 | was = "multiplier" +} +``` + +A qualified variant ends its line, so the newline is its separator. Stored +values naming `Silver` load as `Argent`, stored `ward` bodies load into +`shield`, an enum-keyed `[Grade]int32` keeps its `Silver` slot, and neither +id moves. A `flags` variant refuses `was`: a mask is positional, so a rename +there keeps every stored bit already. So does a variant, an arm or a type +field that no table reaches: there is no wire identity for it to keep. + ### The tables baseline: catching the edits the wire cannot report Think of a save game. A player's file was written two years ago by a build diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 4a27debaf..8238e68bf 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -33,9 +33,9 @@ what proves them across releases, #463, named in its section below. table are identified on the table wire by the hash of their name. Add anywhere, remove, reorder. A rename that must keep its data declares the old name with `was`, and a `was` moves nothing anywhere. `was` is an - attribute of a table's own fields and of a table declaration today. - Variants and arms (#442), and the fields of a `type` that a table reaches - (#478), get it before 3.0.0. + attribute of a table's own fields, of a table declaration, of an enum + variant and a union arm, and of the fields of a `type` that a table + reaches. Every name the table wire carries can be renamed under it. 4. **Two ids and no third.** The *protocol id* versions the packet wire and is the only thing two peers compare before they talk. The *build version* versions the cooked and blocked forms and addresses every cooked asset. An @@ -283,7 +283,11 @@ does today. | a field renamed under `was` | nothing | passes; the edit that adds the `was` hints the `json =` pairing | **nothing**: keyed by wire id, not source name | | a field renamed a second time, the new `was` naming the INTERMEDIATE spelling instead of the first | `unknown` on every old file; the new id was never written to | **refuses** | moves | | a field renamed bare | `unknown` on every old file; the new field reads its default | warns: a removal and an addition in one table in one edit is the shape of a rename | moves | -| a field of a `type` that a table reaches, renamed | `unknown` on every old file; `was` is refused there today (#478) | passes, in silence | moves, and so does the protocol id | +| a field of a `type` that a table reaches, renamed under `was` | nothing: the field id is the old name's hash (SPEC-TABLES.md §5) | passes; the edit that adds the `was` hints the `json =` pairing | **nothing**, and the protocol id does not move either | +| a field of a `type` that a table reaches, renamed bare | `unknown` on every old file, and the field reads its default | warns: a removal and an addition in one body in one edit is the shape of a rename | moves, and so does the protocol id | +| an enum variant or a union arm renamed under `was` | nothing: the id is the old name's hash (SPEC-TABLES.md §5) | passes, and the file records the alias beside the id | **nothing**, and the protocol id does not move either | +| an enum variant or a union arm renamed bare | `unknown` on every old file: the value or the union reads `None` | warns that the old name was removed | moves | +| a variant or an arm renamed a second time, the new `was` naming the intermediate spelling | `unknown` on every old file, and the new id was never written to | **refuses** | moves | | a scalar's default changed | **silent**: the same bytes mean something else | **refuses** | moves (a meaning fact) | | a string, bytes or flags default changed | **silent**: an absent field reads as the new default | **refuses**, as a scalar's default change does (SPEC-TABLES.md §18.2) | moves (a meaning fact) | | a bound raised or lowered, a capacity or array bound grown | `clamped` where a stored value exceeds it | passes; warns on a shrink | moves | @@ -956,9 +960,10 @@ still open. table could have had is gone rather than excepted. The residue is that a table-only enum or union is guarded by the tables baseline and the build version and no longer by the connect gate. -- **A field of a `type` that a table reaches cannot be renamed safely - today** (#478): `was` is refused there, and a bare rename orphans every - stored value. +- **A field of a `type` that a table reaches renames under `was`** exactly + as a table's own field does (SPEC-TABLES.md §5): the nested body's field + ids are name hashes, the alias keeps the id, and a bare rename orphans + every stored value and warns in the baseline. - **`bits(N)` grows freely, and across a storage width it now costs a counter rather than the values**: `bits(9)` to `bits(16)` is one kind and is silent, and `bits(8)` to `bits(9)` moves kind `6` to kind `7`, which the @@ -1049,8 +1054,6 @@ repository not yet behind it. The 3.0.0 release holds the list at zero. previous release and the new one, byte-compared under an equal id. - #432: the cook triple, and the byte-order sentences in five places. - #441: the retired-names ledger. -- #442: `was` for variants and arms; #478: `was` for the fields of a `type` - that a table reaches. - #446: the evolution table's fixtures. - #540: the `[E.Max]T` refusal in a table body, which SPEC-TABLES.md §2.4 and §11 state and the checker does not make, so the positional spelling still diff --git a/internal/format/wasrows_test.go b/internal/format/wasrows_test.go index 63c73c219..f17d849f6 100644 --- a/internal/format/wasrows_test.go +++ b/internal/format/wasrows_test.go @@ -25,7 +25,9 @@ func TestFormatsVariantAndArmWas(t *testing.T) { } } -func contains(s, sub string) bool { return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0) } +func contains(s, sub string) bool { + return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0) +} func indexOf(s, sub string) int { for i := 0; i+len(sub) <= len(s); i++ { diff --git a/test/conformance/cpp/main.cpp b/test/conformance/cpp/main.cpp index e9a7b0282..ac1a080cc 100644 --- a/test/conformance/cpp/main.cpp +++ b/test/conformance/cpp/main.cpp @@ -68,6 +68,8 @@ #include "G1Table.h" #include "W1Table.h" #include "W2Table.h" +#include "R1Table.h" +#include "R2Table.h" // the BYTE BUFFER unit (docs/SPEC-TABLES.md §2.5): a blob at its used size, // pointed at — a variable root like any pointered one #include "AssetsTable.h" @@ -265,6 +267,8 @@ static const Codec codecs[] = { CODEC( "tbla2", tbla2, Root ), CODEC( "tblk1", tblk1, Root ), CODEC( "tblk2", tblk2, Root ), + CODEC( "tblr1", tblr1, Cfg ), + CODEC( "tblr2", tblr2, Cfg ), CODEC( "scalars", scalardemo, SimState ), CODEC( "tblscalars2", scalardemo2, SimState ), // the MESSAGE FORM's units (docs/SPEC-TABLES.md §3.3): their FILE-form diff --git a/test/tables/main.cpp b/test/tables/main.cpp index 5db3ac40a..7dbf00430 100644 --- a/test/tables/main.cpp +++ b/test/tables/main.cpp @@ -43,6 +43,8 @@ #include "G1Table.h" #include "W1Table.h" #include "W2Table.h" +#include "R1Table.h" +#include "R2Table.h" #include "ScalarsTable.h" #include "wirebuilder.h" @@ -8763,6 +8765,97 @@ static void test_was_rows() } } +// ---- the `was` rows, second half: a variant, an arm and a type's field renamed under was ---- +// +// docs/SPEC-TABLES.md §5. An enum value, a union arm and a field of a type a +// table reaches by value ride under the hash of their name, so a rename would +// orphan every stored value, body and field. R2 renames Silver, ward, ping and +// Buff.multiplier under `was`, and R1's bytes and R2's are one wire. + +template +static int64_t save_wasrows_cfg( uint8_t * buffer, int64_t capacity, bool ping ) +{ + typename NS::Cfg cfg; + cfg.grade = NS::silver; + if ( ping ) + { + cfg.effect.type = NS::ping; + } + else + { + cfg.effect.type = NS::ward; + NS::ward_charge( cfg ) = 2.5f; + } + NS::buff_multiplier( cfg ) = 1.5f; + cfg.grades[0] = NS::silver; + cfg.grades[1] = NS::gold; + cfg.grades_count = 2; + cfg.tally[NS::silver] = 7; + return NS::CfgSave( cfg, buffer, capacity ); +} + +// each unit spells the renamed names its own way, and the ids are one +struct wasrows_r1 { using Cfg = tblr1::Cfg; + static constexpr tblr1::Grade silver = tblr1::Grade::Silver, gold = tblr1::Grade::Gold; + static constexpr tblr1::EffectType ward = tblr1::EffectType::Ward, ping = tblr1::EffectType::Ping; + static float & ward_charge( Cfg & c ) { return c.effect.ward.charge; } + static float & buff_multiplier( Cfg & c ) { return c.buff.multiplier; } + static int64_t CfgSave( const Cfg & c, uint8_t * buf, int64_t n ) { return tblr1::CfgSave( c, buf, n ); } }; +struct wasrows_r2 { using Cfg = tblr2::Cfg; + static constexpr tblr2::Grade silver = tblr2::Grade::Argent, gold = tblr2::Grade::Gold; + static constexpr tblr2::EffectType ward = tblr2::EffectType::Shield, ping = tblr2::EffectType::Pong; + static float & ward_charge( Cfg & c ) { return c.effect.shield.charge; } + static float & buff_multiplier( Cfg & c ) { return c.buff.mult; } + static int64_t CfgSave( const Cfg & c, uint8_t * buf, int64_t n ) { return tblr2::CfgSave( c, buf, n ); } }; + +static void test_wasrows_vocabulary() +{ + static uint8_t r1[1024], r2[1024], r1_ping[1024]; + const int64_t n1 = save_wasrows_cfg( r1, sizeof( r1 ), false ); + const int64_t n2 = save_wasrows_cfg( r2, sizeof( r2 ), false ); + const int64_t np = save_wasrows_cfg( r1_ping, sizeof( r1_ping ), true ); + CHECK( n1 > 0 && n2 > 0 && np > 0 ); + + // A `was` MOVES NOTHING (docs/SPEC-TABLES.md §5): the renamed unit writes + // the old unit's bytes, the variant id, the arm id, the keyed slot's id + // and the nested field's id included + CHECK( n1 == n2 && memcmp( r1, r2, (size_t) n1 ) == 0 ); + pin_table_golden( "r1_cfg", r1, n1 ); + pin_table_golden( "r2_cfg", r2, n2 ); + pin_table_golden( "r1_ping", r1_ping, np ); + + // THE CROSS READ, both directions, in silence: every renamed name lands + { + tblr2::Cfg out; + tblr2::TableReport report; + CHECK( tblr2::CfgLoad( out, r1, n1, &report ) ); + CHECK( report.unknown == 0 && report.kind_mismatch == 0 && report.malformed == 0 ); + CHECK( out.grade == tblr2::Grade::Argent ); + CHECK( out.effect.type == tblr2::EffectType::Shield && out.effect.shield.charge == 2.5f ); + CHECK( out.buff.mult == 1.5f ); + CHECK( out.grades_count == 2 && out.grades[0] == tblr2::Grade::Argent && out.grades[1] == tblr2::Grade::Gold ); + CHECK( out.tally[tblr2::Grade::Argent] == 7 && out.tally[tblr2::Grade::Gold] == 0 ); + static uint8_t again[1024]; + const int64_t re = tblr2::CfgSave( out, again, sizeof( again ) ); + CHECK( re == n1 && memcmp( again, r1, (size_t) n1 ) == 0 ); + } + { + tblr1::Cfg out; + tblr1::TableReport report; + CHECK( tblr1::CfgLoad( out, r2, n2, &report ) ); + CHECK( report.unknown == 0 && report.kind_mismatch == 0 && report.malformed == 0 ); + CHECK( out.grade == tblr1::Grade::Silver && out.effect.type == tblr1::EffectType::Ward ); + CHECK( out.effect.ward.charge == 2.5f && out.buff.multiplier == 1.5f && out.tally[tblr1::Grade::Silver] == 7 ); + } + // the PAYLOAD-FREE arm renamed: ping's id is what pong reads + { + tblr2::Cfg out; + tblr2::TableReport report; + CHECK( tblr2::CfgLoad( out, r1_ping, np, &report ) ); + CHECK( report.unknown == 0 && out.effect.type == tblr2::EffectType::Pong ); + } +} + int main() { test_golden_wire(); @@ -8819,6 +8912,7 @@ int main() test_pointer_null_and_empty(); test_pointer_reflection(); test_was_rows(); + test_wasrows_vocabulary(); test_lock_deterministic_on_dirty_heap(); test_depth_agrees_through_by_value_nesting(); diff --git a/testdata/conformance/tables/MANIFEST.txt b/testdata/conformance/tables/MANIFEST.txt index 68b0a5983..e01bbd8dd 100644 --- a/testdata/conformance/tables/MANIFEST.txt +++ b/testdata/conformance/tables/MANIFEST.txt @@ -59,6 +59,8 @@ unit tblk1 test/tables/K1.schema unit tblk2 test/tables/K2.schema unit tblw1 test/tables/W1.schema unit tblw2 test/tables/W2.schema +unit tblr1 test/tables/R1.schema +unit tblr2 test/tables/R2.schema instance root_full tabledemo RootConfig testdata/wire/tables/root_full.bin instance root_default tabledemo RootConfig testdata/wire/tables/root_default.bin @@ -787,3 +789,17 @@ instance w2_fleet tblw2 Fleet testdata/wire/tables/w2_fl report w1_fleet_as_w2 tblw2 Fleet testdata/wire/tables/w1_fleet.bin report w2_fleet_as_w1 tblw1 Fleet testdata/wire/tables/w2_fleet.bin report w1_fleet_default_as_w2 tblw2 Fleet testdata/wire/tables/w1_fleet_default.bin + +# THE VOCABULARY `was` ROWS (docs/SPEC-TABLES.md §5). r1_cfg holds an enum +# value, a union arm with a payload, a keyed slot and a nested type's field +# whose names R2 renames under `was`: Silver to Argent, ward to shield, +# Buff.multiplier to mult. r1_ping selects the payload-free arm R2 renames +# pong. r2_cfg is r1_cfg's value written by the renamed unit, byte-identical. +# The cross reads are silent, and `make tables-wasrows-negative-control` +# strips the four attributes and watches unknown count. +instance r1_cfg tblr1 Cfg testdata/wire/tables/r1_cfg.bin +instance r1_ping tblr1 Cfg testdata/wire/tables/r1_ping.bin +instance r2_cfg tblr2 Cfg testdata/wire/tables/r2_cfg.bin +report r1_cfg_as_r2 tblr2 Cfg testdata/wire/tables/r1_cfg.bin +report r1_ping_as_r2 tblr2 Cfg testdata/wire/tables/r1_ping.bin +report r2_cfg_as_r1 tblr1 Cfg testdata/wire/tables/r2_cfg.bin From 067b13a683ca7271ab6c93b0f51d54d7a444e2be Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sat, 5 Sep 2026 12:43:24 -0700 Subject: [PATCH 4/7] was rows: the pinned goldens and the generated conformance data (#442, #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 --- test/tables/R1.schema | 5 ++++- test/tables/R2.schema | 1 + testdata/conformance/tables/json/r1_cfg.json | 20 ++++++++++++++++++ testdata/conformance/tables/json/r1_ping.json | 18 ++++++++++++++++ testdata/conformance/tables/json/r2_cfg.json | 20 ++++++++++++++++++ testdata/conformance/tables/reports.txt | 3 +++ testdata/wire/tables/r1_cfg.bin | Bin 0 -> 133 bytes testdata/wire/tables/r1_ping.bin | Bin 0 -> 118 bytes testdata/wire/tables/r2_cfg.bin | Bin 0 -> 133 bytes 9 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 testdata/conformance/tables/json/r1_cfg.json create mode 100644 testdata/conformance/tables/json/r1_ping.json create mode 100644 testdata/conformance/tables/json/r2_cfg.json create mode 100644 testdata/wire/tables/r1_cfg.bin create mode 100644 testdata/wire/tables/r1_ping.bin create mode 100644 testdata/wire/tables/r2_cfg.bin diff --git a/test/tables/R1.schema b/test/tables/R1.schema index 248379b95..6aa8bba71 100644 --- a/test/tables/R1.schema +++ b/test/tables/R1.schema @@ -3,7 +3,9 @@ // by value, each renamed in R2 under `was`. Every one of them rides the table // wire under the hash of its name, so a bare rename would leave every stored // value, body and field one the reader cannot name. Distinct packages so both -// generations compile into one test binary. +// generations compile into one test binary. The union's scalar arm keeps it a +// table-closure construct with no packet wire, which is where a payload-free +// arm is C++'s (SPEC §4.8). package tblr1 enum Grade { Bronze, Silver, Gold } @@ -28,6 +30,7 @@ union Effect boost Boost ward Ward ping + count int32 } table Cfg diff --git a/test/tables/R2.schema b/test/tables/R2.schema index b74a59b43..9b3d6cbf5 100644 --- a/test/tables/R2.schema +++ b/test/tables/R2.schema @@ -32,6 +32,7 @@ union Effect boost Boost shield Ward | was = "ward" pong | was = "ping" + count int32 } table Cfg diff --git a/testdata/conformance/tables/json/r1_cfg.json b/testdata/conformance/tables/json/r1_cfg.json new file mode 100644 index 000000000..15fb1f681 --- /dev/null +++ b/testdata/conformance/tables/json/r1_cfg.json @@ -0,0 +1,20 @@ +{ + "grade": "Silver", + "effect": { + "ward": { + "charge": 2.5 + } + }, + "buff": { + "multiplier": 1.5 + }, + "grades": [ + "Silver", + "Gold" + ], + "tally": { + "Bronze": 0, + "Silver": 7, + "Gold": 0 + } +} diff --git a/testdata/conformance/tables/json/r1_ping.json b/testdata/conformance/tables/json/r1_ping.json new file mode 100644 index 000000000..ea5c2d4c0 --- /dev/null +++ b/testdata/conformance/tables/json/r1_ping.json @@ -0,0 +1,18 @@ +{ + "grade": "Silver", + "effect": { + "ping": null + }, + "buff": { + "multiplier": 1.5 + }, + "grades": [ + "Silver", + "Gold" + ], + "tally": { + "Bronze": 0, + "Silver": 7, + "Gold": 0 + } +} diff --git a/testdata/conformance/tables/json/r2_cfg.json b/testdata/conformance/tables/json/r2_cfg.json new file mode 100644 index 000000000..a15470891 --- /dev/null +++ b/testdata/conformance/tables/json/r2_cfg.json @@ -0,0 +1,20 @@ +{ + "grade": "Argent", + "effect": { + "shield": { + "charge": 2.5 + } + }, + "buff": { + "mult": 1.5 + }, + "grades": [ + "Argent", + "Gold" + ], + "tally": { + "Bronze": 0, + "Argent": 7, + "Gold": 0 + } +} diff --git a/testdata/conformance/tables/reports.txt b/testdata/conformance/tables/reports.txt index 8be9411f5..67b6c9ad6 100644 --- a/testdata/conformance/tables/reports.txt +++ b/testdata/conformance/tables/reports.txt @@ -48,6 +48,9 @@ p3_empty_as_p1 0,0,0,0,false,read parts_count_past_bound 0,0,1,0,false,read parts_elem_kind 0,1,0,0,false,read parts_index_out_of_range 0,0,0,0,true,read +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 scalars_edges_as_2 1,2,2,0,false,read scalars_full_as_2 2,2,2,0,false,read trace_count_past_bound 0,0,1,0,false,read diff --git a/testdata/wire/tables/r1_cfg.bin b/testdata/wire/tables/r1_cfg.bin new file mode 100644 index 0000000000000000000000000000000000000000..e0ffa4d5a3f189a61f3d79808e51917959a0b923 GIT binary patch literal 133 zcmZQ%lw)G%XW?aM!JUaIi2kv9L1$!IiEf<(jis z7&WMVe05Li>ES8Pp8~B<95geQeWm?vnq}O^ZQK^j=Z~qK$$au?_P(wEkG01%SS8(= ib#&(UrZPXSo0}z!c1)4=6J!_=006HKC};ox literal 0 HcmV?d00001 diff --git a/testdata/wire/tables/r2_cfg.bin b/testdata/wire/tables/r2_cfg.bin new file mode 100644 index 0000000000000000000000000000000000000000..e0ffa4d5a3f189a61f3d79808e51917959a0b923 GIT binary patch literal 133 zcmZQ%lw)G%XW?aM!JUaIi2kv9L1$!IiEf<(jis z7&WMVe05Li>ES8Pp8~B<95geQeWm?vnq}O^ZQK^j=Z~qK$$au?_P(wEkG01%SS8(= ib#&(UrZPXSo0}z!c Date: Sat, 5 Sep 2026 12:44:53 -0700 Subject: [PATCH 5/7] certify: the projection controls' sabotage matches the wire-name loops 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 --- Makefile | 2 +- ir/projection.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4b7a945e2..f2ab05ac6 100644 --- a/Makefile +++ b/Makefile @@ -1977,7 +1977,7 @@ wide-utf8-read-negative-control: projection-union-arm-order-negative-control: @mkdir -p build @sed -E -e 's|" variant %d name=%s payload=|" variant %d payload=|' \ - -e 's|i\+1, v\.Name, v\.Type\)|i+1, v.Type) // SABOTAGED: the arm names removed|' \ + -e 's|i\+1, v\.WireName\(\), v\.Type\)|i+1, v.Type) // SABOTAGED: the arm names removed|' \ ir/projection.go > build/projection-no-arm-names.gotext @grep -q SABOTAGED build/projection-no-arm-names.gotext || \ { echo "NEGATIVE CONTROL FAILED: the sabotage did not remove the arm names"; exit 1; } diff --git a/ir/projection.go b/ir/projection.go index 2cfbd918d..500098bf6 100644 --- a/ir/projection.go +++ b/ir/projection.go @@ -119,7 +119,8 @@ func WireProjection(u *Unit) string { // projection exists to refuse — and a rename therefore moves the id. fmt.Fprintf(&b, "enum %s max=%d storage=%d variants=%d\n", e.Name, e.Max, e.StorageBits, len(e.Variants)) for i := range e.Variants { - fmt.Fprintf(&b, " variant %d name=%s\n", i+1, e.VariantWireName(i)) + v := e.VariantWireName(i) + fmt.Fprintf(&b, " variant %d name=%s\n", i+1, v) } } From 32e250f0439f7d2ff1a2654775fdcb6d0ae62f4d Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sat, 5 Sep 2026 12:55:04 -0700 Subject: [PATCH 6/7] certify: the was-rows control strips a variant with its comma and counts 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 --- Makefile | 8 +++++--- test/tables/wasrows_control_main.cpp | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f2ab05ac6..02ada35a3 100644 --- a/Makefile +++ b/Makefile @@ -3682,7 +3682,9 @@ tables-was-negative-control: build/tables-generated/.stamp test/tables/was_contr # build copy, regenerates that unit with the SHIPPED compiler, and reads the # same golden through the same program: the value reads None, the union # reads None, the slot is dropped, the field holds its default, and `unknown` -# counts each. The positive half runs first, against the shipped R2. +# counts five: the value, the array element that carries the same value, the +# slot, the arm and the field. The positive half runs first, against the +# shipped R2. .PHONY: tables-wasrows-negative-control tables-wasrows-negative-control: build/tables-generated/.stamp test/tables/wasrows_control_main.cpp @mkdir -p build/tables-wasrows-nc @@ -3692,7 +3694,7 @@ tables-wasrows-negative-control: build/tables-generated/.stamp test/tables/wasro @cat build/tables-wasrows-nc/with-was.log @grep -q '^unknown=0 kind_mismatch=0 malformed=0 grade=Argent effect=shield charge=2.5 mult=1.5 tally_argent=7$$' build/tables-wasrows-nc/with-was.log || \ { echo "CONTROL FAILED: with was, the R1 config did not read in silence under R2"; exit 1; } - @sed -e 's/ | was = "[a-z]*"$$//; s/ | was = "[A-Za-z]*"$$//' test/tables/R2.schema > build/tables-wasrows-nc/R2.schema + @sed -e 's/^\( [A-Z][A-Za-z]*\) | was = "[A-Za-z]*"$$/\1,/' -e 's/ *| was = "[A-Za-z]*"$$//' test/tables/R2.schema > build/tables-wasrows-nc/R2.schema @test $$(grep -c 'was' build/tables-wasrows-nc/R2.schema) -eq $$(grep -c 'was' test/tables/R2.schema | awk '{print $$1 - 4}') || \ { echo "NEGATIVE CONTROL: the was sabotage did not strip exactly four attributes"; exit 1; } @rm -rf build/tables-wasrows-nc/r2 && ./bin/schema generate --lang cpp --out build/tables-wasrows-nc/r2 build/tables-wasrows-nc/R2.schema @@ -3700,6 +3702,6 @@ tables-wasrows-negative-control: build/tables-generated/.stamp test/tables/wasro build/tables-wasrows-nc/r2/R2Table.cpp -o build/tables-wasrows-nc/without-was @./build/tables-wasrows-nc/without-was > build/tables-wasrows-nc/without-was.log @cat build/tables-wasrows-nc/without-was.log - @grep -q '^unknown=4 kind_mismatch=0 malformed=0 grade=None effect=None charge=0 mult=1 tally_argent=0$$' build/tables-wasrows-nc/without-was.log || \ + @grep -q '^unknown=5 kind_mismatch=0 malformed=0 grade=None effect=None charge=0 mult=1 tally_argent=0$$' build/tables-wasrows-nc/without-was.log || \ { echo "NEGATIVE CONTROL FAILED: without was, the R1 config did not read as unknown names under R2"; exit 1; } @echo "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)" diff --git a/test/tables/wasrows_control_main.cpp b/test/tables/wasrows_control_main.cpp index 887d177bd..ddad28394 100644 --- a/test/tables/wasrows_control_main.cpp +++ b/test/tables/wasrows_control_main.cpp @@ -2,8 +2,10 @@ // under R2. Built against the shipped R2 every renamed name lands: the enum // value, the union arm, the keyed slot and the nested type's field. Built // against an R2 whose four `was` attributes were stripped, each is a name -// this reader cannot find: `unknown` counts four, the value and the union -// read None, the slot is dropped, and the field holds its declared default. +// this reader cannot find: `unknown` counts five (the value, the array element +// holding the same value, the slot, the arm and the field), the value and the +// union read None, the slot is dropped, and the field holds its declared +// default. // The Makefile compiles this file twice and requires the two answers to // differ exactly that way. #include "R2Table.h" From 4958658da82f2dfd2625d940cc4e0f7697438e3d Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Sat, 5 Sep 2026 13:29:26 -0700 Subject: [PATCH 7/7] certify: the R rows join the wire fuzz, and the pages say the present 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 --- compiler/wasrows.go | 2 +- docs/COMPETITION.md | 2 +- docs/SPEC.md | 24 +++++++++++++----------- docs/TUTORIAL.md | 9 +++++---- docs/USAGE.md | 4 ++-- internal/check/check.go | 1 - internal/format/wasrows_test.go | 20 +++++--------------- ir/ir.go | 4 ++-- test/cookgen/main.go | 4 ++-- test/tables/R1.schema | 2 +- test/tables/R2.schema | 2 +- test/tables/wire_fuzz_main.cpp | 4 ++++ 12 files changed, 37 insertions(+), 41 deletions(-) diff --git a/compiler/wasrows.go b/compiler/wasrows.go index a4036bd83..a845a7a82 100644 --- a/compiler/wasrows.go +++ b/compiler/wasrows.go @@ -25,6 +25,6 @@ func refuseWasRows(u *ir.Unit, target string) error { return nil } carry, flags := carriers(wasRowTargets) - return fmt.Errorf("unit declares was on an enum variant, a union arm or a type's field (%s) — the three are %s only today, and the %s form is a named follow-on; generate with %s (docs/SPEC-TABLES.md §5)", + return fmt.Errorf("unit declares was on an enum variant, a union arm or a type's field (%s): the three are %s only today, and the %s form is a named follow-on; generate with %s (docs/SPEC-TABLES.md §5)", englishList(names), englishList(carry), target, englishList(flags)) } diff --git a/docs/COMPETITION.md b/docs/COMPETITION.md index d5e60431e..d4b7e8841 100644 --- a/docs/COMPETITION.md +++ b/docs/COMPETITION.md @@ -408,7 +408,7 @@ list at the end, with the section that establishes the claim. - s7. `map[K]V` in a table body: a lookup the runtime provides over entries the wire carries as a sorted array of one generated `{ key, value }` table, spending no wire kind, with `Find` a binary search in place over a locked region, a loaded one or an opened cook (SPEC-TABLES §2.8). Keys are bounded strings and the integer kinds; every other key is refused by name, an enum key naming `[E]T`. 🔶 because a map makes its holder VARIABLE, so it is the reference's and the tool's alone: the eight ports refuse a variable unit's wire by name ([#380](https://github.com/mas-bandwidth/schema/issues/380), [#349](https://github.com/mas-bandwidth/schema/issues/349), SPEC-TABLES §11). - s8. A pointer field `*T` names a node once in a flat node table and every reference is an index (SPEC-TABLES §3.1). The variable class has one wire implementation: PORTING M6 is ✅ for cpp, ❌ [#408](https://github.com/mas-bandwidth/schema/issues/408) for C, whose earlier nested form has a depth cap and no identity map, and ❌ [#349](https://github.com/mas-bandwidth/schema/issues/349) in the other seven columns; the eight ports answer ABSENT on the corpus's four pointered instances. The amplification bound on an untrusted read is [#466](https://github.com/mas-bandwidth/schema/issues/466). - s9. Defaults for `string(N)`, `bytes(N)` and `flags` fields are built ([#396](https://github.com/mas-bandwidth/schema/issues/396)): SPEC §4.2's `Default` production admits a quoted string and a brace list of flags variant names, a field at its declared default elides on the table wire and an absent field reads as it (SPEC-TABLES §4), and the baseline refuses a change to one (SPEC-TABLES §18.2). 🔶 because the C++ reference and the tool carry the three and every other backend refuses a unit that declares one, naming the follow-on, and because a composite default is the adopt-later row of #396. -- s9b. An arm IS a field line, so an arm's type is any type a field's is — a scalar with its bounds, a compressed float, a string, a bounded array, an enum, a `flags` mask, a declared `type`, a `table` inside a table closure, a pointer, another union — and an arm may carry no payload at all, which rides under kind `32` (SPEC §4.8, SPEC-TABLES §2.6, §3). What an arm may not take is a default, a `?`, a `was`, a `json`, an `[E]T`, an `if` guard, a `map` or an unbounded `[]T`, each refused by name. 🔶 because it is the reference's and the tool's: the eight ports answer ABSENT on the message-class cases ([#392](https://github.com/mas-bandwidth/schema/issues/392), SPEC-TABLES §15). +- s9b. An arm IS a field line, so an arm's type is any type a field's is — a scalar with its bounds, a compressed float, a string, a bounded array, an enum, a `flags` mask, a declared `type`, a `table` inside a table closure, a pointer, another union — and an arm may carry no payload at all, which rides under kind `32` (SPEC §4.8, SPEC-TABLES §2.6, §3). What an arm may not take is a default, a `?`, a `json`, an `[E]T`, an `if` guard, a `map` or an unbounded `[]T`, each refused by name. 🔶 because it is the reference's and the tool's: the eight ports answer ABSENT on the message-class cases ([#392](https://github.com/mas-bandwidth/schema/issues/392), SPEC-TABLES §15). - s10. **The table wire answers this row**, because the packet wire has no presence at all. `?T` is the value plus a generated presence bool, so the holder stays fixed size (SPEC-TABLES §2.3). SPEC §4.2's grammar admits `?` in table bodies only, and a `type` body refuses one by name. On the table backends `?T` is fixed class and all nine carry it (`chain_optional` and `chain_optional_empty` in the conformance corpus); `?[N]T` is one of the message-class cases the reference answers alone (PORTING M16, [#392](https://github.com/mas-bandwidth/schema/issues/392)). - s11. Declined: every field optional with a declared default (SPEC-TABLES §4). - s12. Declined. SPEC §4.2 declares no generic parameter and no type-erased value, and the adoption question is closed on [#396](https://github.com/mas-bandwidth/schema/issues/396); the typed bag is a union whose arms are tables (SPEC-TABLES §2.6). diff --git a/docs/SPEC.md b/docs/SPEC.md index 75e680f9b..b3194b9bc 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -917,7 +917,7 @@ sequence uint16 word named, so `| table` draws "table is a reserved word" rather than becoming a tag. **A repeated tag on one line is refused by name** too, and so is a tag that repeats a valued key already on the line. -- **`was = "old_name"` — the rename attribute, table closures only** +- **`was = "old_name"`, the rename attribute, table closures only** (SPEC-TABLES.md §5). A table field's wire id is the hash of its name, so a bare rename would orphan every byte ever written under the old one; `was` keeps the old identity through the rename: `speed float32 | was = @@ -928,7 +928,7 @@ sequence uint16 table field's does, and `was` keeps the id. On a field of a type NO table reaches it is refused naming the type: the packet wire is positional, so a rename there orphans no stored value and there is no identity for `was` - to carry. A bare rename is not a free edit on either wire — field NAMES + to carry. A bare rename is not a free edit on either wire: field NAMES ride in the projection (§3.1), so renaming a `type` field bare moves the protocol id and both sides redeploy together, where a rename under `was` projects the wire name and moves nothing. **An enum variant and a union @@ -1029,17 +1029,19 @@ type Quat | quat4 actions for those types on the other. v1 ships the types; each schema declares its own, and each application's actions bind to them by claiming. -**Front-end status: ONE LINE KIND CARRIES A TAG TODAY.** The rule at every +**Front-end status: TWO LINE KINDS CARRY A TAG TODAY.** The rule at every line kind is specified ahead of its implementation, on the terms SPEC-TABLES.md §3.3 and §6.6 take. What the tree carries is a tag on a -`type` DECLARATION alone, gathered into `ir.Struct.Tags` and emitted as the -inert comment above. Every other line kind refuses one, each under a -diagnostic written for a different rule: a field's pipe draws "unknown -attribute ... the vocabulary is typed and closed per compiler version", a -`union` declaration draws "takes no qualification", a `const` -draws "a constant takes no qualification, and | is never an operator", a -union arm's pipe draws "expected a field type", and an enum or flags variant -carrying one does not parse at all. Owed as schema#523 ruling 4, together +`type` declaration, gathered into `ir.Struct.Tags` and emitted as the inert +comment above, and on a `table` declaration, gathered the same way and +emitted nowhere yet. A field's pipe refuses one under "unknown +attribute ... the vocabulary is typed and closed per compiler version", and +an arm with a payload is a field line and refuses it the same way. A `union` +declaration draws "takes no qualification", and a `const` draws "a constant +takes no qualification, and | is never an operator". An enum variant, a +flags variant and a payload-free arm parse a qualification section for `was` +(a flags variant refuses `was` by ruling) and accept a bare tag in silence, +carrying it nowhere. Owed as schema#523 ruling 4, together with the descriptor columns it feeds (SPEC-TABLES.md §8.1), and this line is deleted by the implementation PR that lands the behavior. diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 1ea8893d3..3c50989ff 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -3884,15 +3884,16 @@ type T ``` $ schema check . -Bad.schema:5:5: field a: was is a table-wire concept — it aliases a renamed field's wire id, and only table fields have wire ids; a `type`'s wire is positional, so a rename there moves no bit (docs/SPEC-TABLES.md) +Bad.schema:3:1: type T: field a carries was = "b", but no table reaches T — was is a table-wire concept, and a field of a type outside a table closure has no wire id for it to keep; the packet wire is positional, so a rename there orphans nothing (docs/SPEC-TABLES.md §5) schema: 1 error(s) ``` Any two fields of one table whose effective ids collide are refused too. -There is no `was` for enum variants or union arms. Renaming a variant makes a -new variant, and old data reads as `unknown`. Rename fields freely, and treat -variant names as permanent. +An enum variant and a union arm take `was` on the same terms, `Argent | was = +"Silver"` and `pong | was = "ping"`, and so does a field of a `type` a table +reaches. Renaming any of them bare makes a new name, and old data reads as +`unknown` (the renaming section below walks through it). Put `max_speed` back before you go on, because the rest of the tutorial uses that name. diff --git a/docs/USAGE.md b/docs/USAGE.md index b049425cb..779f9490b 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -899,7 +899,7 @@ enum Grade { Bronze, Silver, Gold } // v2 — every stored Gold still loads Gol ``` A variant a reader has no name for loads as `None` (enum) or empty (union) -and counts as `unknown` — never as its neighbour. A rename declares the old +and counts as `unknown`, never as its neighbor. A rename declares the old name with `was` on the variant's own line, `Argent | was = "Silver"`, and the stored values keep reading (see renaming below). Renaming one bare is a new variant, and old data reads as unknown. @@ -1991,7 +1991,7 @@ union Value `ping` selects and carries nothing, which is not the union's `None`: `None` says no arm was selected. What an arm may NOT take is a specified default, a `?`, a `json`, an enum-keyed `[E]T`, an `if` guard, a `map` or an -unbounded `[]T` — each refused by name (SPEC-TABLES.md §2.6). `was` it takes, +unbounded `[]T`, each refused by name (SPEC-TABLES.md §2.6). `was` it takes, `pong | was = "ping"`: the arm's rename (see renaming below). ### Pointers: `next *Node` diff --git a/internal/check/check.go b/internal/check/check.go index 1f837635b..45660c5f3 100644 --- a/internal/check/check.go +++ b/internal/check/check.go @@ -1394,7 +1394,6 @@ func (c *checker) resolveField(owner string, f *ast.Field, inTable bool) *ir.Fie // where the closure is known: a `type` a table reaches has table-wire // field ids and a text form and may carry both, and only membership // decides it (docs/SPEC-TABLES.md §5, §16.4). - _ = inTable // the fixed and 128-bit families mirror serialize's own surface exactly // (SPEC §4.3, runtime-first): fixed(I, F) and int128 are RANGED — the diff --git a/internal/format/wasrows_test.go b/internal/format/wasrows_test.go index f17d849f6..f33469063 100644 --- a/internal/format/wasrows_test.go +++ b/internal/format/wasrows_test.go @@ -1,6 +1,9 @@ package format -import "testing" +import ( + "strings" + "testing" +) // The second `was` row through schemafmt: a qualified variant ends its line, // a payload-free arm's section follows its name, and both come back as they @@ -12,7 +15,7 @@ func TestFormatsVariantAndArmWas(t *testing.T) { t.Fatalf("format: %v", err) } for _, want := range []string{" Argent | was = \"Silver\"\n Gold\n", "shield Ward | was = \"ward\"\n", "pong | was = \"ping\"\n"} { - if !contains(string(out), want) { + if !strings.Contains(string(out), want) { t.Errorf("formatted output lacks %q:\n%s", want, out) } } @@ -24,16 +27,3 @@ func TestFormatsVariantAndArmWas(t *testing.T) { t.Errorf("not idempotent:\n%s", again) } } - -func contains(s, sub string) bool { - return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0) -} - -func indexOf(s, sub string) int { - for i := 0; i+len(sub) <= len(s); i++ { - if s[i:i+len(sub)] == sub { - return i - } - } - return -1 -} diff --git a/ir/ir.go b/ir/ir.go index 8228390eb..973b1fd53 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -95,7 +95,7 @@ type Enum struct { // [Enum.VariantWireName]. Was []string Max int64 // top wire value: variant count, or the | max = K widening - StorageBits int // 8 / 16 / 32 / 64 — smallest unsigned fitting Max + StorageBits int // 8 / 16 / 32 / 64, the smallest unsigned fitting Max } // Flags is a `flags` declaration: one bit per variant, consumed as masks @@ -114,7 +114,7 @@ type Union struct { Name string Variants []UnionVariant // declared order — the tag order Max int64 // = len(Variants); the tag wire range is [0, Max] - StorageBits int // 8 / 16 / 32 / 64 — smallest unsigned fitting Max + StorageBits int // 8 / 16 / 32 / 64, the smallest unsigned fitting Max } // UnionVariant is one arm of a [Union]. AN ARM IS A FIELD LINE (SPEC §4.8, diff --git a/test/cookgen/main.go b/test/cookgen/main.go index 727dd81c0..33e6a3ef6 100644 --- a/test/cookgen/main.go +++ b/test/cookgen/main.go @@ -154,9 +154,9 @@ func main() { // ---- the attribution: one entry per node, in index order ---- entry := make([]byte, 16) ord.PutUint64(entry[0:], 0) - ord.PutUint64(entry[8:], ir.TableTypeId(root.Name)) + ord.PutUint64(entry[8:], ir.TableTypeId(root.WireName())) must(w.Write(entry)) - chainTypeId := ir.TableTypeId(chain.Name) + chainTypeId := ir.TableTypeId(chain.WireName()) for i := range nodes { ord.PutUint64(entry[0:], uint64(chainBase+i*chainLayout.Size)) ord.PutUint64(entry[8:], chainTypeId) diff --git a/test/tables/R1.schema b/test/tables/R1.schema index 6aa8bba71..ef8c13fe8 100644 --- a/test/tables/R1.schema +++ b/test/tables/R1.schema @@ -1,4 +1,4 @@ -// R1.schema — the OLD side of the vocabulary rename pair (docs/SPEC-TABLES.md +// R1.schema: the OLD side of the vocabulary rename pair (docs/SPEC-TABLES.md // §5): an enum variant, two union arms and a field of a `type` a table reaches // by value, each renamed in R2 under `was`. Every one of them rides the table // wire under the hash of its name, so a bare rename would leave every stored diff --git a/test/tables/R2.schema b/test/tables/R2.schema index 9b3d6cbf5..ea6100d6d 100644 --- a/test/tables/R2.schema +++ b/test/tables/R2.schema @@ -1,4 +1,4 @@ -// R2.schema — the NEW side of the vocabulary rename pair: Silver is Argent, +// R2.schema: the NEW side of the vocabulary rename pair: Silver is Argent, // the ward arm is shield, the ping arm is pong, and Buff.multiplier is mult, // each declared with `was`, so every id is the old name's hash and an R1 // config reads in silence (docs/SPEC-TABLES.md §5). The negative control diff --git a/test/tables/wire_fuzz_main.cpp b/test/tables/wire_fuzz_main.cpp index a6acd08c9..7eee8e329 100644 --- a/test/tables/wire_fuzz_main.cpp +++ b/test/tables/wire_fuzz_main.cpp @@ -58,6 +58,8 @@ #include "VocabTable.h" #include "W1Table.h" #include "W2Table.h" +#include "R1Table.h" +#include "R2Table.h" struct Reply { @@ -280,6 +282,8 @@ static const Codec codecs[] = { FIXED( "tbla2", tbla2, Root ), FIXED( "scalars", scalardemo, SimState ), FIXED( "tblscalars2", scalardemo2, SimState ), + FIXED( "tblr1", tblr1, Cfg ), + FIXED( "tblr2", tblr2, Cfg ), // the MESSAGE FORM's units in the FILE form (docs/SPEC-TABLES.md §3.3): // their file-form vectors are ordinary instances and are fuzzed as any // instance is, and the message form's own entries are below