RFA-402 · Case file with fixtures · Case 374 of 694 · Compiler evidence
Explicit Discriminants on a Data-Carrying Enum Need an Integer repr
Rust requires a primitive integer representation when explicit discriminants coexist with non-unit enum variants. Add repr only for a real tag-layout contract; otherwise remove the numeric assignments and serialize tags explicitly.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets; ABI details depend on the selected representation
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Explicit discriminants on an enum that also has non-unit variants require a concrete primitive representation so the compiler has an unambiguous tag layout contract.
- First discriminating check
- Check whether any non-unit variant and any explicit discriminant coexist, then add an intentional repr integer or remove numeric discriminants if no layout contract needs them.
I gave enum variants explicit numeric values because the numbers existed in a protocol. The enum also carried data. Rust stopped the declaration with E0732 and asked for #[repr(inttype)].
The failing fixture mixes a unit variant and a tuple variant, both with explicit discriminants. Adding #[repr(u8)] makes the declaration compile.
A discriminant identifies the active variant
The Rust Reference describes enum discriminants as the logical integer associated with a variant. Rust always needs to distinguish variants, even when no number is written in source.
For an ordinary Rust enum, the compiler can choose a representation compatible with the language's layout rules and optimizations. Explicit numeric assignments make part of that choice visible. When fields are also present, Rust requires the programmer to select a primitive representation for this form.
The E0732 explanation states the direct rule: an enum with an explicit discriminant and a non-unit variant must specify an integer repr.
repr(u8) is a layout decision
The repaired fixture uses:
#[repr(u8)]
enum Message {
Ready = 1,
Data(u8) = 2,
}
The Reference documents the primitive representation of enums with fields. Conceptually, the representation contains a tag using the selected integer type plus storage for the active variant's fields, following the specified layout rules.
I choose u8 only if its range covers all discriminants and the layout is the intended contract. Changing to u16 just to silence overflow changes representation and possibly ABI.
Compilation is not a complete serialization format
Even with repr(u8), I do not automatically write the enum's memory bytes to a socket or file. A protocol also needs byte order for payload fields, padding rules, length framing, accepted unknown tags, and version behavior.
Raw enum memory has validity constraints. Not every byte is a valid discriminant, and constructing an enum from an unknown integer through transmutation can be undefined behavior.
For external data I decode the numeric tag explicitly:
1 -> Ready
2 -> read and validate one payload byte -> Data(value)
other -> protocol error or Unknown policy
This gives malformed and future values an ordinary error path rather than manufacturing an invalid Rust value.
Remove explicit numbers when they are not contractual
Sometimes numbers were added only for logging or debugging. In that case the clean repair is to remove the assignments and let Rust manage the internal discriminants.
For logs, I print stable names or define a separate conversion function. This avoids turning a presentation detail into a representation promise that future variants must preserve.
I do not add repr casually. Public layout commitments restrict later refactoring and can become part of FFI expectations even when that was not the original intention.
Casting has its own restrictions
Fieldless enums can often be cast to an integer using as. Data-carrying enums are not general numeric wrappers, and extracting a tag should not be confused with extracting the payload.
std::mem::discriminant can compare variant identity without revealing a numeric value. Pattern matching is normally better when behavior depends on a variant because it gives safe access to fields and forces the code to handle the variant set.
When a protocol number is required, I implement an explicit mapping. The mapping is reviewable, can return Option or Result, and does not depend on reading representation bytes.
Duplicate and overflowing values remain errors
An integer repr does not permit two variants to share a discriminant, and implicit successors still need to fit. Nearby Atlas cases demonstrate collisions and overflow at the representation boundary.
I test the smallest and largest assigned tags, reserved gaps, and unknown input. These are domain tests, while the compiler checks representational validity of the declaration.
Payload layout still matters
Choosing a one-byte tag does not mean the entire enum is one byte. The largest variant payload, its alignment, and padding contribute to the size. A variant containing a strongly aligned type can make the whole enum aligned and larger.
I use size_of assertions only for a deliberately target-specific or ABI-facing contract. Ordinary application logic should not assume that tag width equals enum size.
My repair decision
When E0732 appears, I ask:
- Do the numeric values come from a real external or stable internal contract?
- Is a Rust memory-layout promise actually needed, or only encode/decode functions?
- Which integer width covers current and reserved tags?
- How are unknown values handled before an enum exists?
- Have payload layout and target ABI been tested separately?
The core principle is that variant identity, numeric protocol tags, and in-memory representation are related but not identical. Rust requires an integer repr for this explicit data-carrying form. I add it only when I intend the layout contract, and I still serialize untrusted bytes through checked mappings.