Tracking issue for RFC 2632, `impl const Trait for Ty` and `~const` (tilde const) syntax
ecstatic-morse opened this issue ยท 55 comments
NOTE: See #110395, which tracks a planned rewrite of this feature's implementation
This is the primary tracking issue for rust-lang/rfcs#2632.
The current RFC text can be found at https://internals.rust-lang.org/t/pre-rfc-revamped-const-trait-impl-aka-rfc-2632/15192
This RFC has not yet been accepted. It is being implemented on a provisional basis to evaluate the potential fallout.
cc #57563
The feature gate is const_trait_impl
.
Components
#[const_trait]
attributeimpl const Trait
T: ~const Trait
append_const_msg
onrustc_on_unimplemented
#[derive_const]
trait Destruct
Open issues
- #88155
- this test shows we can currently still call a
const fn
with aTrait
bound even if the concrete type does not implementconst Trait
, but justTrait
. This will fail later during evaluation. Some related discussion can be found in #79287 (comment) - There are no tests for using a
const impl
without defining one without the feature gate enabled. This should be added before anyimpl const
is added to the standard library. - We need some tests and probably code around
#[rustc_const_stable]
and#[rustc_const_unstable]
so we can properly stabilize (or not) the constness of impls in the standard library - #79450 shows that with default function bodies in the trait declaration, we can cause non-const functions to exist in
impl const Trait
impls by leaving out these default functions - We need to determine the final syntax for
~const
. (In the interim, we may want to switch this to use ak#provisional_keyword
or similar.) - If we need
#[default_method_body_is_const]
, determine the syntax for it instead of existing as an attribute -
#[derive_const]
for custom derives (proc macros) #118304
When stabilizing: compiler changes are required:
- Error against putting
rustc_const_unstable
attribute onconst
impl
s as they are now insta-stable. - Treat
default_method_body_is_const
bodies andconst
impl
bodies asstable
const fn
bodies. We need to prevent accidentally stabilizing an implementation that uses unstable lang/libconst fn
features. - Change Rustdoc to display
~const
bounds or what syntax we decided it to be.
Now that I've read up and understand the reasoning for the ~const
syntax, here's my opinion on it.
Basically, we would prefer that constness be a bonus for generic impls to offer, but not a requirement. A trait impl can take all the steps required to make something const, but if its dependencies are not const, we would rather offer a non-const method than a compiler error.
Basically, if we say impl const Add
and the result ends up being something that's not const, it's bad.
But, that's exactly what const does right now. Const doesn't mean to always evaluate things in a constant context; it just means that it can be evaluated in a constant context. Whether it's const depends entirely on the constness of the inputs; 2.pow(3)
is evaluated at const but ,x.pow(y)
is not.
The relevant analogue here would be that the constness of an input implementation of a trait is what determines the constness of the output impl.
But... that's exactly how const works for functions, and we don't annotate arguments with constness. The only case where it would be useful is if some arguments are unused, which is exactly what we have here for the type parameters.
So, really, this annotation is letting us know which type parameters are unused when determining constness. Which seems a lot like what the original ?const
syntax was trying to indicate, although I guess the main difference with ?Sized
is it doesn't actually affect whether an impl is valid, just if it's const or not.
I really do think that it's better to opt out of the constness rather than opt in. But I guess there's probably more reasons that you chose it than I'm seeing.
Whether it's const depends entirely on the constness of the inputs; 2.pow(3) is evaluated at const but ,x.pow(y) is not.
This is wrong. const
and static
initializers, array lengths, and enum discriminant values are evaluated at compile-time, but all other code is evaluated according to runtime rules, and const fn
vs fn
is entirely irrelevant for that. There is no situation in which changing fn
to const fn
in already working code will change its behavior. The inputs don't matter.
In the language that comes after Rust it might be possible to say that all functions are const by default and mark ones which aren't const. However, for Rust that's just not possible because of backwards compatibility reasons. Since functions opt in to being const, then trait impls must also opt in to being const for consistency.
So, I'm a bit confused how #[default_method_body_is_const]
is supposed to interact with ~const
.
Say I have these hypothetical traits:
trait Parent {
fn parenting(self) -> Self;
}
trait Child: Parent {
fn childing(self) -> Self {
self.parenting()
}
}
If I want to make the default method on Child
const, it seems like this is the most logical solution:
trait Child: ~const Parent {
#[default_method_body_is_const]
fn childing(self) -> Self {
self.parenting()
}
}
but ~const
isn't allowed in trait bounds, and so you have to do:
trait Child: Parent {
#[default_method_body_is_const]
fn childing(self) -> Self where Self: ~const Parent {
self.parenting()
}
}
AFAIK, this Self: ~const Parent
bound doesn't actually prevent any implementors from having this method; since ~const Parent
just says "allow using const Parent
methods in const context" and the trait already has the Parent
bound, it should be mostly redundant. Not sure if the #[default_method_body_is_const]
attribute should be implying this or not, or if there should be some other way of adding this.
Just wanted to say firstly that I think this is a fantastic feature overall which I've been enjoying making use of recently (as it is at least in my perspective robust enough to be fully useful in quite a few scenarios).
That said, there's one thing I wanted to clarify, which is basically just whether I'm correct in assuming that what's demonstrated below is simply a byproduct of the feature not yet being completely implemented, or if it has to do with something else:
#![feature(
const_fn_floating_point_arithmetic,
const_fn_trait_bound,
// The next one is necessary to prevent E0658, at least currently
const_refs_to_cell,
const_trait_impl
)]
use std::cmp::Ordering;
const fn const_min<T>(a: T, b: T) -> T
where
T: ~const PartialEq + ~const PartialOrd + ~const Drop,
{
if a <= b {
a
} else {
b
}
}
#[derive(PartialEq, PartialOrd)]
struct NotConst {
f: f32,
}
struct Const {
f: f32,
}
impl const PartialEq for Const {
fn eq(&self, other: &Self) -> bool {
self.f == other.f
}
}
impl const PartialOrd for Const {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.f < other.f {
Some(Ordering::Less)
} else if self.f == other.f {
Some(Ordering::Equal)
} else {
Some(Ordering::Greater)
}
}
}
fn main() {
// Works normally / as expected at runtime, without `const` impls.
let f = NotConst { f: 12.0 };
let g = NotConst { f: 24.0 };
println!("{}", const_min(f, g).f);
// Works normally / as expected at compile-time, with `const` impls.
const F: Const = Const { f: 12.0 };
const G: Const = Const { f: 24.0 };
const H: f32 = const_min(F, G).f;
println!("{}", H);
// Works normally / as expected at runtime on primitive numeric variables.
let ff = 12.0;
let gg = 24.0;
println!("{}", const_min(ff, gg));
// Does not work at compile-time on primitive numeric constants! Uncomment
// the next four lines to see the error messages.
// const FF: f32 = 12.0;
// const GG: f32 = 24.0;
// const HH: f32 = const_min(FF, GG);
// println!("{}", HH);
}
New weird case to cover: I think that ~const Drop
should have its own tracking issue and feature flag, since as I mention in #92385 I think that the semantics are weird enough to warrant their own discussion.
I'm not sure why I'm not subscribed to this issue, I want to clarify a few questions left as comments on this issue:
AFAIK, this
Self: ~const Parent
bound doesn't actually prevent any implementors from having this method; since~const Parent
just says "allow usingconst Parent
methods in const context" and the trait already has theParent
bound, it should be mostly redundant. Not sure if the#[default_method_body_is_const]
attribute should be implying this or not, or if there should be some other way of adding this.
I think the way is to use the syntax for your second code block i.e. explicitly declare supertraits have to be const
: trait Child: ~const Parent
. This is not yet implemented because I'm lazy and it has a relatively straightforward workaround :)
I think that
~const Drop
should have its own tracking issue and feature flag, since as I mention in #92385 I think that the semantics are weird enough to warrant their own discussion.
I think discussions on ~const Drop
should be bundled with the whole RFC and shouldn't require a new feature gate. It's use case is clear and is essential for the const_trait_impl
feature. The only problem I see is the syntax bikeshed, but really we should just decide what to do with ~const Drop
while figuring out the best syntax for ~const
.
As for your specific problem (with ~const Drop
, Err(_)
issues const destructors error, Err(_x)
passes), it is related to MIR. There is a difference in MIR generated for these two cases and that is why the error is only triggered for the first case:
fn wildcard(_1: Result<T, E>) -> Option<T> {
debug x => _1;
let mut _0: std::option::Option<T>;
let mut _2: isize;
let _3: T;
let mut _4: T;
let mut _5: isize;
-snip-
bb5: {
drop(_1) -> bb4; // The actual control flow is a bit complicated,
// but the whole thing is dropped when the variant is `Err`.
}
-snip-
}
fn variable_bound(_1: Result<T, E>) -> Option<T> {
debug x => _1;
let mut _0: std::option::Option<T>;
let mut _2: isize;
let _3: T;
let mut _4: T;
let _5: E;
-snip-
bb1: {
_5 = move ((_1 as Err).0: E);
discriminant(_0) = 0;
drop(_5) -> bb4; // <- the bound variable is dropped instead
}
-snip-
}
// Does not work at compile-time on primitive numeric constants! Uncomment // the next four lines to see the error messages. // const FF: f32 = 12.0; // const GG: f32 = 24.0; // const HH: f32 = const_min(FF, GG); // println!("{}", HH);
This is because f32
does not have a const implementation for PartialOrd
, yet. Comparing primitive types are special cased so they work for constant evaluation, when used in generic code the actual impls are used. #92390 would make it work.
Ah, so it's not necessarily that ~const Trait
bounds on the trait itself aren't wanted, but rather that they aren't implemented. May be worth updating the error in the meantime, then, since it says "not allowed" instead of "not supported."
So, I'm having a very weird issue that I've been trying to replicate, but it's not working out terribly well.
Basically, sometimes, ~const FnOnce(T) -> U
is being interpreted as ~const FnOnce(T) -> U
for the case of type-checking inside a default method body, but ~const FnOnce(T)
(without a return value) on instantiation. Perhaps there's a weird issue in parsing, but I'm not sure how that's possible, since in theory all type-checking should happen after the parsing occurs, and there shouldn't be a case where it's parsed twice.
As you can see in this snippet, I've been trying to create a minimal example and failing: (link)
#![feature(const_trait_impl)]
#![feature(const_fn_trait_bound)]
trait Other {}
impl Other for u32 {}
trait Test {
#[default_method_body_is_const]
fn method<T: ~const Other, F: ~const FnOnce(T) -> bool>(f: F, x: T) -> bool {
f(x)
}
}
struct NonConst;
impl Test for NonConst {
fn method<T: Other, F: FnOnce(T) -> bool>(f: F, x: T) -> bool {
f(x)
}
}
struct Const;
impl const Test for Const {
fn method<T: ~const Other, F: ~const FnOnce(T) -> bool>(f: F, x: T) -> bool {
f(x)
}
}
fn main() {
Const::method(|x| x > 0, 1);
}
I'm not sure if anyone here might know offhand of a case where this might happen in the code, to pinpoint a root cause, but as far as I'm concerned, I'm stumped. I know that it's a ~const
bug because removing ~const
causes type-checking to succeed again.
Actual side question: is it a bug or a feature that marker traits (or, more generally, any trait without methods) can be differentiated between const and non-const variants?
For example, impl const Copy
doesn't enable copying values in a const context; this is always possible. It also makes cases like ~const Eq
weird, since technically it shouldn't matter if something is Eq + ~const PartialEq
or ~const Eq + ~const PartialEq
.
Also, slight bikeshed, it would be nice to have ~const (T + U)
be equivalent to ~const T + ~const U
, and for there to be a lint suggesting to add parentheses to ~const T + U
to clarify if it's supposed to be ~const (T + U)
or (~const T) + U
.
I have an idea for the syntax we'd replace #[default_method_body_is_const]
with:
pub trait MyTrait {
fn my_fn(&self) -> i32
const {
0
}
}
Seems extremely prone to the const *T
vs. *const T
nonsense from C++; it seems like there would be cases where people think of it as returning T const
and that specific sequence of things being different from const T
as a trait bound.
Is there a serious reason why T: ~const TraitName
syntax was chosen? I find it unintuitive because why would there be a tilda? If it distinguishes constexpr
const vs consteval
const, then what do we do about regular const fn?
The tilde indicates covariance; the const can be removed if the inputs are not const, but not the other way around. This is different from a plain const (invariant) and using the question mark like Sized
wouldn't work since that's contravariant instead of covariant. (I think?)
It's basically just a placeholder until a better version exists.
The tilde indicates covariance; the const can be removed if the inputs are not const, but not the other way around. This is different from a plain const (invariant) and using the question mark like
Sized
wouldn't work since that's contravariant instead of covariant. (I think?)It's basically just a placeholder until a better version exists.
According to Rust's reference,
A const fn is a function that one is permitted to call from a const context. Declaring a function const has no effect on any existing uses, it only restricts the types that arguments and the return type may use, as well as prevent various expressions from being used within it. You can freely do anything with a const function that you can do with a regular function.
When called from a const context, the function is interpreted by the compiler at compile time. The interpretation happens in the environment of the compilation target and not the host. So usize is 32 bits if you are compiling against a 32 bit system, irrelevant of whether you are building on a 64 bit or a 32 bit system.
In that regard, Rust's const fn
's are akin to C++ constexpr
-specified functions, which can be computed both at run and compile time (as opposed to consteval
-specified functions, which are permitted to be computed only at compile time).
So it is unclear to me why there is an inconsistency here, i.e. why the same notion of "covariant constness" has subtly different manifestations in code. Wouldn't it be better to leave const fn
syntax as is, keep the "covariant constness" as the default one and distinguish with extra punctuation only the contravariant aspect of constness?
EDIT:
I think I'm missing something. I have to better understand the distinction between "invariant", "covariant", and "contravariant constness".
The variance is mostly in terms of what implementations of traits are okay -- so, const Trait
essentially would mean that impl const
is required, whereas ~const Trait
means that a non-const impl will just make the function non-const, instead of preventing usage altogether.
Yes, we don't have the ability to have T: const Trait
bounds, but the idea is that the const should be covariant, which is why the ~const
is there. This can be compared to a simple T: Trait
bound, which will not affect the constness of the function, e.g. T: Copy
.
Mostly, the matter is making sure that things are consistent with const
being an opt-in thing, while also not confusing the syntax with the contravariant ?Sized
.
The variance is mostly in terms of what implementations of traits are okay -- so,
const Trait
essentially would mean thatimpl const
is required, whereas~const Trait
means that a non-const impl will just make the function non-const, instead of preventing usage altogether.Yes, we don't have the ability to have
T: const Trait
bounds, but the idea is that the const should be covariant, which is why the~const
is there. This can be compared to a simpleT: Trait
bound, which will not affect the constness of the function, e.g.T: Copy
.Mostly, the matter is making sure that things are consistent with
const
being an opt-in thing, while also not confusing the syntax with the contravariant?Sized
.
Thank you so much for the explanation, now it does make sense!
Yeah, unfortunately the complexity of the meaning is reflected in the complexity of the syntax, which is terrible for people who don't fully need to understand the complexity and just want to make their impls work.
I think that the ~const Trait
syntax won't be too different in the final version, but the #[default_method_body_is_const]
attribute definitely needs to be replaced with something better.
Here's a silly idea: we have precedent from RFC 2316 that methods marked unsafe can omit their unsafe markers to become safe upon usage; couldn't we just go along with this and mark default methods as const, while still allowing implementations to mark them as non-const?
It feels like an abuse of notation, but it would remove the need for an attribute.
Here's a silly idea: we have precedent from RFC 2316 that methods marked unsafe can omit their unsafe markers to become safe upon usage; couldn't we just go along with this and mark default methods as const, while still allowing implementations to mark them as non-const?
It feels like an abuse of notation, but it would remove the need for an attribute.
that prevents having a trait that requires a particular function to be const:
trait MyTrait {
const fn f(v: i32) -> i32;
fn g() {
println!("nonconst");
}
}
impl MyTrait for () {
const fn f(v: i32) -> i32 {
v + 1
}
}
const fn f<T: MyTrait>() -> i32 {
// can call T::f for any T, but not T::g
T::f(0)
}
I know this question may be naive. However, I don't understand the reason for ~const
at all. As far as I understand it, evaluation of the constness of a particular function isn't relevant until it is invoked in a const context. At that time, it is fully concretized. So the compiler should be able to lazily decide whether or not the invoked function is const and, if not, why not. Therefore, T: Trait
should be sufficient for the compiler to determine the constness of the function when invoked in a const context.
Therefore, it feels to me like T: ~const Trait
is spilling the compiler internals into the syntax.
My question is this: is ~const
really needed at all?
See this IRLO discussion for more context.
Basically, we want to be able to write a const and non-const impl/fn at the same time, "abstracting over their constness" or "being generic over constness". The trait bounds depend on that extra "generic parameter", so we need some syntax to control that dependency and make it explicit.
Not sure whether this is the right place, but as a counter-proposal to this syntax that was AFAIK only invented because ?const
can't work now, why don't we just wait until the next edition? We've been fine entirely without const generics for many years, we'll be fine without calling traits in const fn
for another two.
The few currently allowed bounds in const fn
could be transitioned to ?const
via deprecation (plus more ?const
bounds allowed) in this edition, then the next edition can change the meaning of trait bounds without ?const
in const fn
and we can have the thing we originally wanted.
This should allow us to have all the nice things we wanted, just not now: In edition <2024, the syntax will still be partially available with a different meaning but it will be linted against, and the lint can even advise you to upgrade to the new edition once it is out. What works now will continue to work, under the syntax that was originally envisioned for it.
?const
would work fine in terms of the language grammar; the issue is that it is semantically wrong -- this is very different from ?Sized
.
My impression was that ?const
was mainly ruled out because it was discovered that some bounds were already accepted on generic const fn
and changing the meaning of those to be equivalent to what's now ~const
on Nightly would be a breaking change.
Where can I read more about the semantic issues with ?const
?
Yeah, I had exactly the same thoughts before I finally understood, and if you read back through my previous messages in this thread I explain how I came to understand it. If any additional clarifications are needed, feel free to add to those as well.
To be clear, nobody is wed to ~const
. This is just a placeholder syntax so that progress can be made until the syntax bikeshed is done. But it is quite clear that ?const
draws some false analogies so that should most likely not be the final syntax.
Specifically, const fn foo<B: ?const Bar>(bar: B)
looks like it means "Bar
might (or might not) be const
-implemented" (in analogy with ?Sized
), but it actually means
for<C: constness> const(C) fn foo<B: const(C) Bar>(bar: B)
In other words, "if foo
is called as a const fn
, then Bar
must be const
-implemented; if foo
is called as a regular fn
then any impl of Bar
will do".
In case the variance definition is more helpful: ?Sized is contravariant, whereas ~const is covariant. Kotlin also uses the terms "in" and "out" to refer to contravariant and covariant respectively.
@RalfJung It seems like we're not talking about the same thing. You seem to think that I am advocating for replacing ~const
with ?const
. That is not the case. I'm advocating for replacing bounds on const fn
generics without ~const
with ?const
, and ~const
bounds with nothing.
To make it formal:
const fn foo<B: const(false) Bar> |
for<constness C> fn foo<B: const(C) Bar> |
|
---|---|---|
~const model |
const fn foo<B: Bar> |
const fn foo<B: ~const Bar> |
?const model |
const fn foo<B: ?const Bar> |
const fn foo<B: Bar> (starting ed. 2024) |
AFAIK this is how ?const
was originally designed.
See this IRLO discussion for more context.
Basically, we want to be able to write a const and non-const impl/fn at the same time, "abstracting over their constness" or "being generic over constness". The trait bounds depend on that extra "generic parameter", so we need some syntax to control that dependency and make it explicit.
@RalfJung I think this comment was in reply to my comment. But I don't think it actually answers my question. What is the difference between these two syntaxes?
// Valid. Methods on Bar cannot be called.
const fn foo<T: Bar>(value: T);
// Valid. Methods on Bar can be called.
const fn foo<T: ~const Bar>(value: T);
The problem I see is that adding ~const
to the bound doesn't actually bind anything additional. There are no additional requirements. Whether ~const
is present or absent, the compiler behavior is exactly the same: if you call a method on Bar
, then T
must have impl const Bar
. This is clear and explicit from the const fn
and the ~const
syntax is altogether redundant.
Once we have impl const Trait
, the first syntax above has no utility.
@npmccallum The difference is that your first foo()
(or a foo with ?const
bound if we were to go that route) can be called in const context with a T
that implements Bar
in a non-const
way.
The problem is that we don't want to look at function bodies in order to decide whether it's ok to call foo
with a type that implements Bar nonconstly. The only reason we need those ~const
bounds (or any other syntax) is that it would be a breaking change to change a body from using a generic param just for its associated types to calling a method on it
Yeah, in hindsight, it might have been better to make const
the default and have an impure
keyword which allows explicitly opting out of const semantics, but since non-constness is the default, the constness of the trait bound must similarly be opt-in.
Basically, we've dug ourselves into this hole with the way the language was designed, and this seems like the best way to do it, but most of the people I've seen discuss this are unhappy with the ~const
syntax in general, at best seeing it as a lesser evil.
@clarfonthey I'm confused. Who are you replying to?
The problem is that we don't want to look at function bodies in order to decide whether it's ok to call
foo
with a type that implements Bar nonconstly. The only reason we need those~const
bounds (or any other syntax) is that it would be a breaking change to change a body from using a generic param just for its associated types to calling a method on it
This is what I'm getting at. The reason ~const
exists is as a hint to the compiler so that the compiler can avoid looking at the function body. Which feels, to me, like we are leaking compiler internals into the language syntax.
But I don't think that this hint even solves a real-world problem. When you are evaluating a function in a const context you HAVE TO look at the function body since ALL const fn
are completely inlined at the call site at compile time.
So it feels to me like we're trying to avoid looking at function bodies in a context where we already have to evaluate function bodies. This all looks to me like an optimization that doesn't actually matter and is spilling its details into the language syntax.
@clarfonthey I'm confused. Who are you replying to?
I was adding further justification for the existence of ~const
beyond the internal requirements @oli-obk was suggesting; even if did look into the method bodies to determine whether constness was required, it wouldn't make sense with how the language is right now.
For the "can't we still have ?const
if we wait for Rust 2024" discussion, I think #93706 is a good place, it tracks the feature gate for trait bounds on generics in const fn
(which recently had a stabilization PR for 1.61 merged ๐ฑ).
This is what I'm getting at. The reason ~const exists is as a hint to the compiler so that the compiler can avoid looking at the function body. Which feels, to me, like we are leaking compiler internals into the language syntax.
Just like the const
in const fn
, this is not about avoiding work for the compiler, but about specifying the intent of the API author so the compiler can double-check it. When you write T: Trait
vs T: ~const Trait
, that's a property that downstream users in other crates can depend on and that you promise not to break without bumping the version number appropriately.
This is what I'm getting at. The reason
~const
exists is as a hint to the compiler so that the compiler can avoid looking at the function body. Which feels, to me, like we are leaking compiler internals into the language syntax.But I don't think that this hint even solves a real-world problem. When you are evaluating a function in a const context you HAVE TO look at the function body since ALL
const fn
are completely inlined at the call site at compile time.So it feels to me like we're trying to avoid looking at function bodies in a context where we already have to evaluate function bodies. This all looks to me like an optimization that doesn't actually matter and is spilling its details into the language syntax.
Oh, I didn't realize this is being proposed. There are then two ?const
models on the table:
const fn uwu<T: Trait>(x: T)
means that to calluwu
in const context,T
must const-implementTrait
(no matter the body ofuwu
).const fn uwu<T: Trait>(x: T)
means that we check the body ofuwu
to determine whetheruwu
can be called with a non-const implementation ofTrait
.
The latter is a big departure from how Rust works literally everywhere else, so IMO it is a non-starter. Type-checking const code and running const-code are still conceptually separate and should be treated much like type-checking regular code and running regular code. Consider that type-checking and running the const-code can happen in different crates, e.g. when a lib crate exposes a pub const fn
or when there is generic const
code! As usual in Rust we want our errors when compiling the const-code, not when compiling some other code that uses our const-code. That's why Rust traits work different than C++ templates, and that's why Rust does not do type inference in function signatures. Let's stay true to those principles. (The reasons we do that have been mentioned by others: semver, making the public contract explicit, avoiding unfixable errors when you use someone else's crate but the checks have been deferred until things are actually used.)
The former would be a reasonable option (basically ~const
would be implicit everywhere), but leads to inconsistency with trait objects and function pointers as discussed in the other thread.
Either way, it is certainly not the case that rustc implementation details are spilling into the language with ~const
. The syntax is awkward, but the concept is sound (as is demonstrated by desugaring to a proper polymorphic effect system).
On a side note: if you were to add #[cfg(library_feature)] to impl const block or ~const Trait bound, the library stops compiling at all, this probably shouldn't happen as other nightly features I've tested are fine with that
An example (playground):
trait T {
fn test();
}
#[cfg(unstable)]
impl const T for () {
fn test() {
try {}
}
}
A quick search didn't find this being tracked anywhere, but for now, closures can't impl ~const Fn*
traits right now, not even trivial ones. Not sure what the plan is for that, but it definitely seems like something that should be available.
Minimal code:
#![feature(const_trait_impl)]
const fn test<F: ~const FnOnce() -> u8>(f: F) -> u8 {
f()
}
const X: u8 = test(|| 2);
Through this I also discovered that this even comes up in regular const eval without going through an existing function:
const X: u8 = (|| 2)();
On a side note: if you were to add #[cfg(library_feature)] to impl const block or ~const Trait bound, the library stops compiling at all, this probably shouldn't happen as other nightly features I've tested are fine with that
An example (playground):
trait T { fn test(); } #[cfg(unstable)] impl const T for () { fn test() { try {} } }
It is not a bug. The thing is, the code below is not valid Rust syntax from the point of view of stable Rust compilers. A reminder what attributes and attributes macros are. They accept valid Rust syntax and return modified Rust syntax. That's why unconst_trait_impl
is implemented is implemented the way it is. Only function-like macros can accept pieces of not Rust code.
EDIT:
Clarification: On stable Rust atm, impl
keyword can precede only something that looks like trait name [I don't know the inner details of the compiler but you get the idea].
Only function-like macros can accept pieces of not Rust code.
This actually allows to workaround the issue using a macro that is defined differently depending on a cfg.
More Fn*
trait issues, it appears that the current implementation is not able to imply that methods of ~const Trait
implement ~const Fn*
traits beyond a single layer of nesting: (playground link)
#![feature(const_trait_impl)]
trait Test {
fn test(self) -> Self;
}
const fn test<T: ~const Test>(val: T) -> T {
val.test()
}
const fn call<T, F: ~const FnOnce(T) -> T>(val: T, f: F) -> T {
f(val)
}
const fn works<T: ~const Test>(val: T) -> T {
call(val, test)
}
const fn fails<T: ~const Test>(val: T) -> T {
call(val, Test::test)
}
(edit: simplified code further after a bit more testing)
I'm not sure whether it makes the most sense to track both this and the other Fn
trait problems in this issue or if there should be a dedicated tracking issue to that. Either way, I think that this should either be added to the issue description as a blocker for stabilisation, or const Fn
traits in general should be put in a separate feature flag.
We're working in #96077 to reimplement the entire logic around all of this, as the current one is inherently broken as you noticed :)
I have noticed! Thank you for the hard work on all of that.
I didn't know if that would affect this or not, so, I figured it'd be best to at least report the shortcomings of the current implementation. Hopefully that fixes a lot of these issues.
it appears that the current implementation is not able to imply that methods of
~const Trait
implement~const Fn*
traits beyond a single layer of nesting
Saw this again and I feel like this is not solved by the new system. Allowing trait methods would require changes in wf to make it so that for FnDef(Test::test): ~const Fn
, Receiver: ~const Test
must be satisfied. Doing this before the refactor or after doesn't change much imo
This is currently used in the standard library, but we're unlikely to stabilize it in its current form. Keyword generics seem likely to supersede this.
@joshtriplett Can you provide the link to the keyword generics issue, please?
Keyword generics would supersede ~const
but not impl const Trait
, unless I'm mistaken? impl const Trait
still has design blockers, but I don't think it's inherently tied to keyword generics or ~const
bounds.
Keyword generics are currently tracked here: https://rust-lang.github.io/keyword-generics-initiative/
Something worth doing at some point: #[marker]
should probably imply #[const_trait]
.
Why is the #[const_trait]
attribute needed? I've gotten a compiler error that says to add it, but not why.
I added some additional notes to the message to explain the "why":
= note: marking a trait with `#[const_trait]` ensures all default method bodies are `const`
= note: adding a non-const method body in the future would be a breaking change
Tracking issues have a tendency to become unmanageable. Please open a dedicated new issue and label it with
F-const_trait_impl