Skip to content
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 return type notation (RFC 3654) #138424

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

compiler-errors
Copy link
Member

Return Type Notation (RTN) Stabilization Report

Stabilization summary

This PR stabilizes return-type notation in where clause and item bound position, both in path and associated type bound forms for methods in traits that have lifetime generics.

fn foo<T, U>()
where
    // Associated type bound
    T: Trait<method(..): Send + 'static>,
    // Path bound
    U: Trait,
    U::method(..): Send + 'static,
{}

trait Trait {
    // In GAT bounds.
    type Item: Trait<method(..): Send + 'static>;
}

// In opaque item bounds too.
fn rpit() -> impl Foo<method(..): Send + 'static>;

This gives users a way to add trait bounds to the anonymous generic associated types (GATs) that are generated from the return-position impl trait in trait (RPITIT) desugaring.

Motivation

Rust now supports AFITs and RPITITs, but we currently lack the ability for users to declare bounds on the anonymous types returned by such functions at the usage site. This is often referred to as the Send bound problem, since it commonly affects futures which may only conditionally be Send depending on the executor that they are involved with.

Return type notation gives users an intuitive way to solve this problem. See the alternatives section of the RFC for other proposed solutions.

This feature makes AFIT more useful in APIs, where users can now bound their methods appropriately at the usage site rather than having to provide a constrained definition like -> impl Future<Output = T> + Send within the trait.

Major design decisions since the RFC

There were no major changes to the design, but there was some consideration whether the bound should be spelled with (..) or (). The implementation commits to (..) since that saves space for explicitly specified parameters in the future like T::method(i32): Trait which can be used to only bound subsets of the RTN type.

What is stabilized?

As mentioned in the summary, RTN is allowed as the self type of a where clause anywhere that a where clause is allowed. This includes shorthand syntax, like where T::method(..): Send, where the trait is not specified, in the same situations that it would be allowed for associated types.

RTN is also allowed in associated type bound position, and is allowed anywhere an associated type bound is allowed, except for dyn Trait types.

Linting

This PR also removes the async_fn_in_trait lint. This lint was introduced to discourage the usage of async fn in trait since users were not able to add where clause bounds on the futures that these methods returned.

This is now possible for functions that have only lifetime generics. The only caveat here is that it's still not possible for functions with type or const generics. However, I think that we should accept that corner case, since the intention was to discourage the usage before RTN was introduced, and not to outright ban AFIT usage. See #116184 for some context.

I could change my mind on this part, but it would still be quite annoying to have to keep this lint around until we fix RTN support for methods with type or const generics.

What isn't stabilized? (a.k.a. potential future work)

RTN is not allowed as a free-standing type. This means that it cannot be used in ADTs (structs/enums/unions), parameters, let statements, or turbofishes.

RTN can only be used when the RPITIT (return-position impl trait in trait) is the outermost type of the return type, i.e. -> impl Sized but not -> Vec<impl Sized>. It may also be used for AFIT (async fn in trait), since that desugars to -> impl Future<Output = T>.

RTN is only allowed for methods with only lifetimes. Since the RTN syntax expands to a for<...> binder of all of the generics of the associated function, and we do not have robust support for non-lifetime binders yet, we reject the case where we would be generating a for<T> binder for now. This restriction may be lifted in the future.

Implementation summary

This feature is generally a straightforward extension of associated types and associated type bounds, but adjusted (in the presence of (..)-style generics) to do associated item resolution in the value namespace to look up an associated function instead of a type.

In the resolver, if we detect a path that ends in (..), we will attempt to resolve the path in the value namespace:

// If we have a path that ends with `(..)`, then it must be
// return type notation. Resolve that path in the *value*
// namespace.
let source = if let Some(seg) = path.segments.last()
&& let Some(args) = &seg.args
&& matches!(**args, GenericArgs::ParenthesizedElided(..))
{
PathSource::ReturnTypeNotation
} else {
PathSource::Type
};

In "resolve bound vars", which is the part of the compiler which is responsible for resolving lifetimes in HIR, we will extend the (explicit or implicit) for<> binder of the clause with any lifetimes params from the method:

// When we have a return type notation type in a where clause, like
// `where <T as Trait>::method(..): Send`, we need to introduce new bound
// vars to the existing where clause's binder, to represent the lifetimes
// elided by the return-type-notation syntax.
//
// For example, given
// ```
// trait Foo {
// async fn x<'r>();
// }
// ```
// and a bound that looks like:
// `for<'a, 'b> <T as Trait<'a>>::x(): Other<'b>`
// this is going to expand to something like:
// `for<'a, 'b, 'r> <T as Trait<'a>>::x::<'r, T>::{opaque#0}: Other<'b>`.
//
// We handle this similarly for associated-type-bound style return-type-notation
// in `visit_segment_args`.

We similarly handle RTN bounds in associated type bounds:

// If the args are parenthesized, then this must be `feature(return_type_notation)`.
// In that case, introduce a binder over all of the function's early and late bound vars.
//
// For example, given
// ```
// trait Foo {
// async fn x<'r, T>();
// }
// ```
// and a bound that looks like:
// `for<'a> T::Trait<'a, x(..): for<'b> Other<'b>>`
// this is going to expand to something like:
// `for<'a> for<'r> <T as Trait<'a>>::x::<'r, T>::{opaque#0}: for<'b> Other<'b>`.

In HIR lowering, we intercept the type being lowered if it's in a where clause:

/// Lower a type, possibly specially handling the type if it's a return type notation
/// which we otherwise deny in other positions.
pub fn lower_ty_maybe_return_type_notation(&self, hir_ty: &hir::Ty<'tcx>) -> Ty<'tcx> {
.

Otherwise, we unconditionally reject the RTN type, mentioning that it's not allowed in arbitrary type position. This can be extended later to handle the lowering behavior of other positions, such as structs and unsafe binders.

The rest of the compiler is already well equipped to deal with RPITITs, so not very much else needed to be changed.

Tests

"Positive tests":

  • "basic" test: tests/ui/associated-type-bounds/return-type-notation/basic.rs.
  • Resolution conflicts prefer methods with .. syntax: tests/ui/associated-type-bounds/return-type-notation/namespace-conflict.rs.
  • Supertraits: tests/ui/async-await/return-type-notation/supertrait-bound.rs.
  • "Path"-like syntax: tests/ui/associated-type-bounds/return-type-notation/path-works.rs.
  • RTN where clauses promoted into item bounds: tests/ui/associated-type-bounds/implied-from-self-where-clause.rs.
  • Exercising higher ranked bounds: tests/ui/associated-type-bounds/return-type-notation/higher-ranked-bound-works.rs.

"Negative tests":

  • Bad inputs or return types: tests/ui/associated-type-bounds/return-type-notation/bad-inputs-and-output.rs.
  • Rejected in expression position: tests/ui/associated-type-bounds/return-type-notation/bare-path.rs.
  • Reject type equality: tests/ui/associated-type-bounds/return-type-notation/equality.rs.
  • Rejected on non-RPITITs: tests/ui/associated-type-bounds/return-type-notation/non-rpitit.rs + tests/ui/associated-type-bounds/return-type-notation/not-a-method.rs.
  • Associated type bound ambiguity: tests/ui/async-await/return-type-notation/super-method-bound-ambig.rs.
  • Shorthand syntax ambiguity: tests/ui/associated-type-bounds/return-type-notation/path-ambiguous.rs.
  • Test rejecting types and consts: tests/ui/async-await/return-type-notation/ty-or-ct-params.rs.

Remaining bugs, FIXMEs, and open issues

What other unstable features may be exposed by this feature?

This feature gives users a way to name the RPIT of a method defined in an impl, but only in where clauses.

Tooling support

Rustfmt: ✅ Supports formatting (..)-style generics.

Clippy: ✅ No support needed, unless specific clippy lints are impl'd to care for RTN specifically.

Rustdoc: ❓ #137956 should fix support for RTN locally. Cross-crate re-exports are still broken, but that part of rustdoc (afaict) needs to be overhauled in general.

Rust-analyzer: ❓ There is parser support. I filed an issue for improving support in the IDE in rust-lang/rust-analyzer#19303.

History

RTN was first implemented in associated type bounds in #109010.

It was then implemented in where T::method(..): Send style bounds in #129629.

The RFC was proposed in rust-lang/rfcs#3654.

There was a call for testing here: https://blog.rust-lang.org/inside-rust/2024/09/26/rtn-call-for-testing.html.

History

Acknowledgments

Thanks to @nikomatsakis for writing the RFC for RTN, and to various reviewers for reviewing the various PRs implementing parts of this feature.

Pending items before stabilization

  • Land rustdoc support
  • Land or defer rust-analyzer support
  • Finalize style for RTN
  • Reference work

@compiler-errors compiler-errors added the T-lang Relevant to the language team, which will review and decide on the PR/issue. label Mar 12, 2025
@rustbot
Copy link
Collaborator

rustbot commented Mar 12, 2025

r? @wesleywiser

rustbot has assigned @wesleywiser.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Mar 12, 2025
@compiler-errors
Copy link
Member Author

cc @rust-lang/lang and @rust-lang/types (though this PR doesn't do that much interesting on the types side other than giving us a new way of naming RPITITs).

@compiler-errors
Copy link
Member Author

cc @rust-lang/rust-analyzer

@rust-log-analyzer

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants