mRFC: RPIT auto-trait "inference"

(Here, “mRFC” means mini RFC. I’m curious about community opinions so wrote this idea up in a rough RFC format. I don’t necessarily support fully adopting this RFC’s ideas, but I think they’re worth discussing.)


Summary

Currently, return position impl trait (that is, -> impl Trait) “leaks” auto-trait implementation based on the concrete type returned. We migrate towards inferring the auto-trait implementation based solely on the function declaration by using early and specific lint warnings.

Guide-level explanation

Though impl Trait is very useful for restricting the client usage of a function, one tricky point comes up around auto traits, such as the Send and Sync markers. Consider the following function definition:

impl<Complex, Type> My<Complex, Type> {
    fn my_complex_future(&self) -> impl Future<Output=()> + '_ {
        /* implementation */
    }
}

For futures especially, but additionally for almost any type, it would be useful to be able to Send this type between threads.

For this use case, Rust infers the auto-trait implementation status from the parameters to the function. That is, if all parameters to the function implement some auto-trait, then the return type is required to implement the auto-trait. If any parameter doesn’t implement the auto-trait, then the return type does not promise that it must implement the auto-trait.

If the return type is inferred to implement the auto-trait but doesn’t, you will receive a warning suggestion to add the ?AutoTrait bound to the return type to indicate that the return type may not implement the auto-trait. Similarly, if the return type is inferred to not implement the auto-trait but does, you will receive a warning suggestion to add either AutoTrait or ?AutoTrait to the return type for your desired semantics.

In editions 2015 and 2018, these are just warnings, and the actual auto-trait implementation of return position impl trait is inherited from the actual concrete type returned. In a future edition, these warnings may be upgraded to errors to support more deliberate auto-trait behavior for RPIT by default.

Reference-level explanation

A lint is added to RPIT functions to enforce the above guidelines for auto-trait behavior in RPIT. Here are the combinations and sketched versions of concrete warnings that could be produced to fulfill this RFC’s intent:

fn a(_: impl  Send) -> impl Trait { /* impl  Send */ }
fn b(_: impl !Send) -> impl Trait { /* impl  Send */ }
fn c(_: impl  Send) -> impl Trait { /* impl !Send */ }
fn d(_: impl !Send) -> impl Trait { /* impl !Send */ }

fn e(_: impl  Send) -> impl Trait +  Send { /* impl  Send */ }
fn f(_: impl !Send) -> impl Trait +  Send { /* impl  Send */ }
fn g(_: impl  Send) -> impl Trait + ?Send { /* impl  Send */ }
fn h(_: impl !Send) -> impl Trait + ?Send { /* impl  Send */ }
fn j(_: impl  Send) -> impl Trait +  Send { /* impl !Send */ }
fn k(_: impl !Send) -> impl Trait +  Send { /* impl !Send */ }
fn m(_: impl  Send) -> impl Trait + ?Send { /* impl !Send */ }
fn o(_: impl !Send) -> impl Trait + ?Send { /* impl !Send */ }
warning: fn b should have a `!Send` return type, but `ReturnType: Send`
suggestion: Add a `?Send` bound to not provide the auto-trait
suggestion: Add a `Send` bound to promise the auto-trait

warning: fn c should have a `Send` return type, but `ReturnType` is not `Send`
help: (trace as to why)
suggestion: Add a `?Send` bound to not provide the auto-trait

error: fn j promises a `Send` return type, but `ReturnType` is not `Send`

error: fn k promises a `Send` return type, but `ReturnType` is not `Send`

Drawbacks

  • We change stable behavior (RPIT auto-trait leakage) for a clarity benefit, introducing an edition deprecation warning
  • This puts a strong requirement on -> impl Future authors to include + ?Unpin for self-referential futures
  • async fn desugaring needs to add a + ?Unpin bound

Rationale

This idea came up when discussing async and Send bounds. async fn in traits would not be able to leak the Send implementation like is done in current unstable for non-trait fn. By inferring RPIT auto-traits by the function declaration rather than the body, this problem goes away.

The warnings are structured such that actual stable behavior is not broken, and most cases should “just work”. In the cases where this model and the actual behavior do not line up, a warning is given, and could potentially be upgraded to an error in the future.

Alternatives

  • Just don’t do this and stick with “leakage” as it exists today, and find some reasonable behavior for RPIT in traits other than this
  • Just do this for RPIT in traits, leave non-trait RPIT alone

Prior art

This inference behaves similarly to lifetime inference, specifically in async fn, as well as the regular auto-trait inference in structures. The inference is the same as if the function just returned a structure holding all of the arguments.

Unresolved questions

  • When should this be a hard error, and when should it be a warning?
  • How practical is the lint in cases where the parameters conditionally implement auto-traits? Can this have a reasonable warning?

Future possibilities

  • Upgrading warnings to a hard error in a future edition (explicitly not part of this RFC)

3 Likes