Skip to content

enums export Count beside Max, in all nine targets (#456) - #489

Merged
gafferongames merged 3 commits into
mainfrom
enum-count
Sep 4, 2026
Merged

enums export Count beside Max, in all nine targets (#456)#489
gafferongames merged 3 commits into
mainfrom
enum-count

Conversation

@gafferongames

Copy link
Copy Markdown
Contributor

Closes #456.

The rule

The owner's ruling, verbatim in effect: Count is the number of declared
variants, excluding None. Max stays the extent
— the largest ordinal the
storage admits, which sizes a keyed array — equal to Count without headroom
and larger with it. Flags keep Count and no Max.

So enum Weapon | max = 15 { Laser, Missile, Railgun } now exports Count = 3
beside Max = 15, and a learner who sized a loop by Max on a plain enum no
longer 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 is
the spelling that target already uses for a flags declaration's Count:

target enum Count
C #define WEAPON_COUNT 3
C++ Count = 3, (an enumerator inside the enum class)
C# Count = 3, (a member of the native enum)
Dart static const int count = 3;
Elixir def count, do: 3
Go WeaponCount Weapon = 3
Java public static final byte count = 3;
JavaScript Count: 3, (in the frozen object)
Rust pub const COUNT: Weapon = Weapon(3);

The generated <Union>Type tag enums carry Max alone: a tag set takes no
headroom, so its count and its extent are one number and a second name for it
would buy nothing.

Schema expressions

E.Count folds the way E.Max does — internal/check's .Count resolver now
answers for enums as well as flags, so [..E.Count]T is a counted array over
the declared variants beside [E.Max]T's keyed one. Unlike .Max it needs no
bound: an enum whose | max = ... failed to resolve still answers .Count
exactly, because the count never depended on the attribute.

The claim

Count is a claimed name under the §4.6 rule, on exactly the terms Max has
always been: the Go/C++/C#/JS spelling <Enum>Count, the C/Rust preprocessor
and constant spelling <ENUM>_COUNT, and the Rust associated-constant slot
COUNT (which is what refuses a variant spelled count as well as Count). A
variant literally named Count is refused up front with its own message.

No wire change on either wire

Neither the protocol id nor the build version moves — Count is generated-code
surface and enters no wire-shape projection and no cook projection.
TestExportedSurfaceMovesNeitherWire pins both numbers for both corpora as
literals rather than reading testdata/golden/, because a wrong re-pin of
those files is precisely the mistake the gate exists to refuse:

examples      protocol 0x91a8e85156dfe2b1   build version 0x5f535f12a00fb522
examples128   protocol 0x42050541a90eea8a   build version 0x2352e62e904fb4da

make conformance-generate moved nothing outside generated/: testdata/conformance/
and testdata/wire/ are byte-identical.

Tests

  • TestEnumDeclaredCountEmitted (internal/codegen/golang/golang_test.go) — a
    generation assertion per target, all nine, over one enum with headroom
    (Count 2, Max 15) and one without (Count 3), plus a direct assertion
    that the headroom case really has Count < Max.
  • TestEnumCountValue (internal/check/projection_test.go) — E.Count and
    E.Max fold to 2/2 and 3/15, and [..Wide.Count]uint8 / [Wide.Max]uint8
    resolve to bounds 3 and 15.
  • TestEnumCountIsClaimedconst WeaponCount beside enum Weapon is
    refused with a diagnostic naming the enum's generated Count.
  • Diagnostics rows: a variant named Count; .Count on a type; .Count
    undefined; and, on the good-corners side, E.Count in a const expression and
    as an array bound.

The negative control

Count removed from the Dart emitter alone — red on Dart and on nothing else,
which is the half that matters:

--- FAIL: TestEnumDeclaredCountEmitted (0.00s)
    golang_test.go:254: Dart: "static const int count = 2;" emitted 0 times, want 1 — every generated enum carries Count beside Max (SPEC §4.2)
    golang_test.go:254: Dart: "static const int count = 3;" emitted 0 times, want 1 — every generated enum carries Count beside Max (SPEC §4.2)
FAIL

Restored, green.

The generated diff

generated/ moves in 54 files, +116 / −6, and the whole of it is the new
constants; the six deletions are go/format re-aligning a const block around
the wider symbol. testdata/golden/ moves the same way, in 27 files, +83 / −6.

generated/cpp/Wire.h
     Railgun = 3,
+    Count = 3, // the declared variant count (SPEC §4.2)
     Max = 15, // the exported extent (SPEC §4.2)

generated/go/Enums.go
-	TeamBlue Team = 2
-	TeamMax  Team = 2 // the exported extent (SPEC §4.2)
+	TeamBlue  Team = 2
+	TeamCount Team = 2 // the declared variant count (SPEC §4.2)
+	TeamMax   Team = 2 // the exported extent (SPEC §4.2)

generated/c/Enums.h    +#define TEAM_COUNT 2
generated/rust/src/enums.rs
                       +    pub const COUNT: Team = Team(2); // the declared variant count (SPEC §4.2)

Docs

  • SPEC §4.2 gains a .Count bullet beside the E.Max one (what it counts,
    how it parts from the extent under headroom, [..E.Count]T against
    [E.Max]T) and a paragraph beside "The extent is exported" defining the
    exported Count, its nine spellings, its reserved-variant status and its
    claim. §4.6's claimed-names bullet now names an enum's Max and Count.
  • USAGE's enum section states the two loop rules once — a loop over
    declared variants runs from one to Count; a loop over every ordinal
    including None runs from zero to Max; size storage and keyed arrays by
    Max — and, where the section prices the enum's bits, one sentence that
    None is one of the wire values, so an enum with a power-of-two declared
    variant count pays one bit for it.
  • docs/COMPARISON-TABLES.md's enum row names Count and Max.

No tutorial page: the draft is not in the tree.

🤖 Generated with Claude Code

@gafferongames

Copy link
Copy Markdown
Contributor Author

Blocked on the C/C++ lock — an owner decision, not a defect

cpp-lock refuses this PR. Every other check is green, including all nine
conformance legs (c, cpp, cs, dart, elixir, go, java, js, rust), generated,
msvc, windows and big-endian.

The refusal is exact and expected: bench/LOCK freezes five prefixes while the
#170 round stands, and the ruling on #456 puts Count in every emitter in one
PR, so two of the five are unavoidable.

generated/c-ludicrous/Ludicrous.h (locked: generated/c-ludicrous/)
generated/c/Enums.h               (locked: generated/c/)
generated/c/Wire.h                (locked: generated/c/)
generated/cpp/Enums.h             (locked: generated/cpp/)
generated/cpp/Wire.h              (locked: generated/cpp/)
generated/cpp/ludicrous/Ludicrous.h (locked: generated/cpp/)
internal/codegen/c/c.go           (locked: internal/codegen/c/)
internal/codegen/cpp/cpp.go       (locked: internal/codegen/cpp/)

What the change actually is on those two emitters. One line each, and
neither is on a codec path:

internal/codegen/c/c.go     + g.pf("#define %s_COUNT %d\n", screaming(d.Name), len(d.Variants))
internal/codegen/cpp/cpp.go + g.pf("    Count = %d, // the declared variant count (SPEC §4.2)\n", len(d.Variants))

The generated movement is one #define per enum and one enumerator per enum —
no storage, no write path, no read path, no wire. The lock's control 2
(reproduce the c/cpp bench rows) is not reached: this PR touches no harness leg
and nothing under generated/bench/{c,cpp}. What it cannot satisfy is control 1
— emitters byte-unchanged — because the ruling is that Count lands in every
emitter at once, and a nine-language vocabulary ruling landed in seven leaves
the two reference targets saying something different from the other seven.

The decision. Either a one-shot carve on the #348 protocol — the owner's
word on #170, a PR suspending the prefixes, this PR, then a PR restoring them —
or a ruling that C and C++ take Count in a follow-on when the round closes,
in which case I split them out and this PR lands at seven.

I have not touched bench/LOCK: the file states that lifting or amending it is
a PR whose diff touches only it, on the owner's word.

Certification is running on this branch (certify.yml, dispatched) since the
change moves the C++ emitters.

@gafferongames

Copy link
Copy Markdown
Contributor Author

The locked paths, exactly — for the carve to name

git diff --name-only origin/main...HEAD -- internal/codegen/c internal/codegen/cpp generated/,
filtered to the five prefixes bench/LOCK carries. Eight paths, and no ninth:

internal/codegen/c/c.go                 (locked: internal/codegen/c/)
internal/codegen/cpp/cpp.go             (locked: internal/codegen/cpp/)
generated/c/Enums.h                     (locked: generated/c/)
generated/c/Wire.h                      (locked: generated/c/)
generated/c-ludicrous/Ludicrous.h       (locked: generated/c-ludicrous/)
generated/cpp/Enums.h                   (locked: generated/cpp/)
generated/cpp/Wire.h                    (locked: generated/cpp/)
generated/cpp/ludicrous/Ludicrous.h     (locked: generated/cpp/)

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 #define or one enumerator per enum —
seven enums across them, and nothing else in either tree.

Control 2 and the bench legs, stated rather than assumed. No harness leg
moves: git diff --name-only origin/main...HEAD -- bench/ test/ is EMPTY, and
bench/LOCK itself is untouched. What does move under generated/bench/ is
four inert lines, which the generated gate requires (a stale committed tree
is a dirty working tree after make test):

generated/bench/c/Bench.h        +#define MIXED_WEAPON_COUNT 15
generated/bench/c/RealWorld.h    +#define PACKET_MODE_COUNT 5
generated/bench/cpp/Bench.h      +    Count = 15, // the declared variant count (SPEC §4.2)
generated/bench/cpp/RealWorld.h  +    Count = 5,  // the declared variant count (SPEC §4.2)

Those four are under no locked prefix — generated/bench/c/ is not
generated/c/ — and cpp-lock did not name them. Naming them here anyway,
because the lock's prose puts generated/bench/{c,cpp} under procedural control
2 and a declaration that costs nothing at runtime is still a reader's call to
make, not mine.

bench/LOCK is untouched and stays that way; the carve is the owner's.

@gafferongames

Copy link
Copy Markdown
Contributor Author

Certification on this branch: one gate, and it is main's, not this PR's

certify.yml dispatched on enum-count (run 33830457045), because the change
moves the C++ emitters and ci.yml says to. It stops at one hard gate, in every
inline-gate leg on both OSes:

ZERO-COST GATE FAILED: testdata/golden/tables/pointers/GraphTable.h moved
ZERO-COST GATE FAILED: testdata/golden/tables/pointers/MarksTable.h moved
ZERO-COST GATE FAILED: testdata/golden/tables/stream/StreamTable.h moved
ZERO-COST GATE FAILED: testdata/golden/tables/blobs/AssetsTable.h moved

(The FAILED: lines above it in the log are the sabotage negative controls
printing the red they exist to print.)

It is not this PR. The diff touches neither the table emitter nor those
goldens:

$ git diff --name-only origin/main...HEAD -- internal/codegen/cpptable testdata/golden/tables
(empty)

And regenerating in a clean origin/main worktree reproduces the same drift,
line for line:

pointers/GraphTable.h        78 changed lines
pointers/MarksTable.h        13 changed lines
stream/StreamTable.h         26 changed lines
blobs/AssetsTable.h          26 changed lines

Every changed line is the tolerant-read scan's unknown_records counting —
int32_t unknown_records = 0;, unknown_records++, and the
if ( nodes.good ) { out->unknown += unknown_records; } tail with its §3.1
comment. That construct entered internal/codegen/cpptable in 7420e29
(#391 / #429)
, and those goldens were last pinned in efb2951 (#259 /
#428)
, one commit earlier — so #429 landed the emitter change without the
make update-goldens re-pin, and make tables-block-zero-cost has been red on
main since.

Filed here rather than fixed here: it is another PR's re-pin, it belongs in its
own diff, and the four files are Go-only to regenerate
(make build/tables-generated/.stamp then the update-goldens copy step), so
it is a short PR for whoever owns #429's tail.

Nothing else in certification had gone red at that point; the run stops there.

gafferongames added a commit that referenced this pull request Sep 4, 2026
Co-authored-by: Rowan Claude <rowan@mas-bandwidth.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames and others added 3 commits September 4, 2026 15:28
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>
@gafferongames

Copy link
Copy Markdown
Contributor Author

Rebased onto main; all 20 checks green

cpp-lock passes — the #495 carve suspends the prefixes, so the two locked
emitter lines land with the other seven.

GitHub's merge of main into this branch is gone: rebased properly onto
6859a75b, three commits, no merge commit
(git log --merges origin/main..HEAD is empty). MERGEABLE / CLEAN.

What main moved, and what I did about it. #487 bumps the cook projection's
form version, so every build version moves; the protocol ids do not. Both
numbers in TestExportedSurfaceMovesNeitherWire are re-taken from main's own
compiler
at 6859a75, in a clean worktree with no Count in it:

examples     protocol 0x91a8e85156dfe2b1  build version 0x69d6a810cfa22717
examples128  protocol 0x42050541a90eea8a  build version 0xedde8274fbab7f85

Those are exactly the numbers this branch produces, which is the whole point of
the gate: the base's wire identities reproduce here unchanged, and Count
reaches neither. The doc comment now also says what may legitimately move these
literals — a projection or form-version change, which moves the corpus goldens
in the same commit — so a literal that has to move alone stays the defect it
names.

Everything regenerated rather than hand-edited: the generated/ stamps, the
golden source trees via go test ./internal/goldens -update, and
make conformance-generate. None of them moved a byte — the committed trees
were already what the rebased compiler emits, so the only file in this third
commit is the test.

make check clean, go test ./... green, gofmt/go vet clean.

Negative control re-run on the rebased tree, Count removed from the Dart
emitter alone — red on Dart and nothing else, then restored:

--- FAIL: TestEnumDeclaredCountEmitted
    Dart: "static const int count = 2;" emitted 0 times, want 1
    Dart: "static const int count = 3;" emitted 0 times, want 1

One check on main's new maps half, since it lands beside this: an enum key on a
map is refused by name (SPEC-TABLES §2.8), and an enum-keyed collection stays
[E]T — so USAGE's two loop rules need no third, and "size storage and keyed
arrays by Max" still covers every case.

Not merging; standing by for the #488 rebase.

@gafferongames
gafferongames merged commit f762d6c into main Sep 4, 2026
20 checks passed
@gafferongames
gafferongames deleted the enum-count branch September 4, 2026 05:35
gafferongames added a commit that referenced this pull request Sep 4, 2026
* 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>
rowan-claude pushed a commit that referenced this pull request Sep 6, 2026
#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.
rowan-claude pushed a commit that referenced this pull request Sep 6, 2026
#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.
gafferongames added a commit that referenced this pull request Sep 6, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enums export Max and no Count; flags export Count and no Max; under headroom Max stops being the count — one vocabulary ruling (#447 F-03, F-04)

1 participant