Summary
Right now the layout algorithm is mixed up with the type of the discriminant.
This RFC splits those up better for more flexibilty and new functionality.
Motivation
Quick! What's the difference between #[repr(Rust, u32)], #[repr(u32)], and #[repr(C, u32)]?
What does #[repr(u128)] do in Rust 1.14? What does #[repr(u48)] do in Rust 1.138?
Why is mem::Discriminant<Option<u8>> bigger than Option<u8>? How can I make a c_long discrinant?
Is enum Foo { A = 2222222222 } something you're allowed to write?
The core problem here is that the repr attribute just isn't a great place for the type of the discriminant.
Specifying it at all changes the layout algorithm. It's unclear what it's promising in semver.
Proc macros that check layout algorithms need to know all possible tokens, so break for new types.
This RFC proposes splitting out the layout and usage choices
- Are layout optimizations allowed?
- How do fields in variants get laid out?
- Can you cast values to integers?
- Who can see the discriminant type you picked?
from the choice of which discriminant type it uses.
Guide-level explanation
It's possible to associate a number with each of the variants in an enum:
enum Symbols {
Octothorp = 0x23,
Ampersand = 0x26,
Asterisk = 0x2A,
}
These are known as discriminants, and each must be unique.
By default they have type isize, so using a value of a different type will fail:
enum Symbols {
Octothorp = b'#',
Ampersand = b'&',
Asterisk = b'*',
}
error[E0308]: mismatched types
--> src/lib.rs:2:17
|
2 | Octothorp = b'#',
| ^^^^ expected `isize`, found `u8`
You can, however, pick a different integer primitive for the type of the discriminants:
enum Symbols : u8 {
Octothorp = b'#',
Ampersand = b'&',
Asterisk = b'*',
}
That type need not just be directly a primitive name; it can be a type alias or path.
Notably, you can use things like : pub core::ffi::c_short in FFI scenarios.
It does need to be resolvable to a specific primitive integer type at declaration time,
however. It cannot be a non-primitive (like String or NonZeroU32) nor can it depend
on any generic parameters of the enum.
If you're just matching the enum with match or if let, then the discriminants are irrelevant.
There's a couple of different ways you can make use of them, however.
From inside the same module, you can always use your_enum.#discriminant to get the value.
The type of that value is whatever type you picked, or isize if nothing is specified.
The visibility on that is treated like a a field would be. Because it's private by default, you can pick whatever type and values are convenient without making a semver commitment.
This is particularly useful inside derive macros, as generated trait impls can thus
get the discriminant values from instances without needing to replicate the layout algorithm.
For example, on a fieldless enum a derive could produce an implementation like
impl Ord for TheEnum {
fn cmp(&self, other: &Self) -> Ordering {
Ord::cmp(&{self.#discriminant}, &{other.#discriminant})
}
}
that will compare using the declared discriminant type without needing to write it anywhere.
If you do want to promise something, however, you can specify a different visibility. For example, on a publicly-exported type you can say
pub enum Symbols : pub u8 {
Octothorp = b'#',
Ampersand = b'&',
Asterisk = b'*',
}
which promises that the discriminant type will stay u8 and that the values of those
discriminants will stay the same. That also lets downstream consumers use .#discriminant.
(Other visibilities like pub(in some::path) are supported as usual too.)
On it's own, specifying a discriminant type doesn't impact layout. The compiler is allowed to look at the specified values and pick a smart encoding to save space.
There's a variety of repr options you can use for more control.
repr(fieldless)
A fieldless enum is similar to a transparent struct around the discriminant type: variants are allowed to have 1-ZST fields, but nothing else.
This guarantees that the size and alignment of the enum type match that of the discriminant type,
and the allowed values are exactly those of the discriminants.
(Note that this still doesn't allow using arbitrary values of the discriminant.)
Additionally, this allows as casting the enum to any primitive type from anywhere
the discriminant type is visible (so long as the enum doesn't implement Drop).
repr(C, union_of_structs)
This uses what RFC#2195 called repr(Int); see that RFC for details.
repr(C, struct_with_union)
This uses what RFC#2195 called repr(C, Int); see that RFC for details.
Migration guide
You're using repr(Int)
If you have an enum like
#[repr(u32)]
pub enum MyEnum {
A(…),
B { … },
}
then you should migrate to
#[repr(C, union_of_structs)]
pub enum MyEnum: u32 {
A(…),
B { … },
}
(Or potentially to pub u32 if you wish to make a commitment.)
If you have an as-castable enum like
#[repr(u32)]
pub enum MyEnum {
A = …,
B = …,
}
then you should migrate to
#[repr(fieldless)]
pub enum MyEnum: pub u32 {
A = …,
B = …,
}
You're using repr(C, Int)
If you have an enum like
#[repr(C, u32)]
pub enum MyEnum {
A(…),
B { … },
}
then you should migrate to
#[repr(C, struct_with_union)]
pub enum MyEnum: u32 {
A(…),
B { … },
}
(Or potentially to pub u32 if you wish to make a commitment.)
If you have an as-castable enum like
#[repr(C, u32)]
pub enum MyEnum {
A = …,
B = …,
}
then you should migrate to
#[repr(fieldless)]
pub enum MyEnum: pub u32 {
A = …,
B = …,
}
You're using as casts but not a repr
If you have an as-casted enum like
pub enum MyEnum {
A = …,
B = …,
}
then you should migrate to
#[repr(fieldless)]
pub enum MyEnum: pub isize {
A = …,
B = …,
}
or consider picking a better discriminant type so lints can better guide your users.
Reference-level explanation
Syntax
The production for an Enumeration is currently
enumIDENTIFIER GenericParams? WhereClause?{EnumVariants?}
That changes to
enumIDENTIFIER GenericParams? EnumDiscriminant? WhereClause?{EnumVariants?}
with
EnumDiscriminant →
:Visibility? TypePath
A new alternative is added for ExpressionWithoutBlockNoAttrs of
EnumDiscriminantExpression → Expression
.#discriminant
Static Analysis
The path in the discriminant type must resolve to a primitive signed (i8, …, i128, isize)
or unsigned (u8, …, u128, usize) primitive type at declaration time.
When resolving the type path in EnumDiscriminant, generics from the type are in-scope for name resolution, but it's an error to mention them.
The expressions in EnumVariantDiscriminant are type-checked with the expected type
matching the previously-resolved primitive discriminant type.
(The same as when it was spelled repr(Int).)
For expr.#discriminant:
- the type of
exprneeds to known to be anenum. (It cannot be generic.) - the type of the result is the discriminant type of that enum.
- the location needs to meet the visibility restriction.
For reprs:
- If the EnumDiscriminant is included, no
Intcan be included in therepr. - If the EnumDiscriminant is included,
Ccannot be included in thereprunless there's exactly one ofstruct_with_unionandunion_of_structs. struct_with_unionandunion_of_structscan only be used withCand with the EnumDiscriminant included.
Lowering
.#discriminant translates to the existing Rvalue::Discriminant in MIR.
Drawbacks
Changing discriminant types from tokens (which proc macros can rely on directly) to paths (which need name resolution) can introduce opportunities for confusion and hygiene complications.
For example, this will be legal with this RFC:
type u8 = u32;
enum Foo: u8 { A = 123456 }
Similar problems already exists in current rust, however, such as in struct fields.
Rationale and alternatives
Why require C for guaranteed layouts?
Today you can only pick your discriminant type by disabling layout optimizations. Safe Rust code, though, doesn't want to give those up but might still want to pick discriminant types and values.
Future work could also add more options, like repr(linear, union_of_structs).
Why add fieldless?
Today it's possible to as cast any enum that happens not to have fields.
That's not the way Rust usually does things, though, since it's something that you can commit to by accident. By having an explicit "yes, I want this" the compiler can check to make sure you don't accidentally add a field that disables it.
It's also helpful to give a direct migration path for types that are as cast today
without needing to support that on other kinds of enums.
Why include visibility?
Without visibility, adding a mechanism for typed discriminant access opts-in everything to a new semver commitment with no opportunity to get ahead of it.
That's also why it's not just accessed via some trait method. (Of course, someone
can offer a trait derive to expose it generically if they so wish.)
Notably, there's no way today to get discriminants as integers from repr(Rust) enums
that have any fields in variants, and type authors should have a chance to consider
what type and values they want to expose, if any.
For example, today Result has isize as its discriminant type. That's clearly not
the type that a two-variant type would pick, but today it's impossible to change it
to something else since that would remove the layout optimizations. It likely doesn't
want to commit to any discriminant type for now, since bool would likely be the
desired choice but this RFC doesn't allow that.
Similarly, it might not want to commit to Ok(()).#disciminant being zero either, as
aligning discriminants with Option may allow more efficient conversions.
(Result of course needs to preserve the stable behaviour of PartialOrd and Ord,
but so long as there's no access to the discriminant order, they can change.)
Why include generics in name resolution but not allow using them?
We may in the future be able to allow them to be used, but adding them to name resolution later would be a breaking change so we can head that off now.
Given the different naming conventions, it's unlikely that this restriction will impact any non-contrived code.
Why the # in the pseudo-field?
Including a # emphasizes that it's something special, rather than a normal field ident.
That keeps it out of the way of any future features, like enum variant types, which might allow accessing variant fields directly.
Why put the discriminant type there?
As described in the prior art section below, this location is common in other braces-based languages.
A trait definition uses the order
trait Foo<Generic> : Base where Generic: Bounds { … }
so this uses the matching order
enum Foo<Generic> : DiscriminantType where Generic: Bounds { … }
and a super-trait is a TypePath just like the discriminant type is.
Prior art
Various other brace-based language use a similar syntax to pick the backing type of enums.
For example, C23 has
enum byte : unsigned char { … };
C++ has
enum class Handle : std::uint32_t { … };
C# has
enum ErrorCode : ushort { … }
Unresolved questions
- Should
expr.#discriminantbe a value expression or a place expression? For now it'd probably have to block any non-copy uses of the place, but for known layouts that force it to be non-niched, we could potentially allow getting a pointer to that field specifically.
Future possibilities
More Types
Obvious candidates to enable would include things like bool, char,
NonZero<Int>, and pattern types.
Generic Types
It could potentially be useful for discriminant types and values to be based on generic parameters.
That will introduce a bunch of complications, though, since for example it'll mean
that the Discriminant rvalue in MIR is no longer simply offered
More special items
Like the foo.#discriminant expression, we could offer things like Foo::#DiscriminantType.
More layouts
If we add repr(linear) for structs, we could have repr(linear, union_of_structs) for enums.
We could also add more partial layout guarantees. Perhaps a repr(Rust, discriminant_firat)
that still allow layout optimization with the constraint that the discriminant would
exist at offset zero with the specified type.