2021-Oct: Lang team update

Summary

  • oli-obk completed a rewrite of the inference engine and is in the process of landing it.
  • oli-obk is also pursuing a rewrite of the region checker to use intersection regions to address some of the shortcomings in the current system.
  • spastorino wrote a number of test cases covering the "feature surface area".
  • spastorino removed the (incorrectly implemented) support for let x: impl Trait; it will be re-evaluated later.
  • nikomatsakis is working on an explainer covering the "big picture" of TAITs.
  • tmandry + nikomatsakis are planning a proposal to permit users to name the types of associated functions from traits or inherent impls.
    • This opens the way for -> impl Trait notation in traits.

Goals for this month

  • Prepare stabilization report
  • Land oli's rewrite
  • Extend explainer
  • Extend evaluation doc to cover naming of impl traits

Questions for discussion

  • How long should we allow new inference engine to "bake" before stabilizing?
    • We have a large suite of test cases.
    • The inference engine is used for all impl Trait, including -> impl Trait and async fn, so it gets a lot of testing once it hits stable.

Type alias impl trait stabilization

The major focus remains "type alias impl trait" (TAIT) stabilization:


#![allow(unused)]
fn main() {
type Foo = impl Trait;

impl SomeTrait for SomeType {
    type AssocType = impl Debug;

    ...
}
}

Inference engine rewrite

oli-obk has completed a rewrite of the TAIT inference engine so that it matches the inference behavior of the RFC. This rewrite has been largely completed on a branch and is in the process of landing. It requires a number of supporting changes, such as improvements to NLL diagnostics.

The semantics that were implemented are briefly defined as follows. First, each TAIT has a "defining scope" based on where it appears:

  • In a module-level type alias: the defining scope is the enclosing module.
  • In the value of an associated type: the defining scope is the impl.

Within that defining scope, each item (function, etc) that references the TAIT defines a value for the TAIT based on the results of type-checking. Essentially, the TAIT's value is assumed at first to be an unbound inference variable, and we solve for whatever value of that variable will make the function type check. Each item must completely define the hidden type without relying on other items; further, each item must provide the same value for the hidden type. Example:


#![allow(unused)]
fn main() {
mod tait {
    pub type TAIT = impl Debug;

    // `foo` infers TAIT to be `u32`,
    // because that is the value needed
    // to make it type check:
    pub fn foo() {
        let x: TAIT = 22_u32;
    }

    // `get` also infers TAIT to be `u32`,
    // because that is the value needed
    // to make it type check:
    pub fn get() -> TAIT {
        22_u32;
    }

    // `take` also infers TAIT to be `u32`,
    // because that is the value needed
    // to make it type check. Calling `take`
    // from *outside* the module would not
    // be possible except by providing a return
    // value from `get`.
    pub fn take(x: TAIT) {
        let y: u32 = x;
    }

    // Other also constraints TAIT to be `u32`,
    // albeit indirectly.
    pub fn other() {
        take(22_u32);
    }
}
}

Outside of the defining scope, references to the type TAIT are opaque. The code is not permitted to rely on the hidden type, though it may peek at the auto traits implemented by the hidden type:

mod tait { ... /* as above */ ... }

fn main() {
    // Allowed
    tait::take(tait::get());

    // ERROR-- The hidden type is not visible outside of 
    // the module `tait`, so we can't rely on it being
    // `u32`.
    tait::take(44_u32);

    // ok -- TAIT is an `impl Debug`
    is_debug::<tait::TAIT>(); 

    // ok -- auto traits can rely on the hidden type,
    // and `u32: Send`
    is_send::<tait::TAIT>();

    // error -- even though `u32` implements `Display`,
    // we don't know the hidden type
    is_display::<tait::TAIT>();
}

fn is_send<T: Send>() { }
fn is_debug<T: Debug>() { }
fn is_display<T: Display>() { }

Naming impl Trait in traits

We would like to support -> impl Trait in traits and impls. One of the questions that has been blocking progress is the question of how to name the resulting associated type. In other words, the syntax


#![allow(unused)]
fn main() {
trait Foo {
    fn bar(&self) -> impl Debug + '_;
}
}

is equivalent to something like this but where Bar is an implicit associated type


#![allow(unused)]
fn main() {
trait Foo {
    type Bar<'me> = impl Debug + 'me;
    fn bar(&self) -> Self::Bar<'_>;
}
}

The problem is that users have no way to name Bar, and hence no way to name the return type of bar.

There have been numerous proposals to address this, but the most straightforward -- and also the most enabling -- seems to be allowing users to get access to the "zero-sized fn type" that represents bar.

The idea would be to define an associated type for every fn item, so that Foo::bar identifies the associated type for this fn in general, and <T as Foo>::bar identifies the associated type for the function as implemented for the type T.

This permits you to access the return type by writing T::bar::Output, for example, although we will have to extend the type checker to accommodate relative associated types like that (the full syntax would be <<T as Foo>::bar as FnOnce>::Output).

These are the known questions to address in the evaluation doc:

  • Can you name top-level functions or other sorts of functions?
    • What about the constructor functions for tuple structs or enum variants?
  • How to manage associated types and fns with the same name?
    • Do we have an unambiguous syntax such as T::fn(bar) to resolve that?
      • (This might apply to constructor functions as well.)