Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-618 · Case file with fixtures · Case 590 of 694 · Compiler evidence

A Rust Transparent Enum Must Have Exactly One Variant

Transparent representation wraps one unambiguous variant. A real state enum needs its own discriminant layout or an explicit wire conversion.

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
A real state enum requiring discriminant information was modelled as a single-path wrapper whose ABI must come from exactly one variant.
First discriminating check
Preserve genuine alternatives under an enum representation or explicit wire conversion, using transparency only for one permanent wrapper variant.

Transparent representation says the outer type is represented like one wrapped field through one variant. If an enum has two possible variants, the runtime must also encode which variant is present. The failing fixture declares Ready and Failed and gets E0731.

Alternatives require discriminant information

An ordinary enum can use a discriminant and payload layout chosen under its representation rules. A transparent wrapper delegates rather than introducing its own alternative tag. Two variants leave no single representation to delegate through.

The official E0731 page requires exactly one variant. Zero variants have no wrapped representation; multiple variants are ambiguous.

This makes a single-variant transparent enum closer to a wrapper than to a state machine.

The fixture repair removes the alternative

The repaired fixture keeps only Ready(u32) and asserts size equality with u32. A transparent tuple struct would usually express this wrapper more simply, but the enum form isolates the exact language rule.

If Failed is a real domain state, removing it is not a valid production repair. I would remove transparent instead, choose an appropriate enum representation if FFI needs one, or translate between a foreign status code and a rich Rust enum.

Compiler satisfaction must not erase a failure state from the domain.

C-style status codes need validation

Foreign APIs often return an integer where zero means success and other values mean specific errors. A Rust enum with repr(i32) can document known codes, but constructing an enum directly from an unknown integer may be invalid.

I parse with a match, preserve unknown values in an Unknown(i32) wrapper or error, and keep the raw ABI type at the boundary. This supports forward compatibility when a newer library adds codes.

Transparent newtypes are excellent for raw code wrappers because every integer bit pattern remains valid. Domain conversion then performs validation separately.

Layout equality is not the whole interface

The Reference defines transparent representation, including exactly one non-zero-sized field across the single variant. I verify size and alignment, but I also test foreign call ABI where the wrapper crosses function boundaries.

Validity, ownership, and semantic ranges can differ from the inner field. A transparent NonZeroId cannot safely be created from zero merely because its layout follows an integer-like field.

Adding zero-sized marker fields may be allowed under rules similar to transparent structs, but auto traits and variance still need review.

One-variant enums can express future intent poorly

Choosing an enum because variants may be added later conflicts with transparent ABI: adding a second variant will fail and necessarily change the representation strategy. A public FFI wrapper should document whether its shape is permanently transparent.

For extensible Rust-only states, use a normal enum from the start. For ABI-stable raw values, use a transparent newtype plus methods. Separating raw and domain types makes future additions explicit and safe.

I include compile fixtures and foreign harness tests in ABI work. Layout changes should fail before release, not appear as corrupted status values in production.

Prefer conversion over shared interpretation

For a status boundary, I often expose a transparent raw-code newtype and convert it into a normal Rust enum after validation. Foreign code and Rust then agree only on the integer contract; they do not need to share every detail of an evolving domain enum. Unknown codes remain representable for logging or forwarding. This design is slightly more explicit, but it makes ownership of compatibility clear and avoids asking repr(transparent) to solve versioning that representation alone cannot solve.

The same split helps tests. Byte or ABI fixtures exercise the raw wrapper, while ordinary exhaustive matches exercise the domain enum. A new domain state cannot accidentally become a silent ABI change.

My E0731 checklist

  • Does the transparent enum have exactly one variant?
  • Is it truly a wrapper, or does the domain require alternatives?
  • Would a transparent tuple struct communicate the wrapper more clearly?
  • If alternatives are real, what discriminant or wire conversion represents them?
  • Can foreign code return unknown status values that need preservation?
  • Are size, alignment, validity, and actual call ABI all verified?
  • Do zero-sized markers affect auto traits or variance?
  • Would future variant addition contradict a promised transparent interface?

The core principle is that transparency delegates representation through one unique path. E0731 prevents a multi-state enum from hiding the information needed to distinguish its states. I preserve domain alternatives and choose a representation that can honestly encode them.