The new desugaring focuses entirely on traits and how they are treated with the effects system.
Before we get to the new desugaring, let's first take a look at the old desugaring:
#[const_trait]
trait Foo {
type Assoc;
fn method();
}
impl const Foo for () {
type Assoc = ();
fn method() {}
}
Before the new proposed desugaring, this currently desugars into:
trait Foo<const RUNTIME: bool> {
type Assoc;
fn method();
}
impl<const RUNTIME: bool> Foo<RUNTIME> for () {
type Assoc = ();
fn method() {}
}
This lead to problems with projections, as it is now possible for <T as Foo<true>>::Assoc != <T as Foo<false>>::Assoc
, and forces us to use syntax like <Type as ~const Foo>::Assoc
.
A discussion of this approach can be found in this Zulip thread. My biggest concern for this would be "how do we teach this", as the requirements for using <T as const Tr>::Assoc
vs <T as ~const Tr>::Assoc
vs <T as Tr>::Assoc
can be unclear/confusing to users.
But note that we would be in this situation only by suggesting that having a #[const_trait]
creates two separate traits for in a const environment vs in a non-const environment. This paradigm would then make associated types ambiguous. So what if we made it a single trait only?
#[const_trait]
trait Foo {
type Assoc;
fn method();
}
impl const Foo for () {
type Assoc = ();
fn method() {}
}
Let's revisit this example. Under the new desugaring, this would result in:
trait Foo {
type Effects;
type Assoc;
fn method<const RUNTIME: bool>() where Self::Effects: Compat<RUNTIME>;
}
impl Foo for () {
type Effects = Maybe;
type Assoc = ();
fn method<const RUNTIME: bool>() where Self::Effects: Compat<RUNTIME> {}
}
We're implementing one trait instead of two traits, which resolves the issue with projecting associated types.
The effects type could be one out of three types: Maybe
, which means both const and runtime environment allowed, NoRuntime
, which only allows running in const, and Runtime
, which only allows running in runtime.
Each method gets their own RUNTIME
parameter, and we use the where clause to guard against const contexts calling methods on traits only implemented for a runtime context. The Compat
trait is defined as follows: (this should be what you expect)
#[lang = "EffectsCompat"]
pub trait Compat<#[rustc_runtime] const RUNTIME: bool> {}
impl Compat<false> for NoRuntime {}
impl Compat<true> for Runtime {}
impl<#[rustc_runtime] const RUNTIME: bool> Compat<RUNTIME> for Maybe {}
Now, there are many places a ~const
bound can appear while interacting with the system of encoding effects through an associated type. Here's a list of them:
We'll take a look at how these can be desugared.
What happens to ~const
bounds on methods? Take the example
#[const_trait]
trait Foo {
fn method<T: ~const Bar>();
}
We'd desugar it as
trait Foo {
type Effects;
fn method<const RUNTIME: bool, T: Bar>() where
Self::Effects: Compat<RUNTIME>,
<T as Bar>::Effects: Compat<RUNTIME>;
}
Thus, when we are calling from a runtime context, T: Bar
must be compatible with calling from runtime, and vice versa when in a const context. The Compat
bound does all the work in checking that effects requirements are satisfied.
TyCompat
trait and the Min
traitSuppose we have a where clause on a trait.
#[const_trait] trait Foo<T> where T: ~const Bar {}
This clearly means that the current effect must be a subset of the effect on T: Bar
. Therefore, we can use a TyCompat
trait (naming bikesheddable) to encode this:
trait Foo<T> where T: Bar {
type Effects: TyCompat<<(<T as Bar>::Effects,) as Min>::Output>;
}
Where we can define TyCompat as
trait TyCompat<Super> {}
impl TyCompat<Maybe> for NoRuntime {}
impl TyCompat<Maybe> for Runtime {}
impl<T> TyCompat<T> for T {}
Where clauses on impls will cause the impl's effect to be the minimum of all effects.
impl<T> const Foo<T> for Uwu<T> where T: ~const Bar + ~const Baz {}
// desugars into
impl<T> Foo<T> for Uwu<T> where T: Bar {
type Effects = <(<T as Bar>::Effects, <T as Baz>::Effects) as Min>::Output;
}
The Min
trait represents an operator for intersection, with for example (Maybe, Runtime): Min<Output = Runtime>
, (Maybe, Maybe): Min<Output = Maybe>
, and (Runtime, NoRuntime): !Min
.
Bounds on associated types need to relate to the effects on the current trait. If we had
#[const_trait]
trait Foo {
type Assoc: ~const Bar;
}
We would expect Assoc
to fully allow the environment impl Foo for Self
has. That is, if Foo
is implemented with Maybe
(both runtime and const allowed), then Self::Assoc
should also be implemented with Maybe
. If Foo
is implemented with Runtime
or NoRuntime
, then Self::Assoc
should be implemented with the same effect or Maybe
which is more relaxed.
We can encode this with a trait bound, to desugar this into type Assoc: Bar<Effects: TyCompat<Self::Effects>>
.
#[const_trait] trait Bar {}
#[const_trait] trait Foo: ~const Bar {}
would desugar into
trait Bar { type Effects; }
trait Foo: Bar {
type Effects: TyCompat<<(<Self as Bar>::Effects,) as Min>::Output>;
}