Pre-RFC: Revamped const_trait_impl aka RFC 2632

Honestly, for the eventual RFC I'd like to see a clear, centralized good argument to exactly why just using : const Trait and : ?const Trait is poorer enough that we should instead introduce the new ~const sigil.

I think I understand the reasoning behind the new sigil[1], but the reasoning hasn't been written down in one central referencable location yet.


[1] My understanding:

  • We very much want : const Trait to require the impl to be const, such that the trait can be used in always const context (such as associated const) in this impl, whether this impl is being used as const or not
    • and even if it doesn't, we still want this semantic available and marked in the signature as significant, not e.g. implicit based on use
  • Using ?Sized as our reference point, : ?const Trait means that we the impl may or may not be const, and we have to assume the weakest position (that it isn't)
  • Due to :sparkles:reasons[2]:sparkles:, : Trait, even in const fn/impl const, must mean : ?const Trait
  • And that leaves us without any syntax space for : ~const Trait, with our desired semantics of "const Trait when used for this impl in a const context, ?const Trait when used for this impl in a nonconst context."

[2] The problem is that we failed to fully prevent the use of generic bounds in const fn, so it's possible to introduce a bound in a const context currently, where it has the semantics of : ?const Trait, and to use it meaningfully. And importantly, I think there's an example of usage of this hole in the wild (and we didn't patch it when it was first discovered, implicitly endorsing it).

The original RFC draft had : Trait in a const context as : ~const Trait. Assuming this is correct in that we can't have this semantic in current editions, I'd like to see an eventual RFC discuss the possibility of making this the default in future editions, warning previous editions on not specifying ?const, and only having ~const as the "desugaring" of the default and for editions where ?const is the (undesirable, but forced) default.

const fn foo<T>()
where
    PhantomData<T>: /* ?const */ Trait
{
    // compiles on stable rustc 1.31.0 - current 😭
}

This description of why ~const also explains why the apparent asymmetry of impl <Ty: ~const Trait> const Trait for _; we're providing const Trait, which can be requested directly, even if it's more common to request ~const Trait.


Addendum: one additional wrinkle to consider is async fn and any implications const Trait syntax has on people intuiting to have async Trait mean something, and thus ~async Trait bounds in async fn f<T: Trait>()... which could be good or bad depending on where the language is going. In the future where the default is changed to ~const I'd potentially be worried about teaching why const fn makes its type arguments ~const while async fn doesn't have a similar effect[3], I suppose...


[3] No pun intended, I swear

12 Likes

Has there ever been a crater run performed to see how widely (if at all) the accidental stabilization is relied upon?

1 Like
  • We can't use : const Trait to mean "const bound if called from a const context, regular bound if not", because it is inconsistent. And in the future we might give meaning to const modifiers, such as const fn(T) -> T which requires the function it points to to always be const. This could be useful for for example a macro, given the name of a function, ensures that the function is const.

  • We can't implicitly change the meaning of T: Trait in const fns because it comes in the way of future extensions. For example, function pointers and dyn Traits are alllowed in consts: const C: &dyn Trait and const C: fn() -> i32 are both stable today. If we introduce const modifier to them instead, it becomes inconsistent with T: Trait on const fns meaning what we have as T: ~const Trait today.

I speculate that people use this as a workaround when they need associated consts on traits on stable with generic functions.

I don't see how this specifically is a problem. dyn Trait and fn() don't show up in generic bounds. However, I do see where &dyn ~const Trait and ~const fn() are things that would be desired in the future, whether ~const is implied in generic bounds or not. (Plus, argument impl Trait is fun... would it follow the dyn Trait default of ?const or the generic argument default of ~const in this world?)

If we see it as always "desugaring" to ?const (allows nonconst), ~const (const if used as const), or const (always const), there's (imho) no inherent problem/inconsistency with generic arguments of const items defaulting to requiring ~const bounds as convenience for the common case (especially since you can make an argument that it's a ~const fn definition, anyway, since it may not be const if provided with nonconst arguments and used in a nonconst context).

It's kind of interesting tbh that const already effectively means const only in a const context. If anything, you can make an argument we want a stronger modifier for generic arguments that are required to be fulfilled with const-capable functionality.

In pure jest, I counterpropose that T: const Trait is only const when the defining item is used as const, and that a new syntax T: const!!! Trait is added for when T needs to always provide a const impl of Trait.

Also in pure jest, how about east const; T const: Trait and/or T: Trait const :stuck_out_tongue: (but please no don't both allow this and give it subtly different semantics)

1 Like

I've wanted to have bounds on const methods and have never even considered such a workaround. I understand that it's possible that some have used it; I am interested in how prevalent reliance on the accidental stabilization actually is.

2 Likes

umm, actually, they totally can show up in generic bounds:

struct AsRefDebuggable<'a>(&'a dyn Debug);
impl<'a> AsRefDebuggable<'a> {
    const fn new<T: ~const AsRef<dyn Debug>>(v: &'a T) -> Self {
        Self(v.as_ref())
    }
}

struct FfiData {
    data: *mut (),
    drop: unsafe extern "C" fn(*mut ()),
}

impl FfiData {
    unsafe const fn new<T: ~const Into<(*mut (), unsafe extern "C" fn(*mut ()))>>(v: T) -> Self {
        let (data, drop) = v.into();
        Self { data, drop }
    }
}

// impl Drop for FfiData { ... }

Likely it's too subtle of a distinction, but I'm making a difference between : AsRef<...>, as the generic bound, and dyn Debug, as a generic parameter (which happens to be within the generic bound, but only the immediate context matters).

@CAD97 to be clear, at least according to my understanding this syntax is entirely provisional. The WG needs some syntax to keep experimenting in this space; hopefully this syntax will not have to be stabilized.

For your actual point, I do think it would be very confusing if these meant very different things:

const fn foo0<T: Trait>(x: T)
const fn foo1(x: impl Trait)
const fn foo2(x: &dyn Trait)

For foo2, we have little choice but make it mean "x points to a type that implements Trait in any way, potentially non-const, so we cannot call trait methods". So I think if the situation in foo1 was different, that would be extremely confusing -- and that extends to foo0.

With the provisional syntax, we could have

const fn foo0<T: ~const Trait>(x: T)
const fn foo1(x: impl ~const Trait)
const fn foo2(x: &dyn ~const Trait)

and everything would still be coherent.

9 Likes

Is it possible that this could land as part of a future edition? cargo fix --edition could easily convert impl Trait to impl Trait + ?const. The only disadvantage (other than waiting until 2024) is that this would limit the use (though not implementation) of const traits in editions ≤ 2021.

What if trait methods with a default implementation couldn't be called from a const context at all unless they were specifically annotated as const fn?

trait Foo {
    fn foo(&self) -> i32;
    const fn bar(&self) -> i32 {
        [...] // something const-friendly here
    }
}

Wouldn't adding a default implementation to an existing method become a breaking change then?

No, because any existing const impl of that trait would have an existing const-friendly implementation which would be used instead of the default implementation.

Likewise you could add additional new non-const fn methods with a default impl to the trait, they just wouldn't be callable from a const context unless an explicit implementation were added to the const impl.

But what about generic users. If a trait

trait Foo {
    fn foo(&self) -> i32;
    fn bar(&self) -> i32;
}

changes to

trait Foo {
    fn foo(&self) -> i32;
    fn bar(&self) -> i32 {
        [...] // something not-const-friendly here
    }
}

then a user

const fn user<T: ~const Foo>(x: &T) {
    x.bar();
}

would break, wouldn’t it?

I'm suggesting that an alternative to "breaking" when adding something that isn't const-friendly is instead that functionality isn't usable in const contexts instead.

In that case the caller isn't expecting to use that functionality in a const context, so why should it break the caller just because some functionality was added which they aren't using?

I know there have been several proposals like this around object safety, namely that adding something which isn't object safe shouldn't break object safety for the whole trait, but rather only the "object safe subset" of the trait is available in that context. I think it even worked that way at one point, but it was changed.

Perhaps something like that could work here, and const callers could use the "const-safe" subset of a trait, allowing default methods which aren't declared "const-safe" to be added without breaking existing const impls.

That would require const-checking for the monomorphized MIR. And have bad performance. For T: ~const SomeTrait we need to go through the function and see its function calls to T, and check if the caller has const implementations for those functions.

Well, library authors should just split that kind of trait into two. One that is possible to have const implementations and one that is not.

In the future we might have some elaborated syntax like where <T as SomeTrait>::some_fn : ~const for fine-grained control for which function should have const implementations. Your suggestion is just inferring these fine-grained bounds from the method body.

After writing impl const From<OtherType> for MyType, I was expecting the corresponding blanket impl to be impl const Into<MyType> for OtherType, but it appears to be merely impl Into<MyType> for OtherType (note the lack of impl const for Into).

I came here to see if this simply hasn't been implemented yet, but saw no mention of this in the RFC. Am I raising this issue in the correct place?

My workaround will be to simply create the impl const Into<MyType> from OtherType directly, as this will provide the best ergonomics for my users, but, of course, there will be no corresponding From impls any more, which will be surprising/unidiomatic.

1 Like

This should work with the const_convert feature. It constifies the Into implementation as well as the other traits for the ? operator.

2 Likes

Oh! I will give that a shot. Thank you!

I gave it a try, but the compiler (rustc 1.57.0-nightly (a8f2463c6 2021-10-09) did not recognize the const_convert feature. Updated and same result for rustc 1.58.0-nightly (bd41e09da 2021-10-18). I also didn't see this feature listed in the Rust Unstable book.

error[E0635]: unknown feature `const_convert`

Can you confirm the feature name works for you?