From ac2b8be5e7db480c77f4d30b94abdc7b0516c4c1 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Wed, 24 May 2017 16:32:19 -0400 Subject: [PATCH 01/11] Add non_exhaustive RFC. --- text/0000-non-exhaustive.md | 390 ++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 text/0000-non-exhaustive.md diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md new file mode 100644 index 00000000000..70986b753b5 --- /dev/null +++ b/text/0000-non-exhaustive.md @@ -0,0 +1,390 @@ +- Feature Name: non_exhaustive +- Start Date: 2017-05-24 +- RFC PR: (leave this empty) +- Rust Issue: (leave this empty) + +# Summary + +This RFC introduces the `#[non_exhaustive]` attribute for enums, which indicates +that more variants may be added to an enum in the future. Adding this hint +will force downstream crates to add a wildcard arm to `match` statements +involving the enum, ensuring that adding new variants is not a breaking change. + +This is a post-1.0 version of [RFC 757], modified to use an attribute instead of +a custom syntax. + +# Motivation + +The most common use of this feature is error types. Because adding features to a +crate may result in different possibilities for errors, it makes sense that more +types of errors will be added in the future. + +For example, the rustdoc for [`std::io::ErrorKind`] shows: + +```rust +pub enum ErrorKind { + NotFound, + PermissionDenied, + ConnectionRefused, + ConnectionReset, + ConnectionAborted, + NotConnected, + AddrInUse, + AddrNotAvailable, + BrokenPipe, + AlreadyExists, + WouldBlock, + InvalidInput, + InvalidData, + TimedOut, + WriteZero, + Interrupted, + Other, + UnexpectedEof, + // some variants omitted +} +``` + +Because the standard library continues to grow, it makes sense to eventually add +more error types. However, this can be a breaking change if we're not careful: + +```rust +use std::io::ErrorKind::*; + +match error_kind { + NotFound => ..., + PermissionDenied => ..., + ConnectionRefused => ..., + ConnectionReset => ..., + ConnectionAborted => ..., + NotConnected => ..., + AddrInUse => ..., + AddrNotAvailable => ..., + BrokenPipe => ..., + AlreadyExists => ..., + WouldBlock => ..., + InvalidInput => ..., + InvalidData => ..., + TimedOut => ..., + WriteZero => ..., + Interrupted => ..., + Other => ..., + UnexpectedEof => ..., +} +``` + +If we were to add another variant to this enum, this `match` would fail, +requiring an additional arm to handle the extra case. But, if force users to +add an arm like so: + +```rust +match error_kind { + // ... + _ => ..., +} +``` + +Then we can add as many variants as we want! + +## How we do this today + +We force users add this arm for [`std::io::ErrorKind`] by adding a hidden +variant: + +```rust +#[unstable(feature = "io_error_internals", + reason = "better expressed through extensible enums that this \ + enum cannot be exhaustively matched against", + issue = "0")] +#[doc(hidden)] +__Nonexhaustive, +``` + +Because this feature doesn't show up in the docs, and doesn't work in stable +rust, we can safely assume that users won't use it. + +A lot of crates take advantage of `#[doc(hidden)]` variants to tell users that +they should add a wildcard branch to matches. However, the standard library +takes this trick further by making the variant `unstable`, ensuring that it +cannot be used in stable Rust. Outside the standard library, here's a look at +[`diesel::result::Error`]: + +```rust +pub enum Error { + InvalidCString(NulError), + DatabaseError(String), + NotFound, + QueryBuilderError(Box), + DeserializationError(Box), + #[doc(hidden)] + __Nonexhaustive, +} +``` + +Even though the variant is hidden in the rustdoc, there's nothing actually stopping a +user from using the `__Nonexhaustive` variant. This code works totally fine on +stable rust: + +```rust +use diesel::Error::*; +match error { + InvalidCString(..) => ..., + DatabaseError(..) => ..., + NotFound => ..., + QueryBuilderError(..) => ..., + DeserializationError(..) => ..., + __Nonexhaustive => ..., +} +``` + +This is obviously unintended, and this is currently the best way to make +non-exhaustive enums outside the standard library. Plus, even the standard +library remarks that this is a hack. Recall the hidden variant for +[`std::io::ErrorKind`]: + +```rust +#[unstable(feature = "io_error_internals", + reason = "better expressed through extensible enums that this \ + enum cannot be exhaustively matched against", + issue = "0")] +#[doc(hidden)] +__Nonexhaustive, +``` + +Using `#[doc(hidden)]` will forever feel like a hack to fix this problem. +Additionally, while plenty of crates could benefit from the idea of +non-exhaustiveness, plenty don't because this isn't documented in the Rust book, +and only documented elsewhere as a hack until a better solution is proposed. + +## Opportunity for optimisation + +Currently, the `#[doc(hidden)]` hack leads to a few missed opportunities +for optimisation. For example, take this enum: + +```rust +pub enum Error { + Message(String), + Other, +} +``` + +Currently, this enum takes up the same amount of space as `String` because of +the non-zero optimisation. If we add our non-exhaustive variant: + +```rust +pub enum Error { + Message(String), + Other, + #[doc(hidden)] + __Nonexhaustive, +} +``` + +Then this enum needs an extra bit to distinguish `Other` and `__Nonexhaustive`, +which is ultimately never used. This will likely add an extra 8 bytes on a +64-bit system to ensure alignment. + +More importantly, take the following code: + +```rust +use Error::*; +match error { + Message(ref s) => /* lots of code */, + Other => /* lots of code */, + _ => /* lots of code */, +} +``` + +As a human, we can determine that the wildcard match is dead code and can be +removed from the binary. Unfortunately, Rust can't make this distinction because +we could still *technically* use that wildcard branch. + +Although these options will unlikely matter in this example because +error-handling code (hopefully) shouldn't run very often, it could matter for +other use cases. + +# Detailed design + +An attribute `#[non_exhaustive]` is added to the language, which will (for now) +fail to compile if it's used on anything other than an enum definition. + +Within the crate that defines the enum, this attribute is essentially ignored, +so that the current crate can continue to exhaustively match the enum. The +justification for this is that any changes to the enum will likely result in +more changes to the rest of the crate. Consider this example: + +```rust +use std::error::Error as StdError; + +#[non_exhaustive] +pub enum Error { + Message(String), + Other, +} +impl StdError for Error { + fn description(&self) -> &str { + match *self { + Message(ref s) => s, + Other => "other or unknown error", + } + } +} +``` + +It seems undesirable for the crate author to use a wildcard arm here, to +ensure that an appropriate description is given for every variant. In fact, if +they use a wildcard arm in addition to the existing variants, it should be +identified as dead code, because it will never be run. + +Outside the crate that defines the enum, users should be required to add a +wildcard arm to ensure forward-compatibility, like so: + +```rust +use mycrate::Error; + +match error { + Message(ref s) => ..., + Other => ..., + _ => ..., +} +``` + +And it should *not* be marked as dead code, even if the compiler does mark it as +dead and remove it. + +## Changes to rustdoc + +Right now, the only indicator that rustdoc gives for non-exhaustive enums is a +comment saying "some variants omitted." This shows up whenever variants are +marked as `#[doc(hidden)]`, and rustdoc should continue to emit this message. + +However, after this message (if any), it should offer an additional message +saying "more variants may be added in the future," to clarify that the enum is +non-exhaustive. It also hints to the user that in the future, they may want to +fine-tune their match code to include future variants when they are added. + +These two messages should be distinct; the former says "this enum has stuff +that you shouldn't see," while the latter says "this enum is incomplete and may +be extended in the future." + +# How We Teach This + +Changes to rustdoc should make it easier for users to understand the concept of +non-exhaustive enums in the wild. + +Additionally, in the chapter on enums, a section should be added specifically +for non-exhaustive enums. Because error types are common in almost all crates, +this case is important enough to be taught when a user learns Rust for the first +time. + +# Drawbacks + +* The `#[doc(hidden)]` hack in practice is usually good enough. +* An attribute may be more confusing than a dedicated syntax. +* `non_exhaustive` may not be the clearest name. +* It's unclear if this attribute should apply to other aspects of the language + than just enums. + +# Alternatives + +* Provide a dedicated syntax for enums instead of an attribute. This would + likely be done by adding a `..` variant, as proposed by the original + [extensible enums RFC][RFC 757]. +* Allow creating private enum variants, giving a less-hacky way to create a + hidden variant. +* Document the `#[doc(hidden)]` hack and make it more well-known. + +# Unresolved questions + +Should there be a way to warn downstream crate users when their match is +non-exuhaustive? What if a user wants to add a warning to their matches using +non-exhaustive enums, to indicate that more code should be added to handle a new +variant? + +Should the below extensions be added? + +## Extensions to structs + +It makes sense to eventually allow this attribute on structs as well. Unlike +enums, it's very easy to make a struct non-exhaustive: + +```rust +pub struct Thing { + pub number_of_eggs: usize, + pub friction_constant: f64, + pub can_swim: bool, + non_exhaustive: (), +} +``` + +Because the `non_exhaustive` field is private, downstream crates cannot +construct a value of `Thing` or exhaustively match its fields, even though they +can access other fields. However, this seems less ergonomic than: + +```rust +#[non_exhaustive] +pub struct Thing { + pub number_of_eggs: usize, + pub friction_constant: f64, + pub can_swim: bool, +} +``` + +Because then the upstream crate won't have to add the `non_exhaustive` field +when constructing a value, or exhaustively matching patterns. + +## Extensions to traits + +Tangentially, it also makes sense to have non-exhaustive traits as well, even +though they'd be non-exhaustive in a different way. Take this example from +[`byteorder`]: + +```rust +pub trait ByteOrder: Clone + Copy + Debug + Default + Eq + Hash + Ord + PartialEq + PartialOrd { + // ... +} +``` + +The `ByteOrder` trait requires these traits so that a user can simply write a +bound of `T: ByteOrder` without having to add other useful traits, like `Hash` +or `Eq`. + +This trait is useful, but the crate has no intention of letting other users +implement this trait themselves, because then adding an additional trait +dependency for `ByteOrder` could be a breaking change. + +The way that this crate solves this problem is by adding a hidden trait +dependency: + +```rust +mod private { + pub trait Sealed {} + impl Sealed for super::LittleEndian {} + impl Sealed for super::BigEndian {} +} + +pub trait ByteOrder: /* ... */ + private::Sealed { + // ... +} +``` + +This way, although downstream crates can use this trait, they cannot actually +implement things for this trait. + +This pattern could again be solved by using `#[non_exhaustive]`: + +```rust +#[non_exhaustive] +pub trait ByteOrder: /* ... */ { + // ... +} +``` + +This would indicate to downstream traits that this trait might gain additional +requirements (dependent traits or methods to implement), and as such, cannot be +implemented downstream. + +[RFC 757]: https://github.com/rust-lang/rfcs/pull/757 +[`std::io::ErrorKind`]: https://doc.rust-lang.org/1.17.0/std/io/enum.ErrorKind.html +[`diesel::result::Error`]: https://docs.rs/diesel/0.13.0/diesel/result/enum.Error.html +[`byteorder`]: https://github.com/BurntSushi/byteorder/tree/f8e7685b3a81c52f5448fd77fb4e0535bc92f880 From 25d8980c57fc13b0e5f2d9ea8188d8a6b0527742 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Fri, 26 May 2017 12:33:02 -0400 Subject: [PATCH 02/11] Incorporate non-exhaustive structs into main RFC. --- text/0000-non-exhaustive.md | 170 ++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 55 deletions(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index 70986b753b5..f3d0807a38c 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -5,10 +5,11 @@ # Summary -This RFC introduces the `#[non_exhaustive]` attribute for enums, which indicates -that more variants may be added to an enum in the future. Adding this hint -will force downstream crates to add a wildcard arm to `match` statements -involving the enum, ensuring that adding new variants is not a breaking change. +This RFC introduces the `#[non_exhaustive]` attribute for enums and structs, +which indicates that more variants/public fields may be added to an enum in the +future. Adding this hint will force downstream crates to add a wildcard arm to +`match` statements involving the enum, ensuring that adding new variants is not +a breaking change. This is a post-1.0 version of [RFC 757], modified to use an attribute instead of a custom syntax. @@ -203,10 +204,68 @@ Although these options will unlikely matter in this example because error-handling code (hopefully) shouldn't run very often, it could matter for other use cases. +## A case for structs + +In addition to enums, it makes sense to extend this feature to structs as well. +For example, consider [`syn::Generics`]: + +```rust +pub struct Generics { + pub lifetimes: Vec, + pub ty_params: Vec, + pub where_clause: WhereClause, +} +``` + +Let's say that the language introduces `use` clauses, as described by +@petrochenkov [here][use clauses]. Now, it seems natural to add a new field to +this struct: + +```rust +pub struct Generics { + pub lifetimes: Vec, + pub ty_params: Vec, + pub where_clause: WhereClause, + pub use_clause: UseClause, +} +``` + +Unfortunately, this is a breaking change. Because we have full knowledge of the +fields in the struct, we can construct `Generics` without a special constructor: + +```rust +let gens = Generics { + lifetimes: Vec::new(), + ty_params: Vec::new(), + where_clause: WhereClause::none(), +}; +``` + +If we add this field, this will turn into a compiler error; we didn't add a +value for the use clause! + +To rectify this, we can add a private field to the struct: + +```rust +pub struct Generics { + pub lifetimes: Vec, + pub ty_params: Vec, + pub where_clause: WhereClause, + non_exhaustive: (), +} +``` + +But this makes it more difficult for the crate itself to construct `Generics`, +because they have to add a `non_exhaustive: ()` field every time they make a new +value. + # Detailed design An attribute `#[non_exhaustive]` is added to the language, which will (for now) -fail to compile if it's used on anything other than an enum definition. +fail to compile if it's used on anything other than an enum or struct +definition. + +## Enums Within the crate that defines the enum, this attribute is essentially ignored, so that the current crate can continue to exhaustively match the enum. The @@ -252,43 +311,74 @@ match error { And it should *not* be marked as dead code, even if the compiler does mark it as dead and remove it. +## Structs + +Like with enums, the attribute is essentially ignored in the crate that defines +the struct, so that users can continue to construct values for the struct. +However, this will prevent downstream users from constructing values, because +fields may be added to the struct in the future. + +For example, using [`syn::Generics`] again: + +```rust +#[non_exhaustive] +pub struct Generics { + pub lifetimes: Vec, + pub ty_params: Vec, + pub where_clause: WhereClause, +} +``` + +This will still allow the crate to create values of `Generics`, but it will +prevent the user from doing so, because more fields might be added in the +future. + +Although it should not be explicitly forbidden by the language to mark a struct +with some private fields as non-exhaustive, it should emit a warning to tell the +user that the attribute has no effect. + ## Changes to rustdoc -Right now, the only indicator that rustdoc gives for non-exhaustive enums is a -comment saying "some variants omitted." This shows up whenever variants are -marked as `#[doc(hidden)]`, and rustdoc should continue to emit this message. +Right now, the only indicator that rustdoc gives for non-exhaustive enums and +structs is a comment saying "some variants/fields omitted." This shows up +whenever variants or fields are marked as `#[doc(hidden)]`, or when fields are +private. rustdoc should continue to emit this message in these cases. However, after this message (if any), it should offer an additional message -saying "more variants may be added in the future," to clarify that the enum is -non-exhaustive. It also hints to the user that in the future, they may want to -fine-tune their match code to include future variants when they are added. +saying "more variants/fields may be added in the future," to clarify that the +enum/struct is non-exhaustive. It also hints to the user that in the future, +they may want to fine-tune any match code for enums to include future variants +when they are added. -These two messages should be distinct; the former says "this enum has stuff -that you shouldn't see," while the latter says "this enum is incomplete and may -be extended in the future." +These two messages should be distinct; the former says "this enum/struct has +stuff that you shouldn't see," while the latter says "this enum/struct is +incomplete and may be extended in the future." # How We Teach This Changes to rustdoc should make it easier for users to understand the concept of -non-exhaustive enums in the wild. +non-exhaustive enums and structs in the wild. -Additionally, in the chapter on enums, a section should be added specifically -for non-exhaustive enums. Because error types are common in almost all crates, -this case is important enough to be taught when a user learns Rust for the first +In the chapter on enums, a section should be added specifically for +non-exhaustive enums. Because error types are common in almost all crates, this +case is important enough to be taught when a user learns Rust for the first time. +Additionally, non-exhaustive structs should be documented in an early chapter on +structs. Public fields should be preferred over getter/setter methods in Rust, +although users should be aware that adding extra fields is a potentially +breaking change. + # Drawbacks * The `#[doc(hidden)]` hack in practice is usually good enough. * An attribute may be more confusing than a dedicated syntax. * `non_exhaustive` may not be the clearest name. -* It's unclear if this attribute should apply to other aspects of the language - than just enums. # Alternatives -* Provide a dedicated syntax for enums instead of an attribute. This would - likely be done by adding a `..` variant, as proposed by the original +* Provide a dedicated syntax instead of an attribute. This would likely be done + by adding a `...` variant or field, as proposed by the original [extensible enums RFC][RFC 757]. * Allow creating private enum variants, giving a less-hacky way to create a hidden variant. @@ -301,39 +391,7 @@ non-exuhaustive? What if a user wants to add a warning to their matches using non-exhaustive enums, to indicate that more code should be added to handle a new variant? -Should the below extensions be added? - -## Extensions to structs - -It makes sense to eventually allow this attribute on structs as well. Unlike -enums, it's very easy to make a struct non-exhaustive: - -```rust -pub struct Thing { - pub number_of_eggs: usize, - pub friction_constant: f64, - pub can_swim: bool, - non_exhaustive: (), -} -``` - -Because the `non_exhaustive` field is private, downstream crates cannot -construct a value of `Thing` or exhaustively match its fields, even though they -can access other fields. However, this seems less ergonomic than: - -```rust -#[non_exhaustive] -pub struct Thing { - pub number_of_eggs: usize, - pub friction_constant: f64, - pub can_swim: bool, -} -``` - -Because then the upstream crate won't have to add the `non_exhaustive` field -when constructing a value, or exhaustively matching patterns. - -## Extensions to traits +## Extending to traits Tangentially, it also makes sense to have non-exhaustive traits as well, even though they'd be non-exhaustive in a different way. Take this example from @@ -387,4 +445,6 @@ implemented downstream. [RFC 757]: https://github.com/rust-lang/rfcs/pull/757 [`std::io::ErrorKind`]: https://doc.rust-lang.org/1.17.0/std/io/enum.ErrorKind.html [`diesel::result::Error`]: https://docs.rs/diesel/0.13.0/diesel/result/enum.Error.html +[`syn::Generics`]: https://dtolnay.github.io/syn/syn/struct.Generics.html +[use clauses]: https://github.com/rust-lang/rfcs/pull/1976#issuecomment-301903528 [`byteorder`]: https://github.com/BurntSushi/byteorder/tree/f8e7685b3a81c52f5448fd77fb4e0535bc92f880 From aae8a31481ce36482270352a80ef0f273a9584f3 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Fri, 26 May 2017 12:37:44 -0400 Subject: [PATCH 03/11] Reword summary. --- text/0000-non-exhaustive.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index f3d0807a38c..8c39dd5adac 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -7,12 +7,13 @@ This RFC introduces the `#[non_exhaustive]` attribute for enums and structs, which indicates that more variants/public fields may be added to an enum in the -future. Adding this hint will force downstream crates to add a wildcard arm to -`match` statements involving the enum, ensuring that adding new variants is not -a breaking change. +future. Adding this hint to enums will force downstream crates to add a wildcard +arm to `match` statements, ensuring that adding new variants is not a breaking +change. Adding this hint to structs will prevent downstream crates from +constructing values, because more fields may be added. This is a post-1.0 version of [RFC 757], modified to use an attribute instead of -a custom syntax. +a custom syntax, and extended to structs. # Motivation From c9c82cf6f42fd11279b3000e688fb7d53bce4f32 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Sun, 28 May 2017 17:58:18 -0400 Subject: [PATCH 04/11] Add note on FRU. --- text/0000-non-exhaustive.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index 8c39dd5adac..579d7a9134a 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -332,7 +332,16 @@ pub struct Generics { This will still allow the crate to create values of `Generics`, but it will prevent the user from doing so, because more fields might be added in the -future. +future. Similarly, this will prevent functional record updates like the below: + +```rust +let val = Generics { + lifetimes: Vec::new(), + ..Default::default() +}; +``` + +Because this would require full knowledge of all of the fields of the struct. Although it should not be explicitly forbidden by the language to mark a struct with some private fields as non-exhaustive, it should emit a warning to tell the From c3581545354296044193120e7e7ffaf7bfa06885 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Wed, 31 May 2017 22:04:06 -0400 Subject: [PATCH 05/11] Replace Generics example with a config, add note on struct patterns. --- text/0000-non-exhaustive.md | 110 +++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index 579d7a9134a..7ce17f1114b 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -208,56 +208,55 @@ other use cases. ## A case for structs In addition to enums, it makes sense to extend this feature to structs as well. -For example, consider [`syn::Generics`]: +The most common use case for this would be config structs, like the one below: ```rust -pub struct Generics { - pub lifetimes: Vec, - pub ty_params: Vec, - pub where_clause: WhereClause, +pub struct Config { + pub window_width: u16, + pub window_height: u16, } ``` -Let's say that the language introduces `use` clauses, as described by -@petrochenkov [here][use clauses]. Now, it seems natural to add a new field to -this struct: +As this configuration struct gets larger, it makes sense that more fields will +be added. In the future, the crate may decide to add more public fields, or some +private fields. For example, let's assume we make the following addition: ```rust -pub struct Generics { - pub lifetimes: Vec, - pub ty_params: Vec, - pub where_clause: WhereClause, - pub use_clause: UseClause, +pub struct Config { + pub window_width: u16, + pub window_height: u16, + pub is_fullscreen: bool, } ``` -Unfortunately, this is a breaking change. Because we have full knowledge of the -fields in the struct, we can construct `Generics` without a special constructor: +Now, code that constructs the struct, like below, will fail to compile: -```rust -let gens = Generics { - lifetimes: Vec::new(), - ty_params: Vec::new(), - where_clause: WhereClause::none(), -}; +``` +let config = Config { window_width: 640, window_height: 480 }; ``` -If we add this field, this will turn into a compiler error; we didn't add a -value for the use clause! +And code that matches the struct, like below, will also fail to compile: + +```rust +if let Ok(Config { window_width, window_height }) = load_config() { + // ... +} +``` -To rectify this, we can add a private field to the struct: +Adding this new setting is now a breaking change! To rectify this, we could +always add a private field: ```rust -pub struct Generics { - pub lifetimes: Vec, - pub ty_params: Vec, - pub where_clause: WhereClause, +pub struct Config { + pub window_width: u16, + pub window_height: u16, + pub is_fullscreen: bool, non_exhaustive: (), } ``` -But this makes it more difficult for the crate itself to construct `Generics`, -because they have to add a `non_exhaustive: ()` field every time they make a new +But this makes it more difficult for the crate itself to construct `Config`, +because you have to add a `non_exhaustive: ()` field every time you make a new value. # Detailed design @@ -316,32 +315,56 @@ dead and remove it. Like with enums, the attribute is essentially ignored in the crate that defines the struct, so that users can continue to construct values for the struct. -However, this will prevent downstream users from constructing values, because -fields may be added to the struct in the future. +However, this will prevent downstream users from constructing values or +exhaustively matching values, because fields may be added to the struct in the +future. -For example, using [`syn::Generics`] again: +For example, using our `Config` again: ```rust #[non_exhaustive] -pub struct Generics { - pub lifetimes: Vec, - pub ty_params: Vec, - pub where_clause: WhereClause, +pub struct Config { + pub window_width: u16, + pub window_height: u16, } ``` -This will still allow the crate to create values of `Generics`, but it will -prevent the user from doing so, because more fields might be added in the -future. Similarly, this will prevent functional record updates like the below: +We can still construct our config within the defining crate like so: + +```rust +let config = Config { window_width: 640, window_height: 480 }; +``` + +And we can even exhaustively match on it, like so: + +```rust +if let Ok(Config { window_width, window_height }) = load_config() { + // ... +} +``` + +But users outside the crate won't be able to construct their own values, because +otherwise, adding extra fields would be a breaking change. + +Users can still match on `Config`s non-exhaustively, as usual: + +```rust +let &Config { window_width, window_height, .. } = config; +``` + +But without the `..`, this code will fail to compile. Additionally, the user +won't be able to perform any functional-record-updates like the below: ```rust -let val = Generics { - lifetimes: Vec::new(), +let val = Config { + window_width: 640, + window_height: 480, ..Default::default() }; ``` -Because this would require full knowledge of all of the fields of the struct. +Because there's no guarantee that the remaining fields will satisfy the +requirements (in this case, `Default`). Although it should not be explicitly forbidden by the language to mark a struct with some private fields as non-exhaustive, it should emit a warning to tell the @@ -455,6 +478,5 @@ implemented downstream. [RFC 757]: https://github.com/rust-lang/rfcs/pull/757 [`std::io::ErrorKind`]: https://doc.rust-lang.org/1.17.0/std/io/enum.ErrorKind.html [`diesel::result::Error`]: https://docs.rs/diesel/0.13.0/diesel/result/enum.Error.html -[`syn::Generics`]: https://dtolnay.github.io/syn/syn/struct.Generics.html [use clauses]: https://github.com/rust-lang/rfcs/pull/1976#issuecomment-301903528 [`byteorder`]: https://github.com/BurntSushi/byteorder/tree/f8e7685b3a81c52f5448fd77fb4e0535bc92f880 From 61a4b14aef06e70b8a78563b3302d1b13b3d216b Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Fri, 9 Jun 2017 18:39:27 -0400 Subject: [PATCH 06/11] Add notes on tuple/unit structs, variants. --- text/0000-non-exhaustive.md | 160 +++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 47 deletions(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index 7ce17f1114b..acfd9067211 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -6,20 +6,25 @@ # Summary This RFC introduces the `#[non_exhaustive]` attribute for enums and structs, -which indicates that more variants/public fields may be added to an enum in the -future. Adding this hint to enums will force downstream crates to add a wildcard -arm to `match` statements, ensuring that adding new variants is not a breaking -change. Adding this hint to structs will prevent downstream crates from -constructing values, because more fields may be added. +which indicates that more variants/fields may be added to an enum/struct in the +future. + +Adding this hint to enums will force downstream crates to add a wildcard arm to +`match` statements, ensuring that adding new variants is not a breaking change. + +Adding this hint to structs or enum variants will prevent downstream crates +from constructing or exhaustively matching, to ensure that adding new fields is +not a breaking change. -This is a post-1.0 version of [RFC 757], modified to use an attribute instead of -a custom syntax, and extended to structs. +This is a post-1.0 version of [RFC 757], with some additions. # Motivation -The most common use of this feature is error types. Because adding features to a -crate may result in different possibilities for errors, it makes sense that more -types of errors will be added in the future. +## Enums + +The most common use for non-exhaustive enums is error types. Because adding +features to a crate may result in different possibilities for errors, it makes +sense that more types of errors will be added in the future. For example, the rustdoc for [`std::io::ErrorKind`] shows: @@ -48,7 +53,8 @@ pub enum ErrorKind { ``` Because the standard library continues to grow, it makes sense to eventually add -more error types. However, this can be a breaking change if we're not careful: +more error types. However, this can be a breaking change if we're not careful; +let's say that a user does a match statement like this: ```rust use std::io::ErrorKind::*; @@ -86,9 +92,10 @@ match error_kind { } ``` -Then we can add as many variants as we want! +Then we can add as many variants as we want without breaking any downstream +matches. -## How we do this today +### How we do this today We force users add this arm for [`std::io::ErrorKind`] by adding a hidden variant: @@ -123,9 +130,9 @@ pub enum Error { } ``` -Even though the variant is hidden in the rustdoc, there's nothing actually stopping a -user from using the `__Nonexhaustive` variant. This code works totally fine on -stable rust: +Even though the variant is hidden in the rustdoc, there's nothing actually +stopping a user from using the `__Nonexhaustive` variant. This code works +totally fine, for example: ```rust use diesel::Error::*; @@ -139,8 +146,8 @@ match error { } ``` -This is obviously unintended, and this is currently the best way to make -non-exhaustive enums outside the standard library. Plus, even the standard +This seems unintended, even tohugh this is currently the best way to make +non-exhaustive enums outside the standard library. In fact, even the standard library remarks that this is a hack. Recall the hidden variant for [`std::io::ErrorKind`]: @@ -158,7 +165,7 @@ Additionally, while plenty of crates could benefit from the idea of non-exhaustiveness, plenty don't because this isn't documented in the Rust book, and only documented elsewhere as a hack until a better solution is proposed. -## Opportunity for optimisation +### Opportunity for optimisation Currently, the `#[doc(hidden)]` hack leads to a few missed opportunities for optimisation. For example, take this enum: @@ -205,10 +212,13 @@ Although these options will unlikely matter in this example because error-handling code (hopefully) shouldn't run very often, it could matter for other use cases. -## A case for structs +## Structs + +The most common use for non-exhaustive structs is config types. It often makes +sense to make fields public for ease-of-use, although this can ultimately lead +to breaking changes if we're not careful. -In addition to enums, it makes sense to extend this feature to structs as well. -The most common use case for this would be config structs, like the one below: +For example, take this config struct: ```rust pub struct Config { @@ -259,11 +269,17 @@ But this makes it more difficult for the crate itself to construct `Config`, because you have to add a `non_exhaustive: ()` field every time you make a new value. +### Other kinds of structs + +Because enum variants are *kind* of like a struct, any change we make to structs +should apply to them too. Additionally, any change should apply to tuple structs +as well. + # Detailed design An attribute `#[non_exhaustive]` is added to the language, which will (for now) -fail to compile if it's used on anything other than an enum or struct -definition. +fail to compile if it's used on anything other than an enum, struct definition, +or enum variant. ## Enums @@ -311,15 +327,23 @@ match error { And it should *not* be marked as dead code, even if the compiler does mark it as dead and remove it. +Note that this can *potentially* cause breaking changes if a user adds +`#[deny(dead_code)]` to a match statement *and* the upstream crate removes the +`#[non_exhaustive]` lint. That said, modifying warn-only lints is generally +assumed to not be a breaking change, even though users can make it a breaking +change by manually denying lints. + ## Structs Like with enums, the attribute is essentially ignored in the crate that defines the struct, so that users can continue to construct values for the struct. -However, this will prevent downstream users from constructing values or -exhaustively matching values, because fields may be added to the struct in the -future. +However, this will prevent downstream users from constructing or exhaustively +matching the struct, because fields may be added to the struct in the future. + +Additionally, adding `#[non_exhaustive]` to an enum variant will operate exactly +the same as if the variant were a struct. -For example, using our `Config` again: +Using our `Config` again: ```rust #[non_exhaustive] @@ -352,23 +376,62 @@ Users can still match on `Config`s non-exhaustively, as usual: let &Config { window_width, window_height, .. } = config; ``` -But without the `..`, this code will fail to compile. Additionally, the user -won't be able to perform any functional-record-updates like the below: +But without the `..`, this code will fail to compile. + +Although it should not be explicitly forbidden by the language to mark a struct +with some private fields as non-exhaustive, it should emit a warning to tell the +user that the attribute has no effect. + +## Tuple structs + +Non-exhaustive tuple structs will operate similarly to structs, however, will +disallow matching directly. For example, take this example on stable today: ```rust -let val = Config { - window_width: 640, - window_height: 480, - ..Default::default() -}; +pub Config(pub u16, pub u16, ()); ``` -Because there's no guarantee that the remaining fields will satisfy the -requirements (in this case, `Default`). +The below code does not work, because you can't match tuple structs with private +fields: -Although it should not be explicitly forbidden by the language to mark a struct -with some private fields as non-exhaustive, it should emit a warning to tell the -user that the attribute has no effect. +```rust +let Config(width, height, ..) = config; +``` + +However, this code *does* work: + +```rust +let Config { 0: width, 1: height, .. } = config; +``` + +So, if we label a struct non-exhaustive: + +``` +#[non_exhaustive] +pub Config(pub u16, pub u16) +``` + +Then we the only valid way of matching will be: + +```rust +let Config { 0: width, 1: height, .. } = config; +``` + +## Unit structs + +Unit structs will work very similarly to tuple structs. Consider this struct: + +```rust +#[non_exhaustive] +pub struct Unit; +``` + +We won't be able to construct any values of this struct, but we will be able to +match it like: + +```rust +let Unit { .. } = unit; +``` ## Changes to rustdoc @@ -400,7 +463,8 @@ time. Additionally, non-exhaustive structs should be documented in an early chapter on structs. Public fields should be preferred over getter/setter methods in Rust, although users should be aware that adding extra fields is a potentially -breaking change. +breaking change. In this chapter, users should be taught about non-exhaustive +enum variants as well. # Drawbacks @@ -413,16 +477,18 @@ breaking change. * Provide a dedicated syntax instead of an attribute. This would likely be done by adding a `...` variant or field, as proposed by the original [extensible enums RFC][RFC 757]. -* Allow creating private enum variants, giving a less-hacky way to create a - hidden variant. +* Allow creating private enum variants and/or private fields for enum variants, + giving a less-hacky way to create a hidden variant/field. * Document the `#[doc(hidden)]` hack and make it more well-known. # Unresolved questions -Should there be a way to warn downstream crate users when their match is -non-exuhaustive? What if a user wants to add a warning to their matches using -non-exhaustive enums, to indicate that more code should be added to handle a new -variant? +It may make sense to have a "not exhaustive enough" lint to non-exhaustive +enums or structs, so that users can be warned if they are missing fields or +variants despite having a wildcard arm to warn on them. + +Although this is beyond the scope of this particular RFC, it may be good as a +clippy lint in the future. ## Extending to traits From 3d568896c29e3071a7582533418e02c6f32fa085 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Sat, 26 Aug 2017 14:51:27 -0400 Subject: [PATCH 07/11] Updated with feedback from FCP. --- text/0000-non-exhaustive.md | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index acfd9067211..90b6f3a89fd 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -278,8 +278,8 @@ as well. # Detailed design An attribute `#[non_exhaustive]` is added to the language, which will (for now) -fail to compile if it's used on anything other than an enum, struct definition, -or enum variant. +fail to compile if it's used on anything other than an enum or struct +definition, or enum variant. ## Enums @@ -417,6 +417,10 @@ Then we the only valid way of matching will be: let Config { 0: width, 1: height, .. } = config; ``` +We can think of this as lowering the visibility of the constructor to +`pub(crate)` if it is marked as `pub`, then applying the standard structure +rules. + ## Unit structs Unit structs will work very similarly to tuple structs. Consider this struct: @@ -433,6 +437,41 @@ match it like: let Unit { .. } = unit; ``` +To users of this crate, this will act exactly as if the struct were defined as: + +``` +#[non_exhaustive] +pub struct Unit {} +``` + +## Functional record updates + +Functional record updates will operate exactly the same regardless of whether +structs are marked as non-exhaustive or not. For example, given this struct: + +``` +#[derive(Debug)] +#[non_exhaustive] +pub struct Config { + pub width: u16, + pub height: u16, + pub fullscreen: bool, +} +impl Default for Config { + fn default() -> Config { + Config { width: 640, height: 480, fullscreen: false } + } +} +``` + +The below code will print `Config { width: 1920, height: 1080, fullscreen: +false }` regardless of which crate is calling it: + +``` +let c = Config { width: 1920, height: 1080, ..Config::default() }; +println!("{:?}", c); +``` + ## Changes to rustdoc Right now, the only indicator that rustdoc gives for non-exhaustive enums and From aa2502f3c47f5ffccb08bfbf244f838f1efa6c60 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Sat, 26 Aug 2017 15:06:27 -0400 Subject: [PATCH 08/11] Add note on unit struct constructors. --- text/0000-non-exhaustive.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index 90b6f3a89fd..adad3e65fb1 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -437,12 +437,8 @@ match it like: let Unit { .. } = unit; ``` -To users of this crate, this will act exactly as if the struct were defined as: - -``` -#[non_exhaustive] -pub struct Unit {} -``` +Similarly to tuple structs, this will simply lower the visibility of the +constructor to `pub(crate)` if it were marked as `pub`. ## Functional record updates From d7c62c4064581152d0a22d479cee34ef64efb415 Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Sat, 26 Aug 2017 15:14:04 -0400 Subject: [PATCH 09/11] Fix FRU. --- text/0000-non-exhaustive.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index adad3e65fb1..2a0c887f6fd 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -442,8 +442,8 @@ constructor to `pub(crate)` if it were marked as `pub`. ## Functional record updates -Functional record updates will operate exactly the same regardless of whether -structs are marked as non-exhaustive or not. For example, given this struct: +Functional record updates will operate very similarly to if the struct had an +extra, private field. Take this example: ``` #[derive(Debug)] @@ -460,14 +460,16 @@ impl Default for Config { } ``` -The below code will print `Config { width: 1920, height: 1080, fullscreen: -false }` regardless of which crate is calling it: +We'd expect this code to work without the `non_exhaustive` attribute: ``` let c = Config { width: 1920, height: 1080, ..Config::default() }; println!("{:?}", c); ``` +Although outside of the defining crate, it will not, because `Config` could, in +the future, contain private fields in the future. + ## Changes to rustdoc Right now, the only indicator that rustdoc gives for non-exhaustive enums and From 09eeb1c141fc195275f8b209faadbcd6ce6f987f Mon Sep 17 00:00:00 2001 From: Clar Charr Date: Sat, 26 Aug 2017 15:15:06 -0400 Subject: [PATCH 10/11] Wording. --- text/0000-non-exhaustive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-non-exhaustive.md b/text/0000-non-exhaustive.md index 2a0c887f6fd..90a2aa4d6a5 100644 --- a/text/0000-non-exhaustive.md +++ b/text/0000-non-exhaustive.md @@ -468,7 +468,7 @@ println!("{:?}", c); ``` Although outside of the defining crate, it will not, because `Config` could, in -the future, contain private fields in the future. +the future, contain private fields that the user didn't account for. ## Changes to rustdoc From 60cf58828ab5747a17c9ebe36524c70ebc8fd035 Mon Sep 17 00:00:00 2001 From: Scott McMurray Date: Sat, 26 Aug 2017 15:57:01 -0700 Subject: [PATCH 11/11] RFC 2008: Future-proofing enums/structs with #[non_exhaustive] attribute --- text/{0000-non-exhaustive.md => 2008-non-exhaustive.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename text/{0000-non-exhaustive.md => 2008-non-exhaustive.md} (99%) diff --git a/text/0000-non-exhaustive.md b/text/2008-non-exhaustive.md similarity index 99% rename from text/0000-non-exhaustive.md rename to text/2008-non-exhaustive.md index 90a2aa4d6a5..a00c9d51b9a 100644 --- a/text/0000-non-exhaustive.md +++ b/text/2008-non-exhaustive.md @@ -1,7 +1,7 @@ - Feature Name: non_exhaustive - Start Date: 2017-05-24 -- RFC PR: (leave this empty) -- Rust Issue: (leave this empty) +- RFC PR: https://github.com/rust-lang/rfcs/pull/2008 +- Rust Issue: https://github.com/rust-lang/rust/issues/44109 # Summary