Skip to content

Conversation

@kupiakos
Copy link

@kupiakos kupiakos commented Dec 9, 2025

Enable ranges of enum discriminants to be reserved ahead of time with an unnamed enum variant. This requires all users of that enum to consider those values as valid - including within the declaring crate.

This is useful for any situation in which recompiling an enum to handle new discriminant values is infeasible, and using a newtype integer is unergonomic due to the type's intended nature as a set of enumerated values. This includes FFI, Protobuf, and embedded syscalls.

If an enum is valid for every discriminant (it has no niches), it is an open enum and may be as cast from its explicit backing integer. For example:

#[repr(u32)]     // Fruit is represented by any `u32` due to the unnamed *open variant* below.
enum Fruit {
    Apple,       // Apple is represented with 0u32.
    Orange,      // Orange is represented with 1u32.
    Banana = 4,  // Banana is represented with 4u32.
    _ = ..,      // The rest of the discriminants in `u32` are reserved.
}
// Using an `as` cast from `u32`, since it is an open enum.
let fruit = 3 as Fruit;

// Does not match any of the known variants.
match fruit {
    Fruit::Apple | Fruit::Orange | Fruit::Banana => unreachable!(),
    // This wildcard branch is required for all exhaustive matches of `Fruit`.
    // `fruit` preserves its value casting back to `u32`.
    _ => assert_eq!(fruit as u32, 3),
}

While writing this RFC I was not aware of the recent work on a similar RFC by @madsmtm. I considered using an attribute to make an open enum before I had learned about this RFC - I explain why I chose unnamed variants instead in the Alternatives.

@rustbot label T-lang

Important

When responding to RFCs, try to use inline review comments (it is possible to leave an inline review comment for the entire file at the top) instead of direct comments for normal comments and keep normal comments for procedural matters like starting FCPs.

This keeps the discussion more organized.

Rendered.

@rustbot rustbot added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 9, 2025
There is no reason for these bytes to be in flash memory - it is merely
one of the applicable use cases.
unnamed_variants is an unambiguous feature name that describes what the
author is now able to spell. However, it takes more mental effort to
remember what a variant is, while "enum variant" is immediately clear.

This also clarifies the zero-copy deserialization motivation language
further.
@kupiakos kupiakos changed the title RFC: Allow reservation of enum discriminants via unnamed variants RFC: Open up your enum with an unnamed variant Dec 10, 2025
@nielsle
Copy link

nielsle commented Dec 10, 2025

Out of curiosity, could the following syntax work? (or something similar)

#[repr(u32)]
#[reserve_discriminants(5..10)]  
#[reserve_discriminants(15..20)]  
enum Fruit {
    Apple,       // Apple is represented with 0u32.
    Orange,      // Orange is represented with 1u32.
    Banana = 4,  // Banana is represented with 4u32.
}

See f17e862 for reasoning.
The `taken_discrimimant_ranges` and `empty_discriminant_ranges` lints
catch the situation in which an unnamed variant has no effect. The
latter is much more dangerous than the former, so it is
`deny`-by-default. However, both should be allowed for certain codegen
and macro cases as described.

This also moves around and expands some language regarding `derive`
difficulty in open-enum, as well as add a suggested alternative.
@kupiakos
Copy link
Author

kupiakos commented Dec 10, 2025

@nielsle Using an attribute to reserve discriminants/open an enum (of which there are many spellings) is discussed here as a considered alternative. Also, please use inline (file/line-level) comments instead of PR-level comments for RFC comments in the future to keep the discussion organized.

@scottmcm
Copy link
Member

This is useful for any situation in which recompiling an enum to handle new discriminant values is infeasible, and using a newtype integer is unergonomic due to the type's intended nature as a set of enumerated values. This includes FFI, Protobuf, and embedded syscalls.

Fundamentally I think I disagree with this. A C-style open enum is a newtype struct, not a rust enum. (Not unlike how a rust enum isn't an open Box<dyn Trait> either.)

If the main complaint is that you don't like how rustdoc shows that or how R-A generates things for it, I think it'd be better to start with tooling changes for that instead of language changes.

How much of this would be solved if you could just have a #[diagnostics::open_enum] on your struct, for example?

And I think that "well I didn't want to reserve all of the values" both

  1. Isn't well motivated in the text, and
  2. Would be better done with struct TaskState(u8 is 0..8); via pattern types that are already "on the list".

- The unstable [`non_exhaustive_omitted_patterns`] lint has no easy way to
work with this enum-alike, even though treating it like a `non_exhaustive`
enum would be more helpful.
- Generated rustdoc is less clear (the pseudo-enum is grouped with `struct`s).
Copy link
Member

@scottmcm scottmcm Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, I've long found this distinction in rustdoc unhelpful always. If it's an opaque type, for example, what do I care if it's a struct or a union? Not to mention that I'd rather have Statement and StatementKind next to each other on https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/ rather than multiple pages apart.

I'd absolutely 👍 an RFC to change rustdoc to group things in a module by namespace instead of item kind, but I don't think the current behaviour is a reason to do a language change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Using colour or an icon or something for struct vs enum to help give my brain an extra cue as I scan? Sure, great! I just don't think it makes sense as a top-level split.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to have more control over rustdoc presentation and organization of types as well, in general. I consider the accurate grouping in rustdoc for an open enum as more removing a papercut than a full motivation for the feature.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like me to change the RFC text to reflect that a hypothetical future change to rustdoc could negate this particular issue?

Comment on lines 1290 to 1292
- `derive`s cannot be easily written with enum variant names. In order to avoid
duplicating the names, a `derive` macro must directly inter-operate with
another macro that generates these pseudo-variants like `open-enum`.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this. Can you clarify? What duplicate names are you referring to? What's an example derive macro that would be difficult to implement with this attribute-based scheme?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose one issue I can foresee is that a derive macro can't enumerate the known variants with this approach. So e.g., you can't implement Debug or Display or similar via a derive macro on such a type--those would have to be provided by a declarative or attribute macro.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate names I'm referring to are the variant names - the impl block contains the names, but a derive macro needs to be able to see it on the struct definition in order to work with the names. See this comment.


However:

- Open enums require even more typing for the desired semantics.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is trivially solved with a macro, right?

Do you expect that this open enum pattern is common outside of specialized and generated code? The motivating examples of bindgen and protobuf are both generated so there is no "typing".

(I personally use open enum patterns in a lot of systems code for ABI types that cross trust boundaries. But I am generally OK with reaching for a macro for these use cases, which I would consider fairly specialized.)

Copy link
Author

@kupiakos kupiakos Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is trivially solved with a macro, right?

"trivially" comes with a big asterisk, as described in this comment. What about non-literal discriminant expressions and implicit discriminants? It's not as trivial as it initially seems.

I'm aware that a macro solves the "extra typing" problem, as is mentioned repeatedly throughout the text and in a following bullet point. It is still a drawback to require a macro in order to avoid extra typing or duplicating discriminant values/variant names. Can you suggest alternative language that is less confusing to you?

Do you expect that this open enum pattern is common outside of specialized and generated code? The motivating examples of bindgen and protobuf are both generated so there is no "typing".

I expect the pattern to be more common once users realize they can as cast from the backing integer (lovely!) and improve some benchmarks by shifting validity checks around without sacrificing too much on ergonomics (that extra match arm). Also, folks should be using bindgen but they often don't - this makes it easy for them to get the open enum aspect right.

@kupiakos
Copy link
Author

@scottmcm

This RFC is about more than open enum ergonomics - it's about empowering Rust users to control ABI compatibility for enums.
I'd love to see a future where a Rust dynamic library could add a variant to an enum with fields and have it be a non-ABI-breaking change, as described in Future possibilities.

Fundamentally I think I disagree with this. A C-style open enum is a newtype struct, not a rust enum. (Not unlike how a rust enum isn't an open Box<dyn Trait> either.)

I'm curious - why do you think that a C-style enum is a newtype struct and not a Rust enum? What role does an enum have that restricts it from representing unnamed variants? What are the negative implications to enums with unnamed variants?

The choice of enum or struct reflects the author's intent and informs how other features and tooling are supposed to interpret the type - a set of names with associated values, or a wrapper around a plain integer. That makes C-style enums much simpler to work with by directly reflecting the author's intent to create an enumeration.

An open enum using unnamed variants:

  • Is still represented by a bounded set of representable values. That set is bounded by the known discriminant type.
  • Has that set of representable values declared in the enum definition by the author of the enum.

That's very unlike a Box<dyn Trait>, which allows users of the type to introduce their own valid representation. An explicit-repr enum remains as a set of known discriminants, it's just easier to expand that known set with an unnamed variant.

A newtype integer is a fine way way to represent a C-style open enum, but is an irritating paradigm in practice. My goal is to vastly improve the user experience for those who handle open enums regularly. Having this be a language feature is friendlier to users of open enums, because a solely diagnostic improvement will always require using a 3rd party macro or duplication in order to avoid significant boilerplate.

Consider this enum that we want to be made open with an open_enum attribute:

#[open_enum]
#[repr(u32)]
enum Foo {
    A = 0,
    B = 1,
    C = 5,
    D = 7,
    E = 9,
    F = 10,
    G = 20,
    H = 21,
}

That converts Foo to this definition:

#[derive(ParitalEq, Eq)]  // for use in `match`
struct Foo(pub u32);
// plus checks to ensure no duplicate discriminants
#[allow(non_uppercase_globals)]  // n/a
impl Foo {
    const A: Foo = Foo(0);
    const B: Foo = Foo(1);
    const C: Foo = Foo(5);
    const D: Foo = Foo(7);
    const E: Foo = Foo(9);
    const F: Foo = Foo(10);
    const G: Foo = Foo(20);
    const H: Foo = Foo(21);
}

Myself and clients have run into some issues with this pattern in practice:

  • If you try to combine #[open_enum] with another attribute macro, the declaration order can unexpectedly matter very much. In our case, it was an attribute macro that conditionally enabled Debug based on some cfgs and that caused issues. We had to replace the attribute macro with the intended cfg_attr (and thus duplicating those conditions anywhere open_enum was used).
  • It's not as clear to readers what's going on - that this is actually creating a struct and they can't treat it exactly the same as an enum. No as casts or use Foo::A, for example.
  • How does the user specify derive(Debug) to format with variant names as intended? The 0.5 version of my attribute macro looks for the literal tokens Debug or std::fmt::Debug or similar in derive and intercepts it, but that's obviously faulty and can't handle a use alias correctly. It also doesn't compose well and can only work with a small set of known macro names.
  • How does a derive for FromStr know what the names are, after the attribute macro has run? The "check the tokens in derive in the attribute macro" hack is tolerable for Debug as a special case, but not for custom derives.

There's a solution to this, but it still has serious drawbacks. With a new design, the open_enum macro transforms this:

#[open_enum]
#[derive(open_enum::Debug, open_enum::FromStr)]
#[repr(u32)]
enum Foo {
    /* variants A-H */
}

into this:

// `open_enum` derive macros read `open_enum_variant_names`, as well as
// other macros that are aware of the attribute.
#[derive(open_enum::Debug, open_enum::FromStr, PartialEq, Eq)]  
#[open_enum_variant_names = "A,B,C,D,E,F,G,H"]
struct Foo(pub u32);

impl Foo { /* ... */ }

With this:

  • derive(Debug) now prints Foo(10) and our own visible derive is needed.
  • derives can now read what the known variant names are (and access the discriminant values with Foo::VariantName.0).
  • If I attempt the "check the tokens for Debug" hack to warn or intercept, I can't distinguish from a custom derive imported as Debug.
  • Each way to construct an open enum needs it own macro ecosystem in order to communicate the variant names to derives that use them. In order to integrate with other libraries, they need to coordinate on how variant names are represented in a custom attribute.
  • Open enums are still considered "not an enum" by most of the ecosystem since the way you manipulate it is still different. There's not an obvious ergonomic way for existing libraries with derives that consider variant names to extend their support to open enums even if they're aware of the attribute.

How much of this would be solved if you could just have a #[diagnostics::open_enum] on your struct, for example?

This is similar to the diagonstic::enum_variant suggested alternative and its drawbacks apply. If that or diagnostic::open_enum (or enum_like) were to be merged, I'd improve open-enum to use it. However, it wouldn't solve the derive or duplication issue in a satisfiable manner.

If a derive is intended to manipulate enum variant names, how do you make it work with a newtype integer? If there's a diagnostic attribute, do you duplicate the names in the attribute so a derive can read it? On a large team, I'd want to lint for the attribute's presence.

Also, for feature parity, use Struct::Name would also need to work like use Enum::Name.

And I think that "well I didn't want to reserve all of the values" both

  1. Isn't well motivated in the text, and

I'm aiming to improve the text - can you be more specific? Rust users have good reasons to create enums with 1010 reserved variants, and it's not great. It's not the first time I've seen an enum like that (and won't be the last).

  1. Would be better done with struct TaskState(u8 is 0..8); via pattern types that are already "on the list".

I disagree - the ergonomics of enums with unnamed variants cannot be simply solved with an attribute, and this applies to struct TaskState(u8 is 0..8). The discriminant values are duplicated between the inner type and the variant names, and so constructing the pseudo-enum would have to be done with a macro. That macro needs to handle non-literal discriminant expressions, which yields a very unwieldy pattern type for complex enums. Pattern types would also need to allow overlapping patterns in order to allow non-literal discriminant expressions to be processed by such a macro. Additionally, pattern types are a much more impactful change to the language than unnamed enum variants.

Pattern types and unnamed variants cooperate well - if you write EnumName as u8, the output integer can be a u8 is PATTERN where PATTERN exhaustively covers all valid discriminants for Enum. Unnamed variants make it easier to define a span of valid discriminants.

@tmccombs
Copy link

How much of this would be solved if you could just have a #[diagnostics::open_enum] on your struct, for example?

Another problem with that is it wouldn't really allow having a lint to check that a match includes all known variants.

@tmvkrpxl0
Copy link

Nice RFC! I want to add few additions that I think are quality of life improvements:

  • _ variant without pattern assigned to it should be same as _ = ..
  • If it's used without #[repr], it should behave same as #[non_exhaustive].
    • Given that it isn't breaking change to replace non_exhaustive, it already work as its replacement. If it cannot guarantee ABI compatibility due to missing #[repr], it should instead ensure API compatibility only, effectively replacing it.
  • There should be way to match for only known enum variants or only unknowns
    • I can see someone printing warning when enum value isn't known.

- Crates must be recompiled to use new enum variants.
- It affects _only_ foreign crates.

By contrast, an unnamed variant affects API _and_ ABI semver compatibility:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we requiring API semver compatibility? In use cases like protobuf, I would like to correctly handle unknown wire variants, while still having the compiler check that my code matches all the "known" variants in the current API version.

It seems to me that ABI and API compatibility are actually orthogonal, and I can't see any reason it would be desirable to bundle in the API part to this feature when that's what #[non_exhaustive] already does.

Copy link
Member

@programmerjake programmerjake Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having it affect API is necessary to handle stuff like:

#[repr(u8)]
enum E {
    A = 0,
    B = 1,
    _ = ..,
}

match 4u8 as E {
    E::A => String::from("A"),
    E::B => String::from("B"),
    // no other match arms, what does this do?
    // the only reasonable option is a compile error.
    // that's why you need a wildcard match arm
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think there should be lint that triggers if you don't explicitly handle one of the known variants.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah a lint might be sufficient. Alternatively I guess there could be special syntax to match unnamed variants, distinct from the generic wildcard which matches named variants.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think there should be lint that triggers if you don't explicitly handle one of the known variants.

there is a nightly lint non_exhaustive_omitted_patterns which you can change to warn when _ matches any enum variants in a #[non_exhaustive] enum.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a nightly lint non_exhaustive_omitted_patterns

Indeed, that's the desired lint. The interaction of this lint with unnamed variants is described in the RFC and allows a maintainer to ensure that a match doesn't forget to list a named variant. As mentioned there, it could be renamed before stabilization if the name mismatch seems confusing. I don't think it makes sense to separate into a different lint.

#[repr(u32)]
enum Bar {
    A,
    B,
    _ = ..,
}
let b = Bar::A;

// warning: some variants are not matched explicitly
//          pattern `Bar::B` not covered
// help: ensure that all 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
#[warn(non_exhaustive_omitted_patterns)]
let name = match b {
    Bar::A => "A",
    _ => "unknown",
};

I guess there could be special syntax to match unnamed variants, distinct from the generic wildcard which matches named variants

It's quite intentional that unnamed variants make naming a previously unnamed variant a non-breaking API change, which a special syntax for matching on named/unnamed variants would violate. However, I do think it's occasionally useful to be able to query whether a value is a named variant, and so this operation is trivial to generate with a macro. It's equivalent to a matches! on a | pattern that lists every named variant. Macros are allowed to cause a non-breaking API change to a type's structure yield a breaking API change; see derive(PartialEq) and anything else that works per-struct-field.

The syntax would thus be something like:

#[repr(u32)]
#[derive(IsNamedVariant)]
enum Bar {
    A,
    B,
    _ = ..,
}
let b = Bar::A;

fn known_value(b: Bar) -> Option<u32> {
    x.is_named_variant().then(|| x as u32)
}

assert_eq!(known_value(0 as Bar), Some(0));

// If you add `C` with discriminant 2, this panics.
assert_eq!(known_value(2 as Bar), None);

I think it's best to leave this functionality to 3rd-party macros.

I can't see any reason it would be desirable to bundle in the API part to this feature when that's what #[non_exhaustive] already does

I describe my reasons for having this replace non_exhaustive rather than require it to use unnamed enum variants here.

variant by changing the `repr`. This is non-obvious and can be avoided by
forbidding `non_exhaustive` when a valid unnamed variant exists.

### Allow an implicit discriminant expression for unnamed variants
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice RFC!

Thanks @tmvkrpxl0 😄

Please reply in this inline comment to keep the main thread cleaner, or resolve the thread.

  • _ variant without pattern assigned to it should be same as _ = ..

I think that's ambiguous - why wouldn't it be equivalent to _ = <previous variant + 1? Unnamed enum variants are unusual enough that I think making it clear that discriminants are involved should be mandatory.

  • If it's used without #[repr], it should behave same as #[non_exhaustive].

I think this adds more confusion than improves ergonomics. I don't think it makes as much sense to talk about assigning "the rest" of the discriminants to an enum without a repr as _ = .. says.

  • Given that it isn't breaking change to replace non_exhaustive, it already work as its replacement. If it cannot guarantee ABI compatibility due to missing #[repr], it should instead ensure API compatibility only, effectively replacing it.

It doesn't replace non_exhaustive, because it can also be applied to structs and enum variants. The unnamed fields RFC also requires repr(C) in order to be used since it's about ABI.

  • There should be way to match for only known enum variants or only unknowns

    • I can see someone printing warning when enum value isn't known.

See this reply - there is both an existing nightly lint to warn about unlisted named variants, and getting a bool for whether a variant is named (for a match guard) can be trivially done with a macro.

@scottmcm
Copy link
Member

scottmcm commented Jan 4, 2026

Consider this enum that we want to be made open with an open_enum attribute:

To me this feels like a fixation on a particular UI for it. You could always have an open_enum! { ... } non-attribute to pass everything to. Or have both the enum Foo and a generated struct FooOpen that's also available via Foo::Open and has a Foo: TryFrom<FooOpen> that's also generated. That'd be useful anyway for cases that don't want to always just pass things along but want to handle the things it actually knows about.

[rest pattern]: https://doc.rust-lang.org/reference/patterns.html?#rest-patterns
[wildcard pattern]: https://doc.rust-lang.org/reference/patterns.html?#wildcard-pattern

### An "other" variant carries unknown discriminants like a tuple variant
Copy link

@tmccombs tmccombs Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a similar alternative. Something like:

#[repr(u32)]
enum IpProto {
    Tcp = 6,
    Udp = 17,
    #[catchall]
    Other,
}

Bikeshed on the exact syntax of the attribute, and whether and how it is possible to specify a specific range.

Possibly also allow including a tuple or struct field to capture the discrimenant in pattern matching.

To address the possibility of creating that variant with the discrimenant of another value, I can think of a few ways of addressing that:

  1. Forbid direct construction of the Other variant. You can only get it as a result of casting an integer to it.
  2. Make construction of it unsafe, and the caller must be responsible for gauranteeing the value is ok.
  3. Don't have a field for the discrimenant, and require specifying a value to use when constructing directly, but casting from an integer can result in a different discrimenent. One downside is this may not work for some use cases if there isn't a reserved value for an unknown discriment
  4. Combine 1 and 2. Only allow direct construction if a specific discriment is specified.

I think currently, this alternative, especially with option 4 for construction is currently my favorite syntax for open enums.

I'll also point out one advantage of using a named variant like this is that it provides a clear distinction between matching against an unknown discriment and a wildcard match, and we get an error for missing a variant in a match for free.

Copy link
Author

@kupiakos kupiakos Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more like the Discriminant ranges for named variants instead of unnamed variants alternative than the alternative this comment is centered on, though yours uses an attribute instead of an explicit discriminant range syntax.

My primary concerns with allowing a named variant to represent multiple discriminants, even with options 1-4, are:

  • Possibly confusing or non-performant behavior of derive(PartialEq).
  • Adding a variant becomes an API-breaking change (whether this represents an ABI-breaking change is debatable).
  • The performance of matches!(x, IpProto::Other) grows non-trivially as further variants are added to IpProto; no other single pattern has this characteristic today and I think it's valuable to keep it that way.
  • I think it is a larger change to the language than unnamed variants.
  • I've not come up with a satisfying syntax that makes option 4 workable.

The utility that specifically allowing a named "opening" variant has could be provided with macro-generated functions that match self as u32, such as the derive(IsNamedVariant) example below.

and we get an error for missing a variant in a match for free

It'd be more consistent in the language if the solution for not accidentally missing a variant were the same as for non_exhaustive (i.e. use warn(non_exhaustive_omitted_patterns) or whatever stabilizes like it).

it provides a clear distinction between matching against an unknown discriment and a wildcard match

Is it actually a good thing for code to automatically take a different branch when a new variant is added? A large point of this is to make adding a variant non-breaking to code that handles the enum ordinarily. I think it should take a macro looking at the enum definition in order for code to directly observe a new named variant being added.

I'm fond of Swift's solution to this, which is to annotate the "unknown discriminant" branch with @unknown in an exhaustive switch.

It allows a clear warning when missing a variant in a match and distinguishes whether a new variant has been added, but doesn't allow a change in control flow when a new variant is added without due consideration by the author of the switch.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reworded and expanded the bullet points for the relevant alternative. Does that effectively reflect your proposal?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more like the Discriminant ranges for named variants instead of unnamed variants alternative

True. I actually originally intended this to be with a field using the discriminant, then realized that the discriminant could also be obtained by casting, so that wasn't entirely necessary, but could be added on.

Possibly confusing or non-performant behavior of derive(PartialEq)

If by possibly confusing, you mean that IpProto::Other doesn't necessarily equal another instance of IpProto::Other, then sure. I guess that is potentially confusing, but I don't think that is any more confusing than having an enum be a value that isn't any of the variants.

Adding a variant becomes an API-breaking change (whether this represents an ABI-breaking change is debatable).

Only if the enum isn't also annotated with #[non_exhaustive]. And it is possible that making that a breaking change is desirable. For example, suppose you want code calling the library to be be aware of all known variants, and handle them, but it's possible to get an unknown variant from the network or a file, etc.

The performance of matches!(x, IpProto::Other) grows non-trivially as further variants are added to IpProto; no other single pattern has this characteristic today and I think it's valuable to keep it that way.

Only if the enum is sparse.

And on the other hand, this means there is a way to easily check if a value an unknown variant, without needing a separate derive macro.

I've not come up with a satisfying syntax that makes option 4 workable.

#[repr(u32)]
enum IpProto {
    Tcp = 6,
    Udp = 17,
    #[catchall]
    Other = 0,
}

??
or

#[repr(u32)]
enum IpProto {
    Tcp = 6,
    Udp = 17,
    #[catchall(default=0)]
    Other,
}

The utility that specifically allowing a named "opening" variant has could be provided with macro-generated functions that match self as u32, such as the derive(IsNamedVariant) example below.

That's an option, but it has some downsides. In particular, it requires deriving the trait on the definition of the enum. So if you are using a crate that didn't do that, then you wouldn't have an equivalent functionality, unless you defined such a function by hand (or we got macros that had some form of compile-time reflection).

It's also a little more awkward to use in some situations, for example:

match x {
  Variant1 => todo!(),
  other if !other.is_named_variant() => todo!(),
  _ => todo!(),
}

but that's a minor issue.

It'd be more consistent in the language if the solution for not accidentally missing a variant were the same as for non_exhaustive (i.e. use warn(non_exhaustive_omitted_patterns) or whatever stabilizes like it).

Is it actually a good thing for code to automatically take a different branch when a new variant is added?

What I'm getting at is that API compatibility and ABI compatibility are kind of independent of each other. #[non_exhaustive] makes an enum's API forward compatible. This RFC is trying to make it so you can have enums that have a forward compatible ABI. In practice, you probably want the API to be forward compatible most of the time when you want the ABI to be forward compatible. But there isn't any technical reason that has to be the case. Perhaps there isn't any real use case for an open enum that isn't non_exhaustive, but how confident are we that is the case?

For being able to branch on them differently, I could see a case where you may want to print different error messages. Or maybe the catchall value actually has information in the bits of the "discriminant". etc.

Copy link
Author

@kupiakos kupiakos Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR: this alternative introduces far more inconsistencies and API-hazards to the language than existing constructs. Unnamed variants have fewer design questions with dissatisfying answers.

When I first started on this RFC, it was this alternative. It does feel nice to be able to explicitly match on the "other" variant! It was only after breaking down the issues and comparing to other Rust constructs that I decided that unnamed variants were the less hazardous choice.

Say we only implemented this alternative, and didn't implement unnamed variants. What should bindgen call the "Other" variant on generated enums? What happens if that name already exists in the enum?

IpProto::Other doesn't necessarily equal another instance of IpProto::Other

Yes, I think that it is confusing for this to be possible: matches!(o, IpProto::Other) && o != IpProto::Other. If instead of comparing discriminants derive(PartialEq) behaved like match, then we risk two linked libraries giving different answers to PartialEq::eq (ignoring the API-compatibility issue). In the presence of dynamic linking and inlining it could be very hard to predict what the actual behavior would be.

I don't think that is any more confusing than having an enum be a value that isn't any of the variants.

I disagree - I wouldn't describe the enum value as "not any of the variants", but rather "some unnamed variant". That same variant may be named in another linked library. You can declare multiple const _: T next to each other - they don't conflict because they're both unnamed.

And on the other hand, this means there is a way to easily check if a value an unknown variant, without needing a separate derive macro.

I really think the separate derive macro should be required. For every construct in the language today, it requires a macro in order for a non-breaking change to a non-exhaustive type definition to cause this sort of branching change. It would be inconsistent to change that.

Only if the enum isn't also annotated with #[non_exhaustive]. And it is possible that making that a breaking change is desirable. For example, suppose you want code calling the library to be be aware of all known variants, and handle them, but it's possible to get an unknown variant from the network or a file, etc.

What I'm getting at is that API compatibility and ABI compatibility are kind of independent of each other
...
there isn't any real use case for an open enum that isn't non_exhaustive

I don't think they are independent; they're deeply intertwined. An ABI-forwards-compatible change must also be API-forwards-compatible. An open enum must have non_exhaustive-like properties in all crates when doing an exhaustive match (except where every discriminant is named). Otherwise, there may be no branch to take for unnamed variants.

So if you are using a crate that didn't do that, then you wouldn't have an equivalent functionality, unless you defined such a function by hand

That is a good thing, if less convenient for particular use cases. An enum author should have to explicitly opt-in to the API-compatibility dangers that using the macro represents. Perhaps static reflection will change that?

I could see a case where you may want to print different error messages.

Is it too burdensome to require the enum author opt-in with a derive or the user wanting bespoke behavior to write their own match? If there's a known range of discriminants that have names, then matching on the discriminant values would work fine.

Or maybe the catchall value actually has information in the bits of the "discriminant". etc.

In that case it would be weird for the enum author to not derive or have some other pub method to get those bits.

I've not come up with a satisfying syntax that makes option 4 workable.

#[repr(u32)]
enum IpProto {
    Tcp = 6,
    Udp = 17,
    #[catchall]
    Other = 0,
}

?? or

#[repr(u32)]
enum IpProto {
    Tcp = 6,
    Udp = 17,
    #[catchall(default=0)]
    Other,
}

What I meant was, what is the syntax to construct a value of IpProto::Other? The best I had was x as IpProto::Other where x is a checked const value or dynamic pattern type.

I added another option to here to reflect what you spell above - the enum author opting in to a particular default discriminant. Using the syntax currently described in the alternative, it'd be something like:

#[repr(u8)]
enum IpProto {
    Tcp = 6,
    Udp = 17,
    #[default_value = 0]
    Other = ..,
}
// Throws an error unless `#[default_value]` is specified on `Other`
assert_eq!(IpProto::Other as u8, 0);

It's also a little more awkward to use in some situations

Personally, I think that example has better local readability than what the alternative would do. We read code much more often than write it:

match x {
  Variant1 => todo!(),
  // How am I supposed to know that this is special and has multiple discriminants?
  // It could be a particular reserved value that means "unknown".
  // Who looks at type definitions? ;-)
  Other => todo!(),
  _ => todo!(),
}

The performance of matches!(x, IpProto::Other) grows non-trivially as further variants are added to IpProto; no other single pattern has this characteristic today and I think it's valuable to keep it that way.

Only if the enum is sparse.

I'm expecting sparse enums to be a relatively common case. Rust is a systems language: there should be no surprises in the performance of something like match.

@kupiakos
Copy link
Author

@scottmcm I made this Zulip thread to discuss open-enum library design further.

callsite. Thus it may be worth adding to Rust even if `.0` isn't.
- When should one prefer the constructor over the `as` cast? Always?

#### Discriminant field access
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There have been a few other proposals for discriminant access syntax. @scottmcm has made a couple, in particular. I'd want to see those captured as alternatives here. .0 is cute but entirely too clever for its own good. :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.0 is definitely peculiar. Alas, I had to mention it- it would make this particular transition so smooth!

Thank you for letting me know about the prior art. I've been having trouble finding those - would you or @scottmcm mind linking?

Green = 1,
// Must specify a non-overlapping range,
// including if `..` is the discriminant.
Unknown = 2..=50,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to explicitly exclude other variants seems onerous. If we're going to allow .. rather than (for instance) 2.., then it doesn't seem unreasonable to allow ..=50 as well. Detecting issues with that seems like the domain of a lint.

That said, I agree that ranges other than .. can go into future work in the first place; .. seems like by far the most important case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The reason why this Future possibility excludes named variants from overlapping other variants is because it resolves most of the issues described in this comment and section of the RFC.
  2. This Future possibility is specifically about named variants with a range of discriminants.
  3. This RFC does describe the behavior of 2.. and ..=50 for unnamed variants.
  4. I agree that .. is by far the most important case, though it's not significantly more work to support smaller ranges. As you say, most of the work is figuring out when you should lint on possibly-confusing cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.