From 504e6cd4e1107ba4631e23a4e65e3b3be5ba761b Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Sat, 1 Mar 2025 14:11:48 +0100 Subject: [PATCH 1/8] feat: add `local-default-generic-bounds` RFC --- text/0000-local-default-generic-bounds.md | 335 ++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 text/0000-local-default-generic-bounds.md diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md new file mode 100644 index 00000000000..6aeb31509ab --- /dev/null +++ b/text/0000-local-default-generic-bounds.md @@ -0,0 +1,335 @@ +- Feature Name: `local_default_bounds` +- Start Date: (fill me in with today's date, YYYY-MM-DD) +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + + +[`forget_marker_trait`]: https://github.com/Ddystopia/rfcs/blob/leak-marker-trait-and-local-default-bounds/text/0000-forget-marker-trait.md + +# Summary +[summary]: #summary + +This RFC proposes a mechanism for crates to define default bounds on generics. By specifying these defaults at the crate level we can reduce the need for verbose and repetitive `?Trait` annotations while maintaining backward compatibility and enabling future language evolution. + +# Motivation +[motivation]: #motivation + +## What are `?Trait` bounds + +Generic parameters on functions (`fn foo()`), associated types in traits `trait Foo { type Assoc; }` and `Self` in traits `trait Foo where Self ...` can have `where` bounds. + +This function expects any `T` that can be compared via `==` operator: + +```rust +fn foo(t: &T) {} +``` + +But Rust introduces some bounds by default. In the code above, `T` must be both `PartialEq` and `Sized`. To opt out of this, users need to write `+ ?Sized` manually: + +```rust +fn foo(t: &T) {} +``` + +## Use of `?Trait` bounds for new features +[applicability-of-default-bounds]: #applicability-of-default-bounds + +A lot of new features (see [#use-cases](#use-cases)) require breaking old code by removing long-established assumptions like `size = stride` or the ability to skip the destructor of a type. To avoid breaking the code, they create a new trait representing an assumption and then define their feature as types that do not implement this trait. Here `?Trait` bounds come in - old code has old assumptions, but new code can add `?Trait` to opt out of them and support more types. + +It is also important to note that in most cases those assumptions are not actually exercised by generic code, they are just already present in signatures - rarely code needs `size = stride`, or to skip the destructor (especially for a foreign type). + +## The problem +[problem-of-default-bounds]: #problem-of-default-bounds + +Quotes from "Size != Stride" [Pre-RFC thread](https://internals.rust-lang.org/t/pre-rfc-allow-array-stride-size/17933): + +> In order to be backwards compatible, this change requires a new implicit trait bound, applied everywhere. However, that makes this change substantially less useful. If that became the way things worked forever, then `#[repr(compact)]` types would be very difficult to use, as almost no generic functions would accept them. Very few functions actually need `AlignSized`, but every generic function would get it implicitly. + + +@scottmdcm + +> Note that every time this has come up -- `?Move`, `?Pinned`, etc -- the answer has been **"we're not adding more of these"**. +> +> What would an alternative look like that doesn't have the implicit trait bound? + +In general, many abstractions can work with both `Trait` and `!Trait` types, and only a few actually require `Trait`. For example, `Forget` bound is necessary for only a few functions in std, such as `forget` and `Box::leak`, while `Option` can work with `!Forget` types too. +However, if Rust were to introduce `?Forget`, every generic parameter in `std` would need an explicit `?Forget` bound. This would create excessive verbosity and does not scale well. + +There is a more fundamental problem noted by @bjorn3: `std` would still need to have `Forget` bounds on all associated items of traits to maintain backward compatibility, as some code may depend on them. This makes `!Forget` types significantly harder to use and reduces their practicality. Fortunately, @Nadrieril proposed a solution to that problem, which resulted in that RFC. + +See [#guide-level-explanation](#guide-level-explanation) for details. + +## Use cases +[use-cases]: #use-cases + +- `!Forget` types - types with a guarantee that destructors will run at the end of their lifetime. Those types are crucial for async and other language features, which are described in [`forget_marker_trait`] Pre-RFC. +- `Size != Stride` is a [frequently requested feature][freaquently-requested-features-size-neq-stride], but it is [fundamentally backward-incompatible change that requires `?AlignSized` bound][size-neq-stride-backward-incompatibe]. +- [`Must move`] types will benefit from this too, further improving async ergonomics. + +[freaquently-requested-features-size-neq-stride]: https://github.com/rust-lang/lang-team/blob/master/src/frequently-requested-changes.md#size--stride +[size-neq-stride-backward-incompatibe]: https://internals.rust-lang.org/t/pre-rfc-allow-array-stride-size/17933#the-alignsized-trait-and-stdarrayfrom_ref-8 +[`Must move`]: https://smallcultfollowing.com/babysteps/blog/2023/03/16/must-move-types/#so-how-would-must-move-work + +The expected outcome is an open road for new language features to enter the language in a backward-compatible way and allow users and libraries to adapt gradually. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +This RFC is targeted at supporting migrations from default being `Trait` to `?Trait`, where `Trait` represents some assumption that is present everywhere but is not really exercised a lot, such as `Forget`, `size = stride` etc. Features like [`DynSized`], as well as [`extern types`], are out of the scope of this RFC, because it does not fit into this category. `DynSized` is not retracting mostly unexercised assumptions in order to make it `?DynSized` the default. + +[`DynSized`]: https://github.com/rust-lang/rfcs/pull/2984 +[`extern types`]: https://github.com/rust-lang/rfcs/pull/1861 + +The syntax is to be bikeshedded, initially, it might be with a crate-level attributes. + +```rust +#![default_generic_bounds(?Forget, PartialEq)] +``` + +The following example demonstrates how the compiler will understand the code. `PartialEq` is used just for illustration purposes. In reality, only a special set of traits would be allowed and would grow with new "breaking" traits, like `Forget`. `PartialEq` would not be one of them. + +```rust +#![default_generic_bounds(?Forget, PartialEq)] + +use std::ops::Deref; + +trait Trait: Deref + ?PartialEq { + type Assoc: Forget; +} + +struct Qux; +struct Foo(T); +struct Bar(T); +struct Baz(T, T::Target, T::Assoc); + +impl Trait for &i32 { + type Assoc = &'static str; +} + +fn use_qux(qux: Qux) { /* ... */ } +fn use_foo(foo: Foo) { /* ... */ } +fn use_bar(bar: Bar) { /* ... */ } +fn use_baz(baz: Baz) { /* ... */ } + +fn main() { + let foo = Foo(Qux); //~ error[E0277]: the trait bound `Qux: PartialEq` is not satisfied + let bar = Bar(Qux); // compiles as expected + let baz = Baz(&3, 3, "assoc"); // compiles as expected +} +``` + +Code above will be observable as (code in today's Rust without any defaults): + +```rust +use std::ops::Deref; + +trait Trait: Deref { + type Assoc; +} + +struct Qux; +struct Foo(T); +struct Bar(T); +struct Baz<'a, T: Trait>(T, &'a T::Target, T::Assoc); + +impl Trait for &i32 { + type Assoc = &'static str; +} + +fn use_qux(qux: Qux) { /* ... */ } +fn use_foo(foo: Foo) { /* ... */ } +fn use_bar(bar: Bar) { /* ... */ } +fn use_baz(baz: Baz) +where + T: ?Forget + PartialEq, // `Trait` has `?PartialEq` for `Self`, but there is no `T: ?PartialEq` + T: Trait +{ + /* ... */ +} + +fn main() { + let foo = Foo(Qux); + let bar = Bar(Qux); + let baz = Baz(&3, 3, "assoc"); +} +``` + +Introducing this feature is backward compatible and does not require an edition. + +RFC tries to be consistent with already existing handling of `Sized`. + +## Example: Migrating to `Forget` + +With this RFC, transitioning to `Forget` is straightforward for any `#![forbid(unsafe)]` crate: + +1. Set the appropriate bounds: + +```rust +#![default_generic_bounds(?Forget)] +``` + +2. Resolve any compilation errors by explicitly adding `+ Forget` where needed. + +3. Optionally: Recurse into your dependencies, applying the same changes as needed. + +Crates using `unsafe` code should beware of `ptr::write` and other unsafe ways of skipping destructors. + +## Implications on the libraries + +### Relax generic bound on public API + +For migrated users it is equivalent to semver's `minor` change, while not migrated uses will observe it as `patch` change. + +### Weakening associated type bound and `Self` bound in traits + +Bounds for associated types and `Self` in traits would be weakened in respect to the new traits from the start: + +```rust +trait Foo: ?Trait { + type Assoc: ?Trait; +} +``` + +This change would not be observable for not migrated crates, because `default_generic_bounds` would default to `Trait`. But if users start migrate before libraries, they will not lock them into old bounds. + +```rust +#![default_generic_bounds(?Forget)] + +async fn foo(bar: T) { + let fut = bar.baz(); + // Compiler will emit an error that `fut` maybe `!Forget` because we set `default_generic_bounds` + // to `?Forget` and `default_assoc_bounds` in `other_crate` is already `?Forget`. Otherwise it + // would have been a breaking change for `other_crate` to make future returned by `baz` `!Forget`, + // as this code would've compiled now but not in the future. + core::mem::forget(fut); +} + +// Libary that has not migrated yet. +mod other_crate { + trait Trait { + async fn baz(); + } +} +``` + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +Introduce new trait level attibute: `default_generic_bounds` used to (non-exhaustively) enumerate overwrides of defaults for different types of bounds. Only a special set of traits would be allowed and would grow with new "breaking" traits, like `Forget`. + +Every trait would initally have its unique default. In practice, bounds for all traits that are stable at the date of RFC except `Sized` would default to `?Trait`. For new "breaking" traits, default would be `Trait`, except bounds for `Self` in traits and associated types in traits. + +[^trait-not-sized-by-default]: https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html + +`default_generic_bounds` is applied for generic parameters. Effectively, it would be observable like that: + +```rust +// crate `b` that has not migrated to `#![default_generic_bounds(?Forget)]` +mod b { + fn foo() {} // Observed as `T: Forget` by `b` and other crates that have not migrated. + struct Bar(T); // Observed as `T: Forget` + // `Self` and `Qux` will be ovservable or other crates, that migrated, without `Forget` bounds + trait Baz { // Observed as `T: Forget` + type Qux; // `U` is observed as `U: Forget` by `b` and other crates that have not migrated. + } + + // Observed as `T: Forget`, `U: Forget`, `for Baz: Forget`. + fn baz, U>() {} + + trait Async { + async fn method(); + } + // Applies to RPITIT too where, so observed as `T::method(..): Forget` + fn async_observer() {} + + trait DerefTrait: Deref { } + + // Associated types in generics are masked with `Forget` too. + // So `>` observed as `Deref` + fn deref_observer() {} + + trait RecursiveTrait { + type Assoc: RecursiveTrait; + } + + // All `::Assoc`, `<::Assoc as Trait>::Assoc`, + // `<<::Assoc as Trait>::Assoc as Trait>::Assoc` etc would be + // observable as `: Forget`. + // `T` is observed as `T: RecursiveTrait + Forget` too. + fn recursive_observer() { } +} +``` + +# Drawbacks +[drawbacks]: #drawbacks + +- It may increase compilation time due to the additional complexity of trait solving. +- It may make reading source files of crates harder, as the reader should first look at the top of the crate to see the defaults, and then remember them. It may increase cognitive load. +- It may take some time for the ecosystem around the language to fully adapt `!Trait`, but it will not include semver breaking changes for libraries or Rust code in general. Similar to `const fn` now. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +This design is simple yet powerful because it offers a backward-compatible way to evolve the language. + +The impact of not accepting this RFC is that language features requiring types like `!Forget`, `MustMove`, +[`!AlignSized`] and many others will not be accepted. + +[`!AlignSized`]: https://internals.rust-lang.org/t/pre-rfc-allow-array-stride-size/17933 + +## [Default Auto Traits] + +This is a very similar proposal which is partially implemted already, could totally be an alternative path. It makes same trick over an edition for traits that we want to remove from defaults. In the case of `Forget`, we may set default bound for crates of edition 2024 and earlier, and lift it for editions after 2024. In terms of this RFC, it would mean that editions would have different presets of default bounds, while users would not be able to manipulate them manually. + +Pros of this is that we do not need a new syntax and implementation should be simpler. + +Cons are that migration is more invasive and enormous, and feels more "forced" - to migrate to the new edition, you must migate to the new bound (or several bounds). The other thing is that [Default Auto Traits] makes no mention of what would happen (but it probably can be added) if library did not migrate to the next edition but users did - would library be locked into `Trait` bounds in associated types (like `async` functions) and need a breaking semver change to remove it? `local_default_bounds` address that issue directly and allows for non-breaking changes. + +[Default auto traits]: https://github.com/rust-lang/rust/pull/120706 + +## Add fine-grained attributes +[split]: #split + +We may have four attributes: `default_generic_bounds`, `default_foreign_assoc_bounds`, `default_trait_bounds` and `default_assoc_bounds` for more fine-grained control over defaults. For example, `Sized` has following defaults: + +```rust +#![default_generic_bounds(Sized)] +#![default_trait_bounds(?Sized)] +#![default_assoc_bounds(Sized)] +#![default_foreign_assoc_bounds(?Sized)] +``` + +Previous version of this RFC was exactly this, you can read it [here](https://github.com/Ddystopia/rfcs/blob/leak-marker-trait-and-local-default-bounds/text/0000-local-default-generic-bounds.md). + +## Alternative syntax +[alternative-syntax]: #alternative-syntax + +We may have a single macro to declare all bounds: + +```rust +declare_default_bounds! { Sized, ?Forget, PartialEq }; +``` + +# Prior art +[prior-art]: #prior-art + +## Links + +- Default auto traits: https://github.com/rust-lang/rust/pull/120706 +- `Self` not `Sized` by default: https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +- [ ] How to handle GATs? Rustc currently does not support proving `for ::Assoc: Forget`. +- [ ] How to solve recursive associated type bounds? `trait Trait { type Assoc: Trait }` +- [ ] Syntax +- [ ] How to display it in Rustdoc +- [ ] Should we allow default `!` bounds? What would it mean? +- [ ] Maybe use the term "implicit" instead of "default". +- [ ] Should we allow `Sized`. +- [ ] Maybe have 4 different attributes for more fine-grained control? +- [ ] Maybe go with [Default auto traits]. + +# Shiny future we are working towards + +Less backward compatibility burden and more freedom to fix old mistakes, to propose new features. From 59dea08f8b270cbca946ccde716a073cbf36e1b1 Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Sat, 1 Mar 2025 14:21:32 +0100 Subject: [PATCH 2/8] fix: update old link --- text/0000-local-default-generic-bounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md index 6aeb31509ab..0fbd6cbb8b0 100644 --- a/text/0000-local-default-generic-bounds.md +++ b/text/0000-local-default-generic-bounds.md @@ -298,7 +298,7 @@ We may have four attributes: `default_generic_bounds`, `default_foreign_assoc_bo #![default_foreign_assoc_bounds(?Sized)] ``` -Previous version of this RFC was exactly this, you can read it [here](https://github.com/Ddystopia/rfcs/blob/leak-marker-trait-and-local-default-bounds/text/0000-local-default-generic-bounds.md). +Previous version of this RFC was exactly this, you can read it [here](https://github.com/Ddystopia/rfcs/blob/49f52526b9f455ddbc333a7b453f8d61f1918534/text/0000-local-default-generic-bounds.md). ## Alternative syntax [alternative-syntax]: #alternative-syntax From bd39d94e791e4bcf92ab6d68548ae381ec9639d1 Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Sat, 1 Mar 2025 14:22:04 +0100 Subject: [PATCH 3/8] fix: update link to forget rfc --- text/0000-local-default-generic-bounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md index 0fbd6cbb8b0..8a18a876334 100644 --- a/text/0000-local-default-generic-bounds.md +++ b/text/0000-local-default-generic-bounds.md @@ -4,7 +4,7 @@ - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) -[`forget_marker_trait`]: https://github.com/Ddystopia/rfcs/blob/leak-marker-trait-and-local-default-bounds/text/0000-forget-marker-trait.md +[`forget_marker_trait`]: https://github.com/rust-lang/rfcs/pull/3782 # Summary [summary]: #summary From 4d7d1ec92a4f668dca8d21f347521ab224cadad5 Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Sat, 1 Mar 2025 20:43:13 +0100 Subject: [PATCH 4/8] feat: add alternative with simpler implementation --- text/0000-local-default-generic-bounds.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md index 8a18a876334..30990c8ddd6 100644 --- a/text/0000-local-default-generic-bounds.md +++ b/text/0000-local-default-generic-bounds.md @@ -309,6 +309,10 @@ We may have a single macro to declare all bounds: declare_default_bounds! { Sized, ?Forget, PartialEq }; ``` +## Do not default `Self` in traits and associated types to `?Trait` from the beginning + +This will drastically reduce implementation complexity as it would be possible to do with a simple desugaring, because recursive bounds would not need to be infinitely bounded. But it will open a possibility for libraries to be locked into `Forget` bounds in some cases. + # Prior art [prior-art]: #prior-art From fba3e0163725ea3a791001d13eaefd6e11608dee Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Sun, 2 Mar 2025 17:54:26 +0100 Subject: [PATCH 5/8] Update text/0000-local-default-generic-bounds.md Co-authored-by: kennytm --- text/0000-local-default-generic-bounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md index 30990c8ddd6..6a33acd0b99 100644 --- a/text/0000-local-default-generic-bounds.md +++ b/text/0000-local-default-generic-bounds.md @@ -214,7 +214,7 @@ mod other_crate { # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -Introduce new trait level attibute: `default_generic_bounds` used to (non-exhaustively) enumerate overwrides of defaults for different types of bounds. Only a special set of traits would be allowed and would grow with new "breaking" traits, like `Forget`. +Introduce new crate level attibute: `default_generic_bounds` used to (non-exhaustively) enumerate overwrides of defaults for different types of bounds. Only a special set of traits would be allowed and would grow with new "breaking" traits, like `Forget`. Every trait would initally have its unique default. In practice, bounds for all traits that are stable at the date of RFC except `Sized` would default to `?Trait`. For new "breaking" traits, default would be `Trait`, except bounds for `Self` in traits and associated types in traits. From b539df352c0df5a3436b40da3d8df60164be509c Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Sun, 2 Mar 2025 20:06:57 +0100 Subject: [PATCH 6/8] feat: add unresolved question about macros, thanks @kennytm --- text/0000-local-default-generic-bounds.md | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md index 6a33acd0b99..0fd7319370e 100644 --- a/text/0000-local-default-generic-bounds.md +++ b/text/0000-local-default-generic-bounds.md @@ -326,6 +326,34 @@ This will drastically reduce implementation complexity as it would be possible t - [ ] How to handle GATs? Rustc currently does not support proving `for ::Assoc: Forget`. - [ ] How to solve recursive associated type bounds? `trait Trait { type Assoc: Trait }` +- [ ] We probably don't want to alter macro output, it would probably be too hard to implement and design. How should we handle this? + +```rust +// macro crate +#![default_generic_bounds(PartialEq)] +#[macro_export] +macro_rules! make_functions { + ( + { $a:item } + { $($b:tt)* } + { $($d:tt)* } + ) => { + $a + $($b)* + pub fn c(c: &C) -> bool { c == c } + pub fn d(_: &D) -> bool { true } + } +} + +// user crate +#![default_generic_bounds(?Forget)] +make_functions! { + { pub fn a(_: &A) -> bool { true } } + { pub fn b(_: &B) -> bool { true } } + {} +} +``` + - [ ] Syntax - [ ] How to display it in Rustdoc - [ ] Should we allow default `!` bounds? What would it mean? From 8d2ff106e1b054ffcfe99457a43fc55eee657ff4 Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Mon, 3 Mar 2025 09:55:25 +0100 Subject: [PATCH 7/8] feat: add a caviat about macros --- text/0000-local-default-generic-bounds.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md index 0fd7319370e..ff94ea7835d 100644 --- a/text/0000-local-default-generic-bounds.md +++ b/text/0000-local-default-generic-bounds.md @@ -211,6 +211,29 @@ mod other_crate { } ``` +### Macros + +If macro-library generates code, some problems during the migration are possible: + +```rust +mod user { + #![default_generic_bounds(?Forget)] + + ::library::make!(); // Will not compile because `T` is `?Forget`. +} + +mod user { + #[macro_export] + macro_rules! make { + () => { + pub fn foo(t: T) { + ::core::mem::forget(t); + } + } + } +} +``` + # Reference-level explanation [reference-level-explanation]: #reference-level-explanation From b1269e3d5906aa93cd82050983087ec7fe771a62 Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Tue, 11 Mar 2025 08:09:06 +0100 Subject: [PATCH 8/8] Update text/0000-local-default-generic-bounds.md Co-authored-by: Jack Rickard --- text/0000-local-default-generic-bounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-local-default-generic-bounds.md b/text/0000-local-default-generic-bounds.md index ff94ea7835d..bdf7fdef3e1 100644 --- a/text/0000-local-default-generic-bounds.md +++ b/text/0000-local-default-generic-bounds.md @@ -45,7 +45,7 @@ Quotes from "Size != Stride" [Pre-RFC thread](https://internals.rust-lang.org/t/ > In order to be backwards compatible, this change requires a new implicit trait bound, applied everywhere. However, that makes this change substantially less useful. If that became the way things worked forever, then `#[repr(compact)]` types would be very difficult to use, as almost no generic functions would accept them. Very few functions actually need `AlignSized`, but every generic function would get it implicitly. -@scottmdcm +@scottmcm > Note that every time this has come up -- `?Move`, `?Pinned`, etc -- the answer has been **"we're not adding more of these"**. >