RFA-348 · Case file with fixtures · Case 320 of 694 · Runtime evidence
mem::discriminant Ignores Enum Payloads
mem::discriminant returns opaque variant identity, not whole-value identity and not a portable numeric tag. Payload equality needs PartialEq or explicit matching, while wire tags need an explicit stable representation.
- 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 discriminant represents only enum variant identity and deliberately excludes the fields carried by values of that variant.
- First discriminating check
- Compare same-variant values with different fields and use PartialEq or pattern matching when the payload belongs to the identity decision.
I once used mem::discriminant as a quick equality shortcut for an enum with large payloads. Ready(1) and Ready(99) produced equal discriminants. The function answered which variant each value used, not whether the values were equal.
The failing program compares those two values. The failed assertion shows both have Discriminant(0) in this run, although application code must not attach meaning to that displayed number.
A discriminant selects the variant
An enum value contains a variant choice and may contain fields belonging to that variant. The Rust Reference describes a logical discriminant associated with each variant.
mem::discriminant returns a value representing that variant identity. Its documentation explicitly states that two values of the same enum variant have equal discriminants even when their payloads differ.
This is useful when I want to group or compare only states such as Ready versus Failed. It is incomplete for comparing the data carried by those states.
Whole-value equality has a different contract
The repaired program derives PartialEq. It proves that the two Ready discriminants are equal while the complete values are unequal.
When payload identity matters, I use PartialEq or pattern match and compare the fields required by the domain. When only the variant matters, discriminant can express that narrow question without borrowing or constructing dummy payloads.
I name helpers accordingly: same_variant and same_state_value should not be one ambiguous same_state function.
The returned value is intentionally opaque
Discriminant<T> supports equality, ordering traits where available through implementations, hashing, copying, and debugging. It does not promise a portable integer representation for arbitrary enums.
The Discriminant(0) debug output in the fixture is evidence for equality in that compilation, not a wire-format tag. The documentation warns that transmuting a discriminant to a primitive is undefined behavior.
If a protocol, database, or file format needs stable numeric tags, I define them explicitly and implement conversion. Internal compiler layout is not an external schema.
Explicit repr solves only a specified problem
The Reference explains how explicit discriminants and primitive representations can define variant values under the relevant enum rules. Even then, payload layout, FFI compatibility, and serialization require their own guarantees.
I do not add repr merely to make a debug print look predictable. I add it when an ABI or representation contract requires it, document the supported targets, and test the conversion boundary.
For ordinary application enums, keeping the discriminant opaque gives the compiler freedom and prevents accidental persistence of implementation details.
Payload-free comparison can still hide state
Suppose Failed { retryable: true } and Failed { retryable: false } share a discriminant. Grouping metrics by variant may be correct, while deciding whether to retry from the discriminant would be a serious bug.
The same issue appears with Some(a) and Some(b): variant identity tells me presence, not contained identity. I review every discriminant use by asking what payload differences are being deliberately erased.
If the answer is unclear, a pattern match normally makes the intended fields easier to see.
Tests should separate three questions
I test same variant with equal payload, same variant with different payload, and different variants. This distinguishes complete equality from variant equality. I also test all variants when a mapping drives metrics or state-machine transitions.
For serialized tags I test exact encoded values independently of mem::discriminant. For FFI I verify the documented representation on every supported target rather than assuming Rust's opaque value is acceptable to C.
The fixture includes an otherwise-unused Failed value precisely so the different-variant category remains visible beside the failing same-variant comparison.
I also review changes to the enum itself. Adding a new variant can make an exhaustive match stop compiling, which is valuable, while a generic discriminant-based map may accept the value but lack a domain policy for it. Compiler completeness and runtime keyability are different guarantees. Tests should state what a newly introduced state is expected to do.
For metrics, I normally map variants to explicit stable names such as "ready" and "failed". This survives debug-format changes, keeps cardinality bounded, and gives dashboards a contract independent from compiler representation. Payload fields belong in carefully selected dimensions only when they are safe and bounded.
The deeper principle is deliberate projection
Systems often compare projections: a variant without its payload, a date without its time, a path without its base, or an identity without its version. The comparison can be correct only if the discarded information is irrelevant to the decision.
mem::discriminant is a clear projection from an enum value to variant identity. I use it when that is the exact question, never as a faster-looking replacement for equality and never as a hidden serialization format.