Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-373 · Case file with fixtures · Case 345 of 694 · Compiler evidence

An Implicit Rust Enum Discriminant Does Not Wrap After u8::MAX

An implicit enum discriminant is the previous value plus one, and it must fit the declared representation. Rust rejects the value after 255 for repr(u8); it never silently wraps the variant table.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
An implicit discriminant is the preceding discriminant plus one, and that value must remain representable by the enum's primitive representation.
First discriminating check
Inspect the variant immediately before the reported one and calculate its successor inside the declared repr range.

I once read an enum as if its implicit numbering behaved like wrapping integer arithmetic. After 255 in a repr(u8) enum, I expected the next unnumbered variant to become zero. Rust refuses this with E0370.

The failing fixture puts ImplicitNext after LastExplicit = 255. Rust says its discriminant overflowed on the value after 255.

Automatic numbering still performs a checked assignment

The Reference defines an implicit discriminant as one higher than the previous variant's discriminant. With repr(u8), the representable range ends at 255. The mathematical successor is 256, not a valid u8 discriminant.

The rule does not say “perform wrapping_add in the representation.” Rust diagnoses the declaration because silent wrapping would usually create a duplicate zero and make the enum ambiguous.

This difference is useful beyond enums. Whenever Rust offers checked, wrapping, saturating, strict, and overflowing integer operations, the policy is named. Enum declaration syntax does not opt into wrapping arithmetic.

Reordering variants can expose the error

The maximum value may exist safely as the last variant. Moving another implicit variant after it causes E0370, even if no numeric expressions changed.

This means source order is part of the implicit numbering calculation. In a large enum with occasional explicit anchors, one edit can affect a long suffix. The reported variant is where the impossible successor appears; the cause is usually the value immediately before it.

My first check is to annotate the effective values from the nearest explicit anchor through the failing line. I do not start by changing the representation, because that can accidentally change an external format.

Explicitly assigning zero may compile but change the contract

Rust's diagnostic can suggest assigning the next variant explicitly if that outcome is desired. For example, ImplicitNext = 0 avoids arithmetic overflow only when zero is not already assigned to another variant.

That does not mean cycling is generally a good protocol design. A wrapped sequence loses monotonicity, may reuse reserved values, and surprises tools that assume adjacent declaration order means adjacent numeric order.

I assign an explicit non-conflicting value only after checking the schema. Compiler acceptance proves representability and uniqueness, not business correctness.

The repaired fixture keeps the range honest

The repaired program changes the explicit anchor to 254. The following implicit variant becomes 255, and assertions record both values.

Other legitimate repairs include moving the max-valued sentinel to the end, choosing a larger representation when the external contract allows it, removing a redundant variant, or assigning an intentional gap value.

If u8 was selected for a wire protocol, changing to u16 is a protocol migration, not a local compiler fix. Existing encoders, C bindings, database columns, and stored bytes may still expect one byte.

Primitive repr is a layout promise with limits

A primitive representation gives the enum a documented discriminant type and constrains its layout. That can be necessary for FFI or binary formats, but it also means the declared variants must fit that finite set.

An enum cannot have more distinct unit variants than the representation has values. Explicit gaps reduce the remaining convenient range but can be valuable for compatibility. I reserve gaps deliberately and document whether later versions may occupy them.

For application state that never crosses an ABI boundary, I avoid choosing repr(u8) only to save imagined bytes. Rust's normal representation can exploit layout optimizations, and the clearer model may matter more than a guessed size. I measure the enclosing type if layout is important.

Protocol parsing should retain unknown integers

Even when all local variants fit, a newer sender can provide a code an older binary does not recognize. I receive the byte as u8, validate it, and return an unknown-code error or preserve it in a raw wrapper.

I never use unchecked transmutation to manufacture an enum. A u8 representation specifies how valid declared variants are represented; it does not turn every byte into a valid value of that enum.

This becomes especially important near maximum sentinels such as 255. Many protocols use that value for unknown, extension, escape, or vendor-specific meanings. An automatic successor is rarely what the schema author intended.

A table test protects the decision

For externally visible enums I assert every (variant, integer) pair. I include the first value, explicit anchors, values immediately after anchors, reserved maximums, and unknown-input behavior.

During review I calculate the next value rather than relying on visual order. If an enum is generated, the generator validates range and uniqueness before emitting code. This keeps E0370 from becoming a late build failure after a schema update.

The core principle is that an enum discriminant is an identity, not a counter allowed to roll over. The representation defines a finite range, and Rust rejects an implicit successor outside it. That strictness protects both memory validity and the stability of numeric contracts.