Summary
Enable ranges of enum discriminants to be considered valid by all users ahead of time. This includes within the declaring crate.
_ = RANGE is an unnamed variant declaration. It specifies that enum
discriminants in RANGE are valid. It is sound to construct unnamed variants
with unsafe, and to handle them over FFI. If there is no invalid discriminant
for an enum, it becomes an open enum. If it is unit-only, it can then be
as cast from its explicit underlying integer.
Motivation
Enums in Rust have a closed representation, meaning the only valid representations of that type are the variants listed, with any violation of this being Undefined Behavior. This is the right default for Rust, since it enables niche optimization and ensures values have a known state, limiting unnecessary or dangerous code paths.
However, a closed enum is not always the best choice for systems programming. The issue lies with compatibility between existing binaries. There are many cases in which code is expected to handle non-yet-known enum values as a non-error.
Consider a complex system that initially uses this TaskState enum to
communicate:
/// `TaskState` v1
non_exhaustive is specified for forward compatibility, since it should be a
non-breaking change for variants to be added to TaskState. This works by
requiring downstream crates to include a wildcard branch when matching. Once a
new Paused variant is added to TaskState, any code that previously compiled
when using the TaskState will continue to do so. However, if any part of the
system is not recompiled, that old code will see the Paused variant as
invalid.
/// `TaskState` v2
What if it isn't feasible to recompile every part of the system that uses the enum in order to avoid the breaking change?
/// `TaskState` v1 is an open enum instead of using `non_exhaustive`.
If every binary is using this definition, it is not an breaking change for
existing binaries using this definition to add Paused = 2. The _ = .. has
required every exhaustive match of TaskState, including in the defining
crate, to handle the case where it's not one of the currently-named variants.
Protobuf
Protocol Buffers (Protobuf), a language-neutral serialization mechanism, is
designed to be forward and backward compatible when extending a schema.
Initially, it defined all of its enums as closed. However, this caused confusing
and often incorrect behavior with repeated enums, and so the proto3 syntax
switched to open enums. Handling unknown values
transparently comes up often in microservices where incremental rollouts cause
schema version skew.
Protobuf generates code for target languages from a schema. On C++, it can
directly generate an enum - C++ enums are open since it's valid to
static_cast an enum from its underlying integer. However, on Rust, the
current implementation simulates an open enum by using an integer newtype with
associated constants for each variant.
While this allows Protobuf enums in Rust to be used mostly like enums, this is a suboptimal experience.
Newtype integers are bad for enumeration
When the point of a type is to give an integer a set of well-known names (like
in C++), a newtype integer isn't as ergonomic to use as an enum:
- It is arduous to read the generated definition - the variants are inside of an
implinstead of next to the name. It hides the type's nature as an enum. - It's invalid to
usethe pseudo-variants like withuse EnumName::*. - The third-party macro ecosystem built around enums can't be simply used.
- Rust is a systems language that can move data around efficiently, and so first-class support for named integers is valuable for embedded programmers.
- Code analysis and lints specific to enums are unavailable.
- No "fill match arms" in rust-analyzer.
- The
non-exhaustive patternserror lists only integer values, and cannot suggest the named variants of the enum. - The unstable [
non_exhaustive_omitted_patterns] lint has no easy way to work with this enum-alike, even though treating it like anon_exhaustiveenum would be more helpful.
- Generated rustdoc is less clear (the pseudo-enum is grouped with
structs). - In order for a pseudo-variant name to match the normal style for an enum
variant name,
allow(non_uppercase_globals)is required. derives that work with names are less useful. The built-inderive(Debug)can't know the variant names to list. Theopen-enumcrate, which provides an attribute macro to construct newtype integers from anenumdeclaration, requires a disctinctderiveecosystem for operations likeTryFrom,Debug,IsKnownVariant, ser/de, etc. - a worse experience than if all derives were capable of reading a first-class openenumdefinition.
If Protobuf instead declared generated Rust enums with a _ = .. variant, users
could have a first-class enum experience with compatible open semantics.
C interop
A closed #[repr(C)] field-less enums is hazardous to
use when interoperating with C, mostly because it is so easy to trigger
Undefined Behavior when unknown values appear. In C, it is idiomatic to do an
unchecked cast from integer to enum. So, even if one ensures that the C and Rust
libraries are compiled at the same time, they must also audit the C source to
ensure that unknown values cannot be exposed to Rust.
With unnamed variants, interoperating with a C enum is very simple: add
#[repr(C)] and _ = .. to a Rust enum and it's compatible with C.
bindgen has multiple ways to generate Rust that
correspond to a C enum, the default being to define a series of const items.
Its best-effort logic to determine the underlying integer type for a C enum does
not always match that of repr(C) on a Rust enum. A future version of
bindgen could use this feature to add a _ = .. variant to a Rust enum by
default, instead of a exposing a less-effective non_exhaustive attribute.
Today, Rust for Linux configures bindgen to generate newtype integers and raw
integers, based on if the enums are mapped to a typedef of the underlying
integer type. It would switch to using a first-class enum type where
possible if they were sound to use with an evolving C enum.
Dynamic Linking
Dynamically linked libraries, Rust or otherwise, are prone to ABI compatibility breakage.
Ensuring ABI compatibility when extending a library requires extra care. While
non_exhaustive grants API compatibility as variants are added, it does not
provide ABI compatibility. By claiming discriminants for
future extensions to an enum, libraries can choose to remain ABI
forward compatible as new variants are added.
Projects like Redox and relibc would use this feature for this reason among others listed.
Enums with reserved discriminants
Enums designed against specific protocols may have reserved values that shouldn't be directly used but must be cleanly handled if encountered. Two practical examples:
Ipv6MulticastScope
The unstable Ipv6MulticastScope enum defines two doc(hidden) and
perma-unstable attributes to reserve discriminants with unnameable variants. It
could instead define _ = 0x0, _ = 0xF and #[repr(u8)] to reserve these
discriminants.
System call interfaces
TockOS is an embedded OS with a separate user space and kernel space. Its
syscall ABI defines that kernel error codes are between 1 and 1024. It's highly
desirable to keep the 0 niche available for Result<(), ErrorCode>, so the
user space library defines an ErrorCode enum with 14
normal variants and 1010 "reserved" variants that will eventually be renamed.
This has drawbacks:
- It clutters the enum definition.
- rust-analyzer's "Fill match arms" inserts a new match arm for each of the reserved names, even though a single wildcard branch would be more appropriate.
- Since the reserved discriminants have named variants, there's nothing
preventing users from using the reserved name. There is no perfect way to
claim a reserved discriminant without breaking the API.
- Declaring an associated
constis the way to prevent an API breakage. - Moving a reserved variant name like
N00014to adeprecatedassociatedconstis better for readability, but breaks any user that wroteuse ErrorCode::N00014. - Declaring the new variant name as an associated
constis harder to read, doesn't interact with code analyzers, and doesn't let users writeuse ErrorCode::NewVariant.
- Declaring an associated
Zero-copy deserialization
A common pattern on embedded systems is to read data structures directly from a
[u8], facilitated by libraries like zerocopy or
bytemuck. In order to do this, the bytes for an
enum must always be validated to be one of the known discriminants.
This scales poorly for performance and code bloat as more enums and variants are
added to be deserialized in a message. It is more flexible to defer wildcard
branches for unknown discriminants to the point when the enum is matched on,
rather than up-front during deserialization. When these checks are undesirable,
ergonomics must be sacrificed for compatibility and performance by using an
integer newtype.
Restricted range integers
Unnamed variants can be used to define integers that are statically restricted to a particular range, including with niches.
make_ranged_int!;
assert!;
assert_eq!;
assert!;
Pattern types are a more direct and flexible way to express this.
Guide-level explanation
Enums have a closed representation by default, meaning that any enum value must be represented by one of the listed variants. Constructing any enum value with an unassigned discriminant is immediate Undefined Behavior:
// Fruit is represented with specific discriminants of `u32`.
// Undefined Behavior: 5 is not a valid discriminant for `Fruit`!
let fruit: Fruit = unsafe ;
// Rust utilizes these invalid discriminants for compiler-dependent
// optimization:
assert_eq!;
However, by declaring an unnamed variant, the discriminant 5 is claimed
by Fruit and becomes sound to transmute from.
// An explicit repr is required to declare an unnamed variant.
// SAFETY: 5 is a valid discriminant for `Fruit`.
let fruit: Fruit = unsafe ;
// `fruit` is not any of the named variants.
assert!;
// These are both rejected: unnamed variants can't be constructed with
// a variant expression, nor pattern match directly on them.
// assert!(!matches!(fruit, Fruit::_));
// let fruit = Fruit::_;
By introducing this special variant, all users of Fruit must include a
wildcard branch when matching, including within the declaring crate. Think of
the _ = 5 as declaring that "discriminant 5 goes in the _ branch when
matching". There's no safe way to construct a Fruit from a 5, but it can
be transmuted or received over FFI.
match fruit
An unnamed variant accepts a range as its discriminant expression, which ensures each discriminant in the range is claimed and valid to use.
// Fruit is represented with specific discriminants of `u32`.
// SAFETY: 7 is a valid discriminant for `Fruit`.
let fruit: Fruit = unsafe ;
By using .. as an unnamed variant range, all bit patterns for the enum become
valid. It is now an open enum and can be constructed from its underlying
representation via as cast:
// Fruit is represented by any `u32` - it is an *open enum*.
// Using an `as` cast from `u32`.
let fruit = 3 as Fruit;
// Does not match any of the known variants.
assert!;
// `fruit` preserves its value casting back to `u32`.
assert_eq!;
// `derive(PartialOrd, PartialEq)` works by discriminant as usual:
assert!;
assert!;
assert!;
// error: incompatible cast: `Fruit` must be cast from a `u32`
// help: to convert from `isize`, perform a conversion to `u32` first:
// let fruit2 = u32::try_from(5isize).unwrap() as Fruit;
let fruit2 = 5isize as Fruit;
This open enum is much like a struct Fruit(u32), except it is treated as an
enum by IDEs and developers.
Interaction with #[non_exhaustive]
#[non_exhaustive] on an enum and an unnamed variant in an enum similarly
affect how match behaves for that type.
non_exhaustive affects source code only:
- It is flexible in how new variants are represented. E.g. it allows adding variants with fields.
- It does not affect what discriminants are currently valid to represent.
- Crates must be recompiled to use new enum variants.
- It affects only downstream crates.
By contrast, an unnamed variant affects what bit patterns are valid for the type:
- It claims specific ranges of discriminants.
- These claimed discriminants are valid to represent without naming the future variants that use them.
- Crates can manipulate these unnamed enum variants without recompilation.
- It affects all crates, including the declaring one.
Because of this, declaring #[non_exhaustive] on an enum with unnamed variants
emits a warning that the attribute is unused. An unnamed variant makes an enum
"universally non-exhaustive" already.
For enums where the discriminant value is important, an unnamed variant may be
a better choice than #[non_exhaustive]. This is often the case for enums
declaring an explicit repr. It's a non-breaking change to replace
#[non_exhaustive] on an enum with at least one unnamed variant.
Syntax "sugar"
An unnamed variant declaration can be thought of as optimized syntax sugar for declaring a variant with an unwritable name for each unused discriminant in the declared range.
// This:
// Is like syntax sugar for:
However, not even the defining crate can write Fruit::_Unnamed2, unlike a
private enum variant. There's also no limit to the
number of unnamed variants an enum can allocate, so the entire range of u32
can be declared as valid discriminants with _ = ...
Reference-level explanation
Unnamed variants
An unnamed variant declaration is an enum variant declaration with _ as
the variant's name. It is assigned a set of claimed discriminants, each
element of that set representing a single unnamed variant of the enum.
These unnamed variants are valid for the enum and their claimed discriminants
may be reassigned to named variants in the future. It is valid to transmute to
an enum type from a claimed discriminant.
An unnamed variant does not declare a constructor scoped under the enum name,
unlike a named variant. EnumName::_ remains an invalid expression and pattern.
An unnamed variant declaration may be specified more than once on the same enum. It is valid to claim multiple ranges of discriminants. Those ranges may be discontiguous.
To declare an unnamed variant, the enum must have an explicit repr(Int) to
indicate a fixed underlying integer for its discriminant. Int is one of
the primitive integers or C. If it is C, then the Int for the discriminant
expression below is isize and the declaration has further
nuances.
An unnamed variant declaration must specify a discriminant expression with one of these types:
-
Int-
Claims a particular discriminant value.
-
The discriminant must not be assigned to another variant of the enum - whether named or unnamed.
// error: discriminant value `1` assigned more than once
-
-
start..end(core::ops::Range<Int>) or
start..=end(core::ops::RangeInclusive<Int>)-
Ensures every discriminant value in the range is claimed.
-
Named variants have higher precedence than unnamed variants when assigning discriminants to variants. Thus, the set of discriminants claimed by an unnamed variant declaration may be a discontiguous subset of the specified range.
-
The range must not overlap with discriminants claimed by other unnamed variants. Multiple unnamed variant declarations have equal claim to a discriminant value.
// error: discriminant value `10` assigned more than once // error: discriminant values `10..=14` assigned more than once -
The range should be non-empty. A
deny-by-default lint is produced if this is violated and the unnamed variant declaration does not introduce an unnamed variant. -
There should be at least one discriminant available to claim in the range. A
warn-by-default lint is produced if this is violated and the unnamed variant declaration does not introduce an unnamed variant.
-
-
start..(core::ops::RangeFrom<Int>)- Equivalent to
start..=Int::MAXfor non-repr(C)enums.
- Equivalent to
-
..end(core::ops::RangeTo<Int>)- Equivalent to
Int::MIN..endfor non-repr(C)enums.
- Equivalent to
-
..=end(core::ops::RangeToInclusive<Int>)- Equivalent to
Int::MIN..=endfor non-repr(C)enums.
- Equivalent to
-
..(core::ops::RangeFull)-
Equivalent to
Int::MIN..=Int::MAXfor non-repr(C)enums. -
Claims the rest of the discriminants for
Int. This always makes an enum open without consideration for named variants' discriminants. -
Because unnamed variants cannot have conflicting discriminants, this is the only unnamed variant declaration allowed on the enum when used.
// error: discriminant value `1` assigned more than once // help: an `_` variant assigned to `..` forbids other `_` variants
-
Type Inference
The discriminant expression for an unnamed variant has its type inferred as if it were an argument to a generic function accepting the valid types for the representation integer:
const
// ... impl ClaimDiscriminants<Int> for Int {} ...
repr(C) behavior
repr(C) enums have special semantics in Rust because the discriminant
expression type, isize, is not the same as the actual underlying integer.
These enums ordinarily share a layout with ffi::c_int, but if any of the
assigned discriminants cannot fit, a larger underlying integer is chosen that
can represent all of them.
Since this behavior is fraught with mismatches on different compiler platforms, allowing enums larger than
c_intorc_uintis currently being phased out via a Future Compatibility Warning.
Sometimes this is overridden by the system's ABI. On some rarer platforms,
repr(C) enums start as small as 1 byte, smaller than the C int. The behavior
is otherwise the same.
The same rules apply for discriminants assigned to unnamed variants:
// Named and unnamed variants can both grow a `repr(C)` enum.
// Emits FCW `repr_c_enums_larger_than_int` for `Big1` and `Big2`.
// On x86_64-unknown-linux-gnu:
const _: = assert!;
The unbounded end of a discriminant range never affects the underlying
integer of a repr(C) enum. For a repr(C) enum, when a range with an
unbounded end (start.., ..end, ..=end, ..) is used as an unnamed variant
declaration's discriminant expression, the effective bound of the claimed range
is dependent on what the underlying integer would be if no unnamed variants were
declared.
// On x86_64-unknown-linux-gnu:
const _: = assert!;
This behavior means that it is sound to expose a C enum defined like this:
;
as this Rust open enum, regardless of the discriminant values assigned:
Grammar changes
EnumVariant is extended to allow an underscore instead of a variant's name:
EnumVariant ->
OuterAttribute* Visibility?
(IDENTIFIER | `_`) ( EnumVariantTuple | EnumVariantStruct )?
EnumVariantDiscriminant?
No field data
This RFC only defines adding unnamed variants to field-less enums, leaving unnamed variants in enums with fields as future work.
Compatibility
Given an enum in crate version A, published first, and version B introducing some change to the enum:
- An enum in A or B may be a
repr(Int)trueenumwith unnamed variants as described by this RFC or a newtypestructwrappingIntwithpubassociated constants for each named variant. - A change is API compatible if idiomatic downstream code designed for
version A behaves correctly when it's upgraded to version B and statically
recompiled.
- This excludes breaking changes due to glob imports and other discouraged behavior.
- Compatibility is required in only one direction: downstream source code written with A must continue to compile with B, not vice versa.
- This corresponds to a minor change as defined by RFC 1105 and the Cargo SemVer Reference; we use "API compatible" here to distinguish from ABI concerns. By contrast, a major change requires non-trivial changes to be made in downstream source code to accommodate it.
- The enum in A is ABI forward compatible with B if code that is compiled to receive enum values of version A functions correctly when it receives values with the ABI of version B.
- The enum in B is ABI backward compatible with A if code that is compiled to receive enum values of version B functions correctly when it receives values with the ABI of version A. This is relevant to separate compilation of interoperating systems, such as with plugins or microservices.
- A change is ABI compatible if dynamically linked libraries compiled with
A and/or B interoperate correctly. Both directions of compatibility must
be considered for ABI: a library compiled with A may produce values that are
then passed to code expecting B, and vice versa.
- This requires that the enum versions are backward and forward ABI compatible with each other.
- This requires all code be compiled with the same version of Rust or to use a
stable
reprand calling convention where ABI compatibility is expected.
- Control Flow Integrity (CFI) introduces further constraints when considering ABI compatibility.
These changes are API and ABI compatible:
- Replace a
repr(transparent)newtypestructwrapping a non-pubIntwith arepr(Int)openenumof the same name and defining the same variant names.- This breaks the defining crate's usage of
.0. - Associated constants may represent multiple variants with the same discriminant.
- For
repr(C), theIntmust be ABI compatible with the target's chosen integer type for a Cenumwith an equivalent definition. This is usuallycore::ffi::c_int. - This defines a
repr(Int)enumas having the same ABI asInt. See Control Flow Integrity for ABI caveats.
- This breaks the defining crate's usage of
- Given an
enumin A with an unnamed variant claiming discriminant D, add a named variant in B claiming discriminant D.- This replaces the unnamed variant, although the unnamed variant declaration may remain unchanged if D is contained in its discriminant range.
- Removing the last unnamed variant may warn for
unreachable_patternsin downstream crates, as a wildcard branch is no longer required. This can be avoided by adding#[non_exhaustive]to the enum when removing the last unnamed variant.
These changes are API compatible and produce a B that is ABI backward compatible with A:
- Replace
#[non_exhaustive]with an unnamed variant on anenum.- This may require changes to the defining crate to add wildcard branches.
- B may produce values that are invalid if passed to code compiled with A.
- Given an
enumin A that has an invalid discriminant D and is either#[non_exhaustive]or contains unnamed variants, add an unnamed variant in B claiming discriminant D.- B may produce values that are invalid if passed to code compiled with A.
These changes are ABI compatible but break API compatibility, and are particularly sensitive to CFI:
- Replace a
repr(transparent)newtypestructwrapping apubIntwith arepr(Int)openenumof the same name and defining the same variant names.- This breaks downstream source code using
.0to access the discriminant. - This breaks downstream source code using the tuple constructor to build a value with a given discriminant.
- This breaks downstream source code using
- Replace a
repr(Int)openenumwith arepr(transparent)newtypestructwrapping a non-pubInt.- This breaks downstream source code that writes
use Enum::Variantbecause associated constants cannot be imported. - If the
structfield in B is insteadpub, it is a possibly-breaking API change due to breaking source code that defines afnwith the same name as the enum.
- This breaks downstream source code that writes
This change produces a B that is ABI backward compatible with A but breaks API compatibility:
- Given an
enumin A that contains no unnamed variants and isn't#[non_exhaustive], add an unnamed variant.- This breaks exhaustive
matchdownstream and in the defining crate when B is substituted.
- This breaks exhaustive
Control Flow Integrity
Control Flow Integrity (CFI) describes a set of checks inserted into a compiled program to make it harder to exploit bugs. One such check validates function pointer calls by aborting if the function type signatures of the dynamic caller and static callee are considered incompatible by the check.
This function signature is encoded into the binary and compared at runtime. To
compose this string, Clang/Rust use the mangled name of an enum, referred
to below as a type's CFI encoding. If two types share a CFI encoding,
CFI considers them compatible for the purposes of function pointer casts.
These all share the same encoding and are compatible for CFI signature checking
when used as parameters or return values in a function using the C ABI
(extern "C" fn):
enum foo { ... }in C/C++ global namespacetypedef enum { ... } fooin C/C++ global namespacetypedef enum foo { ... } barin C/C++ global namespace usingenum fooorbar. Atypedefname is only encoded when the type it names is anonymous.enum class foo { ... }in C++ global namespace#[repr(C)] enum fooin Rust (ignoring modules)enum foo : uint16_tin C andenum class foo : uint16_tin C++ also share this encoding, since Clang doesn't encode the underlying integer for theenum. It remains ABI incompatible with the above types.
These share a different CFI encoding:
inttypedef int foo
This RFC proposes that #[repr(Int)] enum foo encode the same as
enum foo : CEquivalentOfInt / enum class foo : CppEquivalentOfInt when used
in an extern "C" fn signature. As of writing, compatibility with C/C++
encoding is only attempted for repr(C) enum in extern "C" fn.
Because an enum and its underlying integer don't share the same encoding, this
triggers an abort when using CFI:
// On x86_64-unknown-linux-gnu:
// Also aborts with `extern "C" fn`, `repr(i32)` enum, and reversed conversion.
let f: fn = ;
let g: fn = unsafe ;
// As of writing, Miri identifies f / g as ABI-incompatible and aborts as well.
;
The cfi_encoding attribute overrides a type's identifier for CFI. It uses
Itanium C++ ABI mangling to name the type. For example,
#[repr(C)] enum foo encodes as 3foo and i32 as u3i32. To avoid CFI
aborts, this attribute can be used today to:
- Make a
repr(transparent)newtypestructencode the same as a C/C++enum. - Make a
repr(C)Rustenumencode the same as a C/C++typedef int fooas well as a Rustrepr(transparent)newtypestructwrappingc_int. - Make a
repr(Int)Rustenum fooencode the same as a C/C++ fixed-integerenum foo : CEquivalentOfInt.
By default, Clang and Rust do not encode integers of the same size in the same
way: C int and long may encode differently even when they're the same size.
The Clang -fsanitize-cfi-icall-experimental-normalize-integers and Rust
-Zsanitizer-cfi-normalize-integers flags normalize integer encoding across the
languages so that a C int encodes the same as the signed Rust integer of the
same bit width.
Applicable lints
Unused non_exhaustive
The existing unused-attributes lint also detects the #[non_exhaustive]
attribute present on an enum with unnamed variants.
// warning: `non_exhaustive` has no effect on an enum with unnamed variants
// help: `_ = 2` makes this enum match non-exhaustively in all contexts
// note: `#[warn(unused_attributes)]` (part of `#[warn(unused)]`) on by default
An unnamed variant is more impactful than non_exhaustive since it affects the
declaring crate as well - the enum is "universally non-exhaustive".
Empty discriminant ranges
empty-discriminant-ranges is a new deny-by-default lint. It detects when the
discriminant range assigned to an unnamed variant is empty.
- It is almost always a mistake to specify an empty range.
- An empty or negative range could accidentally cause UB if certain
discriminants are expected to be claimed but are not due to reversing the
startandendof the range. Thus, it isdeny-by-default. - If
allowed, the unnamed variant declaration has no effect. - There are rare use cases involving macro or non-literal discriminants in which in may be intentional to declare an empty variant in order to avoid complex discriminant analysis.
Taken discriminant ranges
taken-discriminant-ranges is a new warn-by-default lint. It detects when
every discriminant in the range assigned to an unnamed variant is already
assigned to a named variant. This results in the unnamed variant definition
having no effect. While an unnamed variant is syntactically present, no unnamed
variant is introduced to the enum as it has no discriminants to claim.
This warning should thus be produced when specifying an unnamed variant on an
enum that is already open. Any macro or codegen that intends to make an enum
open can ignore this lint when adding _ = ..:
// Say bindgen generated this from a C enum.
// It shouldn't have to count the number of variants and compare that against
// the `repr` to know if the enum's already open and must avoid placing the
// `_ = ..`. It can just allow the warning.
Truncatable ranges
overlong-discriminant-ranges is a new warn-by-default lint. It detects when
an unnamed variant's discriminant range can be shortened to avoid overlapping
with named variants.
Let start..=end be the range of discriminants that an unnamed variant
definition is assigned to, regardless of the actual range type used. The
overlong-discriminant-ranges lint detects when all of the below are true:
- The bound is specified as a range expression in the variant's discriminant expression, and not as an identifier or block.
- Every discriminant in some prefix or suffix of the range is already assigned.
That is, there exists some
n ≥ 0such that the sub-rangestart..=(start + n)or(end - n)..=endhas every discriminant in that range assigned to a named variant. Let either sub-range for which this is true be called an "overlong side". - An overlong side is specified with a literal integer, and not implicitly defined by an unbounded range.
- The prefix is an overlong side or the following variant, if any, has an explicit discriminant.
- The
taken-discriminant-rangeslint doesn't detect this unnamed variant.
Gap of length one caused by an exclusive range
The existing non-contiguous-range-endpoints lint also detects when:
- There exists some unnamed variant assigned to a
start..endor..enddiscriminant expression, and endis not a valid discriminant for the enum, andend + 1is a valid discriminant for the enum.
Forgot to mention a named variant
The unstable non-exhaustive-omitted-patterns allow-by-default lint also
detects when a match on an enum with unnamed variants mentions some, but not
all, of the named variants.
This uses the same name as the similar lint for non_exhaustive because it is
burdensome to require developers to remember two different lints for such
similar use cases. This requires updating the documentation of the lint to
reference unnamed variants as well as non_exhaustive.
It may also be prudent to rename the lint before stabilization to include unnamed variants.
let b = A;
// warning: some named variants are not matched explicitly
// pattern `Bar::B` not covered
// help: ensure that all named variants are matched explicitly by adding the
// suggested match arms
// note: the matched value is of type `Bar` and the
// `non_exhaustive_omitted_patterns` attribute was found
let name = match b ;
Next variant's implicit discriminant
When a named variant without an explicit discriminant follows an unnamed variant declaration, the assigned implicit discriminant is the next integer after the declared discriminant range for that unnamed variant. If the unnamed variant is assigned to an integer, it is the next integer.
assert_eq!;
assert_eq!;
assert_eq!;
Non-literal discriminant expression
A non-literal range or integer is allowed for an unnamed variant declaration.
const VALID_FOO: = 10..100;
// SAFETY: `15` is a valid discriminant in range `VALID_FOO`.
let _: Foo = unsafe ;
Only variant
An unnamed variant declaration may be the only variant declaration for an enum.
In this case, an as cast or transmute is the only way to construct an enum
value.
Open enum casting
An open enum is defined as an enum for which every value of its underlying
integer is a valid discriminant.
-
An open enum always has an explicit
reprunderlying integer, or isrepr(C). -
An enum is open if every discriminant value for that integer is associated with a named or unnamed variant.
- For a field-less enum, this means every initialized bit pattern is valid.
_ = ..makes any enum open. This should apply for enums with and without fields.
-
A unit-only open enum may be
ascast from its underlying integer only:2u8 as Color. See below forrepr(C)behavior. -
If an expression with the
{integer}inference variable type is used as the source for anascast to an open enum, it is uniquely constrained to the explicit underlying integer type. This excludesrepr(C); see below.let x = 10; // `x` must be a `u8` to be cast to `Foo` let _ = x as Foo; // error: mismatched types, expected `u32`, found `u8` // let _: u32 = x;
repr(C) open enum casting
The actual underlying integer type for a repr(C) enum changes based on the
variants' numeric discriminant values as described above.
A repr(C) unit-only open enum may be as cast from:
constexpressions of typeisize. This is so arepr(C)enum may always beascast from the same discriminant expression assigned to a variant.- Any primitive explicit-width integer that is capable of representing all
variants' discriminants and does not exceed the size of the enum for the
platform. Thus any signedness cast performed to the underlying integer has no
visible effect.
- This means that authors who don't know or care about short-enum platforms
can cast from
c_intandc_uintto mostrepr(C)open enums, while preventing unexpected truncations when necessary.
- This means that authors who don't know or care about short-enum platforms
can cast from
Examples:
const TEN: isize = 10;
// Must be able to represent `u8::MAX`: backed by `u8` or `c_int` or `c_uint`.
// May be backed by `c_int` or `c_uint` or `i8` or `u8`.
// Must be able to represent negative numbers: backed by `i8` or `c_int`.
// Must be able to hold `isize::MIN..=isize::MAX` which may exceed `c_int`:
// may be backed by `isize`, but could be `c_int` if `c_int` is larger.
assert!;
assert!;
assert!;
assert!;
let zero: c_int = 0;
assert!;
// On thumbv7m-none-eabi:
// error: truncating cast to `repr(C)` open enum
// note: `SmallUnsigned` is backed by `u8`, which fallibly converts
// from `i32`
// help: try converting to `u8` first:
// `u8::try_from(zero).unwrap() as SmallUnsigned`
assert!;
let ten: isize = 10;
assert!;
// On x86_64-unknown-linux-gnu:
// error: truncating cast to `repr(C)` open enum
// note: `SmallUnsigned` is backed by `i32`, which fallibly converts
// from `isize`
// note: a `repr(C)` open enum may be cast from constant `isize`
// help: try converting to `i32` first:
// i32::try_from(ten).unwrap() as SmallUnsigned
assert!;
let byte: u8 = 255;
assert!;
assert!;
_ = byte as Small;
// On thumbv7m-none-eabi:
// error: truncating cast to `repr(C)` open enum
// note: `SmallSigned` is backed by `i8`, which fallibly converts
// from `u8`
// help: try converting to `i8` first:
// `i8::try_from(byte).unwrap() as SmallSigned`
_ = byte as SmallSigned;
let signed_byte: i8 = 10;
assert!;
// On thumbv7m-none-eabi:
// error: truncating cast to `repr(C)` open enum
// note: `SmallUnsigned` is backed by `u8`, which fallibly converts
// from `i8`
// help: try converting to `u8` first:
// `u8::try_from(signed_byte).unwrap() as SmallUnsigned`
assert!;
_ = signed_byte as Small;
_ = signed_byte as SmallSigned;
Interaction with the standard library
derive(Debug)formats asEnumName(X)when formatting an unnamed variant:Xis its claimed discriminant. ADebugformat changing is not considering an API-breaking change.Defaultforbids#[default]from being specified on an unnamed variant, but this may change in the future.- The derives
Clone,Copy,Eq,Hash,Ord,PartialEq, andPartialOrdare unaffected by unnamed variants on a field-less enum. They all operate on discriminants, including those assigned to unnamed variants. mem::Discriminantcontinues to operate as before, always treating field-less enum values with the same discriminant integers as equal and those with different discriminant integers as non-equal.
Drawbacks
- The mutual-exclusion with
non_exhaustivedespite having similar motivations could be confusing to explain to new users. - Every new feauture in Rust is another thing to maintain and for users to learn.
- Rust has not put significant efforts towards ABI compatibility in language constructs in the past.
Flag enums
It is possible to define bitflags style enums using enum syntax with
unnamed variants. However, if BitOr is defined on such an enum, then, rather
confusingly, !matches!(Enum::A | Enum::B, Enum::A | Enum::B). This problem
exists for bitflags or integer newtypes that derive(PartialEq) today, which
is why the library defines a bitflags_match! macro that avoids it.
As future work, a lint could trigger when | is used in a pattern with a
non-integer type that defines BitOr and has structural equality.
Rationale and alternatives
Unnamed variants enable a large range of discriminants to be claimed for an
enum, whether it's all or some of them. NonZero, and an enum spelling out
each discriminant are the only other ways to achieve this in stable Rust today.
The open enum conversion from underlying integer is an ergonomic benefit that is made possible by unnamed variants.
Do nothing
Why not just use an integer newtype or macro?
The best way to write a field-less open enum in Rust today is the "newtype enum" pattern that uses associated constants for variants. So, to make this enum open:
the author can write this:
// Optional, but often useful
// In order to work in a `match`
; // Alternatively, make the inner private and `impl From`
// Enum variants are CamelCase
With this syntax, users of an open enum can use these variant names inside a
match with mostly the same syntax as they would with a regular closed enum,
except there must always be a wildcard branch for handling unknown values.
This syntax also provides grouping of related values and associated methods, an
advantage over module-level const items.
However, this pattern has some distinct disadvantages when used to emulate an open enum, as described in the Motivation section above.
Pattern types can constrain the valid values for an integer newtype, but do not help with the enum ergonomics issue.
Attribute to improve diagnostic behavior for associated const
Newtype integers could improve the ergonomics for a "fill match arms" analyzer capabilities and other diagnostics with an attribute placed on pseudo-variants:
;
However:
- Open enums require even more typing for the desired semantics. They remain a degraded experience compared to closed enums.
- It's very hard to compose macros that use this pattern. Macros cannot easily manipulate enum variant names, especially if a macro is responsible for generating the pseudo-variants. A bespoke attribute must be generated and recognized by other macros that support open enums to use.
- This is less discoverable than a user trying to
ascast to an enum and having the compiler inform them of_ = ..as an option. - It's not clear how this would relate to the functionality of the
[
non_exhaustive_omitted_patterns] lint.
As an enum attribute
An enum could be made open by specifying it as part of its repr:
// requires an explicit `repr(Int)`
use *;
// or an unsafe `transmute`
assert!;
This has the same interaction with #[non_exhaustive]. The drawbacks:
-
It's not as clear what the attribute does, in contrast to the
_ = ..syntax mirroring known concepts: we're introducing new valid values,_means "unnamed/wildcard", and..means "the rest" as the discriminants. -
It is not clear why a
reprwould affectmatch/asbehavior, even though this does affect how it is valid to represent the type.- There are many alternative syntaxes for this, such as
#[non_exhaustive(repr)]or[open]/#[open(Range)]. All should require arepr(Int)be specified.
- There are many alternative syntaxes for this, such as
-
Allowing a claim of particular ranges instead of a full opening could be done with a pattern-type-like syntax, but this is less discoverable:
-
Unnamed variants meld well with unnamed fields in
struct/unionfor ABI stability, if that is ever stabilized. -
An
#[repr(u8)] enum E { A, B }has two possible values, but an open enum would instead have 256. Attributes are not typically used to adjust a type's validity to this degree.#[non_exhaustive]is barely an exception; it merely prevents exhaustive matches. Therefore, something stronger than an attribute should be required to open an enum.
Unbounded ranges select discriminants based on surrounding variants
- This prevents the highly desirable one-line declaration that every discriminant is valid.
- Ordinarily a variant with an explicit discriminant expression is not sensitive to the discriminants of surrounding variants.
Consider this enum being processed by a derive macro:
// How does a derive-macro make this enum have no niches?
How does that macro make the Foo enum open? The macro developer might try to
surround the variants with _ = ..:
But what if CONST1 > CONST2? If this compiles then the range of discriminants
(CONST2 + 1)..CONST1 are invalid and it's not an open enum! If it errors out,
then there's no clear way one is supposed to write the opening-macro.
Complicating the macro further can make it work, so long as empty discriminant
ranges are allowed:
Declare niches instead of claiming discriminants
If an enum selects its discriminants such that a desirable niche exists, like
0, perhaps it is better to declare ranges of niches rather than claiming
discriminants?
It can be very confusing to mix positive and negative assertions, and this would be doing that for enum discriminants in likely a different syntax than variant declaration.
Unnamed variants use the same syntax to assign discriminants, except they do not have to have a name and thus can be assigned to discontiguous ranges.
.. at the end
- This is less flexible than
_ = .., is awkward to restrict to smaller or discontiguous ranges, and introduces a larger syntax change. - This resembles the rest pattern more than the full range expression that discriminants are assigned to and the wildcard pattern that it requires.
Discriminant ranges for named variants instead of unnamed variants
What if instead this were valid?
This is not mutually exclusive with unnamed variants, but this RFC chooses to leave claimed ranges of discriminants as anonymous to keep the feature simple. It can be left as future work for the language. Some of the concerns are:
- It is a possibly-breaking change to add
Icmp = 1. It affects the result ofmatches!(1 as IpProto, IpProto::Other); changing the semantics of a pattern match in downstream code is a major (API-breaking) change for all current Rust features. However, upstream code or the Cargo SemVer Reference could warn downstream users that it's not appropriate to depend upon the valid discriminants forOtherremaining the same across a minor version bump because it is a..variant. - It is ambiguous what value should be chosen when
IpProto::Otheris used in an expression. Some reasonable ways to avoid that are:- Define an arbitrary rule to choose a discriminant for an
IpProto::Otherexpression. - The enum author uses an attribute to specify the "default" discriminant for
an
IpProto::Otherexpression. - Forbid direct construction of
IpProto::Other. It can only be constructed viaunsafeor, for open enums,as-cast from the underlying integer toIpProto. There's no check that the discriminant represents anOthervariant. - A discriminant that is valid for
IpProto::Othermust be provided when constructing the variant. Bikeshed syntax:x as IpProto::Other.- A simple implementation requires that the discriminant
xbe aconstvalue to be checked at compile time as a valid discriminant forIpProto::Other. - To support dynamic values, this would either have to be a fallible enum
constructor or use pattern types to ensure that the input value is valid
for the
Othervariant.
- A simple implementation requires that the discriminant
- Define an arbitrary rule to choose a discriminant for an
- Even if the expression ambiguity issue is resolved, it is not clear how
derive(PartialEq)should function. Currently it always compares discriminant values, but if that is kept, then it's possible formatches!(o, IpProto::Other) && o != IpProto::Other. Ifderive(PartialEq)treats allIpProto::Otheras equal, then it may drastically reduce the performance of thederivewithout an obvious opt-in by the author. - If named variants' ranges can overlap other named variants as shown above,
then the performance of
matches!(o, IpProto::Other)degrades as further variants are added and the set of discriminants representingOtherbecomes more sparse. No other pattern has this characteristic where the performance of matching a pattern is affected by unmentioned properties of the matched type. This is not great for a systems language.
The ability to distinguish a known and unknown discriminant granted by this feature can be substituted with unnamed variants and a derive macro.
A "wildcard" tuple variant with an unknown discriminant field
An alternative way to specify a field-less open enum could be to write this:
Because the only valid representations of the field in Other are invalid
representations for the other variants, this could be optimized to be the size
of a u32 and thus an unsafe transmute for 0 to IpProto results in
IpProto::Other(0). Then, if the variant is declared
private, it a minor change to add a new variant to
IpProto.
This would mean that the Other variant is a named way to refer to unlisted
values and works in pattern matching directly, all while being a zero-cost
representation:
// Changes behavior when the pattern type in `Other` changes.
if let Other = proto
assert_eq!;
assert!;
assert!;
Some concerns:
repr(u32)currently disables niche optimization of the enum based on its variants' fields, so, for consistency, a newreprthat enables niche optimization in a stabilized manner should be defined before this is exposed to users.- This requires pattern types to function, which is a larger and more complex feature to implement and for Rust user's to learn than unnamed variants.
- Without private variants, it is a possibly-breaking
change to replace a discriminant from the
Othervariant with a new named variant. This breaks code that tries to construct aOtherwith that discriminant and affects the type signature of the field. It can also affect the semantic behavior of a pattern match which is considered a major change for all current Rust features. However, upstream code could warn downstream users that it's not appropriate to depend upon the pattern type withinOtherremaining the same across minor version bumps, with documentation or a new attribute. - As a result, this feature would have a long critical path to stabilization.
- The wildcard variant optimization shown here has no clear way to extend to
enums with fields in the future. - It requires extra effort to set up correctly, since the pattern type cannot overlap the other field-less variants.
- It requires extra effort to ensure this optimization is actually taking place. Every addition of a variant requires a change to pattern type of the "wildcard variant" field. This would require a static assertion and likely a macro to ensure that all expected variants are covered.
- This has the same issue regarding the complexity of
matchas the type evolves as theOther = ..alternative above. - This would introduce a new concept to Rust users: that a tuple variant field can carry a discriminant directly rather than a payload (which can be used to discriminate).
Like with Other = .., the utility of determining if the discriminant is known
can be provided with a macro, and an as cast
accesses the discriminant value.
Forward compatibility with all newtype structs
As described in Compatibility, it is a minor change to replace
a repr(transparent) newtype struct wrapping a non-pub Int with an open
enum using unnamed variants. In order to prevent this, it would require the
following non-trivial changes to repr(Int/C) enums:
-
The enum name is a constructor
fn(Repr) -> Enum:assert_eq!; assert!- This is valid for any open enum, the same as the
ascast from integer. - This mirrors the
derive(Debug)format, is ergonomic, and is clear at callsite. Thus it may be worth adding to Rust even if.0isn't. - When should one prefer the constructor over the
ascast? Always?
- This is valid for any open enum, the same as the
-
.0provides direct access to the discriminant value ofenums with an explicit representation:let mut c = Blue; assert_eq!; c.0 += 1; assert!; assert_eq!;- This could be supported for any enum with an explicit
repr(Int/C)by having closed enums beunsafeto mutate through.0- it's an unsafe field.
There are some clear benefits:
-
It is possible to get a reference directly to the discriminant, which can be useful when performing lifetime-constrained zero-copy serialization.
-
The type of
.0is exactly therepr, and doesn't require the user specify a type toascast to and possibly truncate. Currently, there's no language feature in Rust that does this - it requires a macro or codegen to guarantee. This can cause subtle bugs, especially forrepr(C):assert_eq!;Instead,
.0accesses the discriminant without fear of truncation:assert_eq!; // mismatched types, expected `i32`, got `i64` // let _: c_int = X::V.0; -
Some discriminant-manipulating operations are simpler than with
ascasts:let mut x = A; assert_eq!; // SAFETY: 1 is a valid discriminant for `X`. unsafe assert!; -
A fielded enum with
#[repr(Int)]and/or#[repr(C)]is guaranteed to have its discriminant values starting from 0. However, for any given value of that enum, there's no built-in way to extract what the integer value of the discriminant is safely. Theunsafemechanism is(&thenum as *const _ as *const Int).read(). For open fielded enums, some direct access to the discriminant would be even more valuable, since the discriminant could be entirely unknown and the user may want to know its value.
However, this is a subjectively ugly and undiscoverable syntax to access the discriminant of an
enum. Perhaps when introduced, these forms could begin as deprecated and throw a warning to recommend a better syntax than.0but still allow the desired forward compatibility forstructnewtype to openenum.This better syntax could resemble the existing proposals to read and write a discriminant directly. They propose alternative syntax, with an
.enum#discriminantfield rather than.0anddiscriminant_of!/set_discriminantbuilt-ins respectively. - This could be supported for any enum with an explicit
Require repr(C, Int) for compatibility with fixed-type C/C++ enum
This RFC proposes that a repr(Int) enum be
compatible with a C/C++ enum specifying the same fixed underlying type.
Instead, it could be required that repr(C) also be included on a repr(Int)
field-less enum in order to guarantee compatibility with an equivalent C/C++
definition. Specifying repr(C, Int) on a field-less enum is currently
rejected.
This approach has these disadvantages:
repr(C)affects the layout ofenums with fields;repr(C, Int)would mean very different things for field-less and fieldedenums.repr(Int)on anenumwith fields is defined as compatible with aunion-of-structs where eachstruct's first field is a C++enum class : CppEquivalentOfInt. It's inconsistent to have compatibility without spellingCfor enums with fields and requireCfor compatibility of field-lessrepr(Int)enums with their C/C++ counterparts.- It is reasonable for users to expect that
repr(Int)enumbe compatible with a C enum using the same fixed underlying type, whereasrepr(C)enumexists to be compatible with a default C definition.
Forbid unnamed variants' discriminants from overlapping named ones
// error: discriminant `200` assigned more than once
This makes it entirely unambiguous which discriminant is assigned to which
variant, without precedence rules. However, _ = .. to "make it open" is still
desirable.
- Forbidding named variant overlaps with
_ = ..makes it nearly useless, since it then must be the only variant for the enum. - Giving
..special behavior to claim "the rest" of the variants is then inconsistent with other ranges' behavior.- There is precedent for
..acting differently than other ranges, such as whenmatching a number orchar. This.., however, is an expression and not a pattern. - It cannot be reasonably be equivalent to
Int::MIN..=Int::MAXwithout that range allowing named variant overlap.
- There is precedent for
Require an unnamed variant claim at least one discriminant
It is a desirable property for an unnamed variant declaration to always claim at least one discriminant.
This would mean that an unnamed variant declaration in an enum always requires a
wildcard branch when matching. Otherwise, a peculiar situation is possible in
which an enum definition declares unnamed variants, but since the set of claimed
discriminants is empty, does not actually define any unnamed variants and thus
no wildcard branch is needed.
However, upholding this requirement prevents _ = .. from always working to
mean "ensure this enum is open". In order for macros or codegen like bindgen
to ensure an enum is open, they would need to handle the particular edge case of
an enum with 256 variants and an 8-bit discriminant and leave out the variant.
Instead, the lints can be allowed for carefully-considered macros/codegen.
Require non_exhaustive rather than lint if it's there
An enum with unnamed variants lints when the
#[non_exhaustive] attribute is present. Perhaps an unnamed variant could
instead require #[non_exhaustive]? This RFC opts against that, with the
following considerations:
Pros:
non_exhaustivealready implies adding another wildcard branch. This could make it easier to explain to new users by fitting the idea of "needs wildcard branch" into one mental bucket.- This would make the unstable
allow-by-defaultnon_exhaustive_omitted_patternslint more obviously correct to apply to enums with unnamed variants.
Cons:
-
It expands the scope of
non_exhaustive: the wildcard branch required by unnamed variants applies to the defining crate as well as downstream crates. This could make it harder to explain to newer users. -
The variant name being an underscore already implies that a wildcard branch is needed.
-
An author must recite two special lines to make an enum open instead of one.
-
Consider this enum:
When adding
X255, thenon_exhaustiveshould also be removed, but as of today, arepr(u8)enum with 256 variants gives no warning if it isnon_exhaustive. This is even though it would necessarily be an API and ABI-breaking change to add a new variant by changing therepr. This is non-obvious and can be avoided by warning againstnon_exhaustivewhen an enum has an unnamed variant.
Rust could also reject non_exhaustive entirely rather than lint, but this is a
stricter approach than Rust otherwise takes for attributes that unambiguously
have no effect.
Allow an implicit discriminant expression for unnamed variants
Consider:
Ordinarily, a variant's implicit discriminant is one more than the previous variant's. However, a common usage of an unnamed variant is to open the entire enum, and so it is ambiguous what exactly the variant does. It is also not a particularly large burden to require an explicit discriminant expression.
Allow usage without repr
Consider if an unnamed variant could be present without a repr. It could be
equivalent to #[non_exhaustive]. However, this is confusing for a syntax that
describes ranges of variants: what does the range _ = .. actually cover? Is
there still ABI compatibility?
Don't introduce a new as cast
This RFC introduces new a as cast from integer to enum that cannot cause
data loss. While it would be excellent for Rust to provide a non-as mechanism
to convert from integer to enum such as a TryFrom derive, such a mechanism
should be provided for all enums, not just those with unnamed variants.
Prior art
Open and closed enums are pre-existing industry terms.
Enum openness in other languages
- C++'s scoped enumerations and C enums are both open enums.
- C♯ uses open enums, with a proposal to add closed enums for guaranteed exhaustiveness.
- Java uses closed enums.
- Protobuf uses closed enums with the
proto2syntax, treating unlisted enum values as unknown fields, and changed the semantics to open enums with theproto3syntax. This was in part because of lessons learned from protocol evolution and service deployment as described above. - Swift uses both closed and open enums for enums with data, based on if it's
compiled in library evolution mode and marked
@frozen. Adefaultbranch is required whenswitching on a nonfrozen enumeration, and an@unknown defaultemits a warning if there are named enumeration cases that utilize that branch. This achieves the same goal as thenon_exhaustive_omitted_range_patternslint in a different manner.
Other crates that use open enums
Users today are simulating open enums with other language constructs, but it's a suboptimal experience:
- open-enum, written by the author of this RFC. It's a procedural
macro which converts any field-less
enumdefinition to an equivalent newtype integer with associated constants. - Bindgen is aware of the problem with FFI and closed enums, and avoids creating Rust enums from C/C++ enums because of this. It provides an option for newtype enums directly.
- ICU4X uses newtype enums for certain properties which must be forward compatible with future versions of the enum.
- OpenTitan's
with_unknown!macro also uses this pattern to create "C-like enums". winapi-rsdefines anENUMmacro which generates plain integers for simpleenumdefinitions.
The newtype-enum crate is an entirely different pattern
than what is described here.
bitflags
The bitflags crate also uses an unnamed value with _ to
specify valid bits without assigning a name to them.
abi_stable
abi_stable::NonExhaustive uses an associated type to hold a typed raw
discriminant for an enum. It is not ergonomic to match on discriminant values
directly, but another macro could improve this.
Unnamed fields
The unnamed fields RFC reserves space for future extension in a struct or
union for FFI purposes, allowing ABI to be planned ahead of time. Unnamed
variants have similar motivations, but no great workaround. The future work
proposed below to allow _(payload) = discriminants further unifies these
concepts by reserving space for payload to be held in the enum.
repr(open) RFC
There's an RFC proposal that defines a repr(open) syntax as
described in the Alternatives section above.
Unresolved questions
Is the Control Flow Integrity encoding of types the only blocker for
repr(Int) enum to be ABI compatible with Int?
Future possibilities
IsNamedVariant derive
There are certain cases in which it's useful to distinguish between known/named
and unknown/unnamed discriminants. Since unnamed variants cannot do that
syntactically, a derive macro can read the definition and generate a
fn(&self) -> bool that determines if an enum value represents an unnamed
discriminant. This fits the Rust precedent of requiring an opt-in macro for a
minor change to a type definition to result in a change in semantics. While this
can be provided by third parties, it may be better for the ecosystem to have a
standard library solution.
// Equivalent to fallibly building `IpProto::Other` from `x` in the
// "wildcard variant" alternatives above.
assert!;
assert!;
assert!;
assert!;
Discriminant ranges for named variants
A future extension could allow for named variants to specify ranges as discriminants. This bikeshed syntax avoids many of the drawbacks in the related Alternatives section above.
// This is fine.
assert_eq!;
// error: ambiguous discriminant for `Color::Unknown`
// help: specify a discriminant with `2 as Color::Unknown`
// let c = Color::Unknown;
// Use an `as` cast to construct `Color::Unknown` without data loss.
let c = 3 as Unknown;
assert_eq!;
// error: invalid discriminant for `Color::Unknown`
// help: `Color::Unknown` has the discriminant range `2..=u8::MAX`
// let c = 0 as Color::Unknown;
let d = 10u8;
// error: non-constant expression used for enum ranged variant cast
// let c = d as Color::Unknown;
// This is fine.
let c = const as Unknown;
// Pattern types could extend this further:
let e = match d ;
assert_eq!;
Unnamed variants on enums with field data
Unnamed variants on enums with field data would allow library authors to plan for future ABI compatibility by claiming discriminants and data space for an enum. This requires significantly more documentation and care regarding ABI stability before this can be stabilized.
For example:
- This claims discriminants
2..=10as valid for theShapeenum. It's not an ABI-breaking change to add new variants with data toShapeusing these discriminants, so long as it doesn't affect the layout of theShape. Dropglue is forbidden for field data (for a similar reason asunion).- The payload bytes of
Shapeare treated as opaque and never as padding.
By putting field data in an unnamed variant, Shape can specifically
reserve the size and alignment needed to hold future variants' fields:
Advanced casting with pattern types
The as cast from enum to Int could implicitly receive the
pattern type Int is P, where P matches every possible
discriminant for the source expression. For a literal or const enum variant
source, P is just that variant's discriminant. For dynamic source values, it
matches every valid discriminant for the enum. The source enum may also be a
pattern type, which constrains P.
Conversely, the as cast from Int to enum type that is defined in this RFC
could be extended to support more situations by requiring the source Int type
be coercible to Int is P, where P matches every possible discriminant for
the destination type. The destination may itself be a pattern type of enum.
While it could be required that the enum type have an explicit repr(Int) or
repr(C), it is not technically necessary nor a breaking change from this RFC's
more conservative proposal. All valid discriminants for an enum have a known
but possibly different value when cast to the underlying integer for the enum,
which can then be integrated into P for a static lossless check.
Example syntax:
// Compatible with `repr(Rust)` since `1` is statically known to be valid
// for any type chosen for the `{integer}`.
let c = 1 as Color;
assert_eq!;
// error: invalid discriminant for `Color::Unknown`
// help: `Color` has valid discriminants in `0..=2 | 5`
// let c = 3 as Color::Unknown;
// No check necessary; casts are bidirectionally infallible.
assert_eq!;
// No check necessary; only returns `Red` or `Yellow`.
// Bidirectional casts require a pattern return type.
| Yellow
// Compatible because `Color::Red as u8` has the type `u8 is 0`.
assert_eq!;
// Compatible because `subset_cast(0) as u8` has the type `u8 is 0 | 5`.
assert_eq!;
// The below *may* be valid if `some_color` is made `const` and keeps returning
// `Color::Red` or `Color::Yellow`.
// error: incompatible pattern type coercion
// help: the source pattern is `0..=2 | 5`
// help: the destination pattern is `0 | 5`
// help: `3..=4` is incompatible
// assert_eq!(subset_cast(some_color() as u8), Color::Red);
// error: incompatible source pattern type for cast
// help: `Color` has valid discriminants in `0..=2 | 5`
// help: the source pattern type is `0..=5`
// help: `Color` is incompatible with `3..=4`
// fn superset_cast(x: u8 is 0..=5) -> Color { x as Color }
match on ranges of enums
let code = unsafe ;
let name = match code
Improved control over CFI encoding
This RFC defines ABI compatibility between repr(Int/C) enums and their
underlying types, which matches the C standard (C23 §6.7.3.3). However,
CFI treats these as incompatible types and aborts.
Two enums with the same underlying type are incompatible: compatibility isn't
transitive.
The cfi_encoding attribute allows the name of a type for CFI be directly
controlled, but it has downsides:
- The CFI encoding of integers is dependent on compiler flags, and so a manual override can be valid for one set of flags but not another.
- It requires extra knowledge of name mangling.
- The mangling is technically platform dependent: Clang on Windows uses MSVC mangling for CFI.
A cfi_encoding_of attribute could instead be used to copy the encoding of
another type:
// Compatible with `typedef int32_t Foo` instead of `enum Foo: int32_t`