tool: the three findings the reference lock held (#521, #447, #545) - #597
Merged
Conversation
…ire range is refused in every build (#521 G-05, #447 F-09) A counted array's count bound is the one range in the language that had no birth value: SPEC §4.6 refuses a scalar or element range that excludes zero because zero initialization would put the field outside its own range, and a count bound above zero fell between the rules. generated/cpp/Wire.h:172 carried it in this repository's own corpus, samples_count born 0 against a declared wire range of [1, 8], and the write under -DNDEBUG packed 0u - 1u truncated to three bits and reported success on bytes the reader refuses. ir.Field.BornCount names the birth value: a counted array's declared minimum, the one wire-legal count a fresh value can carry. Every target's constructed form now carries it the way it carries a specified default, and a [..N] array is born empty as before. The write side refuses rather than asserts. In C, C++, Dart, Java and JavaScript the count's range was held by an assert or a checked-build predicate, gone under -DNDEBUG, without -ea, without --enable-asserts and in the production JavaScript writer. The count guards the element loop and a wrapped count is bytes no reader accepts, so it is refused in every build mode. Go, Rust, C# and Elixir already refused unconditionally through their error-returning runtimes.
… trees re-pinned (#521 G-05, #447 F-09) Two claims, one test each so a regression in either is named on its own: a [A..B] count is born at A in all nine targets, and a count outside its wire range is refused in every build mode in all nine, never through a predicate the build can remove. The second claim also walks every line naming the count and refuses any that carries the target's own build-removable assert spelling. The source-copy goldens and the committed generated tree follow. No wire golden moves and the protocol id is unchanged: a birth value is not a specified default, so the SPEC §3.1 projection never sees it, and the bytes a valid write produces are the bytes it produced before. generated/cpp/Wire.h:172, the reproduction PR #543 recorded in this repository's own corpus, now reads samples_count = 1 against its declared wire range of [1, 8], and generated/cpp/WireWire.h refuses a count outside that range rather than asserting it.
#447 F-15, #521 G-08) A generated <Union>Type looks and acts like a declared enum — implicit None, dense variants, an exported extent, unsigned storage — so a reader logging which message arrived writes the enum's own name call against it. It carried no Count in any target, because #489 gave declared enums one and did not reach tag enums, and a debug-name function in exactly one: C's enum_name_weapon_fire_type. The other eight got "use of undeclared identifier" on the first thing a message system's user does. Each target now emits both, in its own spelling: EnumName overloaded in C++, EnumNameWeaponFireType where the language has no overloads, snake_case in C, Rust and Elixir. The nine-way gate declares an enum of three variants beside a union of two, so a count claim names the tag enum and not its neighbour. Both are diagnostic surface and neither is on a path: Count is a constant, and the name function has no caller in generated code. The read and write benchmarks either side of this PR are in its body.
…riptors do (#447 F-12) Generated storage marks each branch field with the guard that puts it on the wire. The comment spelled it "if on_radar", nested with " / " and negated with a trailing " else", so the else side read "if on_radar else" — which is not a sentence, and says the opposite of what a beginner parses on first read. The reflection descriptors always carried the right spelling, built by each table backend's guardWalk: at_rest, !at_rest, active && has_target. The comment now spells it the same way, so a reader who compares a storage comment against the descriptor a table-JSON walker parses at runtime sees one language. Field.Guard reaches comments only in every backend, so nothing on a wire moves. The nine-way gate nests three branches and takes both sides of the innermost, so the negation, the conjunction and the else side are all under it, and it refuses the stale spelling anywhere in a target's output.
The line walk that proves no assert reaches the count ranged over strings.Split, which the pinned modernize analyzer rewrites to SplitSeq.
A packet union's tag enum carries Count now, so an arm whose exported spelling is Count would define the member twice, which C++ and C# refuse as a redefinition. Max has been refused by name since the tag enum carried an extent; Count joins it, in the declared enum's own wording. The reservation is scoped exactly to where the member exists. A TABLE-CLOSURE union's tag shape is emitted beside the tables and carries Max alone, so the name stays free there — tables/messages/Messages.schema's EditBody uses it, and reserving a name against a member that does not exist would break the corpus for nothing. Both sides are gated. Giving the table-closure tag shape its own Count is a named follow-on, and it is the pass that would reserve the name there too. Generated output for a unit that passes is unchanged, so no golden moves.
rowan-claude
force-pushed
the
unlocked-fixes-521-447
branch
from
September 6, 2026 05:30
ab264bb to
e9efc13
Compare
…hat asserted it (#521 G-05, #447 F-09) Three leg tests pinned the count's OLD contract and go red on the fix, which is the fix arriving where it should: - test/dart and test/java asserted that an out-of-range count trips the checked-twin writer contract. It no longer does, because a count is not a checked-twin contract: the write refuses it in every build. Each now checks the refusal directly, which also holds without --enable-asserts and without -ea, where the old claim was skipped outright. - test/rust asserted that a freshly constructed ProbeSample is WIRE-ILLEGAL, samples_count born 0 against [1, 8]. That is exactly the defect. It now checks the two halves that replace it: the count is born at 1 and a fresh value writes cleanly, and a count set below the minimum is still refused. test/elixir's raise is unchanged: the BEAM has no compile-out assert, so its count contract was already on in every build.
gafferongames
marked this pull request as ready for review
September 6, 2026 06:09
…'s full surface (#521 G-05, #447 F-09, F-15) SPEC §4.2 states a [A..B] count with A above zero as the one exception to zero initialization: born at its declared minimum, because zero is outside that count's own wire range and an array takes no specified default to name another birth value. SPEC §4.6 carries the rule the generated comments cite. The count bound is the stated exception to the excludes-zero refusal, and a count outside [A, B] is refused by the write in every build, in all nine targets, each in its own convention. SPEC §5 states that refusal as the one exception to its debug-assert tiers: C++, Dart and Java refuse from the write rather than through an assert, and JavaScript's flat production tier refuses rather than trusting the caller. SPEC §4.8, §4.11 and §4.2's exported-extent rule state the tag enum's full surface: None, the variants, Count and Max, plus the debug-name function in every target, with the nine spellings listed once. Count is reserved as an arm name on a packet union and refused by name. A table-closure union's tag shape carries Max alone today, which is #601. SPEC §6.1's [Min..N]T row and its union item say the born state and the generated surface. USAGE and TUTORIAL match, and the tutorial's part 4 hazard now shows what the tool emits.
…d the comments state the rule The coverage gate searched the whole generated output for None, Laser, Missile and "???". All four appear elsewhere: Laser and Missile name the arm types, None is the tag enum's own member, and "???" is the declared enum's name function default. Deleting the tag enum's name function left the gate green. It now slices the function's body, from the opening line each target's claim already carries to the out-of-set arm, so a missing function and a missing name are both refused. The nine spellings move into a named struct so one map serves both gates. emitWriteCount's bits == 0 branch is deleted. SPEC §4.6 refuses [Min..N]T with Min at or above N, so a count range is never degenerate and the branch had no case to serve. The generated corpus is byte-identical across the delete. The comments state present-state rules rather than narrating what the tool used to do, in the two test headers, the C and C++ count emitters, and the count-birth comments in every backend. Em dashes and semicolons out of the Go comments this branch added, and neighbour to neighbor.
… slice uses strings.Cut SPEC §4.6's list is compile errors, so the write refusal now says out loud that it is the other half of the same rule and that it is stated there because that is the section every emitted refusal comment cites. §4.11's Count bullet states the reservation rather than dating it. The name function's body slice uses strings.Cut, which the modernize lint asks for and which reads better. The control still goes red with the C++ name function removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 6, 2026
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
…n reaches it (#601) A PACKET union's tag enum carries `None`, the variants, `Count` and `Max` plus a debug-name function in all nine targets (#597, F-15). A TABLE-CLOSURE union's tag shape is a different emitter — `internal/codegen/cpptable`'s `emitTableUnion`, written beside the tables rather than among the packet declarations — and it carried `None`, the variants and `Max` alone. One construct, two surfaces, and `Count` free as an arm name on one side only because the member did not exist there to collide with. THE MEASUREMENT, taken before anything moved. Thirteen tag enums across seven committed C++ table headers carried `Max` with no `Count` beside it: `arms/{Carry,Gate,Nest,Ring}Table.h` (six between them), `lists/SaveTable.h`, `maps/DepthTable.h`, `messages/MessagesTable.h` (three), `stream/StreamTable.h` and `wide/CaptionTable.h`. Against them, `testdata/golden/cpp/`'s packet tag enums each carry `Count = N, // the declared variant count (SPEC §4.2)`. `internal/codegen/cpptable/unions.go:50` is the ONE emitter of that comment outside the nine packet backends, which is what makes this a one-target change where F-15 was a nine-target one: the table layer is the C++ reference's (docs/SPEC-TABLES.md §11, §15). The Go table backend emits no tag enum at all, so there is no oracle view to move. THE EMITTER. `emitTableUnion` now writes `Count` beside `Max` and the `EnumName` overload beneath the enum, to the character the packet emitter writes them, so a table arm and a type arm read as one family. Nothing on the read or write path calls the function and no generated code does. THE RESERVATION. `Count` moves out of `resolveUnion`'s post-loop block and into the switch beside `None`, `Max` and `Type`, which is where it belongs once the member is on every union: the kind no longer has to be known, so the arm errors and stops rather than resolving on. `ir.Union.TableClosureOnly` keeps its three other callers. EVERY ID STAYS PUT. `Count` is a generated member, not a wire fact: `git diff origin/main -- testdata/wire` is empty and `generated/` is untouched (the packet corpus declares no table-closure union). The CORPUS MOVE the reservation forces is a `was` rename, so it moves no id either — `tables/messages`'s `EditBody` arm `count` becomes `tally int32 | min = 0, max = 100, was = "count"`, and every one of its ids in `MessagesTable.h` stays `0xb1e5e28e4479a274`, in the file form's dispatch, the message form's, the reflection descriptor and the arms table. `examples-wide/Caption.schema`'s `Body.count` moves the same way. The baseline records it and says so: `arm tally id=0xb1e5e28e4479a274 kind=4 min=0 max=100 was=count`, "no compatibility-affecting edits". Four fixtures carry no stored data, so they move by plain rename with no alias: `test/tables/R1.schema` and `R2.schema` (`count` -> `amount`, and `tally` was taken there by a keyed array), `test/tables/RT2.schema`, `ir/buildversion_test.go`, `internal/baseline/baseline_test.go`, `compiler/wasrows_test.go` and the harness's `rowdemo` unit. No pinned instance selects any of them. THE GOLDENS, every moved file and why: - `tables/arms/{Carry,Gate,Nest,Ring}Table.h`, `tables/lists/SaveTable.h`, `tables/maps/DepthTable.h`, `tables/stream/StreamTable.h`: the added `Count` line and the added `EnumName` overload, nothing else. - `tables/messages/MessagesTable.h`: the same two additions on three tag enums, plus the arm's rename — `Count` -> `Tally` as a tag member, `count` -> `tally` as storage and as the text key, with every id unmoved. - `tables/wide/CaptionTable.h` and `wide/cpp/CaptionTable.h`: the two additions plus the `Body` arm's rename. - `wide/cpp/WideView.cpp`: the view's arm name and its spelling, twice. The id beside each is unchanged. - `tables/messages/tables.baseline`: the `was` row and a dated history line. - `testdata/conformance/tables/json/message_arm_edits.json` and the two `json-hostile/arm-scalar-*` texts: an arm's name IS its key in the text form (§16.2), so the key follows the rename. The wire pins those texts encode to are byte-identical. THE PAGES, in present tense. SPEC §4.2's tag-enum paragraph, §4.8's reserved-names bullet and its table-closure bullet, and §4.11's `Count` bullet each stated the split and now state the shared surface; the four `#601` links are gone. SPEC-TABLES §2.6 gains a paragraph naming the tag shape's members and its debug-name function outright, since a different emitter writes it, and §11 gains the arm-name refusal beside the other arm refusals. USAGE and TUTORIAL part 5 said "on a packet union" and now say "on every union". `docs/VERSIONING.md` lists no reserved names, so nothing there moved. THE CONTROLS, each turning exactly its own claim red. The `Count` line deleted from `emitTableUnion`: --- FAIL: TestTableClosureUnionTagEnumExportsCountAndItsDebugName (0.00s) tagenumsurface_test.go:257: cpp: the table-closure tag enum's surface is short, " Count = 2, // the declared variant count (SPEC §4.2)" not emitted The name function deleted, against the body-anchored gate: --- FAIL: TestTableClosureTagEnumDebugNameCoversNoneVariantsAndOutOfSet (0.00s) tagenumsurface_test.go:272: cpp: the table-closure tag enum has no name function, "inline const char * EnumName( WeaponFireType value )" not emitted The reservation scoped back to `!un.TableClosureOnly()`: --- FAIL: TestATableClosureUnionArmNamedCountIsRefusedByName (0.00s) tagenumsurface_test.go:192: a table-closure union's arm named count passed check, and its tag enum defines Count twice --- FAIL: TestDiagnostics/union_variant_named_count_on_a_table-closure_union_(exported_spelling) (0.00s) diagnostics_test.go:592: compiled clean — the language broke silently (want "carries its declared variant count as the member Count") Closes #601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 7, 2026
…n reaches it (#601) (#661) * tables: the gates for the table-closure tag shape's Count, red (#601) RED FIRST, committed before the emitter moves so the red is a fact in the history rather than a claim in a report. Three gates and one corpus rename go in together, and all three gates fail at this commit: - `TestTableClosureUnionTagEnumExportsCountAndItsDebugName` asserts the table-closure tag enum carries `Count` and the debug-name function, the way `TestUnionTagEnumExportsCountAndItsDebugName` asserts it for a packet union. The probe unit declares an enum of THREE variants beside a union of two, the trap PR #597 found running the packet control: with both at two, the declared enum's own `Count` line satisfies a claim meant for the tag enum. - `TestTableClosureTagEnumDebugNameCoversNoneVariantsAndOutOfSet` anchors its claim to the function's BODY, from the opening line to the out-of-set `"???"`. `None`, `Laser` and `Missile` all appear elsewhere in the same header, so a whole-output claim stays green with the function deleted. - `TestATableClosureUnionArmNamedCountIsRefusedByName` replaces `TestATableClosureUnionArmNamedCountIsAccepted`, which pinned the present split as law. The reservation reaches every union once the member exists on every union. `internal/check`'s diagnostics table gains the packet and table-closure refusals as a pair, so one rule is read from both sides. The fixture arms that spelled themselves `count` for unrelated cases move to `tally` and `size`: those cases test a default on an arm, an optional arm, a json arm and a range excluding zero, and none of them is about the name. The red, at this commit: --- FAIL: TestATableClosureUnionArmNamedCountIsRefusedByName (0.00s) tagenumsurface_test.go:192: a table-closure union's arm named count passed check, and its tag enum defines Count twice --- FAIL: TestTableClosureUnionTagEnumExportsCountAndItsDebugName (0.00s) tagenumsurface_test.go:257: cpp: the table-closure tag enum's surface is short, " Count = 2, // the declared variant count (SPEC §4.2)" not emitted tagenumsurface_test.go:257: cpp: the table-closure tag enum's surface is short, "inline const char * EnumName( WeaponFireType value )" not emitted --- FAIL: TestTableClosureTagEnumDebugNameCoversNoneVariantsAndOutOfSet (0.00s) tagenumsurface_test.go:272: cpp: the table-closure tag enum has no name function, "inline const char * EnumName( WeaponFireType value )" not emitted --- FAIL: TestDiagnostics/union_variant_named_count_on_a_table-closure_union_(exported_spelling) (0.00s) diagnostics_test.go:592: compiled clean — the language broke silently (want "carries its declared variant count as the member Count") Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tables: the message form's arm dispatch reads the `was` alias (#601) FOUND WHILE MOVING THE CORPUS FOR #601, and landed ahead of it because the move depends on it. `tables/messages`'s `EditBody` arm `count` has to move under the reservation, and it moves under `was` so its id stays the old name's hash and no wire pin moves. That only holds if every id derivation reads the alias, which docs/SPEC-TABLES.md §5 states outright: "Every id derivation reads the alias: the node record, the cooked node directory, the connection's announced vocabulary (§3.3), the baseline and the build version." THREE ARM-DISPATCH SITES DID NOT. The announcement's entry is `ir.TableArmEntry`, whose own comment says it hashes the WIRE name, and the encoder spends that id. The message form's readers matched on the DECLARED name's hash instead: - `internal/codegen/cpptable/messageload.go`, `<Table>LoadMessageBody` - `internal/codegen/cpptable/messagecodec.go`, the union-arm extent walk - `internal/tablewire/messagedecode.go`, the Go engine's arm dispatch So a `was`-renamed arm was written under the old name's hash and read under the new one: LOST BY ITS OWN BUILD, counted `unknown`, in the C++ reference and in the engine alike. The file form was always right — `decode.go` and every other cpptable site call `WireName()` — and no corpus carried a message-form `was` arm, so nothing was pinned that could catch it. The red, before the fix: --- FAIL: TestAWasRenamedArmSurvivesTheMessageForm (0.00s) wasarmmessage_test.go:116: the was-renamed arm was lost: the union reads back None, report {Unknown:1 KindMismatch:0 Widened:0 Clamped:0 Duplicate:0 Malformed:false Refused:false} The C++ gate is anchored to `NoteLoadMessageBody`'s own body, not to the output: the old name's hash is already in the header twice over, in the file form's dispatch and in the reflection descriptor, so a whole-output claim stays green with the message form still wrong. The negative control, the one line put back: --- FAIL: TestTheMessageFormsArmDispatchReadsTheWasAlias (0.00s) wasarmmessageid_test.go:57: cpp: the message form does not dispatch the renamed arm on its old name's hash, "case 0xb1e5e28e4479a274ull: // tally" absent from NoteLoadMessageBody wasarmmessageid_test.go:60: cpp: the message form dispatches the renamed arm on its DECLARED name's hash, "case 0xaa3f4d29d02d54ddull: // tally" — the announcement writes the alias's No committed golden moves at this commit, because no corpus declares a `was` arm that reaches a message form yet. `test/tables/R2.schema` declares two, and R2 pins no golden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tables: the table-closure union tag carries Count, and the reservation reaches it (#601) A PACKET union's tag enum carries `None`, the variants, `Count` and `Max` plus a debug-name function in all nine targets (#597, F-15). A TABLE-CLOSURE union's tag shape is a different emitter — `internal/codegen/cpptable`'s `emitTableUnion`, written beside the tables rather than among the packet declarations — and it carried `None`, the variants and `Max` alone. One construct, two surfaces, and `Count` free as an arm name on one side only because the member did not exist there to collide with. THE MEASUREMENT, taken before anything moved. Thirteen tag enums across seven committed C++ table headers carried `Max` with no `Count` beside it: `arms/{Carry,Gate,Nest,Ring}Table.h` (six between them), `lists/SaveTable.h`, `maps/DepthTable.h`, `messages/MessagesTable.h` (three), `stream/StreamTable.h` and `wide/CaptionTable.h`. Against them, `testdata/golden/cpp/`'s packet tag enums each carry `Count = N, // the declared variant count (SPEC §4.2)`. `internal/codegen/cpptable/unions.go:50` is the ONE emitter of that comment outside the nine packet backends, which is what makes this a one-target change where F-15 was a nine-target one: the table layer is the C++ reference's (docs/SPEC-TABLES.md §11, §15). The Go table backend emits no tag enum at all, so there is no oracle view to move. THE EMITTER. `emitTableUnion` now writes `Count` beside `Max` and the `EnumName` overload beneath the enum, to the character the packet emitter writes them, so a table arm and a type arm read as one family. Nothing on the read or write path calls the function and no generated code does. THE RESERVATION. `Count` moves out of `resolveUnion`'s post-loop block and into the switch beside `None`, `Max` and `Type`, which is where it belongs once the member is on every union: the kind no longer has to be known, so the arm errors and stops rather than resolving on. `ir.Union.TableClosureOnly` keeps its three other callers. EVERY ID STAYS PUT. `Count` is a generated member, not a wire fact: `git diff origin/main -- testdata/wire` is empty and `generated/` is untouched (the packet corpus declares no table-closure union). The CORPUS MOVE the reservation forces is a `was` rename, so it moves no id either — `tables/messages`'s `EditBody` arm `count` becomes `tally int32 | min = 0, max = 100, was = "count"`, and every one of its ids in `MessagesTable.h` stays `0xb1e5e28e4479a274`, in the file form's dispatch, the message form's, the reflection descriptor and the arms table. `examples-wide/Caption.schema`'s `Body.count` moves the same way. The baseline records it and says so: `arm tally id=0xb1e5e28e4479a274 kind=4 min=0 max=100 was=count`, "no compatibility-affecting edits". Four fixtures carry no stored data, so they move by plain rename with no alias: `test/tables/R1.schema` and `R2.schema` (`count` -> `amount`, and `tally` was taken there by a keyed array), `test/tables/RT2.schema`, `ir/buildversion_test.go`, `internal/baseline/baseline_test.go`, `compiler/wasrows_test.go` and the harness's `rowdemo` unit. No pinned instance selects any of them. THE GOLDENS, every moved file and why: - `tables/arms/{Carry,Gate,Nest,Ring}Table.h`, `tables/lists/SaveTable.h`, `tables/maps/DepthTable.h`, `tables/stream/StreamTable.h`: the added `Count` line and the added `EnumName` overload, nothing else. - `tables/messages/MessagesTable.h`: the same two additions on three tag enums, plus the arm's rename — `Count` -> `Tally` as a tag member, `count` -> `tally` as storage and as the text key, with every id unmoved. - `tables/wide/CaptionTable.h` and `wide/cpp/CaptionTable.h`: the two additions plus the `Body` arm's rename. - `wide/cpp/WideView.cpp`: the view's arm name and its spelling, twice. The id beside each is unchanged. - `tables/messages/tables.baseline`: the `was` row and a dated history line. - `testdata/conformance/tables/json/message_arm_edits.json` and the two `json-hostile/arm-scalar-*` texts: an arm's name IS its key in the text form (§16.2), so the key follows the rename. The wire pins those texts encode to are byte-identical. THE PAGES, in present tense. SPEC §4.2's tag-enum paragraph, §4.8's reserved-names bullet and its table-closure bullet, and §4.11's `Count` bullet each stated the split and now state the shared surface; the four `#601` links are gone. SPEC-TABLES §2.6 gains a paragraph naming the tag shape's members and its debug-name function outright, since a different emitter writes it, and §11 gains the arm-name refusal beside the other arm refusals. USAGE and TUTORIAL part 5 said "on a packet union" and now say "on every union". `docs/VERSIONING.md` lists no reserved names, so nothing there moved. THE CONTROLS, each turning exactly its own claim red. The `Count` line deleted from `emitTableUnion`: --- FAIL: TestTableClosureUnionTagEnumExportsCountAndItsDebugName (0.00s) tagenumsurface_test.go:257: cpp: the table-closure tag enum's surface is short, " Count = 2, // the declared variant count (SPEC §4.2)" not emitted The name function deleted, against the body-anchored gate: --- FAIL: TestTableClosureTagEnumDebugNameCoversNoneVariantsAndOutOfSet (0.00s) tagenumsurface_test.go:272: cpp: the table-closure tag enum has no name function, "inline const char * EnumName( WeaponFireType value )" not emitted The reservation scoped back to `!un.TableClosureOnly()`: --- FAIL: TestATableClosureUnionArmNamedCountIsRefusedByName (0.00s) tagenumsurface_test.go:192: a table-closure union's arm named count passed check, and its tag enum defines Count twice --- FAIL: TestDiagnostics/union_variant_named_count_on_a_table-closure_union_(exported_spelling) (0.00s) diagnostics_test.go:592: compiled clean — the language broke silently (want "carries its declared variant count as the member Count") Closes #601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tables: the renamed wide arm takes the formatter's own column (#601) `schema fmt` is the one command that writes a schema file (#473) and a fmt-drift gate holds the corpus canonical. The `was` attribute on `examples-wide/Caption.schema`'s renamed arm sits at the column the formatter picks, which `make fmt` supplied and this commit records. No generated byte moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: the §2.6 Count paragraph without its two em dashes (#601) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The three fix-wave findings PR #543 left for a ruling because the reference
lock froze the path each one needed. #545 lifted the lock, so they land here:
one commit per finding, each carrying its reproduction, its fix, its nine-way
gate and the negative control that turns that gate red. The pages land in the
same PR, ahead of the code they describe.
bench/LOCKholds no prefixes and its standing gate applies. This PR moves theC and C++ packet emitters, so the reproduction rows are re-pinned below from a
stated sitting.
[A..B]count born at 0, and a release build writing bytes no reader takes3eab5c1,4d30733,f146f95Countanywhere, and a name function only in Cfb66780,e9efc13if X elsef3d87f8be935aee11326adb9c8dafollows the three:test/dart,test/javaandtest/rustpinnedthe count's OLD contract, and the fix turns them red where it should. The rust
leg asserted that a freshly constructed
ProbeSampleis WIRE-ILLEGAL,samples_countborn 0 against[1, 8], which is the defect written down as atest. It now checks the two halves that replace it.
The dart and java legs are a different case, and the earlier framing of them
here was wrong. They did not pin a defect. They pinned SPEC §5's stated
checked-twin contract exactly as §5 stated it: an out-of-range count trips the
writer's assert, live under
--enable-assertsand-eaand gone withoutthem. Those legs were correct against the page. What this PR changes is the
page. §5 now carries a stated exception, the count, refused in every build in
all nine targets, and the legs move to match the new rule. Each now checks the
refusal directly, which also holds without
--enable-assertsand without-ea, where the old claim was skipped outright.test/elixir's raise isunchanged: the BEAM has no compile-out assert, so its count contract was
already on in every build.
The merge commit is
origin/mainataf66617bmerged in, not a rebase.No wire golden moves and no protocol id changes. A birth value is not a
specified default, so the SPEC §3.1 projection never sees it. The tag enum's
Countand its name function are not in the projection either, and the guardis a comment.
testdata/golden/id.txtis untouched.THE PAGES, for the cold read
Three files, 160 insertions and 29 deletions, no code in them. Read the page
diff on its own with
git diff af66617b..HEAD -- docs/. What each sectionsays, section by section:
SPEC §4.2, zero initialization. The rule's opening list said "zeroed
buffers and counts" without qualification. It now says "zeroed counts outside
the one stated exception below", and the exception follows the array sentence:
a
[A..B]Tcount with A above zero is BORN AT A, in every target, because zerois outside that count's own wire range and an array takes no specified default
to name another birth value, so the bound itself is it.
[..N]Tis[0..N]T,reaches zero, and is born at 0 under the ordinary rule. The elements are zeroed
either way, and the §5 zero form does not carry the birth value for the same
reason it does not carry a specified default.
SPEC §4.2, the exported extent. The declared-enum
Countparagraph ended"the generated
<Union>Typetag enums carryMaxalone", which is now falsefor a packet union. It says a packet union's tag enum carries
CountbesideMaxin the same nine spellings, so a tag enum and a declared enum present onesurface to a reader, and that the member is there because the surface is
uniform rather than because a tag set's two numbers ever differ. A
table-closure union's tag shape carries
Maxalone today, linked to #601.SPEC §4.6, shape checks. The excludes-zero bullet said "an array field's
range must reach zero" flatly. It now says ELEMENT range, and two indented
paragraphs follow it. The first states the count bound as the legal exception,
because the count is born at A and so is never outside its own range at rest.
The second is the rule the generated code cites by name: a count outside
[A, B] is refused by the WRITE in every build, in all nine targets, and the
paragraph says out loud that it is not a compile error but the other half of
the same rule, stated here because every emitted refusal carries the comment
"a count outside its wire range is refused in every build (SPEC §4.6)". The
mechanism is spelled (the count guards the element loop, the pack subtracts
the low bound, a count below A wraps), and the nine conventions are listed: C
0, C++ and C#false, Dart and Java-1, Goserialize.ErrValueOutOfRange,Rust
Err(serialize::Error::ValueOutOfRange), ElixirArgumentError, andJavaScript
falsefrom the stream writer with-1from BOTH tiers of the flatwriter, production included.
SPEC §4.8, unions. Three changes. The reserved-names bullet adds
Count,refused over the exported spelling on a PACKET union, with the scoping said
plainly: the reservation reaches where the member exists, so the name stays
free on a table-closure union until #601. The tag-enum bullet now lists
Count(the declared variant count,
Noneexcluded) besideMax, and says a tag enumcarries the same four parts a declared enum carries. A new bullet states the
debug-name function: any wire value including out-of-set, returning the
variant's spelling or a fixed unknown marker, for logs and tooling only, on no
read or write path, called by no generated code, with a nine-row table giving
each target's count member and name spelling once. A second new bullet states
the table-closure split as present state and links #601 as the named follow-on.
SPEC §4.11, refused by name. A new bullet for
Countas an arm name on apacket union: the tag enum claims the member, so the arm would define it twice,
which C++ and C# refuse as a redefinition. Refused over the exported spelling,
so
countgoes too, diagnostic naming the tag enum. The table-closure namestays legal, linked.
SPEC §5, trust model. The tier paragraph is untouched. A new paragraph
after it states the one exception: a count outside
[A, B]is refused by thewrite in every build in all nine, so C++, Dart and Java refuse from the write
rather than through the assert their other contracts ride, and JavaScript's
flat production tier refuses rather than trusting the caller. It says why (the
count is not a diagnostic, it guards the loop and the pack wraps below A) and
closes by saying every other write-side contract in the section keeps the tiers
above.
SPEC §6.1. Two one-line rows. The
[Min..N]Tstorage row now reads "countborn at Min and validated to [Min, N]". The per-declaration union item now
lists the tag enum as
None = 0, the variants, thenCountandMax, withits debug-name function.
USAGE. The
Maxparagraph gains a sentence saying the tag enum carries thedebug-name function too, in the same nine spellings a declared enum uses. The
Countparagraph gains the arm-name reservation on a packet union. The unionsection's tag enum line and its C++ code block gain
Count = 4. The Arrayssection, which teaches counts, gains two paragraphs:
ranged_countborn at 2rather than 0 and why, and the every-build write refusal with its mechanism.
TUTORIAL part 4, the counted-array hazard. This is the section that taught
the reader to work around the defect, so it is the largest single change. The
storage block shows
window_count = 2, the write block shows the rangerefusal instead of
serialize_assert, and the prose is rewritten as twoparagraphs, one per end of the rule: born at the declared minimum and why the
bound has to name it, then the every-build refusal with the wrap spelled out
under the
write_bitsline the reader can see. The sentence telling the readerto "reach for
[A..B]with A above zero only where your code sets the countbefore the value is ever written" is deleted. It now says to reach for it
wherever the count genuinely has a floor, because the tool holds both ends.
TUTORIAL part 5, the union. The
WeaponFireTypeblock gainsCount = 2.The paragraph that read "The tag enum is not a full enum. It carries
NoneandMax, and it does not carryCountor a name function, soEnumName( cmd.fire.type )does not compile" now says the opposite, which iswhat the tool does: it is a full enum,
EnumName( cmd.fire.type )compiles andprints "Laser", the switch further down is there to reach the payload rather
than to name the tag, and
countis a refused arm name for the same reasonnoneandmaxare.G-05 / F-09: the count's birth, and its refusal
SPEC §4.6 refuses a scalar or element range that excludes zero, because zero
initialization is the rule and such a field would be born outside its own
range. A
[A..B]count bound with A above zero fell between the rules: born at0, outside its own wire range, and the writer's check was an assert in five of
the nine targets.
ir.Field.BornCountnames the birth value: the declared minimum, the onewire-legal count a fresh value can carry, since an array takes no specified
default to name another one. Every target's CONSTRUCTED form carries it the way
it carries a specified default. The §5 zero form does not, for the same reason
a specified default does not: it is the all-zero form by rule.
The write side refuses rather than asserts. C, C++, Dart, Java and JavaScript
held the count with an assert or a checked-build predicate, gone under
-DNDEBUG, without-ea, without--enable-asserts, and in the productionJavaScript writer. The count guards the element loop and the pack subtracts the
low bound, so a count below the minimum wraps and the write reports success on
bytes no reader accepts. Go, Rust and C# already refused unconditionally
through their error-returning runtimes, and Elixir through an always-on raise.
The reproduction, at the tip, under
-DNDEBUG#521 recorded this, compiled with
-DNDEBUG:The same program at this branch's tip, same flags:
The corpus reproduction
PR #543 named
generated/cpp/Wire.h:172as the finding live in thisrepository's own shipped corpus. Before:
After:
F-15 / G-08: the tag enum's Count and its debug name
A generated
<Union>Typelooks and acts like a declared enum, so a readerlogging which message arrived writes the enum's own name call against it. It
carried no
Countin any target (#489 gave declared enums one and did notreach tag enums) and a debug-name function in exactly one: C's
enum_name_weapon_fire_type. Each target now emits both, in its own spelling:#define WEAPON_FIRE_TYPE_COUNT 2enum_name_weapon_fire_type(already had it)Count = 2,EnumName( WeaponFireType ), overloadedCount = 2,EnumNameWeaponFireType(ulong)static const int count = 2;enumNameWeaponFireType(int)def count, do: 2enum_name_weapon_fire_type/1WeaponFireTypeCountEnumNameWeaponFireType(uint64)public static final byte count = 2;enumNameWeaponFireType(long)Count: 2,EnumNameWeaponFireType(value)pub const COUNTenum_name_weapon_fire_typeAdding
Countreserves the name: an arm whose exported spelling isCountwould define the member twice, which C++ and C# refuse as a redefinition, so
e9efc13refuses it at the source the wayMaxhas been refused since the tagenum carried an extent. The reservation is scoped to where the member exists: a
TABLE-CLOSURE union's tag shape is emitted beside the tables and carries
Maxalone, so the name stays free there, and
tables/messages/Messages.schema'sEditBodyuses it. Both sides are gated. Giving the table-closure tag shapeits own
Countand extending the reservation to reach it is#601, filed here and
linked from SPEC §4.2, §4.8 and §4.11.
F-12: the guard comment
The comment spelled the branch
if on_radar, nesting with/and negatingwith a trailing
else, so the else side readif on_radar else. Thereflection descriptors always carried the right spelling, built by each table
backend's
guardWalk. The comment now spells it the same way:Field.Guardreaches comments only, in every backend, so nothing on a wiremoves.
The coverage anchor, quoted red then green
e11326afixes a gate that could not fail.TestTagEnumDebugNameCoversNoneVariantsAndOutOfSetsearched the WHOLEgenerated output for
None,Laser,Missileand"???". All four appearelsewhere in the same file:
LaserandMissilename the arm types,Noneisthe tag enum's own member, and
"???"is the DECLARED enum's name functiondefault. So the gate passed with the feature it was gating deleted.
Deleting the C++ tag enum's name function from
internal/codegen/cpp/cpp.go,with the test as it stood:
The gate now slices the function's body: from the opening line each target's
claim already carries, to the out-of-set arm,
"???"being the last name everytarget returns. The same deletion, with the anchored test:
And restored:
The nine spellings moved into a named struct, so the count, the extent and the
name function's opening line live in one map and both gates read the same
source.
The same commit deletes
emitWriteCount'sbits == 0branch ininternal/codegen/cpp/functions.go. §4.6 refuses[Min..N]Twith Min at orabove N, so a count range is never degenerate and the zero-bit path a scalar
range needs had no case to serve. Every
generated/tree regeneratesbyte-identical across the delete, which is the proof it was unreachable.
The negative controls, quoted red
(1) the birth value.
ir.Field.BornCountreturns 0 for a counted array:(2) the release-build refusal. The C++ count write put back on
emitWriteRangedFold32, the assert form:The two claims are separate tests, so each control turns exactly its own red.
(3) the tag enum's Count. The C++ tag enum's
Countline removed:The gate's probe unit declares an enum of THREE variants beside a union of two,
found while running this control: with both at two, the declared enum's own
Countline satisfied the tag enum's claim and the control stayed green.(4) the guard spelling. The old
if X/if X elsecomposition put backin
internal/check:...and the same eight lines for cs, dart, elixir, go, java, js and rust.
(5) the debug-name coverage anchor. Quoted above.
The reproduction rows, and the owner law
bench/LOCK's standing gate: this PR moves the C and C++ packet emitters, sothe reproduction rows are re-pinned here with the sitting stated.
The sitting. A LAPTOP SITTING, not a certification run: MacBook Air, Apple
M2, 8 cores, macOS 26.6.2, Apple clang 21.0.0, Release
(
-O3 -DNDEBUG -DSERIALIZE_RELEASE,-ffp-contract=off), no pinning (macOShas no taskset), serialize at
8b765b7, corpus_id6b213fbfa1a03a99on everyrow, seven runs per row,
pinning: noneandnoise: unlabelledin every CSVheader. The machine was otherwise idle apart from this session. THREE
interleaved rounds per side for C++ and TWO for C, before and after
alternating,
bench/run.sh --only cppand--only c. Best (max) rate per thestandard's §2.2 headline statistic, and the per-round values below because the
headline alone reads wrong here. The whole sitting spans five minutes, 05:48 to
05:53 UTC, and the alternation did not fully cancel a machine drift inside it,
which the per-round section takes seriously rather than around. A quiet-box
sitting on the space server precedes the next release, with the owner's
per-run blessing.
beforeisorigin/mainatb5960d2bandafterisdb9c8da1, which arethe commits the runner stamped into every CSV. The branch has since merged
origin/mainataf66617b, and the page and test commits that follow touch noemitter, so the rows stand for the emitter change they were taken for.
What the numbers show, per round
The earlier reading here took the bitpacker's best-to-best movement (0.1% in
C++, 0.3% in C) as a noise band and dropped every
bench_mixeddelta insidetwice it. That is the wrong use of the control. bitpacker's source is
byte-identical across this diff, so a best-to-best comparison of it measures
what translation-unit layout did to an unchanged loop. It is not a
run-to-run variance estimate, and it is not a band any other row's delta can
be checked against.
The PER-ROUND values are, and they say something the summary rows hide. Best
rate per round, in the order the rounds ran, with the runner's own timestamp:
The machine drifted about 2.5% downward across the sitting, and the
bitpacker rows are what show it. On a byte-identical loop, C++ bitpacker
write reads 60,643 at 05:49 and 59,328 at 05:52 on the SAME binary, and
bitpacker read goes 93,574 to 91,334 over the same minutes. The C side does
the same thing: bitpacker write 60,734 at 05:50:53 down to 59,133 at 05:52:32.
That drift is larger than any delta in the summary table, it hits both sides,
and it is why a best-of-all-rounds comparison is not by itself a reading.
Comparing rounds inside the same minutes of the sitting is.
The C++ rows are faster after, both ways. Time-matched, every after round
beats every before round on both
bench_mixedrows. In the early window,after writes 7,689,113 and 7,781,125 against before's 7,543,961 and 7,522,051.
In the late window, after writes 7,589,283 against before's 7,358,554.
Round_trip does the same in both windows. The raw sets separate outright on
write (the slowest after round beats the fastest before round) and overlap on
round_trip only because the 2.5% drift is bigger than the effect. This moves
the WRONG WAY for a cost, since the diff adds two range refusals to
WriteBenchMixed. Layout is the likely cause, which is the same quantity thebitpacker row measures, and it is not a win this PR claims.
The C write row, at -0.6% best to best, is not proven either way. Two
rounds a side, and they interleave rather than pair: before 7,345,285,
after 7,297,060, after 7,301,575, before 7,173,524, in that order. The
bitpacker beside them fell 2.6% over the same 100 seconds on an unchanged
loop, which is four times the delta being read. The two after rounds sit
exactly where that drift puts them, between the two before rounds. One of
those after rounds also carried the noisiest spread in the sitting, 8.5%,
with a low of 6,678,666. Two samples a side, unpinned, on a laptop, inside a
drift larger than the effect: this sitting cannot separate a real 0.6% write
cost from the machine, and nothing here claims the C write path is unchanged.
It is the row where the diff could plausibly cost something, since it is the
row that gained work, so it is the row that gets settled at the quiet-box
sitting on the space server before release, with the owner's per-run
blessing, rather than argued from a laptop.
The C round_trip row is the same case and gets the same treatment: before
3,655,846, after 3,641,366, after 3,558,336, before 3,570,115, which is drift
with the sides shuffled into it.
What the emitters' change reaches in the bench corpus, and what it does not
The whole diff under
generated/bench/{c,cpp}is five files and 39 addedlines. By class, and there is no sixth:
MixedEventTypegainsCount = 3and, in C++, anEnumNameoverload. Cgains
#define MIXED_EVENT_TYPE_COUNT 3beside theenum_name_mixed_event_typeit already had. Neither is called by anygenerated code: a constant and a function with no caller, off the read
path and off the write path.
entities_countis born at 1, its declared minimum of[1, 8], a memberinitializer in C++ and one line in
new_bench_mixed()in C. Construction,not a path.
WriteBenchMixed, onentities_countand
stats_count, replacingserialize_assertcalls that-DNDEBUGremoved. This is real work the release build did not do before, and it
is not a diagnostic: it is the fix. It is what the write rows measure, and
it is the reason the C write row goes to the quiet box.
Bench.handRealWorld.hchange spelling. Comments.No bench-corpus unit's bytes move.
bytes_per_opis 438 in every row onboth sides,
corpus_idis6b213fbfa1a03a99on both sides, and no wire goldenis in this diff. The runners prove it themselves before they will produce a
number: every pinned instance is byte-compared against its wire golden and
round-tripped (write, read, re-write, memcmp), and a runner whose bytes differ
from the corpus refuses to bench at all. Both legs printed
OKon every run.What #366's matrix says
Nothing changes. #366's rows are tables-layer features, the fixed and variable
classes, the text form, reflection, the block form, the cook, the build
version, and none of these three is one. All three land in all nine targets at
once, so no cell moves in either direction.