From 545099bcffea77271a4bf97eb7acb219eb33654c Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Fri, 13 Dec 2024 09:29:40 +0000 Subject: [PATCH 01/23] Add const trait impl RFC --- text/0000-const-trait-impls.md | 726 +++++++++++++++++++++++++++++++++ 1 file changed, 726 insertions(+) create mode 100644 text/0000-const-trait-impls.md diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md new file mode 100644 index 00000000000..820b07cc474 --- /dev/null +++ b/text/0000-const-trait-impls.md @@ -0,0 +1,726 @@ +- Feature Name: `const_trait_methods` +- Start Date: 2024-12-13 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#67792](https://github.com/rust-lang/rust/issues/67792) + +# Summary +[summary]: #summary + +Make trait methods callable in const contexts. This includes the following parts: + +* Allow marking `trait` declarations as const implementable. +* Allow marking `trait` impls as `const`. +* Allow marking trait bounds as `const` to make methods of them callable in const contexts. + +Fully contained example ([Playground of currently working example](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=2ab8d572c63bcf116b93c632705ddc1b)): + +```rust +const trait Default { + fn default() -> Self; +} + +impl const Default for () { + fn default() {} +} + +const fn default() -> T { + T::default() +} + +fn compile_time_default() -> T { + const { T::default() } +} + +const _: () = Default::default(); + +fn main() { + let () = default(); + let () = compile_time_default(); + let () = Default::default(); +} +``` + +# Motivation +[motivation]: #motivation + +Const code is currently only able to use a small subset of Rust code, as many standard library APIs and builtin syntax things require calling trait methods to work. +As an example, in const contexts you cannot use even basic equality on anything but primitives: + +```rust +const fn foo() { + let a = [1, 2, 3]; + let b = [1, 2, 4]; + if a == b {} // ERROR: cannot call non-const operator in constant functions +} +``` + +## Background + +This RFC requires familarity with "const contexts", so you may have to read [the relevant reference section](https://doc.rust-lang.org/reference/const_eval.html#const-context) first. + +Calling functions during const eval requires those functions' bodies to only use statements that const eval can handle. While it's possible to just run any code until it hits a statement const eval cannot handle, that would mean the function body is part of its semver guarantees. Something as innocent as a logging statement would make the function uncallable during const eval. + +Thus we have a marker (`const`) to add in front of functions that requires the function body to only contain things const eval can handle. This in turn allows a `const` annotated function to be called from const contexts, as you now have a guarantee it will stay callable. + +When calling a trait method, this simple scheme (that works great for free functions and inherent methods) does not work. + +Throughout this document, we'll be revisiting the example below. Method syntax and `dyn Trait` problems all also exist with static method calls, so we'll stick with the latter to have the simplest examples possible. + +```rust +const fn default() -> T { + T::default() +} + +// Could also be `const fn`, but that's an orthogonal change +fn compile_time_default() -> T { + const { T::default() } +} +``` + +Neither of the above should (or do) compile. +The first, because you could pass any type T whose default impl could + +* mutate a global static, +* read from a file, or +* just allocate memory, + +which are all not possible right now in const code, and some can't be done in Rust in const code at all. + +It should be possible to write `default` in a way that allows it to be called in const contexts +for types whose `Default` impl's `default` method satisfies all rules that `const fn` must satisfy +(including some annotation that guarantees this won't break by accident). +It must always be possible to call `default` outside of const contexts with no limitations on the generic parameters that may be passed. + +Similarly it should be possible to write `compile_time_default` in a way that also requires calls +outside of const contexts to only pass generic parameters whose `Default::default` method satisifies +the usual `const fn` rules. This is necessary in order to allow a const block +(which can access generic parameters) in the function body to invoke methods on the generic parameter. + +So, we need some annotation that differentiates a `T: Default` bound from one that gives us the guarantees we're looking for. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## Nomenclature and new syntax concepts + +### Const trait impls + +It is now allowed to prefix a trait name in an impl block with `const`, marking that this `impl`'s type is now allowed to +have methods of this `impl`'s trait to be called in const contexts (if all where bounds hold, like ususal, but more on this later). + +An example looks as follows: + +```rust +impl const Trait for Type {} +``` + +Such impls require that the trait is a `const trait`. + +All method bodies in a const trait impl are [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context). + +### Const traits + +Traits need to opt-in to being allowed to have const trait impls. Thus you need to declare your traits by prefixing the `trait` keyword with `const`: + +```rust +const trait Trait {} +``` + +This in turn checks all methods' default bodies as if they were `const fn`, making them callable in const contexts. +Impls can now rely on the default methods being const, too, and don't need to override them with a const body. + +We may add an attribute later to allow you to mark individual trait methods as not-const so that when creating a const trait, one can +add (defaulted or not) methods that cannot be used in const contexts. + +All default method bodies of const trait declarations are [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context). + +Note that on nightly the syntax is + +```rust +#[const_trait] +trait Trait {} +``` + +and a result of this RFC would be that we would remove the attribute and add the `const trait` syntax. + +### Const trait bounds + +Any item that can have trait bounds can also have `const Trait` bounds. + +Examples: + +* `T: const Trait`, requiring any type that `T` is instantiated with to have a const trait impl. +* `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a const trait impl. + * These are not part of this RFC because they require `const fn` function pointers. See [the Future Possibilities section](#future-possibilities). +* `impl const Trait` (in all positions). + * These are not part of this RFC because they require `const fn` function pointers. See [the Future Possibilities section](#future-possibilities). +* `trait Foo: const Bar {}`, requiring every type that has an impl for `Foo` (even a non-const one), to also have a const trait impl for `Bar`. + +Such an impl allows you to use the type that is bound within a const block or any other const context, because we know that the type has a const trait impl and thus +must be executable at compile time. The following function will invoke the `Default` impl of a type at compile time and store the result in a constant. Then it returns that constant instead of computing the value every time. + +```rust +fn compile_time_default() -> T { + const { T::default() } +} +``` + +### Maybe-const trait bounds + +Many generic `const fn` and especially many const trait impls do not actually require a const trait impl for their generic parameters. +As `const fn` can also be called at runtime, it would be too strict to require it to only be able to call things with const trait impls. +Picking up the example from [the beginning](#summary): + +```rust +const trait Default { + fn default() -> Self; +} + +impl const Default for () { + fn default() {} +} + +impl Default for Box { + fn default() -> Self { Box::new(T::default()) } +} + +// This function requires a `const` impl for the type passed for T, +// even if called from a non-const context +const fn default() -> T { + T::default() +} + +const _: () = default(); + +fn main() { + let _: Box = default(); + //~^ ERROR: as Default>::default cannot be called at compile-time +} +``` + +What we instead want is that, just like `const fn` can be called at runtime and compile time, we want their trait bounds' constness +to mirror that behaviour. So we're introducing `~const Trait` bounds, which mean "const if called from const context" (slight oversimplifcation, but read on). + +The only thing we need to change in our above example is the `default` function, changing the `const Default` bound to a `~const Default` one. + +```rust +const fn default() -> T { + T::default() +} +``` + +`~const` is derived from "approximately", meaning "maybe" in this context, or specifically "const impl required if called in const context". +It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "maybe", but from the other direction: `?const` (not proposed here, see the alternatives section for why it was rejected) would mean "no const impl required, even if called in const context". + +### `~const Destruct` trait + +The `Destruct` trait enables dropping types within a const context. + +```rust +const fn foo(t: T) { + // `t` is dropped here, but we don't know if we can evaluate its `Drop` impl (or that of its fields' types) +} +const fn baz(t: T) { + // Fine, `Copy` implies that no `Drop` impl exists +} +const fn bar(t: T) { + // Fine, we can safely invoke the destructor of `T`. +} +``` + +When a value of a generic type goes out of scope, it is dropped and (if it has one) its `Drop` impl gets invoked. +This situation seems no different from other trait bounds, except that types can be dropped without implementing `Drop` +(as they can contain types that implement `Drop`). In that case the type's drop glue is invoked. + +The `Destruct` trait is a bound for whether a type has drop glue. This is trivally true for all types. + +`~const Destruct` trait bounds are satsifed only if the type has a `const Drop` impl or all of the types of its components +are `~const Destruct`. + +## Trivially enabled features + +You can use `==` operators on most types from libstd from within const contexts. + +```rust +const _: () = { + let a = [1, 2, 3]; + let b = [4, 5, 6]; + assert!(a != b); +}; +const _: () = { + let a = Some(42); + let b = a; + assert!(a == b); +}; +``` + +Note that the use of `assert_eq!` is waiting on `Debug` impls becoming `const`, which +will likely be tracked under a separate feature gate under the purview of T-libs. +Similarly other traits will be made `const` over time, but doing so will be +unblocked by this feature. + +## Crate authors: Making your own custom types easier to use + +You can write const trait impls of many standard library traits for your own types. +While it was often possible to write the same code in inherent methods, operators were +covered by traits from `std::ops` and thus not avaiable for const contexts. +Most of the time it suffices to add `const` before the trait name in the impl block. +The compiler will guide you and suggest where to also +add `~const` bounds for trait bounds on generic parameters of methods or the impl. + +Similarly you can make your traits available for users of your crate to implement constly. +Note that this will change your semver guarantees: you are now guaranteeing that any future +methods you add don't just have a default body, but a `const` default body. The compiler will +enforce this, so you can't accidentally make a mistake, but it may still limit how you can +extend your trait without having to do a major version bump. +Most of the time it suffices to add `const` before the `trait` declaration. The compiler will +guide you and suggest where to also add `~const` bounds for super trait bounds or trait bounds +on generic parameters of your trait or your methods. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## How does this work in the compiler? + +These `const` or `~const` trait bounds desugar to normal trait bounds without modifiers, plus an additional constness bound that has no surface level syntax. + +A much more detailed explanation can be found in https://hackmd.io/@compiler-errors/r12zoixg1l#What-now + +We generate a `ClauseKind::HostEffect` for every `const` or `~const` bound. +To mirror how some effectful languages represent such effects, +I'm going to use `::k#host` to allow setting whether the `host` effect is "const" (disabled) or "maybe" (generic). +This is not comparable with other associated bounds like type bounds or const bounds, as the values the associated host effect can +take do neither have a usual hierarchy nor a concrete single value we can compare due to the following handling of those bounds: + +* There is no "always" (enabled), as that is just the lack of a host effect, meaning no `::k#host` bound at all. +* In contrast to other effect systems, we do not track the effect as a true generic parameter in the type system, + but instead just ignore all `Maybe` bounds in host environments and treat them as `Const` in const environments. + +While this could be modelled with generic parameters in the type system, that: + +* Has been attempted and is really complex (fragile) on the impl side and on the reasoning about things side. +* Appears to permit more behaviours than are desirable (multiple such parameters, math on these parameters, ...), so they need to be prevented, adding more checks. +* Is not necessary unless we'd allow much more complex kinds of bounds. So it can be kept open as a future possibility, but for now there's no need. +* Does not quite work in Rust due to the constness then being early bound instead of late bound, cause all kinds of problems around closures and function calls. +* Technically cause two entirely separate MIR bodies to be generated, one for where the effect is on and one where it is off. On top of that it then theoretically allows you to call the const MIR body from non-const code. + +Thus that approach was abandoned after proponents and opponents cooperated in trying to make the generic parameter approach work, resulting in all proponents becoming opponents, too. + +### `const` desugaring + +```rust +fn compile_time_default() -> T { + const { T::default() } +} +``` + +desugars to + +```rust +fn compile_time_default() -> T +where + T: Default, + ::k#host = Const, +{ + const { T::default() } +} +``` + +### `~const` desugaring + +```rust +const fn default() -> T { + T::default() +} +``` + +desugars to + +```rust +const fn default() -> T +where + T: Default, + ::k#host = Maybe, +{ + T::default() +} +``` + +### Why not both? + + +```rust +const fn checked_default() -> T +where + T: const Default, + T: ~const Default, + T: ~const PartialEq, +{ + let a = const { T::default() }; + let b = T::default(); + if a == b { + a + } else { + panic!() + } +} +``` + +Has a redundant bound. `T: const Default` implies `T: ~const Default`, so while the desugaring will include both (but may filter them out if we deem it useful on the impl side), +there is absolutely no difference (just like specifying `Fn() + FnOnce()` has a redundant `FnOnce()` bound). + +## Precedence of `~` + +The `~` sigil applies to the `const`, not the `const Trait`, so you can think of it as `(~const) Trait`, not `~(const Trait)`. +This is both handled this way by the parser, and semantically what is meant here. The constness of the trait bound is affected, +the trait bound itself exists either way. + +## Why do traits need to be marked as "const implementable"? + +### Default method bodies + +Adding a new method with a default body would become a breaking change unless that method/default body +would somehow be marked as `const`, too. So by marking the trait, you're opting into the requirement that all default bodies are const checked, +and thus neither `impl const Trait for Type` items nor `impl Trait for Type` items will be affected if you add a new method with a default body. +This scheme avoids adding a new kind of breaking change to the Rust language, +and instead allows everyone managing a public trait in their crate to continue relying on the +previous rule "adding a new method is not a breaking change if it has a default body". + +### `~const Destruct` super trait + +Traits that have `self` (by ownership) methods, will almost always drop the `self` in these methods' bodies unless they are simple wrappers that just forward to the generic parameters' bounds. + +The following never drops `T`, because it's the job of `` to handle dropping the values. + +```rust +struct NewType(T); + +impl> const Add for NewType { + type Output = Self, + fn add(self, other: Self) -> Self::Output { + NewType(self.0 + other.0) + } +} +``` + +But if any code path could drop a value... + +```rust +struct NewType(T, bool); + +struct Error; + +impl> const Add for NewType { + type Output = Result; + fn add(self, other: Self) -> Self::Output { + if self.1 { + Ok(NewType(self.0 + other.0, other.1)) + } else { + // Drops both `self.0` and `self.1` + Err(Error) + } + } +} +``` + +... then we need to add a `~const Destruct` bound to `T`, to ensure +`NewType` can be dropped. + +This bound in turn will be infectious to all generic users of `NewType` like + +```rust +const fn add( + a: NewType, + b: NewType, +) -> Result, Error> { + a + b +} +``` + +which now need a `T: ~const Destruct` bound, too. +In practice we have noticed that a large portion of APIs will have a `~const Destruct` bound. +This bound has little value as an explicit bound that appears almost everywhere. +Especially since it is a fairly straight forward assumption that a type that has const trait impls will also have a `const Drop` impl or only contain `const Destruct` types. + +Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere. +We may offer an opt out of this behaviour in the future, if there are convincing real world use cases. + +### `~const` bounds on `Drop` impls + +It is legal to add `~const` to `Drop` impls' bounds, even thought the struct doesn't have them: + +```rust +const trait Bar { + fn thing(&mut self); +} + +struct Foo(T); + +impl const Drop for Foo { + fn drop(&mut self) { + self.0.thing(); + } +} +``` + +There is no reason (and no coherent representation) of adding `~const` trait bounds to a type. +Our usual `Drop` rules enforce that an impl must have the same bounds as the type. +`~const` modifiers are special here, because they are only needed in const contexts. +While they cause exactly the divergence that we want to prevent with the `Drop` impl rules: +a type can be declared, but not dropped, because bounds are unfulfilled, this is: + +* Already the case in const contexts, just for all types that aren't trivially free of `Drop` types. +* Exactly the behaviour we want. + +Extraneous `~const Trait` bounds where `Trait` isn't a bound on the type at all are still rejected: + +```rust +impl const Drop for Foo { + fn drop(&mut self) { + self.0.thing(); + } +} +``` + +errors with + +``` +error[E0367]: `Drop` impl requires `T: Baz` but the struct it is implemented for does not + --> src/lib.rs:13:22 + | +13 | impl const Drop for Foo { + | ^^^^^^^^^^ + | +note: the implementor must specify the same requirement + --> src/lib.rs:8:1 + | +8 | struct Foo(T); + | ^^^^^^^^^^^^^^^^^^ +``` + +# Drawbacks +[drawbacks]: #drawbacks + +## Adding any feature at all around constness + +I think we've reached the point where all critics have agreed that this one kind of effect system is unavoidable since we want to be able to write maintainable code for compile time evaluation. + +So the main drawback is that it creates interest in extending the system or add more effect systems, as we have now opened the door with an effect system that supports traits. +Even though I personally am interested in adding an effect for panic-freedom, I do not think that adding this const effect system should have any bearing on whether we'll add +a panic-freedom effect system or other effect systems in the future. This feature stands entirely on its own, and even if we came up with a general system for many effects that is (e.g. syntactically) better in the +presence of many effects, we'll want the syntax from this RFC as sugar for the very common and simple case. + +## It's hard to make constness optional with `#[cfg]` + +One cannot `#[cfg]` just the `const` keyword in `const Trait`, and even if we made it possible by sticking with `#[const_trait]` attributes, and also adding the equivalent for impls and functions, +`~const Trait` bounds cannot be made conditional with `#[cfg]`. The only real useful reason to have this is to support newer Rust versions with a cfg, and allow older Rust versions to compile +the traits, just without const support. This is surmountable with proc macros that either generate two versions or just generate a different version depending on the Rust version. +Since it's only necessary for a transition period while a crate wants to support both pre-const-trait Rust and +newer Rust versions, this doesn't seem too bad. With a MSRV bump the proc macro usage can be removed again. + +## Can't have const methods and nonconst methods on the same trait + +If a trait has methods that don't make sense for const contexts, but some that do, then right now it is required to split that +trait into a nonconst trait and a const trait and "merge" them by making one of them be a super trait of the other: + +```rust +const trait Foo { + fn foo(&self); +} +trait Bar: Foo { + fn bar(&self); +} + +impl const Foo for () { + fn foo(&self) {} +} +impl Bar for () { + fn bar(&self) { + println!("writing to terminal is not possible in const eval"); + } +} +``` + +Such a split is not possible without a breaking change, so splitting may not be feasible in some cases. +Especially since we may later offer the ability to have const and nonconst methods on the same trait, then allowing +the traits to be merged again. That's churn we'd like to avoid. + +Note that it may frequently be that such a trait should have been split even without constness being part of the picture. + +# Alternatives +[alternatives]: #alternatives + +## use `const Trait` bounds for maybe-const, invent new syntax for always-const + +It may seem tempting to use `const fn foo` to mean what in this RFC is `~const Trait`, and then add new syntax for bounds that allow using trait methods in const blocks. + +## use `Trait` or `Trait` instead of `const Trait` + +To avoid new syntax before paths referring to traits, we could treat the constness as a generic parameter or an associated type. +While an associated type is very close to how the implementation works, neither `effect = const` nor `effect: const` are representing the logic correctly, +as `const` implies `~const`, but `~const` is nothing concrete, it's more like a generic parameter referring to the constness of the function. +Fully expanded one can think of + +```rust +const fn foo(t: T) { ... } +``` + +to be like + +```rust +const fn foo(t: T) +where + T: Trait + OtherTrait, + ::bikeshed#effect = const, + ::bikeshed#effect = const, +{ + ... +} +``` + +Note that `const` implies `const` and thus also `for const`, just like `const Trait` implies `~const Trait`. + +We do not know of any cases where such an explicit syntax would be useful (only makes sense if you can do math on the bool), +so a more reduced version could be + +```rust +const fn foo(t: T) +where + T: Trait + OtherTrait, + ::bikeshed#effect = ~const, + ::bikeshed#effect = const, +{ + ... +} +``` + +or + +```rust +const fn foo + OtherTrait>(t: T) { ... } +``` + +## Make all `const fn` arguments `~const Trait` by default and require an opt out `?const Trait` + +We could default to making all `T: Trait` bounds be const if the function is called from a const context, and require a `T: ?const Trait` opt out +for when a trait bound is only used for its associated types and consts. + +This requires a new `~const fn` syntax (sigils or syntax bikesheddable), as the existing `const fn` already has trait bounds that +do not require const trait impls even if used in const contexts. + +A full example how how things would look then + +```rust +const trait Foo: Bar + ?const Baz {} + +impl const Foo for () {} + +const fn foo() -> T { + // cannot call `Baz` methods + ::bar() +} + +const _: () = foo(); +``` + +This can be achieved across an edition by having some intermediate syntax like prepending `#[next_const]` attributes to all const fn that are using the new syntax, and having a migration lint that suggests adding it to every `const fn` that has trait bounds. + +Then in the following edition, we can forbid the `#[next_const]` attribute and just make it the default. + +The disadvantage of this is that by default, it creates stricter bounds than desired. + +```rust +const fn foo() { + T::ASSOC_CONST +} +``` + +compiles today, and allows all types that implement `Foo`, irrespective of the constness of the impl. +With the opt-out scheme that would still compile, but suddenly require callers to provide a const impl. + +The safe default (and the one folks are used to for a few years now), is that trait bounds just work, you just +can't call methods on them. To get more capabilities, you add more syntax. Thus the opt-out approach was not taken. + +# Prior art +[prior-art]: #prior-art + +* I tried to get this accepted before under https://github.com/rust-lang/rfcs/pull/2632. + * While that moved to [FCP](https://github.com/rust-lang/rfcs/pull/2632#issuecomment-481395097), it had concerns raised. + * [T-lang discussed this](https://github.com/rust-lang/rfcs/pull/2632#issuecomment-567699174) and had the following open concerns: + * This design has far-reaching implications and we probably aren't going to be able to work them all out in advance. We probably need to start working through the implementation. + * This seems like a great fit for the "const eval" project group, and we should schedule a dedicated meeting to talk over the scope of such a group in more detail. + * Similarly, it would be worth scheduling a meeting to talk out this RFC in more detail and make sure the lang team is understanding it well. + * We feel comfortable going forward with experimentation on nightly even in advance of this RFC being accepted, as long as that experimentation is gated. + * All of the above have happened in some form, so I believe it's time to have the T-lang meeting again. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +- What parts of the design do you expect to resolve through the RFC process before this gets merged? + * Whether to pick an alternative syntax (and which one in that case). +- What parts of the design do you expect to resolve through the implementation of this feature before stabilization? + * We've already handled this since the last RFC, there are no more implementation concerns. +- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? + * This RFC's syntax is entirely unrelated to discussions on `async Trait`. + * `async Trait` can be written entirely in user code by creating a new trait `AsyncTrait`; there is no workaround for `const`. + * This RFC's syntax is entirely unrelated to discussions on effect syntax. + * If we get an effect system, it may be desirable to allow expressing const traits with the effect syntax, this design is forward compatible with that. + * If we get an effect system, we will still want this shorthand, just like we allow you to write: + * `T: Iterator` and don't require `where T: Iterator, ::Item = U`. + * `T: Iterator` and don't require `where T: Iterator, ::Item: Debug`. + * RTN for per-method bounds: `T: Trait C>` could supplement this feature in the future. + * Very verbose (need to specify arguments and return type). + * Want short hand sugar anyway to make it trivial to change a normal function to a const function by just adding some minor annotations. + * Significantly would delay const trait stabilization. + * Usually requires editing the trait anyway, so there's no "can constify impls without trait author opt in" silver bullet. + * New RTN-like per-method bounds: `T: Trait`. + * Unclear if soundly possible. + * Unclear if possible without incurring significant performance issues for all code (may need tracking new information for all functions out there). + * Still requires editing traits. + * Still want the `~const Trait` sugar anyway. + +## Should we start out by allowing only const trait declarations and const trait impls + +We do not need to immediately allow using methods on generic parameters of const fn, as a lot of const code is nongeneric. + +The following example could be made to work with just const traits and const trait impls. + +```rust +const fn foo() { + let a = [1, 2, 3]; + let b = [1, 2, 4]; + if a == b {} +} +``` + +Things like `Option::map` could not be made const without const trait bounds, as they need to actually call the generic `FnOnce` argument. + + +# Future possibilities +[future-possibilities]: #future-possibilities + +## Migrate to `~const fn` + +`const fn` and `const` items have slightly different meanings for `const`: + +`const fn` can also be called at runtime just fine, while the others are always const +contexts and need to be evaluated by the const evaluator. + +Additionally `const Trait` bounds have a third meaning (the same as `const Trait` in `impl const Trait for Type`): + +They can be invoked at compile time, but also in `const fn`. + +While all these meanings are subtly different, making their differences more obvious will not make them easier to understand. +All that changing to `~const fn` would achieve is that folk will add the sigil when told by the compiler, and complain about +having to type a sigil, when there is no meaning for `const fn` without a sigil. + +While I see the allure from a language nerd perspective to give every meaning its own syntax, I believe it is much more practical to +just call all of these `const` and only separate the `~const Trait` bounds from `const Trait` bounds. + +## `const fn()` pointers + +Just like `const fn foo(x: impl ~const Trait) { x.method() }` and `const fn foo(x: &dyn ~const Trait) { x.method() }` we want to allow +`const fn foo(f: const fn()) { f() }`. + +There is nothing design-wise blocking function pointers and calling them, they mainly require implementation work and extending the +compiler's internal type system representation of a function signature to include constness. From 290d7fed067069302a7d2ee5e4fd9659749abe36 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 13 Jan 2025 17:14:10 +0000 Subject: [PATCH 02/23] Add per-method annotations alternative --- text/0000-const-trait-impls.md | 42 +++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 820b07cc474..24ad1b61fd9 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -641,6 +641,45 @@ With the opt-out scheme that would still compile, but suddenly require callers t The safe default (and the one folks are used to for a few years now), is that trait bounds just work, you just can't call methods on them. To get more capabilities, you add more syntax. Thus the opt-out approach was not taken. +## Per-method constness instead of per-trait + +We could require trait authors to declare which methods can be const: + +```rust +trait Default { + const fn default() -> Self; +} +``` + +This has two major advantages: + +* you can now have const and non-const methods in your trait without requiring an opt-out +* you can add new methods with default bodies and don't have to worry about new kinds of breaking changes + +The specific syntax given here may be confusing though, as it looks like the function is always const, but +implementations can use non-const impls and thus make the impl not usable for `T: ~const Trait` bounds. + +Though this means that changing a non-const fn in the trait to a const fn is a breaking change, as the user may +have that previous-non-const fn as a non-const fn in the impl, causing the entire impl now to not be usable for +`T: ~const Trait` anymore. + +See also: out of scope RTN notation in [Unresolved questions](#unresolved-questions) + +## Per-method and per-trait constness together: + +To get the advantages of the per-method constness alternative above, while avoiding the new kind of breaking change, we can require per-method and per-trait constness: + +A mixed version of the above could be + +```rust +const trait Foo { + const fn foo(); + fn bar(); +} +``` + +where you still need to annotate the trait, but also annotate the const methods. + # Prior art [prior-art]: #prior-art @@ -669,9 +708,10 @@ can't call methods on them. To get more capabilities, you add more syntax. Thus * `T: Iterator` and don't require `where T: Iterator, ::Item = U`. * `T: Iterator` and don't require `where T: Iterator, ::Item: Debug`. * RTN for per-method bounds: `T: Trait C>` could supplement this feature in the future. + * Alternatively `where ::some_fn(..): ~const` or `where ::some_fn \ {const}`. * Very verbose (need to specify arguments and return type). * Want short hand sugar anyway to make it trivial to change a normal function to a const function by just adding some minor annotations. - * Significantly would delay const trait stabilization. + * Significantly would delay const trait stabilization (by years). * Usually requires editing the trait anyway, so there's no "can constify impls without trait author opt in" silver bullet. * New RTN-like per-method bounds: `T: Trait`. * Unclear if soundly possible. From 0c59930b39cdd505a1e898de06c7b881a43ccdcb Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Tue, 14 Jan 2025 09:39:13 +0000 Subject: [PATCH 03/23] s/maybe/conditionally/ --- text/0000-const-trait-impls.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 24ad1b61fd9..57b64a062ff 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -165,7 +165,7 @@ fn compile_time_default() -> T { } ``` -### Maybe-const trait bounds +### Conditionally-const trait bounds Many generic `const fn` and especially many const trait impls do not actually require a const trait impl for their generic parameters. As `const fn` can also be called at runtime, it would be too strict to require it to only be able to call things with const trait impls. @@ -209,8 +209,8 @@ const fn default() -> T { } ``` -`~const` is derived from "approximately", meaning "maybe" in this context, or specifically "const impl required if called in const context". -It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "maybe", but from the other direction: `?const` (not proposed here, see the alternatives section for why it was rejected) would mean "no const impl required, even if called in const context". +`~const` is derived from "approximately", meaning "conditionally" in this context, or specifically "const impl required if called in const context". +It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` (not proposed here, see the alternatives section for why it was rejected) would mean "no const impl required, even if called in const context". ### `~const Destruct` trait @@ -288,13 +288,13 @@ A much more detailed explanation can be found in https://hackmd.io/@compiler-err We generate a `ClauseKind::HostEffect` for every `const` or `~const` bound. To mirror how some effectful languages represent such effects, -I'm going to use `::k#host` to allow setting whether the `host` effect is "const" (disabled) or "maybe" (generic). +I'm going to use `::k#host` to allow setting whether the `host` effect is "const" (disabled) or "conditionally" (generic). This is not comparable with other associated bounds like type bounds or const bounds, as the values the associated host effect can take do neither have a usual hierarchy nor a concrete single value we can compare due to the following handling of those bounds: * There is no "always" (enabled), as that is just the lack of a host effect, meaning no `::k#host` bound at all. * In contrast to other effect systems, we do not track the effect as a true generic parameter in the type system, - but instead just ignore all `Maybe` bounds in host environments and treat them as `Const` in const environments. + but instead just ignore all `Conditionally` bounds in host environments and treat them as `Const` in const environments. While this could be modelled with generic parameters in the type system, that: @@ -340,7 +340,7 @@ desugars to const fn default() -> T where T: Default, - ::k#host = Maybe, + ::k#host = Conditionally, { T::default() } @@ -550,7 +550,7 @@ Note that it may frequently be that such a trait should have been split even wit # Alternatives [alternatives]: #alternatives -## use `const Trait` bounds for maybe-const, invent new syntax for always-const +## use `const Trait` bounds for conditionally-const, invent new syntax for always-const It may seem tempting to use `const fn foo` to mean what in this RFC is `~const Trait`, and then add new syntax for bounds that allow using trait methods in const blocks. From d50c8ef6a7623221120e510f0d06acaf7f3d4bb5 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Tue, 14 Jan 2025 09:46:01 +0000 Subject: [PATCH 04/23] Clarify some things that came up in reviews --- text/0000-const-trait-impls.md | 76 +++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 57b64a062ff..e854606529e 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -132,6 +132,8 @@ Impls can now rely on the default methods being const, too, and don't need to ov We may add an attribute later to allow you to mark individual trait methods as not-const so that when creating a const trait, one can add (defaulted or not) methods that cannot be used in const contexts. +It is possible to split up a trait into the const an non-const parts as discussed [here](#cant-have-const-methods-and-nonconst-methods-on-the-same-trait). + All default method bodies of const trait declarations are [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context). Note that on nightly the syntax is @@ -211,6 +213,44 @@ const fn default() -> T { `~const` is derived from "approximately", meaning "conditionally" in this context, or specifically "const impl required if called in const context". It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` (not proposed here, see the alternatives section for why it was rejected) would mean "no const impl required, even if called in const context". +See [this alternatives section](#make-all-const-fn-arguments-const-trait-by-default-and-require-an-opt-out-const-trait) for an explanation of why we do not use a `?const` scheme. + +### Const fn + +`const` fn have always been and will stay "always const" functions. + +It may appear that a function is suddenly "not a const fn" if it gets passed a type that doesn't satisfy +the constness of the corresponding trait bound. E.g. + +```rust +struct Foo; + +impl Clone for Foo { + fn clone(&self) -> Self { + Foo + } +} + +const fn bar(t: &T) -> T { t.clone() } +const BAR: Foo = bar(Foo); // ERROR: `Foo`'s `Clone` impl is not for `const Clone`. +``` + +But `bar` is still a `const` fn and you can call it from a const context, it will just fail some trait bounds. This is no different from + +```rust +const fn dup(a: T) -> (T, T) {(a, a)} +const FOO: (String, String) = dup(String::new()); +``` + +Here `dup` is always const fn, you'll just get a trait bound failure if the type you pass isn't `Copy`. + +This may seem like language lawyering, but that's how the impl works and how we should be talking about it. + +It's actually important for inference and method resolution in the nonconst world today. +You first figure out which method you're calling, then you check its bounds. +Otherwise it would at least seem like we'd have to allow some SFINAE or method overloading style things, +which we definitely do not support and have historically rejected over and over again. + ### `~const Destruct` trait @@ -237,6 +277,19 @@ The `Destruct` trait is a bound for whether a type has drop glue. This is trival `~const Destruct` trait bounds are satsifed only if the type has a `const Drop` impl or all of the types of its components are `~const Destruct`. +While this means that it's a breaking change to add a type with a non-const `Drop` impl to a type, +that's already true and nothing new: + +```rust +pub struct S { + x: u8, + y: Box<()>, // adding this field breaks code. +} + +const fn f(_: S) {} +//~^ ERROR destructor of `S` cannot be evaluated at compile-time +``` + ## Trivially enabled features You can use `==` operators on most types from libstd from within const contexts. @@ -554,6 +607,14 @@ Note that it may frequently be that such a trait should have been split even wit It may seem tempting to use `const fn foo` to mean what in this RFC is `~const Trait`, and then add new syntax for bounds that allow using trait methods in const blocks. +Examples of possible always const syntax: + +* `=const Trait` +* `const const Trait` (lol) +* `const(always) Trait` (`pub` like) +* `const Trait` (effect generic like) +* `const! Trait` + ## use `Trait` or `Trait` instead of `const Trait` To avoid new syntax before paths referring to traits, we could treat the constness as a generic parameter or an associated type. @@ -608,6 +669,8 @@ for when a trait bound is only used for its associated types and consts. This requires a new `~const fn` syntax (sigils or syntax bikesheddable), as the existing `const fn` already has trait bounds that do not require const trait impls even if used in const contexts. +An example from libstd today is [the impl block of Vec::new](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L406) which has an implicit `A: Allocator` bound from [the type definition](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L397). + A full example how how things would look then ```rust @@ -638,8 +701,17 @@ const fn foo() { compiles today, and allows all types that implement `Foo`, irrespective of the constness of the impl. With the opt-out scheme that would still compile, but suddenly require callers to provide a const impl. -The safe default (and the one folks are used to for a few years now), is that trait bounds just work, you just -can't call methods on them. To get more capabilities, you add more syntax. Thus the opt-out approach was not taken. +The safe default (and the one folks are used to for a few years now on stable), is that trait bounds just work, you just +can't call methods on them. +This is both useful in + +* nudging function authors to using the minimal necessary bounds to get their function +body to compile and thus requiring as little as possible from their callers, +* ensuring our implementation is correct by default. + +The implementation correctness argument is partially due to our history with `?const` (see https://github.com/rust-lang/rust/issues/83452 for where we got it wrong and thus decided to stop using opt-out), and partially with our history with `?` bounds not being great either (https://github.com/rust-lang/rust/issues/135229, https://github.com/rust-lang/rust/pull/132209). An opt-in is much easier to make sound and keep sound. + +To get more capabilities, you add more syntax. Thus the opt-out approach was not taken. ## Per-method constness instead of per-trait From ff7fabe123a3431c9dbc550a616763c08decf2c3 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Tue, 14 Jan 2025 10:41:26 +0000 Subject: [PATCH 05/23] const closures --- text/0000-const-trait-impls.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index e854606529e..885d084d667 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -836,3 +836,13 @@ Just like `const fn foo(x: impl ~const Trait) { x.method() }` and `const fn foo( There is nothing design-wise blocking function pointers and calling them, they mainly require implementation work and extending the compiler's internal type system representation of a function signature to include constness. + +## `const` closures + +Closures need explicit opt-in to be callable in const contexts. +You can already use closures in const contexts today to e.g. declare consts of function pointer type. +So what we additionally need is some syntax like `const || {}` to declare a closure that implements +`const Fn()`. See also [this tracking issue](https://github.com/rust-lang/project-const-traits/issues/10) +While it may seem tempting to just automatically implement `const Fn()` (or `~const Fn()`) where applicable, +it's not clear that this can be done, and there are definite situations where it can't be done. +As further experimentation is needed here, const closures are not part of this RFC. From 64c077313fe315db95f00fb944006f4b70223eca Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Tue, 14 Jan 2025 12:28:54 +0100 Subject: [PATCH 06/23] Clarify `Destruct` rules Co-authored-by: Tim Neumann --- text/0000-const-trait-impls.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 885d084d667..d62faf63853 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -274,8 +274,8 @@ This situation seems no different from other trait bounds, except that types can The `Destruct` trait is a bound for whether a type has drop glue. This is trivally true for all types. -`~const Destruct` trait bounds are satsifed only if the type has a `const Drop` impl or all of the types of its components -are `~const Destruct`. +`~const Destruct` trait bounds are satisfied only if the type's `Drop` impl (if any) is `const` and all of the types of +its components are `~const Destruct`. While this means that it's a breaking change to add a type with a non-const `Drop` impl to a type, that's already true and nothing new: From f7cc5e69e6be194acee22a1fe9520076d576968e Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Tue, 14 Jan 2025 16:41:04 +0000 Subject: [PATCH 07/23] Elaborate on some extensions --- text/0000-const-trait-impls.md | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index d62faf63853..4ea422efa80 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -752,6 +752,17 @@ const trait Foo { where you still need to annotate the trait, but also annotate the const methods. +But it makes it much harder/more confusing to add + +```rust +trait Tr { + const C: u8 = Self::f(); + const fn f() -> u8; +} +``` + +later, where even non-const traits can have const methods, that all impls must implement as a const fn. + # Prior art [prior-art]: #prior-art @@ -846,3 +857,46 @@ So what we additionally need is some syntax like `const || {}` to declare a clos While it may seem tempting to just automatically implement `const Fn()` (or `~const Fn()`) where applicable, it's not clear that this can be done, and there are definite situations where it can't be done. As further experimentation is needed here, const closures are not part of this RFC. + +## Allow impls to refine any trait's methods + +We could allow writing `const fn` in impls without the trait opting into it. +This would not affect `T: Trait` bounds, but still allow non-generic calls. + +This is simialar to other refinings in impls, as the function still satisfies everything from the trait. + +Example: without adjusting `rand` for const trait support at all, users could write + +```rust +struct CountingRng(u64); + +impl RngCore for CountingRng { + const fn next_u32(&mut self) -> u32 { + self.next_u64() as u32 + } + + const fn next_u64(&mut self) -> u64 { + self.0 += 1; + self.0 + } + + const fn fill_bytes(&mut self, dest: &mut [u8]) { + let mut left = dest; + while left.len() >= 8 { + let (l, r) = { left }.split_at_mut(8); + left = r; + let chunk: [u8; 8] = rng.next_u64().to_le_bytes(); + l.copy_from_slice(&chunk); + } + let n = left.len(); + let chunk: [u8; 8] = rng.next_u64().to_le_bytes(); + left.copy_from_slice(&chunk[..n]); + } + + const fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { + Ok(self.fill_bytes(dest)) + } +} +``` + +and use it in non-generic code. From fe97e3f1de9fe997d1abde109eba6ee7e9b1707d Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Sat, 18 Jan 2025 12:33:28 +0100 Subject: [PATCH 08/23] Update text/0000-const-trait-impls.md Co-authored-by: Jubilee --- text/0000-const-trait-impls.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 4ea422efa80..e257cdd98ef 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -106,7 +106,7 @@ So, we need some annotation that differentiates a `T: Default` bound from one th ### Const trait impls It is now allowed to prefix a trait name in an impl block with `const`, marking that this `impl`'s type is now allowed to -have methods of this `impl`'s trait to be called in const contexts (if all where bounds hold, like ususal, but more on this later). +have methods of this `impl`'s trait to be called in const contexts (if all where bounds hold, like usual, but more on this later). An example looks as follows: From 83f31f7cea00f804c6a5839a18ea8b213108522b Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 20 Jan 2025 09:00:04 +0000 Subject: [PATCH 09/23] Address reviews --- text/0000-const-trait-impls.md | 114 +++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 20 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index e257cdd98ef..624fbede152 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -151,12 +151,13 @@ Any item that can have trait bounds can also have `const Trait` bounds. Examples: -* `T: const Trait`, requiring any type that `T` is instantiated with to have a const trait impl. -* `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a const trait impl. - * These are not part of this RFC because they require `const fn` function pointers. See [the Future Possibilities section](#future-possibilities). +* `T: const Trait`, requiring any type that `T` is instantiated with to have a const trait impl for `Trait`. +* `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a const trait impl for `Trait`. + * These are not part of this RFC because they require `const` function pointers. See [the Future Possibilities section](#future-possibilities). * `impl const Trait` (in all positions). - * These are not part of this RFC because they require `const fn` function pointers. See [the Future Possibilities section](#future-possibilities). + * These are not part of this RFC because they require `const` function pointers. See [the Future Possibilities section](#future-possibilities). * `trait Foo: const Bar {}`, requiring every type that has an impl for `Foo` (even a non-const one), to also have a const trait impl for `Bar`. +* `trait Foo { type Bar: const Trait; }`, requiring all the impls to provide a type for `Bar` that has a const trait impl for `Trait` Such an impl allows you to use the type that is bound within a const block or any other const context, because we know that the type has a const trait impl and thus must be executable at compile time. The following function will invoke the `Default` impl of a type at compile time and store the result in a constant. Then it returns that constant instead of computing the value every time. @@ -212,8 +213,9 @@ const fn default() -> T { ``` `~const` is derived from "approximately", meaning "conditionally" in this context, or specifically "const impl required if called in const context". -It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` (not proposed here, see the alternatives section for why it was rejected) would mean "no const impl required, even if called in const context". -See [this alternatives section](#make-all-const-fn-arguments-const-trait-by-default-and-require-an-opt-out-const-trait) for an explanation of why we do not use a `?const` scheme. +It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` +(not proposed here, see [this alternatives section](#make-all-const-fn-arguments-const-trait-by-default-and-require-an-opt-out-const-trait) for why it was rejected) +would mean "no const impl required, even if called in const context". ### Const fn @@ -251,6 +253,33 @@ You first figure out which method you're calling, then you check its bounds. Otherwise it would at least seem like we'd have to allow some SFINAE or method overloading style things, which we definitely do not support and have historically rejected over and over again. +### conditionally const trait impls + +`const` trait impls for generic types work similarly to generic `const fn`. +Any `impl const Trait for Type` is allowed to have `~const` trait bounds. + +```rust +struct MyStruct(T); + +impl> const Add for MyStruct { + type Output = MyStruct; + fn add(self, other: MyStruct) -> MyStruct { + MyStruct(self.0 + other.0) + } +} + +impl const Add for &MyStruct +where + for<'a> &'a T: ~const Add, +{ + type Output = MyStruct; + fn add(self, other: &MyStruct) -> MyStruct { + MyStruct(&self.0 + &other.0) + } +} +``` + +See [this playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=313a38ef5c36b2ddf489f74167c1ac8a) for an example that works on nightly today. ### `~const Destruct` trait @@ -339,15 +368,21 @@ These `const` or `~const` trait bounds desugar to normal trait bounds without mo A much more detailed explanation can be found in https://hackmd.io/@compiler-errors/r12zoixg1l#What-now + +In contrast to other keywords like `unsafe` or `async` (that give you raw pointer derefs or `await` calls respectively), +the `const` keyword on functions or blocks restricts what you can do within those functions or blocks. +Thus the compiler historically used `host` as the internal inverse representation of `const` and `~const` bounds. + We generate a `ClauseKind::HostEffect` for every `const` or `~const` bound. To mirror how some effectful languages represent such effects, -I'm going to use `::k#host` to allow setting whether the `host` effect is "const" (disabled) or "conditionally" (generic). -This is not comparable with other associated bounds like type bounds or const bounds, as the values the associated host effect can -take do neither have a usual hierarchy nor a concrete single value we can compare due to the following handling of those bounds: +I'm going to use `::k#constness` to allow setting whether the `constness` effect is "const" (disabled) or "conditionally" (generic). +This is not comparable with other associated bounds like type bounds or const bounds, as the values the associated constness effect can +take do neither have a usual hierarchy of trait bounds or subtyping nor a concrete single value we can compare due to the following handling of those bounds: -* There is no "always" (enabled), as that is just the lack of a host effect, meaning no `::k#host` bound at all. +* There is no "disabled", as that is just the lack of a constness effect, meaning no `::k#constness` bound at all. * In contrast to other effect systems, we do not track the effect as a true generic parameter in the type system, - but instead just ignore all `Conditionally` bounds in host environments and treat them as `Const` in const environments. + but instead explicitly convert all requirements of `Conditionally` bounds in always-const environments to `Const`. + * in other words: calling a `const fn()` in a const item or const block requires proving that the type used for `T` is `const`, as `~const` can't refer to any conditionally const bound like it can within other const fns. While this could be modelled with generic parameters in the type system, that: @@ -373,7 +408,7 @@ desugars to fn compile_time_default() -> T where T: Default, - ::k#host = Const, + ::k#constness = Const, { const { T::default() } } @@ -393,7 +428,7 @@ desugars to const fn default() -> T where T: Default, - ::k#host = Conditionally, + ::k#constness = Conditionally, { T::default() } @@ -441,6 +476,10 @@ previous rule "adding a new method is not a breaking change if it has a default ### `~const Destruct` super trait +The `Destruct` marker trait is used to name the previously unnameable drop glue that every type has. +It has no methods, as drop glue is handled entirely by the compiler, +but in theory drop glue could become something one can explicitly call without having to resort to extracting the drop glue function pointer from a `dyn Trait`. + Traits that have `self` (by ownership) methods, will almost always drop the `self` in these methods' bodies unless they are simple wrappers that just forward to the generic parameters' bounds. The following never drops `T`, because it's the job of `` to handle dropping the values. @@ -449,7 +488,7 @@ The following never drops `T`, because it's the job of `` to handle dr struct NewType(T); impl> const Add for NewType { - type Output = Self, + type Output = Self; fn add(self, other: Self) -> Self::Output { NewType(self.0 + other.0) } @@ -600,6 +639,30 @@ the traits to be merged again. That's churn we'd like to avoid. Note that it may frequently be that such a trait should have been split even without constness being part of the picture. +Similarly one may want an always-const method on a trait of otherwise non-const methods: + +```rust +const trait InitFoo { + fn init(i: i32) -> Self; +} +trait Foo: const InitFoo { + const INIT: Self = Self::init(0); + fn do_stuff(&mut self); +} +``` + +or even only offer `INIT` if `InitFoo` is `const`: + +```rust +const trait InitFoo: Sized { + fn init(i: i32) -> Self; +} +trait Foo: InitFoo { + const INIT: Self = Self::init(0) where Self: const InitFoo; + fn do_stuff(&mut self); +} +``` + # Alternatives [alternatives]: #alternatives @@ -809,14 +872,23 @@ We do not need to immediately allow using methods on generic parameters of const The following example could be made to work with just const traits and const trait impls. ```rust +struct MyStruct(i32); + +impl const PartialEq for MyStruct { + fn eq(&self, other: &MyStruct) -> bool { + self.0 == other.0 + } +} + const fn foo() { - let a = [1, 2, 3]; - let b = [1, 2, 4]; + let a = MyStruct(1); + let b = MyStruct(2); if a == b {} } ``` -Things like `Option::map` could not be made const without const trait bounds, as they need to actually call the generic `FnOnce` argument. +Things like `Option::map` or `PartialEq` for arrays/tuples could not be made const without const trait bounds, +as they need to actually call the generic `FnOnce` argument or nested `PartialEq` impls. # Future possibilities @@ -843,10 +915,12 @@ just call all of these `const` and only separate the `~const Trait` bounds from ## `const fn()` pointers Just like `const fn foo(x: impl ~const Trait) { x.method() }` and `const fn foo(x: &dyn ~const Trait) { x.method() }` we want to allow -`const fn foo(f: const fn()) { f() }`. +`const fn foo(f: ~const fn()) { f() }`. -There is nothing design-wise blocking function pointers and calling them, they mainly require implementation work and extending the -compiler's internal type system representation of a function signature to include constness. +These require changing the type system, making the constness of a function pointer part of the type. +This in turn implies that a `const fn()` function pointer, a `~const fn()` function pointer and a `fn()` function pointer could have +different `TypeId`s, which is something that requires more design and consideration to clarify whether supporting downcasting with `Any` +or just supporting `TypeId` equality checks detecting constness is desirable. ## `const` closures From 8d9f649e81c9f7988af3496e921ef45c148792ba Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 20 Jan 2025 09:57:02 +0000 Subject: [PATCH 10/23] Elaborate where `~const Trait` bounds can be used --- text/0000-const-trait-impls.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 624fbede152..5c26ea1ccda 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -155,7 +155,7 @@ Examples: * `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a const trait impl for `Trait`. * These are not part of this RFC because they require `const` function pointers. See [the Future Possibilities section](#future-possibilities). * `impl const Trait` (in all positions). - * These are not part of this RFC because they require `const` function pointers. See [the Future Possibilities section](#future-possibilities). + * These are not part of this RFC. * `trait Foo: const Bar {}`, requiring every type that has an impl for `Foo` (even a non-const one), to also have a const trait impl for `Bar`. * `trait Foo { type Bar: const Trait; }`, requiring all the impls to provide a type for `Bar` that has a const trait impl for `Trait` @@ -394,6 +394,17 @@ While this could be modelled with generic parameters in the type system, that: Thus that approach was abandoned after proponents and opponents cooperated in trying to make the generic parameter approach work, resulting in all proponents becoming opponents, too. +### Sites where `const Trait` bounds can be used + +Everywhere where non-const trait bounds can be written, but only for traits that are declared `const Trait`. + +### Sites where `~const Trait` bounds can be used + +* `const fn` +* `impl const Trait for Type` +* NOT in inherent impls, the individual `const fn` need to be annotated instead +* associated types bounds of `const trait Trait` declarations + ### `const` desugaring ```rust @@ -729,7 +740,7 @@ const fn foo + OtherTrait() -> T { // cannot call `Baz` methods ::bar() @@ -922,6 +934,10 @@ This in turn implies that a `const fn()` function pointer, a `~const fn()` funct different `TypeId`s, which is something that requires more design and consideration to clarify whether supporting downcasting with `Any` or just supporting `TypeId` equality checks detecting constness is desirable. +Furthermore `const fn()` pointers introduce a new situation: you can transmute arbitrary values (e.g. null pointers, or just integers) to +`const fn()` pointers, and the type system will not protect you. Instead the const evaluator will reject that when it actually +evaluateds the code around the function pointer or even as late as when the function call happens. + ## `const` closures Closures need explicit opt-in to be callable in const contexts. From 328655af807872966e76a6a77b36888c084f1b56 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 20 Jan 2025 10:17:44 +0000 Subject: [PATCH 11/23] Add comparison for `?const` example --- text/0000-const-trait-impls.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 5c26ea1ccda..3d8a166cfd6 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -745,7 +745,21 @@ do not require const trait impls even if used in const contexts. An example from libstd today is [the impl block of Vec::new](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L406) which has an implicit `A: Allocator` bound from [the type definition](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L397). -A full example how how things would look then +A full example how how + +```rust +const trait Foo: ~const Bar + Baz {} + +impl const Foo for () {} + +const fn foo() -> T { + // cannot call `Baz` methods + ::bar() +} + +const _: () = foo(); +``` + ```rust const trait Foo: Bar + ?const Baz {} From fb621b694c4df0563a47cb8bc5601a68da730f3c Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Thu, 6 Feb 2025 11:05:50 +0000 Subject: [PATCH 12/23] `dyn ~const Trait` as an argument for implicit `~const Destruct` --- text/0000-const-trait-impls.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 3d8a166cfd6..ec351a2f735 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -545,6 +545,10 @@ In practice we have noticed that a large portion of APIs will have a `~const Des This bound has little value as an explicit bound that appears almost everywhere. Especially since it is a fairly straight forward assumption that a type that has const trait impls will also have a `const Drop` impl or only contain `const Destruct` types. +In the future we will also want to support `dyn ~const Trait` bounds, which invariably will require the type to implement `~const Destruct` in order to fill in the function pointer for the `drop` slot in the vtable. +While that can in generic contexts always be handled by adding more `~const Destruct` bounds, it would be more similar to how normal `dyn` safety +works if there were implicit `~const Destruct` bounds for (most?) `~const Trait` bounds. + Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere. We may offer an opt out of this behaviour in the future, if there are convincing real world use cases. From 73e06e46b7bb0c30e0bf810711ebbcda10e6c93c Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Thu, 6 Feb 2025 11:39:01 +0000 Subject: [PATCH 13/23] Const fn traits and not doing this RFC --- text/0000-const-trait-impls.md | 91 ++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index ec351a2f735..9e3030ba129 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -341,6 +341,41 @@ will likely be tracked under a separate feature gate under the purview of T-libs Similarly other traits will be made `const` over time, but doing so will be unblocked by this feature. +### `const Fn*` traits + +All `const fn` implement the corresponding `const Fn()` trait: + +```rust +const fn foo(f: F) { + f() +} + +const fn bar() { + foo(baz) +} + +const fn baz() {} +``` + +Arguments and the return type of such functions and bounds follow the same rules as +their non-const equivalents, so you may have to add `~const` bounds to other generic +parameters, too: + + +```rust +const fn foo(f: F, arg: T) { + f(arg) +} + +const fn bar(arg: T) { + foo(baz, arg) +} + +const fn baz() {} +``` + +For closures and them implementing the `Fn` traits, see the [Future possibilities](#future-possibilities) section. + ## Crate authors: Making your own custom types easier to use You can write const trait impls of many standard library traits for your own types. @@ -681,6 +716,62 @@ trait Foo: InitFoo { # Alternatives [alternatives]: #alternatives +## What is the impact of not doing this? + +We would require everything that wants a const-equivalent to have duplicated traits and not +use `const` fn at all, but use associated consts instead. Similarly this would likely forbid +invoking builtin operators. This same concern had been brought up for the `const fn` stabilization +[7 years ago](https://github.com/rust-lang/rust/issues/24111#issuecomment-385046163). + +Basically what we can do is create + +```rust +trait ConstDefault { + const DEFAULT: Self; +} +``` + +and require users to use + +```rust +const FOO: Vec = ConstDefault::DEFAULT; +``` + +instead of + +```rust +const fn FOO: Vec = Default::default(); +``` + +This duplication is what this RFC is suggesting to avoid. + +Since it has already been possible to do all of this on stable Rust for years, and no major +crates have popped and gotten used widely, I assume that is either because + +* it's too much duplication, or +* everyone was waiting for the work (that this RFC wants to stabilize) to finish, or +* both. + +So while it is entirely possible that rejecting this RFC and deciding not to go down this route +will lead to an ecosystem for const operations to be created, it would result in duplication and +inconsistencies that we'd rather like to avoid. + +Such an ecosystem would also make `const fn` obsolete, as every `const fn` can in theory be represented +as a trait, it would just be very different to use from normal rust code and not really allow nice abstractions to be built. + +```rust +const fn add(a: u32, b: u32) -> u32 { a + b } + +struct Add; + +impl Add { + const RESULT: u32 = A + B; +} + +const FOO: u32 = add(5, 6); +const BAR: u32 = Add<5, 6>::RESULT; +``` + ## use `const Trait` bounds for conditionally-const, invent new syntax for always-const It may seem tempting to use `const fn foo` to mean what in this RFC is `~const Trait`, and then add new syntax for bounds that allow using trait methods in const blocks. From dc1cb644c5f22f5feaaebd67fc13c4efa50680ed Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Fri, 7 Mar 2025 10:19:16 +0000 Subject: [PATCH 14/23] (const) bounds --- text/0000-const-trait-impls.md | 150 ++++++++++++++++----------------- 1 file changed, 72 insertions(+), 78 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 9e3030ba129..6c3b58ede64 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -23,7 +23,7 @@ impl const Default for () { fn default() {} } -const fn default() -> T { +const fn default() -> T { T::default() } @@ -202,17 +202,17 @@ fn main() { ``` What we instead want is that, just like `const fn` can be called at runtime and compile time, we want their trait bounds' constness -to mirror that behaviour. So we're introducing `~const Trait` bounds, which mean "const if called from const context" (slight oversimplifcation, but read on). +to mirror that behaviour. So we're introducing `(const) Trait` bounds, which mean "const if called from const context" (slight oversimplifcation, but read on). -The only thing we need to change in our above example is the `default` function, changing the `const Default` bound to a `~const Default` one. +The only thing we need to change in our above example is the `default` function, changing the `const Default` bound to a `(const) Default` one. ```rust -const fn default() -> T { +const fn default() -> T { T::default() } ``` -`~const` is derived from "approximately", meaning "conditionally" in this context, or specifically "const impl required if called in const context". +`(const)` is derived from "approximately", meaning "conditionally" in this context, or specifically "const impl required if called in const context". It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` (not proposed here, see [this alternatives section](#make-all-const-fn-arguments-const-trait-by-default-and-require-an-opt-out-const-trait) for why it was rejected) would mean "no const impl required, even if called in const context". @@ -233,7 +233,7 @@ impl Clone for Foo { } } -const fn bar(t: &T) -> T { t.clone() } +const fn bar(t: &T) -> T { t.clone() } const BAR: Foo = bar(Foo); // ERROR: `Foo`'s `Clone` impl is not for `const Clone`. ``` @@ -256,12 +256,12 @@ which we definitely do not support and have historically rejected over and over ### conditionally const trait impls `const` trait impls for generic types work similarly to generic `const fn`. -Any `impl const Trait for Type` is allowed to have `~const` trait bounds. +Any `impl const Trait for Type` is allowed to have `(const)` trait bounds. ```rust struct MyStruct(T); -impl> const Add for MyStruct { +impl> const Add for MyStruct { type Output = MyStruct; fn add(self, other: MyStruct) -> MyStruct { MyStruct(self.0 + other.0) @@ -270,7 +270,7 @@ impl> const Add for MyStruct { impl const Add for &MyStruct where - for<'a> &'a T: ~const Add, + for<'a> &'a T: (const) Add, { type Output = MyStruct; fn add(self, other: &MyStruct) -> MyStruct { @@ -281,7 +281,7 @@ where See [this playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=313a38ef5c36b2ddf489f74167c1ac8a) for an example that works on nightly today. -### `~const Destruct` trait +### `(const) Destruct` trait The `Destruct` trait enables dropping types within a const context. @@ -292,7 +292,7 @@ const fn foo(t: T) { const fn baz(t: T) { // Fine, `Copy` implies that no `Drop` impl exists } -const fn bar(t: T) { +const fn bar(t: T) { // Fine, we can safely invoke the destructor of `T`. } ``` @@ -303,8 +303,8 @@ This situation seems no different from other trait bounds, except that types can The `Destruct` trait is a bound for whether a type has drop glue. This is trivally true for all types. -`~const Destruct` trait bounds are satisfied only if the type's `Drop` impl (if any) is `const` and all of the types of -its components are `~const Destruct`. +`(const) Destruct` trait bounds are satisfied only if the type's `Drop` impl (if any) is `const` and all of the types of +its components are `(const) Destruct`. While this means that it's a breaking change to add a type with a non-const `Drop` impl to a type, that's already true and nothing new: @@ -346,7 +346,7 @@ unblocked by this feature. All `const fn` implement the corresponding `const Fn()` trait: ```rust -const fn foo(f: F) { +const fn foo(f: F) { f() } @@ -358,20 +358,20 @@ const fn baz() {} ``` Arguments and the return type of such functions and bounds follow the same rules as -their non-const equivalents, so you may have to add `~const` bounds to other generic +their non-const equivalents, so you may have to add `(const)` bounds to other generic parameters, too: ```rust -const fn foo(f: F, arg: T) { +const fn foo(f: F, arg: T) { f(arg) } -const fn bar(arg: T) { +const fn bar(arg: T) { foo(baz, arg) } -const fn baz() {} +const fn baz() {} ``` For closures and them implementing the `Fn` traits, see the [Future possibilities](#future-possibilities) section. @@ -383,7 +383,7 @@ While it was often possible to write the same code in inherent methods, operator covered by traits from `std::ops` and thus not avaiable for const contexts. Most of the time it suffices to add `const` before the trait name in the impl block. The compiler will guide you and suggest where to also -add `~const` bounds for trait bounds on generic parameters of methods or the impl. +add `(const)` bounds for trait bounds on generic parameters of methods or the impl. Similarly you can make your traits available for users of your crate to implement constly. Note that this will change your semver guarantees: you are now guaranteeing that any future @@ -391,7 +391,7 @@ methods you add don't just have a default body, but a `const` default body. The enforce this, so you can't accidentally make a mistake, but it may still limit how you can extend your trait without having to do a major version bump. Most of the time it suffices to add `const` before the `trait` declaration. The compiler will -guide you and suggest where to also add `~const` bounds for super trait bounds or trait bounds +guide you and suggest where to also add `(const)` bounds for super trait bounds or trait bounds on generic parameters of your trait or your methods. # Reference-level explanation @@ -399,16 +399,16 @@ on generic parameters of your trait or your methods. ## How does this work in the compiler? -These `const` or `~const` trait bounds desugar to normal trait bounds without modifiers, plus an additional constness bound that has no surface level syntax. +These `const` or `(const)` trait bounds desugar to normal trait bounds without modifiers, plus an additional constness bound that has no surface level syntax. A much more detailed explanation can be found in https://hackmd.io/@compiler-errors/r12zoixg1l#What-now In contrast to other keywords like `unsafe` or `async` (that give you raw pointer derefs or `await` calls respectively), the `const` keyword on functions or blocks restricts what you can do within those functions or blocks. -Thus the compiler historically used `host` as the internal inverse representation of `const` and `~const` bounds. +Thus the compiler historically used `host` as the internal inverse representation of `const` and `(const)` bounds. -We generate a `ClauseKind::HostEffect` for every `const` or `~const` bound. +We generate a `ClauseKind::HostEffect` for every `const` or `(const)` bound. To mirror how some effectful languages represent such effects, I'm going to use `::k#constness` to allow setting whether the `constness` effect is "const" (disabled) or "conditionally" (generic). This is not comparable with other associated bounds like type bounds or const bounds, as the values the associated constness effect can @@ -417,7 +417,7 @@ take do neither have a usual hierarchy of trait bounds or subtyping nor a concre * There is no "disabled", as that is just the lack of a constness effect, meaning no `::k#constness` bound at all. * In contrast to other effect systems, we do not track the effect as a true generic parameter in the type system, but instead explicitly convert all requirements of `Conditionally` bounds in always-const environments to `Const`. - * in other words: calling a `const fn()` in a const item or const block requires proving that the type used for `T` is `const`, as `~const` can't refer to any conditionally const bound like it can within other const fns. + * in other words: calling a `const fn()` in a const item or const block requires proving that the type used for `T` is `const`, as `(const)` can't refer to any conditionally const bound like it can within other const fns. While this could be modelled with generic parameters in the type system, that: @@ -433,7 +433,7 @@ Thus that approach was abandoned after proponents and opponents cooperated in tr Everywhere where non-const trait bounds can be written, but only for traits that are declared `const Trait`. -### Sites where `~const Trait` bounds can be used +### Sites where `(const) Trait` bounds can be used * `const fn` * `impl const Trait for Type` @@ -460,10 +460,10 @@ where } ``` -### `~const` desugaring +### `(const)` desugaring ```rust -const fn default() -> T { +const fn default() -> T { T::default() } ``` @@ -487,8 +487,8 @@ where const fn checked_default() -> T where T: const Default, - T: ~const Default, - T: ~const PartialEq, + T: (const) Default, + T: (const) PartialEq, { let a = const { T::default() }; let b = T::default(); @@ -500,15 +500,9 @@ where } ``` -Has a redundant bound. `T: const Default` implies `T: ~const Default`, so while the desugaring will include both (but may filter them out if we deem it useful on the impl side), +Has a redundant bound. `T: const Default` implies `T: (const) Default`, so while the desugaring will include both (but may filter them out if we deem it useful on the impl side), there is absolutely no difference (just like specifying `Fn() + FnOnce()` has a redundant `FnOnce()` bound). -## Precedence of `~` - -The `~` sigil applies to the `const`, not the `const Trait`, so you can think of it as `(~const) Trait`, not `~(const Trait)`. -This is both handled this way by the parser, and semantically what is meant here. The constness of the trait bound is affected, -the trait bound itself exists either way. - ## Why do traits need to be marked as "const implementable"? ### Default method bodies @@ -520,7 +514,7 @@ This scheme avoids adding a new kind of breaking change to the Rust language, and instead allows everyone managing a public trait in their crate to continue relying on the previous rule "adding a new method is not a breaking change if it has a default body". -### `~const Destruct` super trait +### `(const) Destruct` super trait The `Destruct` marker trait is used to name the previously unnameable drop glue that every type has. It has no methods, as drop glue is handled entirely by the compiler, @@ -533,7 +527,7 @@ The following never drops `T`, because it's the job of `` to handle dr ```rust struct NewType(T); -impl> const Add for NewType { +impl> const Add for NewType { type Output = Self; fn add(self, other: Self) -> Self::Output { NewType(self.0 + other.0) @@ -548,7 +542,7 @@ struct NewType(T, bool); struct Error; -impl> const Add for NewType { +impl> const Add for NewType { type Output = Result; fn add(self, other: Self) -> Self::Output { if self.1 { @@ -561,13 +555,13 @@ impl> const Add for NewType { } ``` -... then we need to add a `~const Destruct` bound to `T`, to ensure +... then we need to add a `(const) Destruct` bound to `T`, to ensure `NewType` can be dropped. This bound in turn will be infectious to all generic users of `NewType` like ```rust -const fn add( +const fn add( a: NewType, b: NewType, ) -> Result, Error> { @@ -575,21 +569,21 @@ const fn add( } ``` -which now need a `T: ~const Destruct` bound, too. -In practice we have noticed that a large portion of APIs will have a `~const Destruct` bound. +which now need a `T: (const) Destruct` bound, too. +In practice we have noticed that a large portion of APIs will have a `(const) Destruct` bound. This bound has little value as an explicit bound that appears almost everywhere. Especially since it is a fairly straight forward assumption that a type that has const trait impls will also have a `const Drop` impl or only contain `const Destruct` types. -In the future we will also want to support `dyn ~const Trait` bounds, which invariably will require the type to implement `~const Destruct` in order to fill in the function pointer for the `drop` slot in the vtable. -While that can in generic contexts always be handled by adding more `~const Destruct` bounds, it would be more similar to how normal `dyn` safety -works if there were implicit `~const Destruct` bounds for (most?) `~const Trait` bounds. +In the future we will also want to support `dyn (const) Trait` bounds, which invariably will require the type to implement `(const) Destruct` in order to fill in the function pointer for the `drop` slot in the vtable. +While that can in generic contexts always be handled by adding more `(const) Destruct` bounds, it would be more similar to how normal `dyn` safety +works if there were implicit `(const) Destruct` bounds for (most?) `(const) Trait` bounds. -Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere. +Thus we give all `const trait`s a `(const) Destruct` super trait to ensure users don't need to add `(const) Destruct` bounds everywhere. We may offer an opt out of this behaviour in the future, if there are convincing real world use cases. -### `~const` bounds on `Drop` impls +### `(const)` bounds on `Drop` impls -It is legal to add `~const` to `Drop` impls' bounds, even thought the struct doesn't have them: +It is legal to add `(const)` to `Drop` impls' bounds, even thought the struct doesn't have them: ```rust const trait Bar { @@ -598,26 +592,26 @@ const trait Bar { struct Foo(T); -impl const Drop for Foo { +impl const Drop for Foo { fn drop(&mut self) { self.0.thing(); } } ``` -There is no reason (and no coherent representation) of adding `~const` trait bounds to a type. +There is no reason (and no coherent representation) of adding `(const)` trait bounds to a type. Our usual `Drop` rules enforce that an impl must have the same bounds as the type. -`~const` modifiers are special here, because they are only needed in const contexts. +`(const)` modifiers are special here, because they are only needed in const contexts. While they cause exactly the divergence that we want to prevent with the `Drop` impl rules: a type can be declared, but not dropped, because bounds are unfulfilled, this is: * Already the case in const contexts, just for all types that aren't trivially free of `Drop` types. * Exactly the behaviour we want. -Extraneous `~const Trait` bounds where `Trait` isn't a bound on the type at all are still rejected: +Extraneous `(const) Trait` bounds where `Trait` isn't a bound on the type at all are still rejected: ```rust -impl const Drop for Foo { +impl const Drop for Foo { fn drop(&mut self) { self.0.thing(); } @@ -630,7 +624,7 @@ errors with error[E0367]: `Drop` impl requires `T: Baz` but the struct it is implemented for does not --> src/lib.rs:13:22 | -13 | impl const Drop for Foo { +13 | impl const Drop for Foo { | ^^^^^^^^^^ | note: the implementor must specify the same requirement @@ -655,7 +649,7 @@ presence of many effects, we'll want the syntax from this RFC as sugar for the v ## It's hard to make constness optional with `#[cfg]` One cannot `#[cfg]` just the `const` keyword in `const Trait`, and even if we made it possible by sticking with `#[const_trait]` attributes, and also adding the equivalent for impls and functions, -`~const Trait` bounds cannot be made conditional with `#[cfg]`. The only real useful reason to have this is to support newer Rust versions with a cfg, and allow older Rust versions to compile +`(const) Trait` bounds cannot be made conditional with `#[cfg]`. The only real useful reason to have this is to support newer Rust versions with a cfg, and allow older Rust versions to compile the traits, just without const support. This is surmountable with proc macros that either generate two versions or just generate a different version depending on the Rust version. Since it's only necessary for a transition period while a crate wants to support both pre-const-trait Rust and newer Rust versions, this doesn't seem too bad. With a MSRV bump the proc macro usage can be removed again. @@ -774,7 +768,7 @@ const BAR: u32 = Add<5, 6>::RESULT; ## use `const Trait` bounds for conditionally-const, invent new syntax for always-const -It may seem tempting to use `const fn foo` to mean what in this RFC is `~const Trait`, and then add new syntax for bounds that allow using trait methods in const blocks. +It may seem tempting to use `const fn foo` to mean what in this RFC is `(const) Trait`, and then add new syntax for bounds that allow using trait methods in const blocks. Examples of possible always const syntax: @@ -788,11 +782,11 @@ Examples of possible always const syntax: To avoid new syntax before paths referring to traits, we could treat the constness as a generic parameter or an associated type. While an associated type is very close to how the implementation works, neither `effect = const` nor `effect: const` are representing the logic correctly, -as `const` implies `~const`, but `~const` is nothing concrete, it's more like a generic parameter referring to the constness of the function. +as `const` implies `(const)`, but `(const)` is nothing concrete, it's more like a generic parameter referring to the constness of the function. Fully expanded one can think of ```rust -const fn foo(t: T) { ... } +const fn foo(t: T) { ... } ``` to be like @@ -808,7 +802,7 @@ where } ``` -Note that `const` implies `const` and thus also `for const`, just like `const Trait` implies `~const Trait`. +Note that `const` implies `const` and thus also `for const`, just like `const Trait` implies `(const) Trait`. We do not know of any cases where such an explicit syntax would be useful (only makes sense if you can do math on the bool), so a more reduced version could be @@ -817,7 +811,7 @@ so a more reduced version could be const fn foo(t: T) where T: Trait + OtherTrait, - ::bikeshed#effect = ~const, + ::bikeshed#effect = (const), ::bikeshed#effect = const, { ... @@ -827,10 +821,10 @@ where or ```rust -const fn foo + OtherTrait>(t: T) { ... } +const fn foo + OtherTrait>(t: T) { ... } ``` -## Make all `const fn` arguments `~const Trait` by default and require an opt out `?const Trait` +## Make all `const fn` arguments `(const) Trait` by default and require an opt out `?const Trait` We could default to making all `T: Trait` bounds be const if the function is called from a const context, and require a `T: ?const Trait` opt out for when a trait bound is only used for its associated types and consts. @@ -843,11 +837,11 @@ An example from libstd today is [the impl block of Vec::new](https://github.com/ A full example how how ```rust -const trait Foo: ~const Bar + Baz {} +const trait Foo: (const) Bar + Baz {} impl const Foo for () {} -const fn foo() -> T { +const fn foo() -> T { // cannot call `Baz` methods ::bar() } @@ -913,11 +907,11 @@ This has two major advantages: * you can add new methods with default bodies and don't have to worry about new kinds of breaking changes The specific syntax given here may be confusing though, as it looks like the function is always const, but -implementations can use non-const impls and thus make the impl not usable for `T: ~const Trait` bounds. +implementations can use non-const impls and thus make the impl not usable for `T: (const) Trait` bounds. Though this means that changing a non-const fn in the trait to a const fn is a breaking change, as the user may have that previous-non-const fn as a non-const fn in the impl, causing the entire impl now to not be usable for -`T: ~const Trait` anymore. +`T: (const) Trait` anymore. See also: out of scope RTN notation in [Unresolved questions](#unresolved-questions) @@ -974,17 +968,17 @@ later, where even non-const traits can have const methods, that all impls must i * If we get an effect system, we will still want this shorthand, just like we allow you to write: * `T: Iterator` and don't require `where T: Iterator, ::Item = U`. * `T: Iterator` and don't require `where T: Iterator, ::Item: Debug`. - * RTN for per-method bounds: `T: Trait C>` could supplement this feature in the future. - * Alternatively `where ::some_fn(..): ~const` or `where ::some_fn \ {const}`. + * RTN for per-method bounds: `T: Trait C>` could supplement this feature in the future. + * Alternatively `where ::some_fn(..): (const)` or `where ::some_fn \ {const}`. * Very verbose (need to specify arguments and return type). * Want short hand sugar anyway to make it trivial to change a normal function to a const function by just adding some minor annotations. * Significantly would delay const trait stabilization (by years). * Usually requires editing the trait anyway, so there's no "can constify impls without trait author opt in" silver bullet. - * New RTN-like per-method bounds: `T: Trait`. + * New RTN-like per-method bounds: `T: Trait`. * Unclear if soundly possible. * Unclear if possible without incurring significant performance issues for all code (may need tracking new information for all functions out there). * Still requires editing traits. - * Still want the `~const Trait` sugar anyway. + * Still want the `(const) Trait` sugar anyway. ## Should we start out by allowing only const trait declarations and const trait impls @@ -1015,7 +1009,7 @@ as they need to actually call the generic `FnOnce` argument or nested `PartialEq # Future possibilities [future-possibilities]: #future-possibilities -## Migrate to `~const fn` +## Migrate to `(const) fn` `const fn` and `const` items have slightly different meanings for `const`: @@ -1027,19 +1021,19 @@ Additionally `const Trait` bounds have a third meaning (the same as `const Trait They can be invoked at compile time, but also in `const fn`. While all these meanings are subtly different, making their differences more obvious will not make them easier to understand. -All that changing to `~const fn` would achieve is that folk will add the sigil when told by the compiler, and complain about +All that changing to `(const) fn` would achieve is that folk will add the sigil when told by the compiler, and complain about having to type a sigil, when there is no meaning for `const fn` without a sigil. While I see the allure from a language nerd perspective to give every meaning its own syntax, I believe it is much more practical to -just call all of these `const` and only separate the `~const Trait` bounds from `const Trait` bounds. +just call all of these `const` and only separate the `(const) Trait` bounds from `const Trait` bounds. ## `const fn()` pointers -Just like `const fn foo(x: impl ~const Trait) { x.method() }` and `const fn foo(x: &dyn ~const Trait) { x.method() }` we want to allow -`const fn foo(f: ~const fn()) { f() }`. +Just like `const fn foo(x: impl (const) Trait) { x.method() }` and `const fn foo(x: &dyn (const) Trait) { x.method() }` we want to allow +`const fn foo(f: (const) fn()) { f() }`. These require changing the type system, making the constness of a function pointer part of the type. -This in turn implies that a `const fn()` function pointer, a `~const fn()` function pointer and a `fn()` function pointer could have +This in turn implies that a `const fn()` function pointer, a `(const) fn()` function pointer and a `fn()` function pointer could have different `TypeId`s, which is something that requires more design and consideration to clarify whether supporting downcasting with `Any` or just supporting `TypeId` equality checks detecting constness is desirable. @@ -1053,7 +1047,7 @@ Closures need explicit opt-in to be callable in const contexts. You can already use closures in const contexts today to e.g. declare consts of function pointer type. So what we additionally need is some syntax like `const || {}` to declare a closure that implements `const Fn()`. See also [this tracking issue](https://github.com/rust-lang/project-const-traits/issues/10) -While it may seem tempting to just automatically implement `const Fn()` (or `~const Fn()`) where applicable, +While it may seem tempting to just automatically implement `const Fn()` (or `(const) Fn()`) where applicable, it's not clear that this can be done, and there are definite situations where it can't be done. As further experimentation is needed here, const closures are not part of this RFC. From 7da95b8e63cd30d949c682f46a0384048aab78a5 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Fri, 7 Mar 2025 10:24:24 +0000 Subject: [PATCH 15/23] require `(const) fn` on all methods --- text/0000-const-trait-impls.md | 356 ++++++++++++++------------------- 1 file changed, 154 insertions(+), 202 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 6c3b58ede64..c8fdee19971 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -15,12 +15,18 @@ Make trait methods callable in const contexts. This includes the following parts Fully contained example ([Playground of currently working example](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=2ab8d572c63bcf116b93c632705ddc1b)): ```rust -const trait Default { - fn default() -> Self; +trait Default { + (const) fn default() -> Self; +} + +impl Default for () { + const fn default() {} } -impl const Default for () { - fn default() {} +struct Thing(T); + +impl Default for Thing { + (const) fn default() -> Self { Self(T::default()) } } const fn default() -> T { @@ -103,84 +109,117 @@ So, we need some annotation that differentiates a `T: Default` bound from one th ## Nomenclature and new syntax concepts -### Const trait impls +### Const trait methods -It is now allowed to prefix a trait name in an impl block with `const`, marking that this `impl`'s type is now allowed to -have methods of this `impl`'s trait to be called in const contexts (if all where bounds hold, like usual, but more on this later). +Traits can declare methods as `const`. Doing so is a breaking change, as all impls are now required to provide a `const` method, +which existing impls can't. + +```rust +trait Trait { + const fn method(); +} +``` -An example looks as follows: +These methods need to be implemented as `const`: ```rust -impl const Trait for Type {} +impl Trait for Type { + const fn method() {} +} ``` -Such impls require that the trait is a `const trait`. +### Const trait bounds -All method bodies in a const trait impl are [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context). +Any item that can have trait bounds can also have `const Trait` bounds. -### Const traits +Examples: -Traits need to opt-in to being allowed to have const trait impls. Thus you need to declare your traits by prefixing the `trait` keyword with `const`: +* `T: const Trait`, requiring any type that `T` is instantiated with to have a trait impl with `const` methods for `Trait`. +* `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a trait impl with `const` methods for `Trait`. + * These are not part of this RFC because they require `const` function pointers. See [the Future Possibilities section](#future-possibilities). +* `impl const Trait` (in all positions). + * These are not part of this RFC. +* `trait Foo: const Bar {}`, requiring every type that has an impl for `Foo` (even a non-const one), to also have a trait impl with `const` methods for `Bar`. +* `trait Foo { type Bar: const Trait; }`, requiring all the impls to provide a type for `Bar` that has a trait impl with `const` methods for `Trait` + +Such an impl allows you to use the type that is bound within a const block or any other const context, because we know that the type has a trait impl with `const` methods and thus +must be executable at compile time. The following function will invoke the `Default` impl of a type at compile time and store the result in a constant. Then it returns that constant instead of computing the value every time. ```rust -const trait Trait {} +fn compile_time_default() -> T { + const { T::default() } +} ``` -This in turn checks all methods' default bodies as if they were `const fn`, making them callable in const contexts. -Impls can now rely on the default methods being const, too, and don't need to override them with a const body. +### Conditionally const traits methods -We may add an attribute later to allow you to mark individual trait methods as not-const so that when creating a const trait, one can -add (defaulted or not) methods that cannot be used in const contexts. +Traits need to opt-in to allowing their impls to have const methods. Thus you need to prefix the methods you want to be const callable with `(const)`: -It is possible to split up a trait into the const an non-const parts as discussed [here](#cant-have-const-methods-and-nonconst-methods-on-the-same-trait). +```rust +trait Trait { + (const) fn thing(); +} +``` -All default method bodies of const trait declarations are [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context). +A `(const)` method's (optional) default body must satisfy everything a `const fn` body must, making them callable in const contexts. +Impls can now rely on the default methods being const, too, and don't need to override them with a const body. Note that on nightly the syntax is ```rust #[const_trait] -trait Trait {} +trait Trait { + fn thing(); +} ``` -and a result of this RFC would be that we would remove the attribute and add the `const trait` syntax. +and a result of this RFC would be that we would remove the attribute and add the `(const) fn` syntax for *methods*. +Free functions are unaffected and will stay as `const fn`. -### Const trait bounds +### Impls for conditionally const methods -Any item that can have trait bounds can also have `const Trait` bounds. +Methods that are declared as `(const)` on a trait can now be made `const` in an impl: -Examples: +```rust +impl Trait for Type { + const fn thing() {} +} +``` -* `T: const Trait`, requiring any type that `T` is instantiated with to have a const trait impl for `Trait`. -* `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a const trait impl for `Trait`. - * These are not part of this RFC because they require `const` function pointers. See [the Future Possibilities section](#future-possibilities). -* `impl const Trait` (in all positions). - * These are not part of this RFC. -* `trait Foo: const Bar {}`, requiring every type that has an impl for `Foo` (even a non-const one), to also have a const trait impl for `Bar`. -* `trait Foo { type Bar: const Trait; }`, requiring all the impls to provide a type for `Bar` that has a const trait impl for `Trait` +If a single `(const)` method is declared as `const` in the impl, all `(const)` methods must be declared as such. +It is still completely fine to just declare no methods `const` and keep the existing behaviour. Thus adding `(const)` +methods to traits is not a breaking change. Only marking more methods as `(const)` if there are already some `(const)` +methods is. -Such an impl allows you to use the type that is bound within a const block or any other const context, because we know that the type has a const trait impl and thus -must be executable at compile time. The following function will invoke the `Default` impl of a type at compile time and store the result in a constant. Then it returns that constant instead of computing the value every time. +### `const` methods and non-`const` methods on the same trait ```rust -fn compile_time_default() -> T { - const { T::default() } +trait Foo { + (const) fn foo(&self); + fn bar(&self); +} + +impl Foo for () { + const fn foo(&self) {} + fn bar(&self) { + println!("writing to terminal is not possible in const eval"); + } } ``` ### Conditionally-const trait bounds -Many generic `const fn` and especially many const trait impls do not actually require a const trait impl for their generic parameters. -As `const fn` can also be called at runtime, it would be too strict to require it to only be able to call things with const trait impls. +Many generic `const fn` and especially many trait impls of traits with `(const)` methods do not actually require a const methods in the trait impl for their generic parameters. +As `const fn` can also be called at runtime, it would be too strict to require it to only be able to call things with const methods in the trait impls. Picking up the example from [the beginning](#summary): ```rust -const trait Default { - fn default() -> Self; +trait Default { + (const) fn default() -> Self; } -impl const Default for () { - fn default() {} +impl Default for () { + const fn default() {} } impl Default for Box { @@ -212,7 +251,7 @@ const fn default() -> T { } ``` -`(const)` is derived from "approximately", meaning "conditionally" in this context, or specifically "const impl required if called in const context". +`(const)` means "conditionally" in this context, or specifically "const impl required if called in const context". It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` (not proposed here, see [this alternatives section](#make-all-const-fn-arguments-const-trait-by-default-and-require-an-opt-out-const-trait) for why it was rejected) would mean "no const impl required, even if called in const context". @@ -246,34 +285,34 @@ const FOO: (String, String) = dup(String::new()); Here `dup` is always const fn, you'll just get a trait bound failure if the type you pass isn't `Copy`. -This may seem like language lawyering, but that's how the impl works and how we should be talking about it. +This may seem like language lawyering, but that's how the impl works and how I believe we should be talking about it. It's actually important for inference and method resolution in the nonconst world today. You first figure out which method you're calling, then you check its bounds. Otherwise it would at least seem like we'd have to allow some SFINAE or method overloading style things, which we definitely do not support and have historically rejected over and over again. -### conditionally const trait impls +### Impls with const methods for conditionally const trait methods -`const` trait impls for generic types work similarly to generic `const fn`. -Any `impl const Trait for Type` is allowed to have `(const)` trait bounds. +trait impls with const methods for generic types work similarly to generic `const fn`. +Any `impl Trait for Type` is allowed to have `(const)` trait bounds if it has `const` methods: ```rust struct MyStruct(T); -impl> const Add for MyStruct { +impl> Add for MyStruct { type Output = MyStruct; - fn add(self, other: MyStruct) -> MyStruct { + const fn add(self, other: MyStruct) -> MyStruct { MyStruct(self.0 + other.0) } } -impl const Add for &MyStruct +impl Add for &MyStruct where for<'a> &'a T: (const) Add, { type Output = MyStruct; - fn add(self, other: &MyStruct) -> MyStruct { + const fn add(self, other: &MyStruct) -> MyStruct { MyStruct(&self.0 + &other.0) } } @@ -378,22 +417,29 @@ For closures and them implementing the `Fn` traits, see the [Future possibilitie ## Crate authors: Making your own custom types easier to use -You can write const trait impls of many standard library traits for your own types. +You can make trait impls of many standard library traits for your own types have `const` methods. While it was often possible to write the same code in inherent methods, operators were covered by traits from `std::ops` and thus not avaiable for const contexts. -Most of the time it suffices to add `const` before the trait name in the impl block. +Most of the time it suffices to add `const` before the methods in the impl block. The compiler will guide you and suggest where to also add `(const)` bounds for trait bounds on generic parameters of methods or the impl. Similarly you can make your traits available for users of your crate to implement constly. -Note that this will change your semver guarantees: you are now guaranteeing that any future -methods you add don't just have a default body, but a `const` default body. The compiler will -enforce this, so you can't accidentally make a mistake, but it may still limit how you can -extend your trait without having to do a major version bump. -Most of the time it suffices to add `const` before the `trait` declaration. The compiler will +Note that this has two caveats that are actually the same: + +* you cannot mark more methods as `(const)` later, +* you must decide whether to make a new method `(const)` or not when adding a new method with a default body. + +This is necessary as otherwise users of your crate may have impls where only some `(const)` methods from the trait +have been marked as `const`, making that trait unusable in `const Trait` or `(const) Trait` bounds. + +Most of the time it suffices to add `(const)` before all methods of your trait. The compiler will guide you and suggest where to also add `(const)` bounds for super trait bounds or trait bounds on generic parameters of your trait or your methods. +It should be rare that you are marking some methods as `(const)` and some not, and such unusual cases should +get some documentation explaining the oddity. + # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -403,7 +449,6 @@ These `const` or `(const)` trait bounds desugar to normal trait bounds without m A much more detailed explanation can be found in https://hackmd.io/@compiler-errors/r12zoixg1l#What-now - In contrast to other keywords like `unsafe` or `async` (that give you raw pointer derefs or `await` calls respectively), the `const` keyword on functions or blocks restricts what you can do within those functions or blocks. Thus the compiler historically used `host` as the internal inverse representation of `const` and `(const)` bounds. @@ -412,7 +457,7 @@ We generate a `ClauseKind::HostEffect` for every `const` or `(const)` bound. To mirror how some effectful languages represent such effects, I'm going to use `::k#constness` to allow setting whether the `constness` effect is "const" (disabled) or "conditionally" (generic). This is not comparable with other associated bounds like type bounds or const bounds, as the values the associated constness effect can -take do neither have a usual hierarchy of trait bounds or subtyping nor a concrete single value we can compare due to the following handling of those bounds: +take do neither have a usual hierarchy of trait bounds nor subtyping nor a concrete single value we can compare due to the following handling of those bounds: * There is no "disabled", as that is just the lack of a constness effect, meaning no `::k#constness` bound at all. * In contrast to other effect systems, we do not track the effect as a true generic parameter in the type system, @@ -431,17 +476,23 @@ Thus that approach was abandoned after proponents and opponents cooperated in tr ### Sites where `const Trait` bounds can be used -Everywhere where non-const trait bounds can be written, but only for traits that are declared `const Trait`. +Everywhere where non-const trait bounds can be written, but only for traits that have `(const)` methods. ### Sites where `(const) Trait` bounds can be used * `const fn` -* `impl const Trait for Type` +* `(const) fn` +* trait impls of traits with `(const)` methods * NOT in inherent impls, the individual `const fn` need to be annotated instead -* associated types bounds of `const trait Trait` declarations +* `trait` declarations with `(const)` methods + * super trait bounds + * where bounds + * associated type bounds ### `const` desugaring +In a-mir-formality + ```rust fn compile_time_default() -> T { const { T::default() } @@ -453,8 +504,7 @@ desugars to ```rust fn compile_time_default() -> T where - T: Default, - ::k#constness = Const, + T: Default, { const { T::default() } } @@ -462,6 +512,8 @@ where ### `(const)` desugaring +In a-mir-formality + ```rust const fn default() -> T { T::default() @@ -474,7 +526,7 @@ desugars to const fn default() -> T where T: Default, - ::k#constness = Conditionally, + do: , { T::default() } @@ -503,18 +555,11 @@ where Has a redundant bound. `T: const Default` implies `T: (const) Default`, so while the desugaring will include both (but may filter them out if we deem it useful on the impl side), there is absolutely no difference (just like specifying `Fn() + FnOnce()` has a redundant `FnOnce()` bound). -## Why do traits need to be marked as "const implementable"? +## Why do traits methods need to be marked as `(const)` -### Default method bodies -Adding a new method with a default body would become a breaking change unless that method/default body -would somehow be marked as `const`, too. So by marking the trait, you're opting into the requirement that all default bodies are const checked, -and thus neither `impl const Trait for Type` items nor `impl Trait for Type` items will be affected if you add a new method with a default body. -This scheme avoids adding a new kind of breaking change to the Rust language, -and instead allows everyone managing a public trait in their crate to continue relying on the -previous rule "adding a new method is not a breaking change if it has a default body". -### `(const) Destruct` super trait +## `(const) Destruct` super trait The `Destruct` marker trait is used to name the previously unnameable drop glue that every type has. It has no methods, as drop glue is handled entirely by the compiler, @@ -572,28 +617,28 @@ const fn add( which now need a `T: (const) Destruct` bound, too. In practice we have noticed that a large portion of APIs will have a `(const) Destruct` bound. This bound has little value as an explicit bound that appears almost everywhere. -Especially since it is a fairly straight forward assumption that a type that has const trait impls will also have a `const Drop` impl or only contain `const Destruct` types. +Especially since it is a fairly straight forward assumption that a type that has trait impls with `const` methods will also have a `Drop::drop` method that is `const` or only contain `const Destruct` types. In the future we will also want to support `dyn (const) Trait` bounds, which invariably will require the type to implement `(const) Destruct` in order to fill in the function pointer for the `drop` slot in the vtable. While that can in generic contexts always be handled by adding more `(const) Destruct` bounds, it would be more similar to how normal `dyn` safety works if there were implicit `(const) Destruct` bounds for (most?) `(const) Trait` bounds. -Thus we give all `const trait`s a `(const) Destruct` super trait to ensure users don't need to add `(const) Destruct` bounds everywhere. -We may offer an opt out of this behaviour in the future, if there are convincing real world use cases. +Thus we require that all `trait`s with `(const)` methods also have a `(const) Destruct` super trait bound to ensure users don't need to add `(const) Destruct` bounds everywhere. +We may relax this requirement in the future or make it implied. -### `(const)` bounds on `Drop` impls +## `(const)` bounds on `Drop` impls It is legal to add `(const)` to `Drop` impls' bounds, even thought the struct doesn't have them: ```rust -const trait Bar { - fn thing(&mut self); +trait Bar { + (const) fn thing(&mut self); } struct Foo(T); -impl const Drop for Foo { - fn drop(&mut self) { +impl Drop for Foo { + const fn drop(&mut self) { self.0.thing(); } } @@ -611,8 +656,8 @@ a type can be declared, but not dropped, because bounds are unfulfilled, this is Extraneous `(const) Trait` bounds where `Trait` isn't a bound on the type at all are still rejected: ```rust -impl const Drop for Foo { - fn drop(&mut self) { +impl Drop for Foo { + const fn drop(&mut self) { self.0.thing(); } } @@ -624,7 +669,7 @@ errors with error[E0367]: `Drop` impl requires `T: Baz` but the struct it is implemented for does not --> src/lib.rs:13:22 | -13 | impl const Drop for Foo { +13 | impl Drop for Foo { | ^^^^^^^^^^ | note: the implementor must specify the same requirement @@ -654,59 +699,6 @@ the traits, just without const support. This is surmountable with proc macros th Since it's only necessary for a transition period while a crate wants to support both pre-const-trait Rust and newer Rust versions, this doesn't seem too bad. With a MSRV bump the proc macro usage can be removed again. -## Can't have const methods and nonconst methods on the same trait - -If a trait has methods that don't make sense for const contexts, but some that do, then right now it is required to split that -trait into a nonconst trait and a const trait and "merge" them by making one of them be a super trait of the other: - -```rust -const trait Foo { - fn foo(&self); -} -trait Bar: Foo { - fn bar(&self); -} - -impl const Foo for () { - fn foo(&self) {} -} -impl Bar for () { - fn bar(&self) { - println!("writing to terminal is not possible in const eval"); - } -} -``` - -Such a split is not possible without a breaking change, so splitting may not be feasible in some cases. -Especially since we may later offer the ability to have const and nonconst methods on the same trait, then allowing -the traits to be merged again. That's churn we'd like to avoid. - -Note that it may frequently be that such a trait should have been split even without constness being part of the picture. - -Similarly one may want an always-const method on a trait of otherwise non-const methods: - -```rust -const trait InitFoo { - fn init(i: i32) -> Self; -} -trait Foo: const InitFoo { - const INIT: Self = Self::init(0); - fn do_stuff(&mut self); -} -``` - -or even only offer `INIT` if `InitFoo` is `const`: - -```rust -const trait InitFoo: Sized { - fn init(i: i32) -> Self; -} -trait Foo: InitFoo { - const INIT: Self = Self::init(0) where Self: const InitFoo; - fn do_stuff(&mut self); -} -``` - # Alternatives [alternatives]: #alternatives @@ -834,12 +826,18 @@ do not require const trait impls even if used in const contexts. An example from libstd today is [the impl block of Vec::new](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L406) which has an implicit `A: Allocator` bound from [the type definition](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L397). -A full example how how +A full example: ```rust -const trait Foo: (const) Bar + Baz {} +trait Foo: (const) Bar + Baz { + (const) fn baz(); + fn buz(); +} -impl const Foo for () {} +impl Foo for () { + const fn baz() {} + fn buz() {} +} const fn foo() -> T { // cannot call `Baz` methods @@ -849,11 +847,18 @@ const fn foo() -> T { const _: () = foo(); ``` +can be represented as ```rust -const trait Foo: Bar + ?const Baz {} +trait Foo: Bar + ?const Baz { + fn baz(); + ?const fn buz(); +} -impl const Foo for () {} +impl const Foo for () { + fn baz() {} + ?const fn buz() {} +} #[next_const_fn] const fn foo() -> T { @@ -891,56 +896,6 @@ The implementation correctness argument is partially due to our history with `?c To get more capabilities, you add more syntax. Thus the opt-out approach was not taken. -## Per-method constness instead of per-trait - -We could require trait authors to declare which methods can be const: - -```rust -trait Default { - const fn default() -> Self; -} -``` - -This has two major advantages: - -* you can now have const and non-const methods in your trait without requiring an opt-out -* you can add new methods with default bodies and don't have to worry about new kinds of breaking changes - -The specific syntax given here may be confusing though, as it looks like the function is always const, but -implementations can use non-const impls and thus make the impl not usable for `T: (const) Trait` bounds. - -Though this means that changing a non-const fn in the trait to a const fn is a breaking change, as the user may -have that previous-non-const fn as a non-const fn in the impl, causing the entire impl now to not be usable for -`T: (const) Trait` anymore. - -See also: out of scope RTN notation in [Unresolved questions](#unresolved-questions) - -## Per-method and per-trait constness together: - -To get the advantages of the per-method constness alternative above, while avoiding the new kind of breaking change, we can require per-method and per-trait constness: - -A mixed version of the above could be - -```rust -const trait Foo { - const fn foo(); - fn bar(); -} -``` - -where you still need to annotate the trait, but also annotate the const methods. - -But it makes it much harder/more confusing to add - -```rust -trait Tr { - const C: u8 = Self::f(); - const fn f() -> u8; -} -``` - -later, where even non-const traits can have const methods, that all impls must implement as a const fn. - # Prior art [prior-art]: #prior-art @@ -956,13 +911,9 @@ later, where even non-const traits can have const methods, that all impls must i # Unresolved questions [unresolved-questions]: #unresolved-questions -- What parts of the design do you expect to resolve through the RFC process before this gets merged? - * Whether to pick an alternative syntax (and which one in that case). - What parts of the design do you expect to resolve through the implementation of this feature before stabilization? * We've already handled this since the last RFC, there are no more implementation concerns. - What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? - * This RFC's syntax is entirely unrelated to discussions on `async Trait`. - * `async Trait` can be written entirely in user code by creating a new trait `AsyncTrait`; there is no workaround for `const`. * This RFC's syntax is entirely unrelated to discussions on effect syntax. * If we get an effect system, it may be desirable to allow expressing const traits with the effect syntax, this design is forward compatible with that. * If we get an effect system, we will still want this shorthand, just like we allow you to write: @@ -980,7 +931,7 @@ later, where even non-const traits can have const methods, that all impls must i * Still requires editing traits. * Still want the `(const) Trait` sugar anyway. -## Should we start out by allowing only const trait declarations and const trait impls +## Should we start out without `const Trait` bounds We do not need to immediately allow using methods on generic parameters of const fn, as a lot of const code is nongeneric. @@ -989,8 +940,8 @@ The following example could be made to work with just const traits and const tra ```rust struct MyStruct(i32); -impl const PartialEq for MyStruct { - fn eq(&self, other: &MyStruct) -> bool { +impl PartialEq for MyStruct { + const fn eq(&self, other: &MyStruct) -> bool { self.0 == other.0 } } @@ -1056,7 +1007,7 @@ As further experimentation is needed here, const closures are not part of this R We could allow writing `const fn` in impls without the trait opting into it. This would not affect `T: Trait` bounds, but still allow non-generic calls. -This is simialar to other refinings in impls, as the function still satisfies everything from the trait. +This is similar to other refinings in impls, as the function still satisfies everything from the trait. Example: without adjusting `rand` for const trait support at all, users could write @@ -1093,3 +1044,4 @@ impl RngCore for CountingRng { ``` and use it in non-generic code. +It is not clear this is doable soundly for generic methods. From 73db002f88211046e08309035fe08f889255ef13 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Fri, 7 Mar 2025 12:27:08 +0000 Subject: [PATCH 16/23] Address review comments --- text/0000-const-trait-impls.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index c8fdee19971..6e0b361a2b0 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -873,7 +873,7 @@ This can be achieved across an edition by having some intermediate syntax like p Then in the following edition, we can forbid the `#[next_const]` attribute and just make it the default. -The disadvantage of this is that by default, it creates stricter bounds than desired. +The disadvantage of this is that sometimes, it creates stricter bounds than desired. ```rust const fn foo() { @@ -884,15 +884,13 @@ const fn foo() { compiles today, and allows all types that implement `Foo`, irrespective of the constness of the impl. With the opt-out scheme that would still compile, but suddenly require callers to provide a const impl. -The safe default (and the one folks are used to for a few years now on stable), is that trait bounds just work, you just -can't call methods on them. -This is both useful in +The alternative proposed above (and the one folks are used to for a few years now on stable), is that trait bounds mean the same on all functions, you just can't call methods on them in `const fn`. * nudging function authors to using the minimal necessary bounds to get their function body to compile and thus requiring as little as possible from their callers, * ensuring our implementation is correct by default. -The implementation correctness argument is partially due to our history with `?const` (see https://github.com/rust-lang/rust/issues/83452 for where we got it wrong and thus decided to stop using opt-out), and partially with our history with `?` bounds not being great either (https://github.com/rust-lang/rust/issues/135229, https://github.com/rust-lang/rust/pull/132209). An opt-in is much easier to make sound and keep sound. +The implementation correctness argument is partially due to our history with `cosnt fn` trait bounds (see https://github.com/rust-lang/rust/issues/83452 for where we got "reject all trait bounds" wrong and thus decided to stop using opt-out), and partially with our history with `?` bounds not being great either (https://github.com/rust-lang/rust/issues/135229, https://github.com/rust-lang/rust/pull/132209). An opt-in is much easier to make sound and keep sound. To get more capabilities, you add more syntax. Thus the opt-out approach was not taken. From 58343dd12778e0eabc5f06c579b206a8513fbc7e Mon Sep 17 00:00:00 2001 From: Predrag Gruevski <2348618+obi1kenobi@users.noreply.github.com> Date: Fri, 21 Mar 2025 00:50:07 -0400 Subject: [PATCH 17/23] typo: `prexisting -> preexisting` --- text/0000-const-trait-impls.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 6e0b361a2b0..1ec291a6773 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -252,7 +252,7 @@ const fn default() -> T { ``` `(const)` means "conditionally" in this context, or specifically "const impl required if called in const context". -It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` +It is the opposite of `?` (preexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` (not proposed here, see [this alternatives section](#make-all-const-fn-arguments-const-trait-by-default-and-require-an-opt-out-const-trait) for why it was rejected) would mean "no const impl required, even if called in const context". From 0a82e9e98c7efd9bf7df9e93c56f8711142a5310 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 31 Mar 2025 15:35:10 +0200 Subject: [PATCH 18/23] Update text/0000-const-trait-impls.md Co-authored-by: Josh Triplett --- text/0000-const-trait-impls.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 1ec291a6773..98d95d4c022 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -1043,3 +1043,7 @@ impl RngCore for CountingRng { and use it in non-generic code. It is not clear this is doable soundly for generic methods. + +## Macro matcher + +In the future, we may want to provide a macro matcher for this optional component of a function declaration or trait declaration, similar to `:vis` for an optional visibility. This would allow macros to match it conveniently, and may encourage forwards compatibility with future things in the same category. However, we should not add such a matcher right away, until we have a clearer picture of what else we may add to the same category. From c1981e45cfc4047be926f5bbbdfff6cb4dba7508 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 31 Mar 2025 13:41:30 +0000 Subject: [PATCH 19/23] Add derives --- text/0000-const-trait-impls.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 98d95d4c022..6e39158e45a 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -292,7 +292,7 @@ You first figure out which method you're calling, then you check its bounds. Otherwise it would at least seem like we'd have to allow some SFINAE or method overloading style things, which we definitely do not support and have historically rejected over and over again. -### Impls with const methods for conditionally const trait methods +### Impls with const methods for conditionally const trait methods trait impls with const methods for generic types work similarly to generic `const fn`. Any `impl Trait for Type` is allowed to have `(const)` trait bounds if it has `const` methods: @@ -320,6 +320,32 @@ where See [this playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=313a38ef5c36b2ddf489f74167c1ac8a) for an example that works on nightly today. +### Derives + +Most of the time you don't want to write out your impls by hand, but instead derive them as the implementation is obvious from your data structure. + +```rust +#[const_derive(PartialEq, Eq)] +struct MyStruct(T); +``` + +generates + +```rust +impl PartialEq for MyStruct { + (const) fn eq(&self, other: &Rhs) -> bool { + self.0 == other.0 + } +} + +impl Eq for MyStruct {} +``` + +For THIS RFC, we stick with `derive_const`, because it interacts with other ongoing bits of design work (e.g., RFC 3715) +and we don't want to have to resolve all design questions at once to do anything. +We encourage another RFC to integrate const/unsafe and potentially other modifiers into the derive syntax in a better way. +If this lands prior to stabilization, we should implement the const portion of it, otherwise we'll deprecate `derive_const`. + ### `(const) Destruct` trait The `Destruct` trait enables dropping types within a const context. @@ -958,6 +984,12 @@ as they need to actually call the generic `FnOnce` argument or nested `PartialEq # Future possibilities [future-possibilities]: #future-possibilities +## Better derive syntax than `#[derive_const(Trait)]` + +Once `unsafe` derives have been finalized, we can separately design const derives and +deprecate `derive_const` at that time (mostly by just removing it from any documents explaining it, +so that the ecosystem slowly migrates, maybe with an actual deprecation warning later). + ## Migrate to `(const) fn` `const fn` and `const` items have slightly different meanings for `const`: From cb7f02d34cbfb5f6610bfeeba49e191d71175322 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 31 Mar 2025 14:18:41 +0000 Subject: [PATCH 20/23] Move back to an explicit annotation on traits and impls --- text/0000-const-trait-impls.md | 76 ++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 6e39158e45a..9037e598835 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -15,17 +15,17 @@ Make trait methods callable in const contexts. This includes the following parts Fully contained example ([Playground of currently working example](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=2ab8d572c63bcf116b93c632705ddc1b)): ```rust -trait Default { +const trait Default { (const) fn default() -> Self; } -impl Default for () { +impl const Default for () { const fn default() {} } struct Thing(T); -impl Default for Thing { +impl const Default for Thing { (const) fn default() -> Self { Self(T::default()) } } @@ -66,7 +66,7 @@ This RFC requires familarity with "const contexts", so you may have to read [the Calling functions during const eval requires those functions' bodies to only use statements that const eval can handle. While it's possible to just run any code until it hits a statement const eval cannot handle, that would mean the function body is part of its semver guarantees. Something as innocent as a logging statement would make the function uncallable during const eval. -Thus we have a marker (`const`) to add in front of functions that requires the function body to only contain things const eval can handle. This in turn allows a `const` annotated function to be called from const contexts, as you now have a guarantee it will stay callable. +Thus we have a marker, `const`, to add in front of functions that requires the function body to only contain things const eval can handle. This in turn allows a `const` annotated function to be called from const contexts, as you now have a guarantee it will stay callable. When calling a trait method, this simple scheme (that works great for free functions and inherent methods) does not work. @@ -153,10 +153,11 @@ fn compile_time_default() -> T { ### Conditionally const traits methods -Traits need to opt-in to allowing their impls to have const methods. Thus you need to prefix the methods you want to be const callable with `(const)`: +Traits need to opt-in to allowing their impls to have const methods. Thus you need to mark the trait as `const` and prefix the methods you want to be const callable with `(const)`. +Doing this at the same time is not a breaking change. Adding more `(const)` methods later is a breaking change (unless they are entirely new methods with default bodies). ```rust -trait Trait { +const trait Trait { (const) fn thing(); } ``` @@ -173,33 +174,34 @@ trait Trait { } ``` -and a result of this RFC would be that we would remove the attribute and add the `(const) fn` syntax for *methods*. +and a result of this RFC would be that we would remove the attribute and add the `(const) fn` syntax for *methods* and the `const trait` syntax +for trait declarations. Free functions are unaffected and will stay as `const fn`. ### Impls for conditionally const methods -Methods that are declared as `(const)` on a trait can now be made `const` in an impl: +Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl cosnt Trait`: ```rust -impl Trait for Type { +impl const Trait for Type { const fn thing() {} } ``` -If a single `(const)` method is declared as `const` in the impl, all `(const)` methods must be declared as such. -It is still completely fine to just declare no methods `const` and keep the existing behaviour. Thus adding `(const)` -methods to traits is not a breaking change. Only marking more methods as `(const)` if there are already some `(const)` -methods is. + ### `const` methods and non-`const` methods on the same trait +If there is no `(const)` modifier on a method in a `const trait`, it is treated as any normal method is today. +So `impl const Trait` blocks cannot mark them as `const` either. + ```rust -trait Foo { +const trait Foo { (const) fn foo(&self); fn bar(&self); } -impl Foo for () { +impl const Foo for () { const fn foo(&self) {} fn bar(&self) { println!("writing to terminal is not possible in const eval"); @@ -209,20 +211,20 @@ impl Foo for () { ### Conditionally-const trait bounds -Many generic `const fn` and especially many trait impls of traits with `(const)` methods do not actually require a const methods in the trait impl for their generic parameters. +Many generic `const fn` and especially many `const trait`s do not actually require a const methods in the trait impl for their generic parameters. As `const fn` can also be called at runtime, it would be too strict to require it to only be able to call things with const methods in the trait impls. Picking up the example from [the beginning](#summary): ```rust -trait Default { +const trait Default { (const) fn default() -> Self; } -impl Default for () { +impl const Default for () { const fn default() {} } -impl Default for Box { +impl const Default for Box { fn default() -> Self { Box::new(T::default()) } } @@ -294,20 +296,20 @@ which we definitely do not support and have historically rejected over and over ### Impls with const methods for conditionally const trait methods -trait impls with const methods for generic types work similarly to generic `const fn`. -Any `impl Trait for Type` is allowed to have `(const)` trait bounds if it has `const` methods: +`const trait` impls for generic types work similarly to generic `const fn`. +Any `impl const Trait for Type` is allowed to have `(const)` trait bounds: ```rust struct MyStruct(T); -impl> Add for MyStruct { +impl> const Add for MyStruct { type Output = MyStruct; const fn add(self, other: MyStruct) -> MyStruct { MyStruct(self.0 + other.0) } } -impl Add for &MyStruct +impl const Add for &MyStruct where for<'a> &'a T: (const) Add, { @@ -332,16 +334,16 @@ struct MyStruct(T); generates ```rust -impl PartialEq for MyStruct { +impl const PartialEq for MyStruct { (const) fn eq(&self, other: &Rhs) -> bool { self.0 == other.0 } } -impl Eq for MyStruct {} +impl const Eq for MyStruct {} ``` -For THIS RFC, we stick with `derive_const`, because it interacts with other ongoing bits of design work (e.g., RFC 3715) +For this RFC, we stick with `derive_const`, because it interacts with other ongoing bits of design work (e.g., RFC 3715) and we don't want to have to resolve all design questions at once to do anything. We encourage another RFC to integrate const/unsafe and potentially other modifiers into the derive syntax in a better way. If this lands prior to stabilization, we should implement the const portion of it, otherwise we'll deprecate `derive_const`. @@ -446,8 +448,10 @@ For closures and them implementing the `Fn` traits, see the [Future possibilitie You can make trait impls of many standard library traits for your own types have `const` methods. While it was often possible to write the same code in inherent methods, operators were covered by traits from `std::ops` and thus not avaiable for const contexts. -Most of the time it suffices to add `const` before the methods in the impl block. -The compiler will guide you and suggest where to also +Most of the time it suffices to add `const` after the `impl`. + +The compiler will then guide you and suggest where to also +add `const` before methods and add `(const)` bounds for trait bounds on generic parameters of methods or the impl. Similarly you can make your traits available for users of your crate to implement constly. @@ -459,8 +463,8 @@ Note that this has two caveats that are actually the same: This is necessary as otherwise users of your crate may have impls where only some `(const)` methods from the trait have been marked as `const`, making that trait unusable in `const Trait` or `(const) Trait` bounds. -Most of the time it suffices to add `(const)` before all methods of your trait. The compiler will -guide you and suggest where to also add `(const)` bounds for super trait bounds or trait bounds +Most of the time it suffices to add `(const)` before all methods of your trait `const` before the `trait` keyword. +The compiler will guide you and suggest where to also add `(const)` bounds for super trait bounds or trait bounds on generic parameters of your trait or your methods. It should be rare that you are marking some methods as `(const)` and some not, and such unusual cases should @@ -508,9 +512,9 @@ Everywhere where non-const trait bounds can be written, but only for traits that * `const fn` * `(const) fn` -* trait impls of traits with `(const)` methods +* `impl const Trait` blocks * NOT in inherent impls, the individual `const fn` need to be annotated instead -* `trait` declarations with `(const)` methods +* `const trait` declarations * super trait bounds * where bounds * associated type bounds @@ -657,20 +661,20 @@ We may relax this requirement in the future or make it implied. It is legal to add `(const)` to `Drop` impls' bounds, even thought the struct doesn't have them: ```rust -trait Bar { +const trait Bar { (const) fn thing(&mut self); } struct Foo(T); -impl Drop for Foo { +impl const Drop for Foo { const fn drop(&mut self) { self.0.thing(); } } ``` -There is no reason (and no coherent representation) of adding `(const)` trait bounds to a type. +There is currently no reason (and no coherent representation) of adding `(const)` trait bounds to a type. Our usual `Drop` rules enforce that an impl must have the same bounds as the type. `(const)` modifiers are special here, because they are only needed in const contexts. While they cause exactly the divergence that we want to prevent with the `Drop` impl rules: @@ -682,7 +686,7 @@ a type can be declared, but not dropped, because bounds are unfulfilled, this is Extraneous `(const) Trait` bounds where `Trait` isn't a bound on the type at all are still rejected: ```rust -impl Drop for Foo { +impl const Drop for Foo { const fn drop(&mut self) { self.0.thing(); } From bc5cc29a38598353bf4cb3f5b34eb54a48c3f278 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 31 Mar 2025 15:13:39 +0000 Subject: [PATCH 21/23] Typos --- text/0000-const-trait-impls.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 9037e598835..8c1923ba933 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -658,7 +658,7 @@ We may relax this requirement in the future or make it implied. ## `(const)` bounds on `Drop` impls -It is legal to add `(const)` to `Drop` impls' bounds, even thought the struct doesn't have them: +It is legal to add `(const)` to `Drop` impls' bounds, even though the struct doesn't have them: ```rust const trait Bar { @@ -714,7 +714,7 @@ note: the implementor must specify the same requirement ## Adding any feature at all around constness -I think we've reached the point where all critics have agreed that this one kind of effect system is unavoidable since we want to be able to write maintainable code for compile time evaluation. +I think we've reached the point where all critics have agreed that this one kind of effect system is unavoidable since we want to be able to write maintainable generic code for compile time evaluation. So the main drawback is that it creates interest in extending the system or add more effect systems, as we have now opened the door with an effect system that supports traits. Even though I personally am interested in adding an effect for panic-freedom, I do not think that adding this const effect system should have any bearing on whether we'll add From e18f4756f63d9127f07e2083c0e4b5dada531c7c Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 31 Mar 2025 15:17:10 +0000 Subject: [PATCH 22/23] RPIT --- text/0000-const-trait-impls.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 8c1923ba933..80c59792ecb 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -518,6 +518,7 @@ Everywhere where non-const trait bounds can be written, but only for traits that * super trait bounds * where bounds * associated type bounds +* return position impl trait ### `const` desugaring @@ -653,8 +654,9 @@ In the future we will also want to support `dyn (const) Trait` bounds, which inv While that can in generic contexts always be handled by adding more `(const) Destruct` bounds, it would be more similar to how normal `dyn` safety works if there were implicit `(const) Destruct` bounds for (most?) `(const) Trait` bounds. -Thus we require that all `trait`s with `(const)` methods also have a `(const) Destruct` super trait bound to ensure users don't need to add `(const) Destruct` bounds everywhere. -We may relax this requirement in the future or make it implied. +Thus we lint all `const trait`s with `(const)` methods that take `self` by value to also have a `(const) Destruct` super trait bound to ensure users don't need to add `(const) Destruct` bounds everywhere. +Other traits may want to add them, and some traits with `self` by value methods may not want to add them. Since it is not backwards compatible to require or relax that super trait bound later, +we aren't requiring users to choose either, but are suggesting good defaults via lints. ## `(const)` bounds on `Drop` impls From fdd11fbd14df158d91a1cca9fc7343f9c2d74466 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Wed, 16 Apr 2025 08:53:56 +0000 Subject: [PATCH 23/23] Do not allow `const` refinement of `(const)` methods in impls --- text/0000-const-trait-impls.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/text/0000-const-trait-impls.md b/text/0000-const-trait-impls.md index 80c59792ecb..1d236440994 100644 --- a/text/0000-const-trait-impls.md +++ b/text/0000-const-trait-impls.md @@ -20,7 +20,7 @@ const trait Default { } impl const Default for () { - const fn default() {} + (const) fn default() {} } struct Thing(T); @@ -180,11 +180,11 @@ Free functions are unaffected and will stay as `const fn`. ### Impls for conditionally const methods -Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl cosnt Trait`: +Methods that are declared as `(const)` on a trait can now also be made `(const)` in an impl, if that impl is marked as `impl const Trait`: ```rust impl const Trait for Type { - const fn thing() {} + (const) fn thing() {} } ``` @@ -202,7 +202,7 @@ const trait Foo { } impl const Foo for () { - const fn foo(&self) {} + (const) fn foo(&self) {} fn bar(&self) { println!("writing to terminal is not possible in const eval"); } @@ -221,11 +221,11 @@ const trait Default { } impl const Default for () { - const fn default() {} + (const) fn default() {} } impl const Default for Box { - fn default() -> Self { Box::new(T::default()) } + (const) fn default() -> Self { Box::new(T::default()) } } // This function requires a `const` impl for the type passed for T, @@ -304,7 +304,7 @@ struct MyStruct(T); impl> const Add for MyStruct { type Output = MyStruct; - const fn add(self, other: MyStruct) -> MyStruct { + (const) fn add(self, other: MyStruct) -> MyStruct { MyStruct(self.0 + other.0) } } @@ -314,7 +314,7 @@ where for<'a> &'a T: (const) Add, { type Output = MyStruct; - const fn add(self, other: &MyStruct) -> MyStruct { + (const) fn add(self, other: &MyStruct) -> MyStruct { MyStruct(&self.0 + &other.0) } } @@ -605,7 +605,7 @@ struct NewType(T); impl> const Add for NewType { type Output = Self; - fn add(self, other: Self) -> Self::Output { + (const) fn add(self, other: Self) -> Self::Output { NewType(self.0 + other.0) } } @@ -620,7 +620,7 @@ struct Error; impl> const Add for NewType { type Output = Result; - fn add(self, other: Self) -> Self::Output { + (const) fn add(self, other: Self) -> Self::Output { if self.1 { Ok(NewType(self.0 + other.0, other.1)) } else { @@ -670,7 +670,7 @@ const trait Bar { struct Foo(T); impl const Drop for Foo { - const fn drop(&mut self) { + (const) fn drop(&mut self) { self.0.thing(); } } @@ -689,7 +689,7 @@ Extraneous `(const) Trait` bounds where `Trait` isn't a bound on the type at all ```rust impl const Drop for Foo { - const fn drop(&mut self) { + (const) fn drop(&mut self) { self.0.thing(); } }