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

11 Likes

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

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

4 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?