owned this note
owned this note
Published
Linked with GitHub
---
title: "T-types TAIT overview"
tags: T-types, 2023-meetup-BRU, minutes
date: 2023-10-10 / 2023-10-11
url: https://hackmd.io/QOsEaEJtQK-XDS_xN4UyQA
---
# TAIT overview
## Old solver
Inferring opaques in the old solver is mainly the responsibility of two parts of the type system:
* Relations - `Eq`, `Sub`, `TypeRelating` (NLL) handle cases when opaques are equated with other types.
* `replace_opaque_types_with_inference_vars`, which manually folds a type, replacing any in-scope opaques with inference variables, which is itself used in to places:
* Checking a function body, replacing the TAITs and RPITs that show up in the signature with infer vars eagerly. This is necessary to handle fallback correctly.
* In projection predicates. This is necessary to not break backwards-compatibility with nested RPITs that don't meet bounds.
* [real world example in linkerd](https://github.com/rust-lang/rust/issues/99536)
* [A minimal test using no nightly features](https://github.com/rust-lang/rust/blob/master/tests/ui/impl-trait/nested-return-type2.rs)
Old solver (more notes):
- Replace with infer vars for RPITs and TAITs in function signatures — main place both are constrained
- For other cases, such as local variables and structs with TAIT return type, the eq and sub relations eagerly, depending on DefineOpaqueTypes
- Bubble is too inaccurate so we need to *not* define types eagerly in some cases to avoid spurious errors.
- Borrowck does not replace opaques eagerly with infer vars, and handles opaques in the relation
- Opaques in queries act subtly different due to DefiningAnchor::Bubble
- Projections are weird, can coincidentally replace opaques with infer vars outside of queries due to hack (link issue)
### `DefineOpaqueTypes`
Since `Eq` and similar relations all succeed when comparing an opaque type against any type satisfying its bound, we sometimes run into issues (similar to how comparing against inference vars always succeeds).
Contrary to its outdated documentation, it is not used just
> to prevent predicate matching from matching anything against opaque types.
But most usage sites (2/3rd) are now rejecting opaque types from being equal to any type fulfilling its bounds and registering said type as a hidden type.
It is a feature grown out of necessity of how we do type checking and how opaque types in the old solver work.
```rust
type Foo = impl Debug;
fn test() {
// case 1:
let x: Foo = 22_i32; // Coercion from i32 to Foo?
}
```
```rust
type Foo = impl Debug;
fn test() {
// case 2:
let a: Option<_> = None;
let x: Foo = a.unwrap(); // Coercion from ?X to Foo?
}
```
## Bubbles!!
:left_speech_bubble:
`DefiningAnchor::Bubble` --
* collect all the possible impact cases, re-apply at the call site, which is a bit risky
* in case we drop those annotations on the floor
* can define if things hold "modulo opaques"
* can also impact the flow of trait resolution in "potentially unexpected" ways
one challenging case:
* opaque1 = opaque2, which is the hidden type of the other?
* in bubble environment, we cannot know
* we currently create a predicate and bubble that out
## Background
- Don't want to go into much detail about how the `type_of` query actually delegates out to the `typeck` queries to find out where an opaque is defined. This doesn't change between the old solver, and is pretty well detailed [in the dev guide](https://rustc-dev-guide.rust-lang.org/opaque-types-impl-trait-inference.html#within-the-type_of-query).
New solver:
- Alias-relate predicate is responsible for all opaque eq ty, opaques are only constrained in obligations
- Returns constrained opaques in query response
```rust
type Foo = impl Sized;
fn main() {
let x: Foo = 22;
println!("{x}"); // does this compile or not? Yes in new solver, no in old solver.
let x: impl Debug = 22; // Does this just require Debug or is it like RPIT?
println!("{x}");
}
```
```rust
fn tyeq<T>(_: T, _: T) {}
fn main() {
type Foo = impl Sized;
let x: Foo = 42u32;
let y: impl Sized = 42u32;
tyeq(x, 0u32); // ?
tyeq(y, 0u32); // ?
}
```
`Self::Something = impl`
```rust
impl X {
type Something = impl Whatever;
fn foo() {
let x: Self::Something = ...;
}
}
```
Michael's point:
- Given a type lambda application `Lambda<...Ti...>`, it should be valid to change to `?X = Ti` and `Lambda<...?x...>`.
- Specific example: `T::Foo` normalization
errs: Recursive calls act as real opaques, not replaced w/ infer vars
```rust
fn foo() -> impl Debug {
if something {
foo()
} else {
22
}
}
```
```rust
fn foo() -> impl Debug {
let x: u32 = foo();
loop {}
}
```
errs (old solver):
```rust
fn eq<T>(a: T, _: T) -> T { a }
fn foo() -> impl Sized {
let x = eq(foo(), 1u32);
println!("{x}"); // error :(
loop {}
}
```
(Workaround is:)
```rust
fn eq<T>(a: T, _: T) -> T { a }
fn foo() -> impl Sized {
let x = eq(foo(), 1u32);
println!("{}", x as u32); // OK
loop {}
}
```
- Happens in super_combine for all relations (eq and sub don’t have the opaque handling code special to them)
- Opaques can end up being revealed locally later on
- Pre-populate borrowck
- no fulfillment context
Problems with new solver:
- Opaques can only be defined once modulo regions
- opaque use in the defining scope
```rust
type TAIT = impl Display;
fn use(x: TAIT) {
// this side-steps autoderef
Display::fmt(&x, ...);
// in old solver: this works
// in new solver: errors with ambiguity
x.fmt(...)
}
fn define() -> TAIT {
1
}
```
```rust
// Compiles today
// Fails in new solver
fn rpit_example(n: u32) -> impl std::fmt::Display {
if n > 0 {
rpit_example(n - 1).to_string()
} else {
format!("Hello, Ferris")
}
}
fn main() { }
```
project goals in non-defining use in defining scope: https://rust.godbolt.org/z/e5sbrrjP8
```rust
trait TAIT = impl Iterator<Item = u32>;
fn impls_iter<T: Iterator<Item = U>, U>(_: T) {}
fn non_defining_use(x: TAIT) {
impls_iter(x)
}
```
TC: Is there an old-solver/new-solver difference for?: https://rust.godbolt.org/z/1cTh8r5Mh
```rust
#![feature(type_alias_impl_trait)]
type Foo = impl Sized;
fn foo() -> Foo {}
fn bar() -> impl Sized { foo() }
//^~ error: opaque type's hidden type cannot be another opaque type from the same scope
// Question is:
// - do we choose to constrain `Foo` to the RPIT of `bar` (error) or vice versa (ok)
```
## How do we align new and old solver behavior
* Pass in a specific defining anchor instead of bubble
* this will impact caching
* something something wf
## Expected outcomes
- Some more confidence about the future of TAIT and its interaction with the new trait solver.
- ?
## Key questions for discussion
- ?
## Notes from the meetup itself
Niko: unsound examples of opaque types?
https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3AI-unsound+label%3AA-impl-trait
NM: What do we have to do to stabilize TAIT before the new trait solver is stabilized?
CE: I feel like, with a couple of exceptions, the new solver accepts more code than the old solver.
```rust
// Ok only in old solver, fundamental limitation in new solver
type Tait<'x> = impl Sized;
fn foo<'a, 'b>(x: &'a (), y: &'b ()) -> (Tait<'a>, Tait<'b>) {
(x, y)
}
// Ok in new and old solver
type Tait<T> = impl Sized;
fn foo<X, Y>(x: X, y: Y) -> (Tait<X>, Tait<Y>) {
(x, y)
}
// hacky workaround
fn foo<'a, 'b, T, U>(x: T, y: U) -> (Tait<T>, Tait<U>)
where
T: Into<&'a ()>,
U: Into<&'b ()>,
{
(x, y)
}
```
TC: What if it's non-defining?
```rust
#![feature(type_alias_impl_trait)]
// OK in old solver, overflow in new solver.
type Tait<'x> = impl Sized;
fn define<'x>(x: &'x u8) -> Tait<'x> { x }
fn non_defining<'a, 'b>(x: &'a u8, y: &'b u8) -> (Tait<'a>, Tait<'b>) {
(define(x), define(y))
// ^~ error[E0275]: overflow evaluating the requirement `Tait<'_> <: Tait<'a>`
}
```
https://rust.godbolt.org/z/TnsjeP4xP
Projection goals non-defining use in defining scope
```rust=
// passes with old impl, ambiguous with the new one.
#![feature(type_alias_impl_trait)]
trait Trait {
type Assoc;
}
impl<T: Copy> Trait for T {
type Assoc = T;
}
fn needs_trait<T: Trait<Assoc = T>>() {}
type Tait = impl Copy;
fn define() -> Tait {}
fn check() {
needs_trait::<Tait>();
//[new]~^ ERROR type annotations needed
//[new]~| NOTE cannot satisfy `<Tait as Trait>::Assoc == Tait`
}
fn main() {}
```
* oli made a bad PR :laughing:
* https://github.com/rust-lang/rust/pull/113169
## Things to do
* remove `Bubble` and always pass in the actual anchor, test perf on old solver
* `opaque_types_defined_by` query should return not just a list of opaques in the signature, but also a list of opaques that are in scope (and thus are in their defining scope)
* need to walk up the module tree and check all direct module members for type aliases that define TAITs (not walk into children of anything).
* get rid of `check_opaque_type_well_formed` or `check_opaque_meets_bounds`
* error if the same opaque type is constrained twice (with different lifetimes) in the same body (to be forward compat with the new solver)
* error on RPITs that are only constrained in dead code (to fix https://github.com/rust-lang/rust/issues/112417)
* make `opaque_types_defined_by` reject functions that mention the same TAIT twice with different lifetime params
* Error if projection in signature normalizes to include a TAIT.
* opaque_types_defined_by may have cycle errors if we do that (says errs, oli thinks he already implemented that and go no cycles)
* go through uses of `DefineOpaqueTypes::No` and either document or change them
* `E-easy`: flip it to `Yes`, see what tests fail, document the site by mentioning the test. If no tests fail, change them and leave a comment that there's no test excersising it.
* -Zunleash-the-tait-inside-of-you to turn off any errors that are not needed for soundness (e.g. only needed for semver)
-----
# Min-TAIT discussion
- Date: 2023-10-11
## Technical constraints...
- Cannot define TAITs twice modulo regions (New) :fearful: but :heavy_check_mark:
- This constraint will remain
- Can make the old solver act more like this
- can we create good diagnostics here
- also affects RPIT
- need additional book-keeping in new solver to be able to improve the region error: open an issue for this, not blocking anything
- Should make it an error when we have a defining function with two taits modulo regions
- TAITs act weirdly when aliases are in its defining usage's generic arguments (New) :heavy_check_mark:
- We can fix this in the new trait solver
- improve diagnostics plz
- underspecified where we define TAITs and where we treat them as rigid (New, Old)
- TAITs being defined to other TAIT :heavy_check_mark:
- One defining scope, one not-defining scope (Old)
- should define, possible by always providing the defining scope
- achievable if we don't use bubble 🫧🫧🫧
- Both defining scope (New, old)
- should always either hard error or be ambig
- auto trait leakage (New, old) :heavy_check_mark:
- Cycle errors (revealing auto traits)
- non-defining use in defining scope in new solver? doesn't work once there is an auto trait bound
- `Projection` goals for non-defining use in defining scope (New) :heavy_check_mark:
- defining scope changes will be breaking (new, old)
- (e.g.) No projection normalization in signature restriction
- could make everything defining and error if it's not allowed
- forces us to handle non-defining uses in defining scopes
```rust
// Emit an eager error here?
type Tait<'x> = impl Sized;
fn define<'a, 'b>(x: &'a u8, y: &'b u8) -> (Tait<'a>, Tait<'b>) {
(x, y)
}
```
```rust
// OK in old solver, overflow in new solver.
type Tait<'x> = impl Sized;
fn define<'x>(x: &'x u8) -> Tait<'x> { x }
fn non_defining<'a, 'b>(x: &'a u8, y: &'b u8) -> (Tait<'a>, Tait<'b>) {
(define(x), define(y))
// ^~ error[E0275]: overflow evaluating the requirement `Tait<'_> <: Tait<'a>`
}
```
```rust
// OK in old solver and new solver.
mod m {
pub type Tait<'x> = impl Sized;
pub fn define<'x>(x: &'x u8) -> Tait<'x> { x }
}
use m::*;
fn non_defining<'a, 'b>(x: &'a u8, y: &'b u8) -> (Tait<'a>, Tait<'b>) {
(define(x), define(y))
}
// https://rust.godbolt.org/z/7M89PhWxW
struct Inv<'a>(Cell<&'a ()>);
type Tait2<'x> = impl Sized;
fn non_defining_2<'a>(x: Inv<'a>) -> Tait2<'a> {
let q = ();
let p = &q;
if false {
let _: i32 = non_defining_2(Inv(Cell::new(p)));
}
1i32
}
```
## ...and how to avoid them
List of core issues:
- non-defining use in defining scope is not allowed
- we check it during type_of or whatever
ideas:
- Only define TAIT once
- Maybe once modulo regions, maybe once per body, once at all
- Treat only taits in signature as defining scope
Conceptually, we want TAIT to act as much like RPIT as possible.
- oli: having TAIT in arguments is not much more complicated
- errs: let statements too?
- lcnr: If the tait is somewhere in signature, theoretically it doesn't matter if you're able to name the TAIT in the function. It does matter if you can name the TAIT, people are gonna do weird stuff with it.
- errs: If you define it once, then you can name it any time you want.
- lcnr: It would prevent non-defining usages in defining scope, though.
```rust
// The minimum desirable?
mod tait {
type Tait = impl Sized;
struct Foo(Tait);
fn must_define() -> Foo { Foo(()) }
}
```
oli: Annoying rustc changes:
- https://github.com/rust-lang/rust/pull/113169/files#diff-39dc2906167489754ec214d1c9375ae076b3e70193c40daa00ee702ff658b006
- https://github.com/rust-lang/rust/pull/113169/files#diff-53c8c413368c8fc6167648c14aefe191cba4ac675fdf87935e693b9442519f9f
Newtype replacement pattern:
```rust
#![feature(type_alias_impl_trait)]
fn is_send<T: Send>(_t: &T) {}
pub use self::hidden::Opaque;
struct Concrete;
mod hidden {
use super::Concrete;
pub type Opaque = impl Sized;
pub(super) fn into_opaque(x: Concrete) -> Opaque { x }
pub(super) fn from_opaque(x: Opaque) -> Concrete { x as Concrete }
}
pub fn init() -> Opaque {
hidden::into_opaque(Concrete)
}
pub fn frob(x: Opaque) -> Opaque {
is_send(&x); // No cycle errors.
let x: Concrete = hidden::from_opaque(x);
hidden::into_opaque(x)
}
```
### Proposal :zero: (preferred)
- Forever:
- A function must have the TAIT (or a type that contains it) in the signature.
- or some defines syntax in the future
- For now (we never expect to relax):
- TAITs can only be defined once modulo regions.
- For now (can be relaxed later):
- A function that mentions the TAIT (or a type that contains it) in the signature, it must define it.
- Because of lazy norm and how the new trait solver is more complete, this is an area of difference between the old and new solver. It would be easy to break things here. And the old trait solver is underspecified here. (Oli: It is fixable in the old solver.) So we're saving space here.
- Only one function in the defining scope can mention the TAIT (or a type that contains it) in the signature.
- Can create a dedicated diagnostic for this case, avoiding all cycle errors and other hard to diagnose issues for users.
- This is the most arbitrary. We have the machinery to allow this. But it prevents people from writing functions that are passthrough. It allows us to write earlier and better diagnostics. But this is an artificial restriction we could lift easily. We could put this behind a separate feature gate.
- Error if projection in signature normalizes to include a TAIT.
- Saves space for making opaque_types_defined_by query smarter.
- Properties:
- All cycle errors are *real* cycle errors in the new solver.
- Changes that allow more items to define the TAIT to the signature rule would be breaking changes.
#### future work
- non-defining uses in the defining scope, "pass through pattern"
- defining uses outside of the defining scope: defines syntax
- attribute can be used to mark multiple (all defining!) functions as defining, and then the error goes away
- multiple functions can define the same TAIT
- change signature check to normalize projections
- normalizing to opaque types has to be a hard error before then, otherwise people use this as a design pattern
oli: if defines attr acts like an XOR then it's simple.
lcnr: XOR behavior is bad for IDEs
```rust
type Alias = impl Iterator<Item = u32>;
fn wrapper() -> Alias {
define(0)
}
fn define(x: u32) -> Alias {
[x].into_iter()
}
```
Defines in the WC outside of the defining scope:
```rust
mod taits {
pub type Foo<T> = impl Future<Output = T>;
}
use taits::*;
fn make_foo<T>() -> Foo<T>
where
defines(Foo<T>)
{ todo!() }
```
https://docs.rs/tokio-util/latest/tokio_util/sync/struct.ReusableBoxFuture.html
### Proposal :one:
- modules with a tait can only have a single function
- strictly more restrictive than proposal 0
### Proposal :two: :skull_and_crossbones:
- attributes ftw has to match fn to tait?
- annoying to implement :<
- unclear how exactly it should work, requires a bunch of design work
- probably gonna be blocked by T-lang
### Proposal :three:
oli: Only allow one usage of a TAIT in the entire crate
errs: no
### design constraints
- future compatibility
- clearly communicate constraints, e.g. lints/clear errors