Effects Summary

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:

  • Bounds on a trait/impl method
  • Bounds on an associated type in a trait
  • Super traits
  • Where clauses on trait/impl

We'll take a look at how these can be desugared.

bounds on method

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.

where clauses, the TyCompat trait and the Min trait

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

assoc type bounds in traits

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

super traits

#[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>;
}