enums export Count beside Max, in all nine targets (#456) - #489
Conversation
Blocked on the C/C++ lock — an owner decision, not a defect
The refusal is exact and expected: What the change actually is on those two emitters. One line each, and The generated movement is one The decision. Either a one-shot carve on the #348 protocol — the owner's I have not touched Certification is running on this branch ( |
The locked paths, exactly — for the carve to name
The two emitter lines, in full: // internal/codegen/c/c.go, in emitEnum, immediately above the _MAX define
g.pf("#define %s_COUNT %d\n", screaming(d.Name), len(d.Variants))
// internal/codegen/cpp/cpp.go, in emitEnum, immediately above the Max enumerator
g.pf(" Count = %d, // the declared variant count (SPEC §4.2)\n", len(d.Variants))The six generated files move by one Control 2 and the bench legs, stated rather than assumed. No harness leg Those four are under no locked prefix —
|
Certification on this branch: one gate, and it is main's, not this PR's
(The It is not this PR. The diff touches neither the table emitter nor those And regenerating in a clean Every changed line is the tolerant-read scan's Filed here rather than fixed here: it is another PR's re-pin, it belongs in its Nothing else in certification had gone red at that point; the run stops there. |
Co-authored-by: Rowan Claude <rowan@mas-bandwidth.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The owner's ruling: `Count` is the number of DECLARED variants, excluding the implicit `None`. `Max` stays the extent — equal to `Count` without headroom, the declared `| max = K` with it. Flags keep `Count` and no `Max`. Every emitter gains it in its own idiom, the spelling each already uses for a flags declaration's `Count`: `Count = N,` in C++, C# and JS objects, `<Enum>Count` in Go, `pub const COUNT` in Rust, `#define <ENUM>_COUNT` in C, `static const int count` in Dart, `public static final <typ> count` in Java, and `def count, do: N` in Elixir. The generated `<Union>Type` tag enums carry `Max` alone: a tag set takes no headroom, so its count and its extent are one number. `E.Count` folds in schema expressions the way `E.Max` does, so `[..E.Count]T` is a counted array over the declared variants beside `[E.Max]T`'s keyed one, and unlike `.Max` it needs no bound — an enum whose `| max = ...` failed to resolve still answers it exactly. `Count` is a claimed name and a refused variant name, on the same terms as `Max`. NEITHER WIRE MOVES. Count is generated-code surface: it enters no wire-shape projection and no cook projection, and a new gate pins both corpora's protocol id and build version as literals rather than reading the goldens a wrong re-pin would carry along. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`ExprHasEnumMax` and `ast.MaxExpr` both described `.Count` as a flags-only spelling. It reaches enums now; the behavior was already right. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#487 bumped the cook projection's form version, which moves every build version; the two protocol ids are unmoved. Both numbers are taken from main's own compiler at 6859a75, so the gate still says what it is for: this branch reproduces the base's wire identities exactly, and `Count` reaches neither. examples protocol 0x91a8e85156dfe2b1 build version 0x69d6a810cfa22717 examples128 protocol 0x42050541a90eea8a build version 0xedde8274fbab7f85 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
f42ffaa to
52eaff6
Compare
Rebased onto main; all 20 checks green
GitHub's merge of main into this branch is gone: rebased properly onto What main moved, and what I did about it. #487 bumps the cook projection's Those are exactly the numbers this branch produces, which is the whole point of Everything regenerated rather than hand-edited: the
Negative control re-run on the rebased tree, One check on main's new maps half, since it lands beside this: an enum key on a Not merging; standing by for the #488 rebase. |
* bench/LOCK: the carve is spent, the prefixes are back #488 and #489 merged under the suspended prefixes. The lock re-freezes at the new reference; this diff is the file alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * bench/LOCK: keep the narrative line commented Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Rowan Claude <rowan@mas-bandwidth.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
#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.
#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.
…597) * tool: a [A..B] count is born at its minimum and a count outside its wire 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. * tool: the nine-way gate on the count's birth and its refusal, and the 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. * tool: a union's tag enum exports Count and its debug name, in all nine (#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. * tool: the guard comment spells the branch the way the reflection descriptors 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. * tool: the count gate walks lines with strings.SplitSeq (#521 G-05) The line walk that proves no assert reaches the count ranged over strings.Split, which the pinned modernize analyzer rewrites to SplitSeq. * check: a packet union's arm named Count is refused by name (#447 F-15) 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. * test: the count's contract is a refusal in every build, in the legs that 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. * docs: the count's birth and its every-build refusal, and the tag enum'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. * test: the debug-name gate is anchored to the name function's body, and 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. * docs, test: §4.6 says why the write refusal lives there, and the body 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. * docs: the page reader's four fixes on the count and tag-enum law Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Closes #456.
The rule
The owner's ruling, verbatim in effect:
Countis the number of declaredvariants, excluding
None.Maxstays the extent — the largest ordinal thestorage admits, which sizes a keyed array — equal to
Countwithout headroomand larger with it. Flags keep
Countand noMax.So
enum Weapon | max = 15 { Laser, Missile, Railgun }now exportsCount = 3beside
Max = 15, and a learner who sized a loop byMaxon a plain enum nolonger gets it wrong the first time they meet headroom.
The per-target spellings
Each is the idiom already standing beside that target's
Max, and the word isthe spelling that target already uses for a flags declaration's
Count:Count#define WEAPON_COUNT 3Count = 3,(an enumerator inside theenum class)Count = 3,(a member of the native enum)static const int count = 3;def count, do: 3WeaponCount Weapon = 3public static final byte count = 3;Count: 3,(in the frozen object)pub const COUNT: Weapon = Weapon(3);The generated
<Union>Typetag enums carryMaxalone: a tag set takes noheadroom, so its count and its extent are one number and a second name for it
would buy nothing.
Schema expressions
E.Countfolds the wayE.Maxdoes —internal/check's.Countresolver nowanswers for enums as well as flags, so
[..E.Count]Tis a counted array overthe declared variants beside
[E.Max]T's keyed one. Unlike.Maxit needs nobound: an enum whose
| max = ...failed to resolve still answers.Countexactly, because the count never depended on the attribute.
The claim
Countis a claimed name under the §4.6 rule, on exactly the termsMaxhasalways been: the Go/C++/C#/JS spelling
<Enum>Count, the C/Rust preprocessorand constant spelling
<ENUM>_COUNT, and the Rust associated-constant slotCOUNT(which is what refuses a variant spelledcountas well asCount). Avariant literally named
Countis refused up front with its own message.No wire change on either wire
Neither the protocol id nor the build version moves —
Countis generated-codesurface and enters no wire-shape projection and no cook projection.
TestExportedSurfaceMovesNeitherWirepins both numbers for both corpora asliterals rather than reading
testdata/golden/, because a wrong re-pin ofthose files is precisely the mistake the gate exists to refuse:
make conformance-generatemoved nothing outsidegenerated/:testdata/conformance/and
testdata/wire/are byte-identical.Tests
TestEnumDeclaredCountEmitted(internal/codegen/golang/golang_test.go) — ageneration assertion per target, all nine, over one enum with headroom
(
Count2,Max15) and one without (Count3), plus a direct assertionthat the headroom case really has
Count < Max.TestEnumCountValue(internal/check/projection_test.go) —E.CountandE.Maxfold to 2/2 and 3/15, and[..Wide.Count]uint8/[Wide.Max]uint8resolve to bounds 3 and 15.
TestEnumCountIsClaimed—const WeaponCountbesideenum Weaponisrefused with a diagnostic naming the enum's generated
Count.Count;.Counton atype;.Countundefined; and, on the good-corners side,
E.Countin a const expression andas an array bound.
The negative control
Countremoved from the Dart emitter alone — red on Dart and on nothing else,which is the half that matters:
Restored, green.
The generated diff
generated/moves in 54 files, +116 / −6, and the whole of it is the newconstants; the six deletions are
go/formatre-aligning a const block aroundthe wider symbol.
testdata/golden/moves the same way, in 27 files, +83 / −6.Docs
.Countbullet beside theE.Maxone (what it counts,how it parts from the extent under headroom,
[..E.Count]Tagainst[E.Max]T) and a paragraph beside "The extent is exported" defining theexported
Count, its nine spellings, its reserved-variant status and itsclaim. §4.6's claimed-names bullet now names an enum's
MaxandCount.declared variants runs from one to
Count; a loop over every ordinalincluding
Noneruns from zero toMax; size storage and keyed arrays byMax— and, where the section prices the enum's bits, one sentence thatNoneis one of the wire values, so an enum with a power-of-two declaredvariant count pays one bit for it.
CountandMax.No tutorial page: the draft is not in the tree.
🤖 Generated with Claude Code