RFA-619 · Case file with fixtures · Case 591 of 694 · Compiler evidence
Rust Data-Carrying Enum Discriminants Need an Integer repr
Explicit tags on an enum that also carries payload require a defined discriminant integer type. Choose width from the real ABI and validate raw values.
- 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
- Numeric tag assignments were added to a data-carrying enum without selecting the integer type needed to define its tagged representation.
- First discriminating check
- Choose repr width from the actual ABI or remove incidental numeric tags, then validate every raw discriminant before enum construction.
An enum with payload variants needs a layout for both its tag and payload. When code assigns explicit numeric discriminants while mixing unit and data-carrying variants, Rust requires the discriminant integer type to be declared. The failing fixture omits it and gets E0732.
Numeric tags become part of the representation contract
Empty = 0 is not only variant order. It states a concrete tag value. With Data(u8) also present, Rust needs a well-defined way to represent and, in low-level contexts, locate that tag.
The official E0732 explanation requires #[repr(inttype)] for this combination. I choose the integer type from a protocol or ABI specification rather than selecting u8 because the values currently fit.
Without an external numeric contract, removing explicit discriminants may be better and lets Rust choose its normal layout.
The repair declares repr(u8)
The repaired fixture adds #[repr(u8)]. Rust can now represent explicit and implicit tags under that width.
The small discriminant assertion uses Rust's safe discriminant API only to identify equal variants. I do not assume that API returns the numeric u8. Extracting raw layout through pointer casts needs the exact Reference guarantees and an unsafe proof.
The Reference documents primitive representation of enums with fields, including the tagged-union model.
repr does not make arbitrary integers valid enums
Only declared discriminants correspond to valid variants. Transmuting an unknown u8 into the enum can create an invalid Rust value and undefined behaviour. A network or FFI decoder must match raw tags before constructing safe variants.
I keep a raw representation type at the boundary. Parsing tag zero creates Empty; known data tags validate payload; unknown tags become an error or preserved unknown variant in a separate safe model.
This is especially important for forward-compatible protocols. A newer sender may add a tag older code does not know. Rejecting or preserving it explicitly is safer than unchecked conversion.
Payload layout needs equal attention
Knowing the tag width does not fully define wire serialization. Padding, field alignment, byte order, and union layout matter. repr(u8) is a memory-layout attribute, not a complete network-format derive.
For file and network protocols I usually encode and decode bytes explicitly. For shared-memory or C ABI exchange, I compare against a foreign declaration, assert layout, and run cross-language tests on each target.
Data-bearing enum representation can be subtle. I rely on current Rust Reference guarantees rather than diagrams copied from a compiler version or blog.
Discriminant ranges and evolution need planning
Adding enough variants can exhaust a small tag type or collide with reserved values. Explicit values must remain unique and representable. I reserve ranges only when the external protocol defines them and add compile or schema tests.
Changing repr width or numeric assignments is a breaking ABI and serialization change. Variant reordering may also affect implicit values after explicit ones. I assign every external tag explicitly in the translation layer so source reordering cannot change the protocol.
Rust-only enums can often avoid these commitments and benefit from compiler layout optimisations. I keep ABI enums at the edge and use richer domain enums internally.
Test meaning, not only size
An assertion that the enum occupies an expected number of bytes is necessary in some FFI work, but it cannot prove that both sides assign the same meaning to tag 2. I keep a table of raw values and expected domain variants, exercise it from both languages where possible, and include unknown values. That catches semantic drift caused by copied headers or reordered definitions even when size and alignment remain unchanged.
My E0732 checklist
- Does the enum mix explicit discriminants with non-unit variants?
- Is the numeric tag required by an external ABI or only incidental?
- Which integer width and signedness does the specification require?
- Are every declared and future-reserved value representable and unique?
- Is raw input matched and validated rather than transmuted?
- Are byte order, padding, payload layout, and target ABI also specified?
- Could a separate raw type and domain enum improve forward compatibility?
- Do cross-language or byte-vector tests protect tag stability?
The core principle is that explicit discriminants turn variant identity into a numeric representation promise. E0732 makes the width of that promise explicit when payloads are involved. I anchor it in the real boundary and validate every raw value before it becomes a Rust enum.