rust-lang/rust

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] attribute
  • impl const Trait
  • T: ~const Trait
  • append_const_msg on rustc_on_unimplemented
  • #[derive_const]
  • trait Destruct

Open issues

  • #88155
  • this test shows we can currently still call a const fn with a Trait bound even if the concrete type does not implement const Trait, but just Trait. 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 any impl 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 a k#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 on const impls as they are now insta-stable.
  • Treat default_method_body_is_const bodies and const impl bodies as stable const fn bodies. We need to prevent accidentally stabilizing an implementation that uses unstable lang/lib const 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);
}

Playground link for the above.

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:

@clarfonthey:

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.

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-
}

play

@slightlyoutofphase:

    // 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 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.

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)
}

Looks like the min_specialization implementation needs to be updated to account for ~const trait bounds. See #95186 and #95187.

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 call uwu in const context, T must const-implement Trait (no matter the body of uwu).
  • const fn uwu<T: Trait>(x: T) means that we check the body of uwu to determine whether uwu can be called with a non-const implementation of Trait.

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 `#![feature(const_trait_impl)]` for absolutely any topics you want to discuss or have questions about. See rust-lang/compiler-team#739 for details and discussions on this prospective policy.