-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stabilize associated type position impl Trait (ATPIT) #120700
base: master
Are you sure you want to change the base?
Conversation
r? @davidtwco (rustbot has picked a reviewer for you, use r? to override) |
r? @oli-obk |
This comment has been minimized.
This comment has been minimized.
1a53f70
to
a47c353
Compare
This comment has been minimized.
This comment has been minimized.
f60e854
to
f029558
Compare
This comment has been minimized.
This comment has been minimized.
f029558
to
f550ebe
Compare
Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt |
@rfcbot fcp merge This is a great step forward! Super excited to see this. I'm happy we included the design axioms. Even if I drafted them initially, I had forgotten them, so it was nice to re-read them. |
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
Can we please get a documentation PR approved before merging? |
Thanks for your work on this, @traviscross, and everyone who was mentioned in the stabilization report. I'm excited to see this move forward. @rfcbot reviewed |
edit: rfcbot doesn't listen to me on T-lang FCPs Actually landing this is also blocked on switching opaque type inference to an eager scheme (see https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/eager.20inference.20replacement.20for.20opaques/near/419219306) added blockers as todo items to the main post. The opaque type inference change is a T-types implementation detail that almost never affects any code, but in the edge cases where it does, we need it to be consistent between our two solvers and it's also just a simpler strategy. So the FCP can continue, we'll delay actual stabilization if necessary. |
I'm afraid I cannot parse this sentence. Where is the verb? Is it "witnesses", and there's a "type" missing before it? This could benefit from a rephrasing that puts the verb closer to the beginning of the sentence. :) It's also confusing to have this before explaining the rules for which items are allowed/required to register a hidden type. |
I'm afraid I find this definition pretty unclear. What exactly are "all types"? If the signature is
For an arbitrary Rust type |
This comment was marked as resolved.
This comment was marked as resolved.
@rfcbot concern nested-opaques-capture-bound-lifetimes Nested opaque types in ATPIT does not capture the lifetimes of This is a bug since ATPIT is intended to follow the new lifetime capture rules. #![feature(impl_trait_in_assoc_type)]
trait Tr<'a> { type Assoc; }
impl Tr<'_> for () { type Assoc = (); }
// Works for ATPIT but it shouldn't
trait ATPIT {
type Assoc;
fn def() -> Self::Assoc;
}
impl ATPIT for u8 {
type Assoc = impl for<'a> Tr<'a, Assoc = impl Sized>; // OK!
fn def() -> Self::Assoc {}
}
// Doesn't compile for RPITIT as expected
trait RPITIT {
fn def() -> impl for<'a> Tr<'a, Assoc = impl Sized>;
//~^ ERROR higher kinded lifetime bounds on nested opaque types are not supported yet
}
impl RPITIT for u8 {
fn def() -> impl for<'a> Tr<'a, Assoc = impl Sized> {}
//~^ ERROR higher kinded lifetime bounds on nested opaque types are not supported yet
} |
…ything, r=oli-obk Make TAITs and ATPITs capture late-bound lifetimes in scope This generalizes the behavior that RPITs have, where they duplicate their in-scope lifetimes so that they will always *reify* late-bound lifetimes that they capture. This allows TAITs and ATPITs to properly error when they capture in-scope late-bound lifetimes. r? `@oli-obk` cc `@aliemjay` Fixes rust-lang#122093 and therefore rust-lang#120700 (comment)
Rollup merge of rust-lang#122103 - compiler-errors:taits-capture-everything, r=oli-obk Make TAITs and ATPITs capture late-bound lifetimes in scope This generalizes the behavior that RPITs have, where they duplicate their in-scope lifetimes so that they will always *reify* late-bound lifetimes that they capture. This allows TAITs and ATPITs to properly error when they capture in-scope late-bound lifetimes. r? `@oli-obk` cc `@aliemjay` Fixes rust-lang#122093 and therefore rust-lang#120700 (comment)
@rfcbot resolve nested-opaques-capture-bound-lifetimes |
@rustbot labels -I-lang-nominated At this point, this is waiting on some final work to be completed. There's nothing in particular left for T-lang here, so let's remove the nomination. |
@rfcbot concern may-define-implies-must-define According to the rule "may define == must define", this code should be rejected as fn #![feature(impl_trait_in_assoc_type)]
pub trait Trait {
type Assoc;
fn give() -> Self::Assoc;
fn take(_: Self::Assoc);
}
impl Trait for () {
type Assoc = impl Sized;
fn give() -> Self::Assoc {}
fn take(_: Self::Assoc) {}
} |
The linked issue has been unnominated with the comment "Given that we found other paths forward", is it still relevant for the concern here (or are there other details to link to for it)?
This has landed
This code now produces an error:
|
Hi @nikomatsakis! Is there anything we can do to help push this forward? |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment has been minimized.
This comment has been minimized.
Hold y'all's horses. It's not in FCP because the concerns have not been unblocked. Several of the people leading this work are currently away. These concerns are not necessarily unblocked just because other people have commented that some associated PRs are landed--this work is incredibly tied up into the new trait solver, so until we guarantee that the new trait solver will not be jeopardized by inference behavior that we will not be able to replicate on it, this stabilization PR remains blocked. |
Stabilization report
Summary
We are stabilizing
#![feature(impl_trait_in_assoc_type)]
, commonly called either "associated type position impl Trait" (ATPIT) or "impl Trait in associated type" (ITIAT).Among other things, this feature allows
async
blocks andasync fn
to be used in more places (instead of writing implementations ofFuture
by hand), filling gaps in the story of async Rust.Stabilizing ATPIT helps the many Rust users who have well known use cases, including, e.g., the Tower library (in particular, e.g., its
Service
trait) and its extensive ecosystem.The theme of this stabilization report is simplicity. Through much work, we've found that we can stabilize a subset of RFC 2515 that solves the most demanded use cases while enabling a simple and robust implementation in the compiler, supporting efficient implementations in other tooling, and answering all previously-open language design questions in a principled way.
This is a partial stabilization of RFC 2515 and #63063.
What is stabilized
Summary of stabilization
We now allow
impl Trait
to appear in type position in associated type definitions within trait implementations.For the first time, we can now use
async
blocks to implementIntoFuture
without boxing. E.g.:Just as with RPIT,
impl Trait
may appear syntactically multiple times within the type, and any item that does not register a hidden type for the opaque witnesses it as an opaque type. No items outside of the impl block may register hidden types forimpl Trait
opaques defined within. The rules for which items within the impl block may do so are a simple extension of the RPIT rules and are described below.Opaqueness
When we say that an item can only use a type opaquely, what we mean is that item can only use the type via the interfaces defined by the traits that the type is declared to implement and those of the leaked auto traits (as with RPIT).
A type that can only be used opaquely by some items is known as an opaque type, or, in context, as simply an opaque. The concrete type or type constructor that underlies the opaque is known as the hidden type, or, in context, as the hidden.
When an item chooses a concrete hidden type to underlie some opaque, we say that it has registered a hidden for that opaque, or equivalently, that it has "defined the hidden type of the opaque", or, more loosely and in context, has "defined the opaque type".
We describe below which items may and must register a hidden type for a given opaque. Other items may only use such types opaquely.
Parallel to RPIT
ATPIT is the extension of RPIT to associated type definitions and trait implementations. Everything that is true about RPIT opaque types is true about ATPIT opaque types modulo that:
impl Trait
opaque type appears in the signature of an item, that item may and must register a hidden type for that opaque.impl Trait
opaque type in the same impl block is syntactically reachable from an associated type in the signature of an item, that item may and must register a hidden type for that opaque.Syntactic reachability rule
The rule to decide whether an item may and must register a hidden type for an opaque is strictly syntactic and strictly local to the impl block. It's an extension from considering just the signature, as with RPIT, to also considering the definitions of associated types in the same impl.
Intuitively, we collect all
impl Trait
types within the same trait impl that are syntactically reachable from the signature of an item, considering only that item's signature and the associated type definitions within the impl block. We don't recurse into ADTs (e.g. the fields of a struct).More precisely, the rule is as follows:
To determine the set of
impl Trait
opaque types for which an item may and must register a hidden type, from the signature (including from anywhere
clauses), we:Collect all types and generic type arguments1 without normalization, and for each, we:
a. Collect RPIT-like ("existential") uses of
impl Trait
into the set of opaque types for which this item may and must register a hidden type.2b. Recurse syntactically (i.e. normalize one step) into the definition of associated types from the same trait when all of the generic arguments (including
Self
) match, and from there, we repeat step 1.Note that RPIT already performs steps 1 and 1a. The stabilization of ATPIT adds only step 1b.
Example of syntactic reachability rule
Following the rules for syntactic reachability, this works as we would expect:
May define == must define rule
If an
impl Trait
opaque type is syntactically reachable from the signature of an item according to the syntactic reachability rule, then a hidden type may and must be registered by that item for the opaque.Items not satisfying this predicate may not register a hidden type for the opaque.
Sibling only rule
Only the syntactic items that are direct children of the impl block may register hidden types for an
impl Trait
opaque type in an associated type. Nested syntactic items within those items may not do so. As with RPIT, closures andasync
blocks, which are in some sense items but are not syntactic ones and which share the generics andwhere
clauses of their parent, may register hidden types.Coherence rule
Coherence checking is the process by which we ensure that no two trait impls may overlap. To decide that, we must have a rule for whether any two types may be equal, for the purposes of coherence.
During coherence checking, when an opaque type is related with another type we assume that the two types may be equal unless that other type does not fulfill any of the item bounds of the opaque.
Design principles
The design described in this document for ATPIT adheres to the following principles.
Convenience and minimal overhead for the common cases
We believe that
impl Trait
syntax in return position and in associated type position is for convenience and should have minimal overhead to use.The design proposed for stabilization here solves common and important use cases well, conveniently, and with minimal overhead, similar to RPIT, by e.g. leaning on the syntactic reachability rule. Other use cases may prefer to wait for full type alias impl Trait.
Local reasoning
We believe that it should be easy to determine whether an item may and must register a hidden type of an
impl Trait
opaque type.In certain edge cases, whether or not an item may register a hidden type for an opaque can affect method selection and type inference. It should therefore be straightforward for the user to look syntactically at the impl block only to determine whether or not an item may register a hidden type for any opaque. This is what the syntactic reachability rule achieves.
Crisp behavior
We believe that the behavior of
impl Trait
should be very crisp; if an item may register the hidden type for an opaque, then within that item the type should act exactly like an inference variable (existential quantification). If it cannot, then within that item the type should act like a generic type (universal quantification).If items were allowed to register hidden types without being required to do so, then it is believed to be either difficult or impossible to maintain this kind of crispness in all circumstances. Consequently, this design adopts the "may define == must define" rule to preserve this crisp behavior.
Motivation
We long ago stabilized
async fn
andasync { .. }
blocks as these make writing async Rust more pleasant than having to implementFuture
everywhere by hand.However, there's been a lingering problem with this story. The type of the futures returned by
async fn
andasync { .. }
blocks cannot be named, and we often need to name types to do useful things in Rust. This means that we can't useasync
everywhere that we might want to use it, and it means that if our dependencies do use it, that can create problems for us that we can't fix ourselves.It's for this reason that using RPIT in public APIs has long been considered an anti-pattern. Using the recently-stabilized RPITIT and AFIT in public APIs carries these same pitfalls while adding a new one: the inability for callers to set bounds on these types.
It is these problems, in the context of interfaces defined as traits, that ATPIT addresses.
Using
async
in more placesToday, if we want to implement
IntoFuture
for a type so that it can be awaited using.await
, we have no choice but to implementFuture::poll
for some wrapper type by hand so that it can be named in the associated type ofIntoFuture
.With ATPIT, for the first time, we can use
async { .. }
blocks to implementIntoFuture
. E.g.:Naming types, expressing bounds, object-safety, and precise capturing
If someone were writing a
Service
-like trait today, now that AFIT and RPITIT are stable, that person may think to write it as follows so thatasync
blocks orasync fn
could be used in the impl:However, as compared with using associated types, that would create four problems for users of the trait:
Future
can't be named so as e.g. to store it in a struct.Future
can't be named so as to set bounds on it, e.g. to require aSend
,FusedFuture
, orTryFuture
bound.Future
captures a lifetime we may not want to capture.In the future, there may be other and better solutions to some of these problems (those solutions may in fact desugar essentially to ATPIT). But today, without ATPIT, trait authors face a dilemma. They must either accept all of these drawbacks or, alternatively, must accept that implementors of the trait will not be able to use
async
blocks and will have to write manual implementations ofFuture
.ATPIT offers us a way out of this dilemma. Trait authors can allow implementors to use
async
blocks (and conceivably, in the future,async fn
) to implement the trait while preserving the object safety of the trait, allow users to name the type so as to store it and set bounds, and express precise capturing of type and lifetime generic parameters.Related issues
impl for<'a> Trait<'a>
#104288DefineOpaqueTypes::No
and either document or change them #116652Self
resolution #121404DefiningAnchor::Bind
only store the opaque types that may be constrained, instead of the current infcx root item. #121796DefineOpaqueTypes
by usingYes
across the compiler #127034Open items
"Must define before use"
Add
Projection
/NormalizesTo
goals for opaque typesProjection
/NormalizesTo
goals for opaque types to fix the issue raised here. Even though the example linked there results in an ICE, the ICE only happens when trying to exploit the unsoundness; the overlapping impls are simply accepted incorrectly. There is no known generally-applicable way to exploit this into actual unsoundness, but we treat accepting overlapping impls as unsound by itself. We'll ensure this is handled before merging, regardless of completion of the FCP.Update the Rust Reference
Acknowledgments
The stabilization of ATPIT would not have been possible without, in particular, the ongoing and outstanding work of @oli-obk, who has been quietly pushing forward on all aspects of type alias impl Trait for years. Thanks are also due, in no small part, to @compiler-errors for pushing forward both on this work directly and on critical foundations which have made this work possible. Similarly, we can't say enough positive things about the work that @lcnr has been and is doing on the new trait solver; that work has shaped this proposal and is also what gives us confidence about the ability to support and extend this feature into the long term.
Separately, the author of this stabilization report thanks @oli-obk, @compiler-errors, @tmandry, and @nikomatsakis for their personal support on this work.
Thanks are due to the types team generally for helping develop the "Mini-TAIT" proposal that preceded this work. And thanks are of course due to the authors of RFC 1522, RFC 1951, RFC 2071, and RFC 2515, @Kimundi, @aturon, @cramertj, and @varkor, and to all those who contributed usefully to those designs and discussions.
Thanks to @nikomatsakis for setting out the design principles articulated in this document, and to @tmandry, @oli-obk, and @compiler-errors for reviewing drafts. All errors and omissions remain those of the author alone.
Footnotes
ADTs with generic parameters, non-unit tuples, arrays, slices, pointers, references, function pointers, and
impl Trait
anddyn Trait
types with generic parameters or associated types are type constructors. Traits with generic parameters or associated types, including theFn*
traits, are bounds constructors. When we say, "generic type arguments" above, we mean types that are used to parameterize these type constructors or bounds constructors, including for associated types. When we collect generic type arguments, we do so syntactically (i.e. without normalization and looking only at what is written) and recursively (i.e. within arbitrary levels of syntactic composition). ↩There are existing restrictions on where an RPIT
impl Trait
may appear within a type and within a signature. Those restrictions remain and are not being changed by this stabilization. ↩