--- title: "Design meeting 2023-11-08: Minimal TAIT stabilization proposal from T-types (Mini-TAIT)" date: 2023-11-08 tags: T-lang, design-meeting, minutes discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/Design.20meeting.202023-11-08 url: https://hackmd.io/CDj92gZdTzilDWORdKhLQA --- # Minimal TAIT stabilization proposal from T-types (Mini-TAIT) The T-types meetup on 2023-10-11 included a discussion on the challenges that type alias `impl Trait` may present 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 in this document. # Recent history of TAIT / executive summary TAIT ([RFC 2071][] / [RFC 2515][]) with module scoping was nearly stabilized in 2022. It was delayed due to documentation concerns, then by concerns over how it might impact `rust-analyzer`. To address the `rust-analyzer` concerns, we devised the *signature restriction*, designed as a strict subset of the RFC-approved module-scoped TAIT, and presented this at the T-lang design meeting on [2023-05-31][T-lang design meeting 2023-05-31]. The consensus of that meeting to move forward with this plan was captured in an [FCP on #107645][#107645 FCP]. However, there have been some lingering concerns from T-types about how stabilizing TAIT without further restrictions under the old trait solver may create backward compatibility hazards when stabilizing the new trait solver. In collaboration with T-types, we propose a series of restrictions to address those concerns. There are three classes of restrictions proposed: 1. Those that are permanent once we stabilize TAIT. - **The signature restriction:** This restriction was accepted in the [FCP on #107645][#107645 FCP], but the rules of what code to accept were defined in such a way that the restriction could be later removed. We propose to commit to this restriction. 2. Those that will be permanent once we stabilize the new trait solver. - **Once modulo regions restriction:** This restriction is related to the fact that lifetimes do not participate in trait selection in Rust. The new trait solver relies on this invariant, and it results in this restriction. 3. Those that may be lifted once we stabilize the new trait solver. - **Mention must define restriction**: This restriction is to remove areas of potential incompatibility between the old trait solver and the new one. There is one further restriction which *may* (or may not) need to exist temporarily: - **Cannot normalize to opaque restriction:** This one is to leave room for [#116819][] to land. This represents a minor compiler limitation and we propose that T-lang allow this restriction to be lifted once these normalizations are supported without further action from T-lang. As a consequence of these restrictions, we propose to *lift* a restriction that was part of the [#107645 FCP][]: - **Nested inner items restriction:** Due to the details of this proposal, this restriction cannot necessarily be lifted later, so we must commit to the right choice, and for reasons we'll describe below, the right choice may be to lift it. We'll discuss each of these restrictions below. We'll also discuss the future possibility of allowing the hidden type to be defined outside of the defining scope with new syntax. We'll discuss what we're committing to and why we should commit to that. And we'll talk about what options we are leaving ourselves going forward. # Reminder of the motivation for TAIT The `async`/`await` feature of Rust was [carefully designed][Why async Rust?] in such a way that it does not require futures to be boxed. This zero-cost approach allows users to hit high performance targets and to use it in restricted environments such as embedded. However, in practice, because futures generated by `async` blocks cannot be named and because we have not yet stabilized TAIT, users must either [box][ReusableBoxFuture] most futures or use nightly Rust. This stabilization is, in a sense, the completion of the `async`/`await` story. In a broader sense, by allowing opaque types to be named, TAIT allows *interfaces* to return opaque types without creating problems for downstream users. Currently, in Rust, it's considered an anti-pattern to return an opaque type (using RPIT) from an API. Because the user of such an API cannot name the returned type, that user cannot e.g. store that type in a struct unboxed. If the trait in the bounds of the opaque type is not object-safe, the effective limitation is even more severe. It's for this reason that new `Iterator` combinators in the standard library are not implemented using RPIT. Many other libraries in the ecosystem follow similar rules. TAIT solves this problem entirely. See [Appendix B][] for more discussion of this. # Signature restriction > *If an item within the defining scope does not pass the signature restriction (as defined in [#107645][#107645 FCP]) for a particular opaque type, the compiler can assume that the item does not define the hidden type, and it will not be backward compatible to allow in the future such an item to define the hidden type without some new syntax.* To address the concerns that delayed the 2022 stabilization of TAIT, we introduced the signature restriction, built on top of the existing module scoping of the approved RFCs, and presented it at the T-lang design meeting on [2023-05-31][T-lang design meeting 2023-05-31]. The consensus of that meeting to move forward with module scoping and the signature restriction was captured in an [FCP on #107645][#107645 FCP]. This restriction requires that for items that constrain a hidden type, the opaque type must fall within a type that appears in the signature of the item. This was done, among other reasons, as it allows for a more simple and efficient implementation of incremental compilation in `rustc` and provides similar benefits for other tools such as `rust-analyzer`. In the May 2023 proposal, this signature restriction was carefully specified such that it could be removed in favor of allowing any item within the defining scope to constrain the hidden type regardless of the contents of its signature and without any additional syntax. **We propose to instead commit to this restriction.** Due to the tooling advantages, it's very unlikely that we would ever want to remove this restriction. By committing to this restriction, and in combination with the "mention must define" restriction described later, we achieve two things: First, we leave space for the new trait solver. In the new trait solver, lazy normalization causes us to try to define hidden types in different places than under the old solver. We have open questions on exactly how this lazy normalization will work, and we need to save some space. These restrictions do that. Second, we eliminate all spurious cycle errors. Even after lifting all restrictions we propose to later allow lifting, we can guarantee there will be no spurious cycle errors. Spurious cycle errors have been the main generator of negative feedback about TAIT in nightly. They can be opaque, frustrating, and can drive people away from using TAIT. Eliminating them entirely as this proposal does is a serious improvement to the user experience. Committing to the signature restriction is still forward compatible with later adding a syntax that would allow specifying the items that may define the hidden type without needing to pass the signature restriction. The signature restriction does not limit expressiveness, and many common and intended patterns for using TAIT trivially pass the signature restriction. See [Appendix B][] for further details and potential caveats. # Once modulo regions restriction > *Within a single item, when multiple defining uses of an opaque type differ only by lifetime arguments, we must be able to prove that those lifetime arguments are equal, otherwise the code will be rejected, and once the new trait solver is stabilized, it will likely not be possible to lift this restriction in a fully backward compatible way.* In Rust, lifetimes do not participate in trait selection. One consequence of that is that we accept this code: ```rust trait Trait<X> { fn method(); } fn test<A, B, T>() where T: Trait<A> + Trait<B>, {} ``` But we reject this seemingly-similar code: ```rust trait Trait<'x> { fn method(); } fn test<'a, 'b, T>() where T: Trait<'a> + Trait<'b>, // ~^ ERROR type annotations needed: cannot satisfy `T: Trait<'a>` // ~| NOTE multiple `impl`s or `where` clauses satisfying `T: Trait<'a>` found {} ``` In the context of RPIT opaque types, we accept this code: ```rust trait Trait<T> {} impl<T, U> Trait<T> for U {} fn capture<A, B>() -> impl Sized + Trait<A> + Trait<B> {} fn test<A, B>() -> impl Sized + Trait<A> + Trait<B> { capture::<A, B>() } ``` But we reject this code: ```rust trait Trait<'a> {} impl<T> Trait<'_> for T {} fn capture<'a, 'b>() -> impl Sized + Trait<'a> + Trait<'b> {} fn test<'a, 'b>() -> impl Sized + Trait<'a> + Trait<'b> { // ~^ ERROR lifetime may not live long enough // ~| HELP consider adding the following bound: `'a: 'b` // ~| HELP consider adding the following bound: `'b: 'a` // ~| HELP `'a` and `'b` must be the same: replace one with the other capture::<'a, 'b>() } ``` Similarly, for type alias `impl Trait`, we will accept this code: ```rust #![feature(type_alias_impl_trait)] type Tait<T> = impl Sized; fn define<A, B>(x: A, y: B) -> (Tait<A>, Tait<B>) { (x, y) } ``` But we must reject this code: ```rust #![feature(type_alias_impl_trait)] type Tait<'x> = impl Sized; fn define<'a, 'b>(x: &'a (), y: &'b ()) -> (Tait<'a>, Tait<'b>) { // ~^ ERROR lifetime may not live long enough // ~| HELP consider adding the following bound: `'a: 'b` // ~| HELP consider adding the following bound: `'b: 'a` // ~| HELP `'a` and `'b` must be the same: replace one with the other (x, y) } ``` This is the "once modulo regions" restriction. Within a single item, when multiple defining uses of an opaque type differ only by lifetime arguments, we must be able to prove that those lifetime arguments are equal, otherwise the code will be rejected, and once the new trait solver is stabilized, it will likely not be possible to lift this restriction in a fully backward compatible way. The restriction is scoped within a single item because borrow checking happens at the level of single items and that is where we do opaque type inference. This restriction is required for the new trait solver. It's tied to a type system invariant that [trait solving must be lifetime agnostic][]. We never expect this restriction to be lifted. See [Appendix C][] for further details. # Mention must define restriction > *Until the new trait solver is stabilized, any item within the defining scope that passes the signature restriction for some opaque type must constrain the corresponding hidden type.* Currently an item that passes the signature restriction *may* or *may not* actually constrain the hidden type. Such an item 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 passthrough(x: Tait) -> Tait { x } ``` There are many use-cases for this. However, we propose that, for now, any item 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 normalization 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 may later lift this restriction after the new trait solver lands and we better understand the interactions between lazy normalization and how hidden types for opaques are defined. We conceivably could also lift this restriction with some form of new syntax that would indicate that certain items cannot define some or all hidden types. # Cannot normalize to opaque restriction > *Until [#116819][] or a similar PR lands, if any projection within the signature of an item normalizes to an opaque type for which that item is in the defining scope and for which that item was not already considered defining, the code will be rejected. This restriction represents a minor compiler limitation and may be lifted once these normalizations are supported without further action from T-lang.* There's one restriction that we may or may not need. There are no open questions here. It's simply a matter of implementation progress. The restriction has to do with the fact that, because we are now relying on the signature restriction for type inference, we cannot change these rules later. So we need to save space for any parts of the signature restriction check that are not fully implemented. There is one case of this. It's that we currently do not normalize projections when checking the signature restriction. In PR [#116819][], we are working to fix this. Until [#116819][] or a similar PR lands, if any projection within the signature of an item normalizes to an opaque type for which that item is in the defining scope and for which that item was not already considered defining, the code will be rejected. This restriction represents a minor compiler limitation and we propose that T-lang allow it to be later lifted once these normalizations are supported without further action from T-lang. E.g., we would temporarily reject this code: ```rust #![feature(type_alias_impl_trait)] type Opaque = impl Sized; trait Trait { type Assoc; } struct Type; impl Trait for Type { type Assoc = Opaque; } fn define() -> Opaque {} fn error(_: <Type as Trait>::Assoc) {} // ~^ ERROR item signature contains opaque type through projection ``` # Lifting nested inner items restriction [nested-inner-items]: #Lifting-nested-inner-items-restriction > *Revising one element on the [#107645][#107645 FCP] consensus, items nested inside of functions may define the hidden type for opaques declared outside of those functions without those functions having to recursively pass the signature restriction.* The [#107645 FCP][] included the following restriction: > Nested functions may not constrain a hidden type from an outer scope unless the outer function also includes the hidden type in its signature. This restriction was part of saving space for [RFC PR 3373][] to potentially land for Rust 2024. That RFC would break backward compatibility to restrict "sneaky inner impls" and any other features of Rust that might require parsing and type checking all other function bodies in the crate in order to type check the current function body. E.g., this is a "sneaky inner impl" allowed in Rust today: ```rust trait Trait {} struct S; fn foo() { impl Trait for S {} } ``` People who write IDEs would prefer that this were not possible. Because of how opaque types leak auto traits, TAIT potentially presents a similar case. For example: ```rust // Not allowed under the signature restriction. type Foo = impl Sized; fn foo() { let _: Foo = (); } ``` To know whether `Foo: Send`, we need to know the hidden type. To know the hidden type, if that were allowed, we would need to parse and type check all function bodies within the defining scope of `Foo`. However, due to the restrictions we propose in this document, we know from the signature exactly which functions within the defining scope define the hidden type. We only need to parse and type check *one* of those functions to determine whether `Foo: Send`. **So that's not a problem.** However, there's another case: nested inner *items*. E.g., the current implementation allows this code: ```rust type Foo = impl Sized; fn foo() { fn inner() -> Foo {} let _ = inner(); } ``` Here, `inner` is defining the hidden type but `foo` is not. To know whether `Foo: Send`, we must *parse* and perform the *signature restriction check* (but not type check) all functions within the defining scope. This allows us to find any functions that define the hidden type. We then must type check *one* of those functions. We propose that unless and until [RFC PR 3373][] is accepted and becomes part of some future edition, this is acceptable and that this restriction should be lifted. In support of this, we argue: - The restriction would rule out certain useful patterns. - It would be inconsistent with Rust until and unless we accept RFC PR 3373. - There's no value to partially solving the sneaky inner impls problem. - RFC PR 3373 is unlikely to be accepted for Rust 2024, considering the timeline. - If RFC PR 3373 were later accepted, we could fix it then. - Only parsing and signature restriction checking are required, not type checking, and this may be a significant difference. - We're speculating rather than measuring, and the burden of proof should fall on those proposing to change the status quo and impose this restriction, as imposing it has costs. - We cannot necessarily lift this later, so we must commit to the correct choice now. In the interest of making this document readable in one meeting, we have moved the elaboration of each of these points into [Appendix G][]. Please consult that appendix for further details. It's worth highlighting in particular, however, why there is no value in partial solutions. **matklad** [stated this][matklad on 3373 AON] concisely: > I guess my overall thrust here is that, if we do this, its important to do 100% of it. We don't want to forbid this because it's an unreadable mess: no need to forbid something which isn't used anywhere outside of dtolnay's famous quiz. We want to forbid this, because we want to make the implementation faster. In some future edition, we want to just throw every function onto a thread pool and type check the world on all cores, without any synchronization. If there's at least one case where we need to invalidate the type checking results of `foo` when we learn something new when checking `bar`, this optimization simply doesn't work. **In summary**: We have to commit to a decision here as we can't necessarily change this later. The right decision is to be consistent with the rest of the language as it exists today. # Properties of this proposal The main virtues of this proposal are twofold: 1. All code that might be accepted by the old trait solver but rejected by the new trait solver is disallowed. 2. All cycle errors produced by the new trait solver will be *real* cycle errors. One of the major 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 lifting the restrictions that we allow to be lifted. # Future work: Constraining outside of the defining scope [defines-outside]: #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 within the same crate but outside of the defining scope using a new syntax. E.g.: ```rust #![feature(type_alias_impl_trait)] #![feature(todo_define_tait_anywhere_in_crate)] use taits::*; mod taits { type Tait<T> = impl Sized; } fn foo<T>() defines Tait<T>, {} ``` One useful property of such future work is that those who wish to not rely on the signature restriction and wish to always specify with syntax which items may constrain the hidden type of some opaque may do so simply by placing their TAITs in a submodule as above. # On a `defines` syntax [on-defines]: #On-a-defines-syntax Prior to the full design and adoption of the signature restriction, there had been discussion of using some new syntax to specify which items could define the hidden type of an opaque. For reasons of implementation experience, open design questions, and other factors, this option was discarded prior to the 2023-05-31 T-lang design meeting. For example, on 2023-04-11, T-lang [decided][T-lang 2023-04-11 consensus] via meeting consensus that a new `defines` syntax would require a new RFC. Therefore, in the 2023-05-31 design meeting, we focused on the details of the signature restriction and how it, combined with the module scoping rules adopted in [RFC 2071][] and [RFC 2515][], addressed the then-blocking concerns. Since `defines` was not discussed in that meeting, for sake of completeness, we will summarize briefly in this section why a `defines` syntax was and should be set aside for the time being. The main points are as follows: - We can add a `defines` syntax later and while preserving backward compatibility. See the [future work][defines-outside] section above for an example of this, and [Appendix D][] for the forward compatibility matrix. - A new `defines` syntax would raise novel questions of design and implementation that do not have clear answers. - There exist concrete and significant technical problems that impeded attempts to implement earlier speculative `defines` proposals. - We do not expect that these design or implementation hurdles could be resolved early enough for TAIT using a new `defines` syntax to land ahead of Rust 2024, and if that does not happen, in addition to prolonging the other problems that accrue by not having stable TAIT, that jeopardizes stabilizing the 2024 lifetime capture rules for RPIT opaque types. - If we are unable to stabilize the 2024 lifetime capture rules, then the behavior of RPIT would be inconsistent, a point that will be made more apparent by the newly stabilized RPITIT, throughout the lifetime of Rust 2024. In the interest of ensuring this document can be read within a single meeting, the elaboration of each of these points has been moved to [Appendix I][]. Please consult that appendix for further details. For details on how the signature restriction captures common patterns, please refer to [Appendix B][]. **In summary**: The current proposal is forward compatible with everything we might conceivably want to do with a `defines` syntax. Let's move forward with the current carefully constructed plan and implementation and consider all of the interesting ways we might extend this with future work later. # Conclusion The plan we propose in this document allows for TAIT to be stabilized while leaving the necessary room for the new trait solver to also be stabilized. This plan was formed in close collaboration with T-types. Stabilizing TAIT helps to complete the story of `async`/`await` and makes it possible to return opaque types from (and accept those opaque types back into) APIs without that creating problems for downstream users. This eliminates a surprising anti-pattern from the language. We propose the following language for a T-lang FCP: T-lang agrees that, for the stabilization of type alias `impl Trait` (TAIT): - If an item within the defining scope does not pass the signature restriction (as defined in [#107645][#107645 FCP]) for a particular opaque type, the compiler can assume that the item does not define the hidden type, and it will not be backward compatible to allow in the future such an item to define the hidden type without some new syntax. - Within a single item, when multiple defining uses of an opaque type differ only by lifetime arguments, we must be able to prove that those lifetime arguments are equal, otherwise the code will be rejected, and once the new trait solver is stabilized, it will likely not be possible to lift this restriction in a fully backward compatible way. - Until the new trait solver is stabilized, any item within the defining scope that passes the signature restriction for some opaque type *must* constrain the corresponding hidden type. - Until [#116819][] or a similar PR lands, if any projection within the signature of an item normalizes to an opaque type for which that item is in the defining scope and for which that item was not already considered defining, the code will be rejected. This restriction represents a minor compiler limitation and may be lifted once these normalizations are supported without further action from T-lang. - Revising one element on the [#107645][#107645 FCP] consensus, items nested inside of functions may define the hidden type for opaques declared outside of those functions without those functions having to recursively pass the signature restriction. # Acknowledgments Thanks to Oli (@oli) and to Michael Goulet (@compiler-errors) for helpful discussions and insights on this topic. Thanks for Tyler Mandry (@tmandry) for his ongoing collaboration. And thanks to both Michael Goulet (@compiler-errors) and Tyler Mandry (@tmandry) for reading drafts of this document and providing helpful notes. All errors and omissions remain those of the author alone. # Done reading here Once you've made it here, please indicate that you're done reading. If you have time, you can peruse the appendices while others finish reading. Jump to the questions down in the [minutes][]. # Appendix A: References - [2023-11-08 T-lang design meeting: Mini-TAIT stabilization proposal](https://hackmd.io/CDj92gZdTzilDWORdKhLQA) - [2023-11-05 Mini-TAIT stabilization proposal](https://hackmd.io/sgr4TbJrR_CxEu5jLezI-A) - [2023-11-05 Mini-TAIT stabilization proposal - supplemental materials](https://hackmd.io/cuq02C6JSl-nIS_0YeK-Ew) - [2023-10-31 Attempt to resolve #107645](https://hackmd.io/mHmsSToOSkCFP5cOezFScw) - [2023-10-30 Attempt to resolve #107645](https://hackmd.io/TsRfxTpfQGKy_zXcQm8KDA) - [2023-10-26 Description of T-types proposal](https://hackmd.io/qiy4_I3WRYyhpYvjbYBrew) - [2023-10-19 Prevent opaque types being instantiated twice with different regions within the same function - #116935](https://github.com/rust-lang/rust/pull/116935) - [2023-10-16 Normalize when collecting TAITs in signature - #116819](https://github.com/rust-lang/rust/pull/116819) - [2023-10-11 T-types TAIT session minutes](https://hackmd.io/QOsEaEJtQK-XDS_xN4UyQA) - [2023-09-13 RPITIT stabilization - #115822](https://github.com/rust-lang/rust/pull/115822) - [2023-07-26 RFC 3498 - Lifetime capture rules 2024](https://github.com/rust-lang/rfcs/pull/3498) ([design meeting](https://hackmd.io/sFaSIMJOQcuwCdnUvCxtuQ)) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/design.20meeting.202023-07-26)) - [2023-07-11 T-lang triage diffs on TAIT](https://hackmd.io/_eMqgF3JQgGEN4Y6C9C1pg#Diffs-on-TAIT-TC) - [2023-06-29 TAIT must be constrained if in signature PR](https://github.com/rust-lang/rust/pull/113169) - [2023-06-29 Oli/lcnr meeting on TAIT](https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/lcnr.20oli.20meeting/near/370710606) - [2023-06-29 TAIT mini-design meeting](https://hackmd.io/r1oqcjrzTAK5e_T1IOXeXg) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/TAIT.20mini-design.20meeting.202023-06-29)) - [2023-06-27 T-lang triage attempt to revise nested inner items restriction](https://hackmd.io/hTUmwMrbSSqN1eU2k90Iwg#TAIT-nested-inner-functions-restriction-take-2-TC) - [2023-06-13 TAIT tracking issue proposed stabilization FCP canceled](https://github.com/rust-lang/rust/issues/63063#issuecomment-1588994092) - [2023-06-12 T-types TAIT in new trait solver document](https://hackmd.io/llGcGMR7SvCP1C1MulcDQw) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/326132-t-types.2Fmeetings/topic/2023-06-12.20TAIT.20in.20new.20solver/near/365570768)) - [2023-06-06 lcnr resolves concern about allowing WCs in signature restriction](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/design.20meeting.202023-05-31.20TAITs/near/363984835) - [2023-06-01 TAIT defining scope options proposed FCP](https://github.com/rust-lang/rust/issues/107645#issuecomment-1571789814) - [2023-05-31 T-lang TAIT design meeting](https://hackmd.io/IVFExd28TZWm6iyNIq66PA) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/design.20meeting.202023-05-31.20TAITs)) - [2023-05-31 TAIT draft stabilization report](https://hackmd.io/oTC4J-2XRnukxC7lSq_PVA) (not updated with T-types proposal) - [2023-04-11 T-lang decides a new RFC would be required for a `defines` syntax](https://github.com/rust-lang/rust/issues/107645#issuecomment-1504041903) - [2023-03-20 lcnr update on new trait solver concern](https://github.com/rust-lang/rust/issues/63063#issuecomment-1476196975) - [2023-02-06 lcnr concern over new trait solver](https://github.com/rust-lang/rust/issues/63063#issuecomment-1418741032) - [2023-02-03 TAIT defining scope options - #107645](https://github.com/rust-lang/rust/issues/107645) - [2023-01-20 Proposed RFC: Avoid non-local definitions in functions - #3373](https://github.com/rust-lang/rfcs/pull/3373) - [2023-01-17 TAIT tracking issue concern over defining scope](https://github.com/rust-lang/rust/issues/63063#issuecomment-1386064436) - [2022-12-24 TAIT tracking issue concern over updating reference](https://github.com/rust-lang/rust/issues/63063#issuecomment-1364525286) - [2022-12-20 proposed FCP merge of TAIT stabilization](https://github.com/rust-lang/rust/issues/63063#issuecomment-1360043060) - [2022-12-16 TAIT stabilization report](https://github.com/rust-lang/rust/issues/63063#issuecomment-1354392317) - [2021-03-09 FCP close on "Consider deprecating weird nesting of items in next edition - #65516"](https://github.com/rust-lang/rust/issues/65516#issuecomment-794211337) - [2019-10-17 Consider deprecating weird nesting of items in next edition - #65516](https://github.com/rust-lang/rust/issues/65516) - [2019-06-28 TAIT tracking issue - #63063](https://github.com/rust-lang/rust/issues/63063) - [2018-08-05 RFC 2515 - Permit impl Trait in type aliases](https://github.com/rust-lang/rfcs/pull/2515) - [2017-07-20 RFC 2071 - Named existentials and impl Trait variable declarations](https://github.com/rust-lang/rfcs/pull/2071) - [2017-03-15 RFC 1951 - Finalize syntax and parameter scoping for impl Trait, while expanding it to arguments](https://github.com/rust-lang/rfcs/pull/1951) - [2016-03-01 RFC 1522 - Minimal impl Trait](https://github.com/rust-lang/rfcs/pull/1522) - [impl Trait initiative repository](https://rust-lang.github.io/impl-trait-initiative/) - [TAIT project tracking board](https://github.com/orgs/rust-lang/projects/22/views/1) [#107645 FCP]: https://github.com/rust-lang/rust/issues/107645#issuecomment-1571789814 [#116819]: https://github.com/rust-lang/rust/pull/116819 [#41619]: https://github.com/rust-lang/rust/issues/41619 [#63063 FCP]: https://github.com/rust-lang/rust/issues/65516#issuecomment-794211337 [RFC 2071]: https://github.com/rust-lang/rfcs/pull/2071 [RFC 2515]: https://github.com/rust-lang/rfcs/pull/2515 [RFC 3498]: https://github.com/rust-lang/rfcs/pull/3498 [RFC PR 3373]: https://github.com/rust-lang/rfcs/pull/3373 [ReusableBoxFuture]: https://docs.rs/tokio-util/0.7.10/tokio_util/sync/struct.ReusableBoxFuture.html [T-lang 2023-04-11 consensus]: https://github.com/rust-lang/rust/issues/107645#issuecomment-1504041903 [T-lang design meeting 2023-05-31]: https://hackmd.io/IVFExd28TZWm6iyNIq66PA [Why async Rust?]: https://without.boats/blog/why-async-rust/ [matklad on 3373 AON]: https://github.com/rust-lang/rfcs/pull/3373#issuecomment-1406888432 [trait solving must be lifetime agnostic]: https://rustc-dev-guide.rust-lang.org/solve/invariants.html#trait-solving-must-be-free-lifetime-agnostic- # Appendix B: Signature restriction use cases [Appendix B]: #Appendix-B-Signature-restriction-use-cases ## Opaques in APIs Type alias `impl Trait` allows naming types that currently cannot be named. The key benefit of this is that it allows *interfaces* to return opaque types without creating problems for downstream users. Currently, in Rust, it's considered an anti-pattern to return an opaque type (using RPIT) from an API. Because the user of such an API cannot name the returned type, that user cannot e.g. store that type in a struct unboxed. If the trait in the bounds of the opaque type is not object-safe, the effective limitation is even more severe. It's for this reason that new `Iterator` combinators in the standard library are not implemented using RPIT. Many other libraries in the ecosystem follow similar rules. For solving this problem, the signature restriction works seamlessly. We might write, e.g.: ```rust pub type Once<T> = impl Iterator<Item = T>; pub fn once<T>(value: T) -> Once<T> { .. } ``` Since the opaque type is being returned through an API, it has no choice but to appear within a type in the signature. There's no other way it could be part of the API. So passing the signature restriction is trivial. This is expected to be the largest and most important use case for TAIT. The ability to name `async` futures is essentially a subset of this use case. ## Opaque wrapping Let's say that we have a type that can be named, and that sometimes we want values of that type treated opaquely, and sometimes we want those values treated concretely. Imagine, e.g., that when we return these values from our API, we want to only return them opaquely. But then after accepting back these opaque values as input to our API, we want to "unwrap" them so they can be passed around concretely within our library. To do this, we can use the wrap/unwrap pattern. E.g.: ```rust #![feature(type_alias_impl_trait)] struct MyType; type MyTait = impl Sized; fn wrap(x: MyType) -> MyTait { x } fn unwrap(x: MyTait) -> MyType { x } ``` ## Storing opaque types in `static` items Static items require the full type to be named. For this reason, it has not been possible to store futures generated by `async` in static items in stable Rust. TAIT now makes this possible. Where these static items are initialized at runtime, particular patterns may need to be used to pass the signature restriction. Consider: ```rust #![feature(type_alias_impl_trait)] #![feature(sync_unsafe_cell)] use core::{cell::SyncUnsafeCell, future::Future}; type MyFut1 = impl Future<Output = ()>; type MyFut2 = impl Future<Output = ()>; struct MyFuts { fut1: MyFut1, fut2: MyFut2, } impl MyFuts { fn new() -> Self { Self { fut1: async move { todo!() }, fut2: async move { todo!() }, } } } static FUTS: SyncUnsafeCell<Option<MyFuts>> = SyncUnsafeCell::new(None); pub fn initialize() { unsafe { *FUTS.get() = Some(MyFuts::new()) }; } ``` We've used a pattern here to ensure that the opaque types are contained within the return type of a method. Alternately, we could include these in an argument type. E.g.: ```rust #![feature(type_alias_impl_trait)] #![feature(sync_unsafe_cell)] use core::{cell::SyncUnsafeCell, future::Future}; type MyFut1 = impl Future<Output = ()>; type MyFut2 = impl Future<Output = ()>; struct MyFuts { fut1: MyFut1, fut2: MyFut2, } static FUTS: SyncUnsafeCell<Option<MyFuts>> = SyncUnsafeCell::new(None); fn inner_initialize(x: *mut Option<MyFuts>) { unsafe { *x = Some(MyFuts { fut1: async move { todo!() }, fut2: async move { todo!() }, }); } } pub fn initialize() { inner_initialize(FUTS.get()); } ``` There are other ways we could do this. For example, we could include the types in the signature using a trivial bound in a where clause. However, the patterns above are probably the most idiomatic. ## Avoiding writing out full type signatures / generalized cross-function type inference Some people want to use TAIT as a general cross-function non-opaque or semi-opaque type inference mechanism to avoid writing out full type signatures. For example: ```rust // This code is not valid. struct Thing<A, B, C>(A, B, C); type MyTait = impl Sized; fn callee(x: MyTait) { x.method_on_thing(); // Uses `Thing` concretely. } fn caller() { let x = make_complex_thing(); // ~^ Returns `Thing<Some, Complex, Types>`. callee(x); } ``` The goal here is that `callee` would not need to write out the full and potentially complicated type signature but could nonetheless use the argument value concretely. The people who want this hope that TAIT could be used for this and that something similar to the above could be made to work. In fact, TAIT can be used in this way, but under the signature restriction, it comes at the cost of having to write out the full type signature in the `wrap`/`unwrap` helpers described above. Of course, this goal could also be achieved, probably more idiomatically, using the `newtype` pattern. In the view of the authors, providing generalized cross-function non-opaque or semi-opaque type inference and helping people to pass values of concrete types between functions without writing full type signatures was not the original motivation for the design of TAIT, and is not the main intended use case. Some might even find this to be an anti-pattern. There have previously been proposals for non-opaque type inference in items. These have so far failed to gain any traction. Even a very minimal proposal to allow [eliding the array size][] in items was rejected. For people who want to use TAIT in this way, the signature restriction can be an impediment. Note that the proposal in this document is forward compatible with later allowing the hidden type to be defined outside of the constraining scope with some additional syntax as described [above][defines-outside], and if we were to do this, the signature restriction would no longer be an impediment to this use case. [eliding the array size]: https://github.com/rust-lang/rfcs/pull/2545 # Appendix C: Once modulo regions restriction details [Appendix C]: #Appendix-C-Once-modulo-regions-restriction-details Within a single item, when multiple defining uses of an opaque type differ only by lifetime arguments, we must be able to prove that those lifetime arguments are equal, otherwise the code will be rejected, and once the new trait solver is stabilized, it will likely not be possible to lift this restriction in a fully backward compatible way. 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. This is a type system invariant ([trait solving must be lifetime agnostic][]), and is motivated by requirements for MIR borrow check and codegen to be sane and to not have trait solving that changes between those stages and HIR type check. As a bonus, this approach is also better for caching. ## Once modulo regions example Consider: ```rust #![feature(type_alias_impl_trait)] type Tait<'x> = impl Sized; fn define<'a, 'b>(x: &'a (), y: &'b ()) -> (Tait<'a>, Tait<'b>) { (x, y) } ``` ## Once modulo regions handling in old trait solver For the example above, the old trait solver works as follows: In `typeck`, we observe that: ```rust Tait<'a> := &'a () Tait<'b> := &'b () ``` Then, in `borrowck`, we perform remapping and observe that: ```rust Tait<'a> := &'a () ~~> Tait<'x> := &'x () Tait<'b> := &'b () ~~> Tait<'x> := &'x () ``` Since these types are equal after remapping, `borrowck` is happy and this code is accepted. ## Once modulo regions handling in new trait solver For the example above, the new trait solver works as follows: It observes that `Tait<'a>` must be equal to `&'a ()`. Since we haven't inferred anything yet for `Tait<'a>`, we register the definition `Tait<'a> := &'a ()` in our opaque type storage. We then notice that `Tait<'b>` must be equal to `&'b ()`. Since we've already inferred that `Tait<'a> := &'a ()`, we do two things: First, we require that the opaque signatures `Tait<'b> = Tait<'a>`. This requires that `'b = 'a`. Next, we set the hidden types `&'b () = &'a ()`. Note that this also requires `'b = 'a`. As in the old solver, this happens twice, once during `typeck` and once during `borrowck`. During `typeck`, we throw away the regions. However, during `borrowck`, we fail to prove that `'a = 'b`, which results in the error. E.g.: ```rust #![feature(type_alias_impl_trait)] type Tait<'x> = impl Sized; fn define<'a, 'b>(x: &'a (), y: &'b ()) -> (Tait<'a>, Tait<'b>) { // ~^ ERROR lifetime may not live long enough // ~| NOTE requires that `'a` must outlive `'b` // ~| NOTE requires that `'b` must outlive `'a` (x, y) } ``` ## Once modulo regions effect on inference Here's an example of how, under the new trait solver, inference could change if we later made different design decisions. This is due to being able to observe the hidden type: ```rust type Tait<'a> = impl Sized; fn foo<'a: 'b, 'b: 'a>() -> Tait<'a> { let x: Tait<'a> = [1, 2, 3]; { let y = foo::<'b, 'a>(); // We know `y` is `[i32; 3]`. y.into_iter(); } x } ``` ## Further annotated examples This is the canonical example of what cannot be accepted by the new trait solver and that we will consequently disallow: ```rust #![feature(type_alias_impl_trait)] type Tait<'x> = impl Sized; fn define<'a, 'b>(_: &'a (), _: &'b ()) -> (Tait<'a>, Tait<'b>) { // ~^ ERROR lifetime may not live long enough // ~| NOTE requires that `'a` must outlive `'b` // ~| NOTE requires that `'b` must outlive `'a` ((), ()) } ``` Note that it doesn't matter whether or not the hidden type actually uses these regions. If due to the bounds, we can prove that these regions are in fact equal, then we will allow the code. E.g.: ```rust #![feature(type_alias_impl_trait)] type Tait<'x> = impl Sized; fn define<'a: 'b, 'b: 'a>(_: &'a (), _: &'b ()) -> (Tait<'a>, Tait<'b>) { ((), ()) } ``` Note that this restriction only applies to lifetime parameters. It's OK for defined opaque types to be equal modulo types. E.g, this is OK: ```rust #![feature(type_alias_impl_trait)] type Tait<X> = impl Sized; fn define<A, B>(_: A, _: B) -> (Tait<A>, Tait<B>) { ((), ()) } ``` It's OK for two separate items to independently define an opaque type twice in a way that is equal modulo regions. E.g.: ```rust #![feature(type_alias_impl_trait)] type Tait<'x> = impl Sized; fn define_1<'x>(_: &'x ()) -> Tait<'x> {} fn define_2<'x>(_: &'x ()) -> Tait<'x> {} ``` Note that it's OK for non-defining uses of the opaque type to use the opaque types in a way that is equal modulo regions. E.g., this is OK and works in both the new and the old solver: ```rust #![feature(type_alias_impl_trait)] 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)) } ``` Even if `non_defining` were in the defining scope, this would be allowed under this rule because the opaque type is only defined once, though it would be currently disallowed because of the rule that requires items that mention opaque types in their signatures within the defining scope to define them. E.g., this would be allowed once the other restriction is lifted: ```rust #![feature(type_alias_impl_trait)] 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)) } ``` Note, however, that this currently results in an overflow on the new solver. We believe this will be fixed by [#116369](https://github.com/rust-lang/rust/pull/116369). In any case, this would be supported on a best-effort basis and may be subject to compiler limitations. # Appendix D: Signature restriction forward compatibility matrix [Appendix D]: #Appendix-D-Signature-restriction-forward-compatibility-matrix [![Diagram](https://mermaid.ink/img/pako:eNq9VcFuozAQ_ZWR97itBARCElU9VLkjNb2t9-DCkFgFO2uMqqjqv9cGwrBqu1Tbqrc34zdvnmcseGK5LpBtgJWVfswPwli4u-GKq6a93xtxPMAWS6mwyVR1-sXZEIEPOfvNVbZy2exopVaw4q6SqzNHKig8lGoPTa6P2BesqWA9V8DVzz7lTnVrG1kg6PItWVTFX7ZvtD3c4p9WGixcQx_COe59LMnHsvexk3slbGsQDDbWyNwfdh5qoQphtTmNbt65XEqi6VeI_v8Axr631NaZIzuTfCfAFcCrrfctp4snE9uzid1oAiALaQLh3AQ-djMnGpFo9HlRz9KdmqhmFup6L6j34tt6e1ollYVSGxD3DTo4sAdfjT1V6Nw5YPQDbn6UZRkEgT8ankIWkPPgX867lxsTOZ675geHlyWkmXyB5txQhpEkr0YyDgQuL6_dEyUYEVwQjAkmXWFI7HBkuyCCq0kUEysmzXjg9EoJcZKp0rIPAoIhwYjggmBMMCGYElwRXHdNUlJOSTkl5XTKXo0BuwBWo6mFLPy_4okze8DaLXkDnBXCPHD27DmitXp3UrnLl6Jq0KXao_vG4VYK912pz_nnF1xPNbE?type=svg)](https://mermaid.live/view#pako:eNq9VcFuozAQ_ZWR97itBARCElU9VLkjNb2t9-DCkFgFO2uMqqjqv9cGwrBqu1Tbqrc34zdvnmcseGK5LpBtgJWVfswPwli4u-GKq6a93xtxPMAWS6mwyVR1-sXZEIEPOfvNVbZy2exopVaw4q6SqzNHKig8lGoPTa6P2BesqWA9V8DVzz7lTnVrG1kg6PItWVTFX7ZvtD3c4p9WGixcQx_COe59LMnHsvexk3slbGsQDDbWyNwfdh5qoQphtTmNbt65XEqi6VeI_v8Axr631NaZIzuTfCfAFcCrrfctp4snE9uzid1oAiALaQLh3AQ-djMnGpFo9HlRz9KdmqhmFup6L6j34tt6e1ollYVSGxD3DTo4sAdfjT1V6Nw5YPQDbn6UZRkEgT8ankIWkPPgX867lxsTOZ675geHlyWkmXyB5txQhpEkr0YyDgQuL6_dEyUYEVwQjAkmXWFI7HBkuyCCq0kUEysmzXjg9EoJcZKp0rIPAoIhwYjggmBMMCGYElwRXHdNUlJOSTkl5XTKXo0BuwBWo6mFLPy_4okze8DaLXkDnBXCPHD27DmitXp3UrnLl6Jq0KXao_vG4VYK912pz_nnF1xPNbE) (Click above to enlarge.) # Appendix E: Mini-TAIT forward compatibility matrix [![Diagram](https://mermaid.ink/img/pako:eNq1k8FqwzAMhl9F-LxAk9xyGAx2GWyXprd5BxMrqZktF1uhDaXvPqfpUmhXBhu9Sf8vy59ley8ar1FUIFrrt81aBYbXpSRJ9eJdijdDJls9vaxkkiQFjBxMw8ZTBZ4aBOd1bz0E7JIWL0sc0hiA6yODxtYQXpZMampmB9gaXhuaJEMdxMZvUIqPBJP_CUbZdKgKyFM2N-0jRrje5Ses0f0NrLjXlE7sZ5Ceo9FpUu0Fx5HutC5CHIjVbmIrRza1u-fQ_k8pKfJgEeoFMO44U9Z0VFlseXbym05x0ymvnfFNQ5Y9pobnsDjq-ZSUx6SYE_EAwmFwyujxi-yl4DW6dPMVSKFV-JTiMNaonn09UJP0VtmISeo3WjE-G9UF5b71wxemjT4s?type=svg)](https://mermaid.live/view#pako:eNq1k8FqwzAMhl9F-LxAk9xyGAx2GWyXprd5BxMrqZktF1uhDaXvPqfpUmhXBhu9Sf8vy59ley8ar1FUIFrrt81aBYbXpSRJ9eJdijdDJls9vaxkkiQFjBxMw8ZTBZ4aBOd1bz0E7JIWL0sc0hiA6yODxtYQXpZMampmB9gaXhuaJEMdxMZvUIqPBJP_CUbZdKgKyFM2N-0jRrje5Ses0f0NrLjXlE7sZ5Ceo9FpUu0Fx5HutC5CHIjVbmIrRza1u-fQ_k8pKfJgEeoFMO44U9Z0VFlseXbym05x0ymvnfFNQ5Y9pobnsDjq-ZSUx6SYE_EAwmFwyujxi-yl4DW6dPMVSKFV-JTiMNaonn09UJP0VtmISeo3WjE-G9UF5b71wxemjT4s) (Click above to enlarge.) # Appendix F: Only one item may define non-restriction The original proposal from T-types included a restriction that only one item could define the hidden type of any opaque. This restriction was explicitly arbitrary. We have the machinery to allow multiple items to define the hidden type, and restricting this does not help in leaving space for the new trait solver. Imposing this restriction would rule out, e.g., the wrap/unwrap pattern: ```rust #![feature(type_alias_impl_trait)] type Tait = impl Sized; fn wrap(x: ()) -> Tait { x } fn unwrap(x: Tait) -> () { x } ``` There are many use-cases for this, as we describe in [Appendix B][]. Some members of T-types felt that, given the other restrictions, we should add enough further restrictions such that together the restrictions would be onerous enough that we might be more motivated to lift them. While the authors are sympathetic to that, given the explicit arbitrariness of this restriction and the value of allowing the patterns that it would preclude, we have elected to drop this restriction from the proposal. # Appendix G: Nested inner items restriction details [Appendix G]: #Appendix-G-Nested-inner-items-restriction-details This appendix elaborates on the bullet points introduced in the section on [lifting the nested inner items restriction][nested-inner-items]. ## The restriction would rule out certain useful patterns Rust is a remarkably regular language, and Rust users expect to be able to nest functions inside of other functions without encountering surprises. This restriction would rule out some otherwise reasonable patterns, and when combined with the other restrictions we're imposing in this proposal, the restrictions could seem more severe. See [Appendix H][] for worked examples. ## It would be inconsistent with Rust until and unless we accept RFC PR 3373 For better or worse, Rust currently accepts sneaky inner impls. Until and unless we accept a plan for changing that, it would be inconsistent and potentially surprising to users for Rust to impose this limitation on only some new features. ## There's no value to partially solving the sneaky inner impls problem The value of solving the sneaky inner impls problem comes from solving it entirely. There is no value in partial solutions. **matklad** [stated this][matklad on 3373 AON] concisely: > I guess my overall thrust here is that, if we do this, its important to do 100% of it. We don't want to forbid this because it's an unreadable mess: no need to forbid something which isn't used anywhere outside of dtolnay's famous quiz. We want to forbid this, because we want to make the implementation faster. In some future edition, we want to just throw every function onto a thread pool and type check the world on all cores, without any synchronization. If there's at least one case where we need to invalidate the type checking results of `foo` when we learn something new when checking `bar`, this optimization simply doesn't work. As long as sneaky inner impls are otherwise valid in Rust, restricting the ability of nested items to define the hidden type of outer opaques wins us little or nothing. ## RFC PR 3373 is unlikely to be accepted for Rust 2024 There has been no movement on [RFC PR 3373][] since February 2023. Given the degree of the proposed breakage and the limited time left, based purely on the timeline, it seems unlikely that this will be accepted for Rust 2024. A similar proposal was [rejected via FCP][#63063 FCP] for Rust 2021. While it can sometimes be reasonable to leave room, it would seem strange to leave this much room for a proposal that may or may not ever be accepted, and is very unlikely to be accepted in the next edition. ## If RFC PR 3373 were later accepted, we could fix it then [RFC PR 3373][] represents a breaking change to Rust. If this RFC were ever to later be accepted for a future edition, then the case under consideration here could be rejected at that time, just as other currently-valid code would at that time change to being rejected. The cost of adding one more relatively minor thing to the list of what would in any case already be a substantial breaking change does not seem significant enough to justify efforts to design around it for this feature. ## Only parsing and signature restriction checking are required, not type checking Lifting this restriction would only require that all functions and methods within the defining scope be *parsed* and checked according to the signature restriction, but it would not require that they or their bodies be *type checked*. This distinction may be substantial in estimating the effect on performance. ## We're speculating rather than measuring We're spending considerable design energy here on perhaps leaving room to maybe one day allow for skipping the parsing of all functions and methods in a crate. But how expensive is that really? Clearly all *files* within the crate must be at least partially parsed. How big is the win from not parsing the function bodies? One of the authors recently wrote a tool that uses `syn` to fully parse all crates on `crates.io` to collect some statistics. This tool parses all `crates.io` crates on a relatively small machine in just a few minutes and may in fact be I/O bound. Is parsing really the bottleneck? It seems that to justify this restriction, we should expect measurements, and that this burden of proof should fall on the people asking for the restriction since the status quo is that Rust does not have this restriction and there are costs to imposing it. ## We cannot necessarily lift this later To achieve what this proposal does, we are relying on the details of the signature restriction for type inference, and we are consequently *committing* to these restrictions. This is important for eliminating cycle errors and for leaving space for the new trait solver. If we were to ship with this inner items restriction, we would be committing to it, and we could not necessarily remove it later without a breaking change. ## Summary on nested inner items restriction We have to commit to a decision here as we can't necessarily change this later. The right decision is to be consistent with the rest of the language as it exists today. # Appendix H: Nested inner items restriction examples [Appendix H]: #Appendix-G-Nested-inner-items-restriction-examples The restriction on defining outer opaques from nested inner items without recursively passing the signature restriction would rule out certain patterns. We'll describe those in this section. ## Example: Inner function to define statics For example, consider this code from [Appendix B][] that initializes static items: ```rust #![feature(type_alias_impl_trait)] #![feature(sync_unsafe_cell)] use core::{cell::SyncUnsafeCell, future::Future}; type MyFut1 = impl Future<Output = ()>; type MyFut2 = impl Future<Output = ()>; struct MyFuts { fut1: MyFut1, fut2: MyFut2, } static FUTS: SyncUnsafeCell<Option<MyFuts>> = SyncUnsafeCell::new(None); fn inner_initialize(x: *mut Option<MyFuts>) { unsafe { *x = Some(MyFuts { fut1: async move { todo!() }, fut2: async move { todo!() }, }); } } pub fn initialize() { inner_initialize(FUTS.get()); } ``` We could arguably make this code more idiomatic by using a nested inner function. E.g.: ```rust #![feature(type_alias_impl_trait)] #![feature(sync_unsafe_cell)] use core::{cell::SyncUnsafeCell, future::Future}; type MyFut1 = impl Future<Output = ()>; type MyFut2 = impl Future<Output = ()>; struct MyFuts { fut1: MyFut1, fut2: MyFut2, } static FUTS: SyncUnsafeCell<Option<MyFuts>> = SyncUnsafeCell::new(None); pub fn initialize() { fn inner(x: *mut Option<MyFuts>) { unsafe { *x = Some(MyFuts { fut1: async move { todo!() }, fut2: async move { todo!() }, }); } } inner(FUTS.get()); } ``` However, if we do not lift the nested inner items restriction, then this seemingly normal refactoring would be rejected. We might think that we could work around this by adding trivial where bounds to `initialize`. E.g.: ```rust #![feature(type_alias_impl_trait)] #![feature(sync_unsafe_cell)] use core::{any::Any, cell::SyncUnsafeCell, future::Future}; type MyFut1 = impl Future<Output = ()>; type MyFut2 = impl Future<Output = ()>; struct MyFuts { fut1: MyFut1, fut2: MyFut2, } static FUTS: SyncUnsafeCell<Option<MyFuts>> = SyncUnsafeCell::new(None); pub fn initialize() where MyFuts: Any, { fn inner(x: *mut Option<MyFuts>) { unsafe { *x = Some(MyFuts { fut1: async move { todo!() }, fut2: async move { todo!() }, }); } } inner(FUTS.get()); } ``` However, this too would fail under the rules of this proposal, as the outer `initialize` function would be mentioning the opaque type without defining it, and would therefore fall afoul of the "mention must define" restriction. Additionally, this workaround would cause `MyFuts` to appear in the documentation for `initialize`, which would be undesirable, and would trigger a "private-in-public" warning. ## Example: Defining both in items outside of and inside of a function It may seem strange to want to define a hidden type whose opaque is declared outside of a function from an item nested within a function. However, we must consider that the hidden type may be defined *both* outside of the function and by an item nested within it, and that this could be a reasonable thing to do. Imagine, e.g., if we were exporting a `static` item from our API and we wanted it to have an opaque type, but within the crate we need to access the underlying concrete type of that `static`. We would end up defining the hidden type more than once, and it would be reasonable to expect that people may write nested helper functions in whose signature the type could appear. E.g.: ```rust #![feature(type_alias_impl_trait)] pub type IdTy = impl Copy; pub static ID: IdTy = 0u64; fn some_big_function() { // ... lots of code ... fn helper(x: IdTy) -> IdTy { (x as u64) + 1 } let next = helper(ID); // ... lots of code ... } ``` # Appendix I: On a `defines` syntax details [Appendix I]: #Appendix-I-On-a-defines-syntax-details This appendix elaborates on the bullet points introduced in the section on [a potential `defines` syntax][on-defines]. ## We can do it later This stabilization proposal is forward compatible with adding a new `defines` syntax later. See, e.g., the [future work][defines-outside] section above for how this might work. Such a syntax could be used to allow items outside of the defining scope to define the hidden type, and it could be used to allow items inside of the defining scope that don't pass the signature restriction to define the hidden type. Variations on such syntax could also conceivably allow for specifying that items inside the defining scope that *do* pass the signature restriction cannot constrain the hidden type. If we were to later find ourselves entirely unhappy with module scoping and the signature restriction, in the worst case, we would introduce this new `defines` syntax, allow it to be used within the defining scope, then begin to lint against items in the defining scope that define the hidden type without using this syntax. The end result of such linting could be to remove module scoping and the signature restriction in a later edition. Obviously the authors do not believe this will be necessary or desirable. We believe there will be general *happiness* with the stabilization of TAIT as proposed here, especially after the restrictions that can be lifted are lifted. But it is important to know that we are retaining many options here. ## A new `defines` syntax would raise novel design and implementation questions Most obviously, to add a new `defines` syntax, we would need to choose what that syntax would be. But the question goes deeper than one might expect. Most critically, we would need to decide whether the syntax would support specifying generic arguments. People have expressed views on both sides of this. Allowing generic arguments to be specified would be critical if we wanted normalization to occur. This raises questions of whether to what degree the types passed as arguments to the new syntax should be normalized and whether and to what degree we should recurse into those types looking for opaques. Because of how the new trait solver needs to rely on knowing whether or not an item can define the hidden type to guide inference, we would be required to commit to the answers to these questions, and it would be a breaking change to later change our minds. Before we might console ourselves that the answers to questions are easy, we would first need to consider how we might expand from TAIT to `impl Trait` Everywhere. That is, we may still want to stabilize more of what was specified in [RFC 2071][] and [RFC 2515][], and those extensions could easily change how we would feel about some of the answers above. At the very least, we'd need to work through many use cases. But ideally, we'd have some experience with the implementation and use on nightly before having to commit to these answers. Even just on how to spell the syntax, there are interesting design questions to consider. Some of the first proposals for such a syntax were to use an attribute. However, in other arguably similar places, we have made moves away from using attributes. E.g. in [#41619][], the tracking issue for RFC 1868 "A portability lint", we completed an FCP to close the issue on the basis that we would prefer to use where clauses rather than attributes to express the capabilities of the item. Is whether an item can define some type a similar kind of capability in this sense? More concretely, there are implementation problems with using attributes for this syntax. The Rust compiler does not do name or type resolution inside of attributes. This limitation is more than skin deep. Oli, the real MVP and longtime implementor of TAIT, tried to implement a `defines` syntax using attributes unsuccessfully. His feeling is that this is not possible without substantial refactoring of `rustc`, and he has said that he will not work on this or reattempt an attribute-based implementation until this refactoring is done. Beyond where clauses and attributes, we could of course add a `defines` syntax elsewhere in the signature of items. These are all interesting questions we would prefer to leave for a later date. ## Timeline and Rust 2024 TAIT with module scoping has been implemented and baking in nightly for years. The signature restriction, which builds on top of that, has been baking in nightly since June. Work on implementing the restrictions proposed in this document has already started. Additional to these implementation factors, an enormous amount of design work has gone into the rules for TAIT. The module scoping rules go back to [RFC 2071][] and [RFC 2515][]. On top of those rules, the signature restriction was developed. These rules were a significant body of design work that resulted in the [#107645 FCP][]. Finally, on top of those rules, a consensus was built with the types team about how to resolve concerns related to the new trait solver, leading to the design work encapsulated in this document and that is being discussed in this meeting. If we were to set aside this work and embark on solving the novel design questions of a `defines` syntax, try to build a new consensus around that, try to get it implemented, and try to get that stabilized, it's the view of the authors that TAIT would be very unlikely to be stabilized within the next two years, and could of course take appreciably longer. Such a delay would mean that TAIT would not be stabilized ahead of Rust 2024. Not stabilizing TAIT ahead of Rust 2024 would jeopardize the stabilization in Rust 2024 of the [RFC 3498] Lifetime Capture Rules 2024 that enabled the stabilization of RPITIT. There are other options than TAIT that could be considered to unblock this stabilization, but the time would be short to RFC and implement those other options, so unquestionably, delaying the stabilization of TAIT would put this plan in *jeopardy*. The cost of not stabilizing the [RFC 3498][] rules in Rust 2024 is that the behavior of RPIT would be notably inconsistent throughout the lifetime of the Rust 2024 edition. With the upcoming stabilization of RPITIT in Rust 1.75, this would be a significant wart to carry. ## Summary about `defines` The current proposal is forward compatible with everything we might want to do with a `defines` syntax. Let's move forward with the current carefully constructed plan and implementation and consider all of the interesting ways we might extend this with future work later. --- # Design meeting minutes [minutes]: #Design-meeting-minutes Attendance: TC, compiler-errors, nikomatsakis, tmandry, scottmcm, waffle, Dario Niewenhuis, Urgau, eholk Minutes, driver: TC ## What are we ruling OUT? nikomatsakis: The document does a great job defining and justifying the proposed restrictions. I am generally in favor. But it's a bit less concrete about the arguments *against*, or at least the kinds of patterns that we are aware of which are not yet supported by this proposal. Let me give my best effort to summarize what I know of, and maybe we can cover other things in meeting... * defining a TAIT outside of the defining module (or where the TAIT does not appear in the signature) * We're only ruling out applying the signature restriction outside of the defining scope. We can later add defining outside of the defining scope using other means such as some new syntax. * passthrough examples, obviously, that don't constrain TAITs that *do* defing things in their signature * This is a restriction we can lift later. TC: This is the subject of [Appendix B][]. See also the [future work section][defines-outside]. There's also discussion of this in [Appendix I][] and in the main body [section on this][on-defines]. TC: Also see the diagram in [Appendix D][]. Dario: I've compiled a list of these, with links to real-world code here https://hackmd.io/i-xpzf-LR7q75pSRTDVSaA#Appendix-A-Issues-with-the-current-implicit-rules - IME these come up relatively often TC: One main thing we're ruling out is lifting the signature restriction in the defining scope. NM: I feel fine about ruling out not lifting the signature restriction within the defining scope. NM: The other argument for the signature restriction is `impl Trait` for associated types. Do we have a doc on the question of exactly why we're not doing defines to start with? TC: Yes. This is that document. Most of the elaboration was moved into [Appendix I][]. ## What other questions do we have to move forward NM: What questions do lang team members have they would want answered to feel comfortable moving forward. tmandry: I'd rather ship a feature than have it my exact preferred way. And we have a good forward compatibility story here. NM: I'd like to write a min-max document. But I don't think that blocks it. tmandry: We have to make a decision now about what's implicitly allowed. Everything can be changed on an edition boundary or adding lints after the fact. scottmcm: What about cycle errors? TC: We rule those out entirely. NM: We should decide in the next day or two whether we want to move forward here. scottmcm: Is there a middle ground? NM: Not really. We either accept this, or we restart the design process on `defines`. tmandry: It wouldn't necessarily be a restart. It could build on other work. scottmcm: It would be a substantial pivot in any case. NM: Similar to AFIT, we'd be handling a subset of use cases here. TC: Let's discuss this at the next triage meeting. NM: Hopefully we can discuss it sooner with the interested group. (The meeting ended here.) ## TAITs still don't solve all the reasons that iterator adapters have named types scottmcm: I don't think this is a flaw in this proposal, but in relation to > It’s for this reason that new Iterator combinators in the standard library are not implemented using RPIT. I wanted to note that the biggest reason that combinators don't use RPIT is that it doesn't allow *conditional* traits. `Map<I>` wants to be `ExactSizeIterator` iff `I: ExactSizeIterator`, and if `Iterator::map` returned `impl Iterator`, there's no way to say that. TAITs will *absolutely* help for being able to store iterators, but they're not a complete solution to the named-adapters problem. TC: This is an important point. The more general point here is about `Iterator`-like APIs, and for many of these, this isn't a problem. ## How do we define *signature*? nikomatsakis: I'm still a bit confused about what it means for a TAIT to appear *in a signature*. I'd like to see it written out in a bit more detail. errs: Whether the TAIT shows up anywhere in the inputs or outputs, or any types in the WC. Walk into adt fields, and (possibly, except we may implement this as an error to begin with) also normalize types and see if the opaque shows up in the normalized type. For statics and consts, this just means the type of the static or const, currently. errs: Anything specifically unclear about this procedure? nikomatsakis: We walk into adt fields? so e.g. ```rust type Bar = impl Debug; struct Foo { b: Bar } fn something() -> Foo { } ``` constrains `Bar`? TC: Yes. This was part of the [#107645 FCP][] and is defined there. NM: I forgot that. I'll have to go re-read. It seems surprising to me. I can imagine the reasons. TC: You found it surprising too in the last meeting on 2023-05-31, then decided that it made sense. errs: lol NM: I see that I wrote that. OK, I'm going to trust "past Niko" but it is surprising. :) errs: It's useful for adapters that contain a TAIT but don't want to use the TAIT as its own type. But I think the justification is probably best stated in the original FCP. TC: The example we talked about in recent ITE meetings was this one: ```rust #![feature(type_alias_impl_trait)] use core::future::Future; type JobFut = impl Future<Output = u64>; struct Job { id: u64, fut: JobFut, } impl Job { fn new(id: u64) -> Self { Job { id: id, fut: async move { id } } } } ``` TC: And a similar variant that uses the builder pattern. NM: As long as we are abiding the rules I apparently agreed with earlier, I'm good. =) ## Clarification: "AssocIT", i.e., impl trait in the value of an associated type nikomatsakis: Is this document expected to also cover ```rust impl SomeTrait { type Future = impl Future<Output = ()>; fn some_method() -> Self::Future { ... } } ``` and, if so, what are the rules regarding "appearing in a signature" there. I assume that the "self trait ref" is detected. errs: Currently we have special-cased around normalization for associated types with `Self` matching the trait ref, but we eventually intend to consider all opaque we can reach via normalization. This is a bit fraught in the old trait solver, so will likely error if an opaque shows up in a signature after normalization but not before it, to save space for eventually considering normalized types. **See "cannot normalize to opaque" restriction above.** ## Data Dario: What data do we have to justify the design decisions of the "parent mod + signature restriction" rules? For example, what percentage of the time do they match the author's intent? How would it change if we e.g. removed "constraining by encapsulation"? I've collected some here https://hackmd.io/i-xpzf-LR7q75pSRTDVSaA#Data but it's very inconclusive. Depending on how you interpret it, the current rules work between ~40% and ~80% of the time. ## I like the note about the wrap/unwrap pattern (low pri; feel free to skip in discussion) scottmcm: It's interesting that that gives maybe 80% of the "I want to say exactly what my opaque type is" feature request without actually adding a new language feature. I like it. TC: +1. ## About "Lifting nested inner items restriction" > In summary: We have to commit to a decision here as we can’t necessarily change this later. The right decision is to be consistent with the rest of the language as it exists today. wffl: can we change this in a later edition at the same time with the other things (trait impls in bodies)? errs: Yes, technically, and I think that would be the the right time to do it. We would need to be able to lift the "mentions must define" restriction, though, or else you'd get into cases where inner items can't define an opaque because outer items can't name an unnameable type. But we could just decide outright that inner items can't define opaques or something. wffl: OK. It seemed to me that "We have to commit" means forever, but if we can change this over an edition I feel better. (with we could accept #65516...) ## About "Future work: Constraining outside of the defining scope" wffl: the syntax probably should be placed on *tait*, so that IDEs and such can know where to search for the definition? ```rust // something like this #[defined_in(path::to::function)] type Tait = impl Sized; ``` dario: I've analyzed the pros and cons here https://hackmd.io/i-xpzf-LR7q75pSRTDVSaA#Explicit1 , my conclusion was it's better to put it on the defining scope because otherwise you need TAIT and defining fn to be visible mutually which is more restrictive. wffl: you could make `defined_in` ignore privacy ig? It seems a lot better for compiler/IDE to have a definitive pointer, instead of having to search, so if we could do that, I think we should.