--- title: ITE meeting 2023-10-19 tags: impl-trait-everywhere, triage-meeting, minutes date: 2023-10-19 discussion: https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/ITE.20triage.20meeting.202023-10-19 url: https://hackmd.io/OGw0xADiTQ265M0B3-JvSw --- # ITE meeting agenda - Meeting date: 2023-10-19 ## Attendance - People: CE, TC, oli ## Announcements or custom items (Meeting attendees, feel free to add items here!) ### Minimal TAIT stabilization proposal from T-types (Mini-TAIT) [TC:] The T-types meetup on 2023-10-11 included a discussion over the challenges that TAIT presents for the new trait solver and how to overcome those challenges. The result of the session was a consensus proposal for a minimal stabilization of TAIT that satisfies all T-types concerns. We'll describe that consensus proposal below. #### Signature restriction In the [last push](https://hackmd.io/oTC4J-2XRnukxC7lSq_PVA) to stabilize TAIT, we had introduced a signature restriction. This restriction requires that for functions and methods that constrain a hidden type, the opaque type whose hidden type will be constrained must fall within a type that appears in the signature of the function or method. This was done, among other reasons, as it improves the performance of the compiler and other tools such as rust-analyzer. The signature restriction was later constrained in the implementation such that projections are not normalized ahead of performing this check. This was done to make the implementation simpler. In the original proposal, this signature restriction was carefully specified in such a way that it could be removed in a backward compatible way. That is, we were leaving the door open for T-lang to later decide to remove the signature restriction and allow any function within the defining scope to constrain the hidden type. The main cost of leaving this door open is that, under the current implementation, we would emit cycle errors that would not be emitted if we closed this door. In the T-types proposal, we would close this door. The signature restriction would become a part of TAIT that could not be removed in a backward compatible way. As we'll describe below, combined with the other aspects of this proposal, the net result is that we will not emit *any* spurious cycle errors in the new solver. Any cycle error emitted will be a *real* cycle error, similar to the other ones that we emit in Rust today. #### Normalizes to opaque restriction As mentioned, for implementation reasons, the signature restriction has been constrained such that projections are not normalized ahead of the check. We want to leave room to later make this check smarter and allow some level of normalization to occur. Consequently, under this proposal, we would reject code with an error if any projection within the signature of an item normalized to an opaque type for which the item is within the defining scope. We would plan to lift this restriction incrementally for any classes of projections that the signature restriction was extended to allow. #### Once modulo regions restriction Consider: ```rust #![feature(type_alias_impl_trait)] type Tait<'x> = impl Sized; fn define<'a, 'b>(x: &'a u8, y: &'b u8) -> (Tait<'a>, Tait<'b>) { (x, y) // Old system: // // typeck: // Tait<'a> := &'a () // Tait<'b> := &'b () // // borrowck: // Tait<'a> := &'a () ~~> Tait<'x> := &'x () // Tait<'b> := &'b () ~~> Tait<'x> := &'x () // since they're equal after remapping, :check: // New solver: // Tait<'a> needs to be equal to &'a () // Since we haven't inferred anything yet for `Tait<'a>`, we register that definition in our opaque type storage. // Tait<'b> needs to be equal to &'b () // Since we have already inferred `Tait<'a> := &'a ()` // 1. Set tait signatures Tait<'b> = Tait<'a>, which requires 'b = 'a // 2. Set hidden types &'b () = &'a (), which also requires ^^ // again, this happens twice. once during typeck, where we throw away regions. // then during borrowck, which fails to prove 'a = 'b. } ``` Conceptually, this function is constraining the hidden type for the opaque `for<'x> Tait<'x>` to `()`. It constrains the hidden type twice, but that should be OK because each constraint is to the same type. The current trait solver accepts this code. The new solver does not. Furthermore, it's anticipated that the new solver will *never* accept this code for architectural reasons. In the new trait solver, we canonicalize all lifetimes to unique existential lifetimes in the root universe. This is to make sure that we avoid any lifetime dependence when proving obligations (architecturally required for borrowck and codegen to be sane and not have responses change between that and HIR typeck), and also it's better for caching (this isn't a blocking reason, just another). We propose a restriction such that... Multiple definitions of opaque types in the same body must not be equal modulo regions. Within a single item (because borrow checking happens within an single item and that's where we do opaque type inference), multiple uses of an opaque type that contains at least one lifetime parameter must not constrain the hidden type of that opaque to a hidden... This decision would not, strictly speaking, close a door on the *language* side to later allowing constraint twice modulo regions. However, T-types is asking for T-lang to consider this door as closed in a practical sense given the design of the new trait solver. TODO: Explain the differences between these examples: ```rust #![feature(type_alias_impl_trait)] // OK in old solver; error in new solver. #[cfg(False)] pub mod twice_modulo_lifetime_parameters { pub type Tait<'x> = impl Sized; pub fn define<'a, 'b>( _: &'a (), _: &'b (), ) -> (Tait<'a>, Tait<'b>) { ((), ()) } } // OK in both old and new solver. pub mod twice_modulo_type_parameters { pub type Tait<X> = impl Sized; pub fn define<A, B>(_: A, _: B) -> (Tait<A>, Tait<B>) { ((), ()) } } // OK in both old and new solver. pub mod twice_two_functions { pub type Tait<'x> = impl Sized; pub fn define_1<'x>(_: &'x ()) -> Tait<'x> {} pub fn define_2<'x>(_: &'x ()) -> Tait<'x> {} } // OK in old solver; overflow in new solver. // Forbidden for now by the must-define rule. // Not forbidden by the equal modulo regions rule. // But best-effort on implementation; may have implementation limitatinos. // Will probably be fixed by <https://github.com/rust-lang/rust/pull/116369>. But it may turn into an ambiguity rather than overflow, but probably not. #[cfg(False)] pub mod once_passthrough_twice { pub type Tait<'x> = impl Sized; pub fn define<'x>(_: &'x ()) -> Tait<'x> {} pub fn non_defining<'a, 'b>( x: &'a (), y: &'b (), ) -> (Tait<'a>, Tait<'b>) { (define(x), define(y)) } } // OK in both old and new solver. // Non-defining in `non_defining`. pub mod once_passthrough_twice_outside_scope { use inner::*; mod inner { pub type Tait<'x> = impl Sized; pub fn define<'x>(_: &'x ()) -> Tait<'x> {} } pub fn non_defining<'a, 'b>( x: &'a (), y: &'b (), ) -> (Tait<'a>, Tait<'b>) { (define(x), define(y)) } } ``` This should probably work due to the bound. It does work currently on both the new and the old solver. ```rust type Tait<'x> = impl Sized; fn define<'a: 'b, 'b: 'a>(x: &'a u8, y: &'b u8) -> (Tait<'a>, Tait<'b>) { ((), ()) } ``` #### Mention must define restriction Currently a function that passes the signature restriction *may* or *may not* actually constrain the hidden type. Such a function may, e.g., simply pass through the opaque type without constraining its hidden type. E.g.: ```rust #![feature(type_alias_impl_trait)] type Tait = impl Sized; fn define() -> Tait {} fn non_defining(x: Tait) -> Tait { x } ``` There are many use-cases for this. However, we propose that, for now, any function or method within the defining scope that passes the signature restriction *must* constrain the hidden type of any opaque type contained within a type that appears in the signature. This restriction is helpful because, due to lazy norm and the new trait solver being more *complete*, this is an area where there could be various subtle differences in what code might be accepted by the new and old trait solver. This is also an area where the old trait solver is underspecified, though it's believed that *could* be fixed. We would plan to later lift this restriction. #### One item may define restriction TODO: Rework wording a bit to make clear that only one item may define *anywhere*. Currently any number of items in the defining scope may include a single opaque type within a type mentioned in their signatures. E.g.: ```rust #![feature(type_alias_impl_trait)] type Tait = impl Sized; fn define() -> Tait {} fn non_defining(x: Tait) -> Tait { x } ``` There are many use-cases for this. However, we propose that, for now, only one item within the defining scope may mention within its signature any types that contain any single opaque type. This restriction is somewhat arbitrary. We have the machinery to allow this. However, adding this restriction prevents people from writing functions that pass through the opaque type before we're ready to allow that, and it will help us to more easily craft helpful error messages. We would plan to later lift this restriction. #### Properties of this proposal The main virtues of this proposal are twofold: 1. All code that might be accepted by the old solver but rejected by the new solver is disallowed. 2. All cycle errors produced by the new trait solver will be *real* cycle errors. One of the pain points of TAIT has been the presence of surprising cycle errors within the defining scope due to current implementation limitations surrounding the leakage of auto traits. Under this proposal, these surprising cycle errors will disappear *entirely* under the new solver, even after we lift the restrictions that we plan to lift. #### Future work: Constraining outside of the defining scope This proposal is forward compatible with future work that would allow the hidden type to be constrained outside of the defining scope using a new syntax. E.g.: ```rust #![feature(type_alias_impl_trait)] use taits::*; mod taits { type Tait<T> = impl Sized; } fn define<T>() where constrains(Tait<T>) { () } ``` ### Anonymous modules [TC:] (This is a very early draft of an idea... please be gentle...) One papercut with using TAIT to solve overcapturing is that the type alias may have to be placed far away from the method using that type alias. E.g.: ```rust #![feature(type_alias_impl_trait)] struct Foo; type CaptureRet = impl Sized; impl Foo { // ... // Pages and pages of methods... // ... // ...then... fn capture(&self) -> CaptureRet {} } ``` What if we could define an anonymous scope to contain the TAIT? Then we could say, e.g.: ```rust #![feature(type_alias_impl_trait)] struct Foo; impl Foo { // ... // Pages and pages of methods... // ... // ...then... mod { type CaptureRet = impl Sized; impl fn capture(&self) -> CaptureRet {} } } ``` This would also be better in the sense of more precisely matching the RPIT semantics. Under RPIT, the opaque type is not accessible by name elsewhere in the module. When desugaring to TAIT, we end up in practice having to make this name available in a wider scope. Having the ability to introduce smaller scopes would allow the RPIT desugaring while restoring a tighter scope. Here's how it would work. First consider the case outside of `impl` blocks. This... ```rust mod { type CaptureRet = impl Sized; pub fn capture() -> CaptureRet {} } ``` ...would desugar to... ```rust use _0::*; mod _0 { type CaptureRet = impl Sized; pub fn capture() -> CaptureRet {} } ``` And this... ```rust pub mod { type CaptureRet = impl Sized; pub fn capture() -> CaptureRet {} } ``` ...would desugar to... ```rust pub use _0::*; mod _0 { type CaptureRet = impl Sized; pub fn capture() -> CaptureRet {} } ``` Aside from TAIT, this would help with, e.g., correctly scoping `static` items that are used by more than one function. E.g.: ```rust mod { static FOO: () = (); pub fn one() { .. } pub fn two() { .. } } ``` Now, how would we handle use of this within `impl` blocks? We would define this such that `mod` within impl blocks essentially "lifts" the contents of the `mod` outside of the `impl` block. Then items that are specially marked `impl fn` get automatic methods defined that *forward* to the corresponding item outside of the impl block. For example, this... ```rust #![feature(type_alias_impl_trait)] struct Foo; impl Foo { // ... // Pages and pages of methods... // ... // ...then... mod { type CaptureRet = impl Sized; impl fn capture(&self) -> CaptureRet {} } } ``` ...would desugar to this... ```rust #![feature(type_alias_impl_trait)] struct Foo; mod _0 { type CaptureRet = impl Sized; fn capture(x: &Foo) -> CaptureRet {} } impl Foo { // ... // Pages and pages of methods... // ... // ...then... fn capture(&self) -> _0::CaptureRet { _0::capture(self) } } ``` ## Project board issues ### "AFIT: impl can't add extra lifetime restrictions, unlike non-async" #104689 - **Link:** https://github.com/rust-lang/rust/issues/104689 ### "Weird interaction between specialization and RPITITs" #108309 - **Link:** https://github.com/rust-lang/rust/issues/108309 ### "RPITIT with Send trait marker breaks borrow checker" #111105 - **Link:** https://github.com/rust-lang/rust/issues/111105 ### "`Failed to normalize` `async_fn_in_trait` ICE for indirect recursion of async trait method calls" #112047 - **Link:** https://github.com/rust-lang/rust/issues/112047 ### "Exponential compile times for chained RPITIT" #102527 - **Link:** https://github.com/rust-lang/rust/issues/102527 ### "Mysterious "higher-ranked lifetime error" with async fn in trait and return-type notation" #110963 - **Link:** https://github.com/rust-lang/rust/issues/110963 ### "AFIT: strange errors on circular impls" #112626 - **Link:** https://github.com/rust-lang/rust/issues/112626 ### "Nightly (warning): Async traits Self return requires type specification" #113538 - **Link:** https://github.com/rust-lang/rust/issues/113538 ### "hrtb + infer types break auto traits with return type notation " #109924 - **Link:** https://github.com/rust-lang/rust/issues/109924 ### "`async_fn_in_trait` and `return_type_notation` cause awkward awaits" #112569 - **Link:** https://github.com/rust-lang/rust/issues/112569 ## Pending PRs on the impl-trait-initiative repo None. ## Open PRs ### "Consider alias bounds when computing liveness in NLL (but this time sound hopefully)" rust#116733 - **Link:** https://github.com/rust-lang/rust/pull/116733 - **Labels:** final-comment-period, S-waiting-on-review, A-impl-trait, disposition-merge, F-type_alias_impl_trait, F-generic_associated_types, T-types ### "stricter hidden type wf-check" rust#115008 - **Link:** https://github.com/rust-lang/rust/pull/115008 - **Labels:** S-waiting-on-review, A-impl-trait, proposed-final-comment-period, disposition-merge, T-types, WG-trait-system-refactor