rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
99.2k stars 12.81k forks source link

Confusing interaction between associated types, `async fn` and `impl Future` #89657

Open jpdoyle opened 3 years ago

jpdoyle commented 3 years ago

With the following files: Cargo.toml:

[package]
name = "future_pin_issue"
version = "0.1.0"
authors = ["Joe Doyle <joethedoyle@protonmail.com>"]
edition = "2018"

[dependencies]
futures = "0.3.16"

[features]
use_async_fn = []

src/main.rs:

use futures::Future;
use std::pin::Pin;

pub struct MyBackend<'a> {
    _marker: std::marker::PhantomData<&'a ()>,
}

impl<'a> Backend<'a> for MyBackend<'a> {
    type MyStorage = AtomicStorage;
}

pub trait Storage<'a> {}

pub struct AtomicStorage {}
impl<'a> Storage<'a> for AtomicStorage {}

pub trait Backend<'a>: Send {
    type MyStorage: Storage<'a>;
    fn store<F>(&mut self, _update: F) -> Pin<Box<dyn Future<Output = ()> + Send>>
    where
        F: Fn(Self::MyStorage),
    {
        unimplemented!()
    }
}

// https://stackoverflow.com/questions/50547766/how-can-i-get-impl-trait-to-use-the-appropriate-lifetime-for-a-mutable-reference
pub trait Captures<'a> {}
impl<'a, T: ?Sized> Captures<'a> for T {}

#[cfg(feature = "use_async_fn")]
pub async fn transfer<'a, B: 'a + Backend<'a> + Send + Sync>(backend: &mut B) {
    backend.store(|_t| {}).await
}

#[cfg(not(feature = "use_async_fn"))]
pub fn transfer<'a, 'b, B: 'a + Backend<'a> + Send + Sync>(
    backend: &'b mut B,
) -> impl 'b + Captures<'a> + std::future::Future<Output = ()> + Send {
    async move { backend.store(|_t| {}).await }
}

// This works
fn _test1<'a>(mut backend: impl 'a + Backend<'a> + Send + Sync) {
    let _: Pin<Box<dyn Send>> = Box::pin(async {
        transfer(&mut backend).await;
    });
}

// This doesn't
fn _test2<'a>(mut backend: MyBackend<'a>) {
    let _: Pin<Box<dyn Send>> = Box::pin(async {
        transfer(&mut backend).await;
    });
}

fn _hidemytype<'a>(backend: MyBackend<'a>) -> impl 'a + Backend<'a> + Send + Sync {
    backend
}

// This does!
fn _test3<'a>(backend: MyBackend<'a>) {
    let _: Pin<Box<dyn Send>> = Box::pin(async {
        transfer(&mut _hidemytype(backend)).await;
    });
}

// And so does this
fn _test4<'a>(backend: MyBackend<'a>) {
    _test1(backend);
}

fn main() {}

It succeeds with cargo build (using the second transfer), but if you use the first transfer by running cargo build --features use_async_fn, you get a very confusing error:

$ cargo build --features use_async_fn
   Compiling future_pin_issue v0.1.0 (/home/joe/issue-test)
error: implementation of `Backend` is not general enough
  --> src/main.rs:52:33
   |
52 |       let _: Pin<Box<dyn Send>> = Box::pin(async {
   |  _________________________________^
53 | |         transfer(&mut backend).await;
54 | |     });
   | |______^ implementation of `Backend` is not general enough
   |
   = note: `Backend<'1>` would have to be implemented for the type `MyBackend<'0>`, for any two lifetimes `'0` and `'1`...
   = note: ...but `Backend<'2>` is actually implemented for the type `MyBackend<'2>`, for some specific lifetime `'2`

error: could not compile `future_pin_issue` due to previous error

The second transfer implementation (used when use_async_fn is off) is a workaround based on the desugar of async fn, plus a Captures trait I found on stackoverflow.

Confusingly, the error only ever occurs in _test2. _test3 is especially concerning, because it means that "forgetting" information about the type makes the typechecking succeed somehow!

Removing the Backend::<'a>::MyStorage associated type, changing store to store<F,MyStorage: Storage<'a>>, and calling backend.store::<_,AtomicStorage>(...) rather than backend.store(...) also fixes the error.

This looks like a potential type inference bug, but it might be expected behavior. If it's expected, I think the error messages could use some work.

Meta

rustc --version --verbose:

$ rustc --version --verbose
rustc 1.55.0 (c8dfcfe04 2021-09-06)
binary: rustc
commit-hash: c8dfcfe046a7680554bf4eb612bad840e7631c4b
commit-date: 2021-09-06
host: x86_64-unknown-linux-gnu
release: 1.55.0
LLVM version: 12.0.1
$ cargo --version --verbose
cargo 1.55.0 (32da73ab1 2021-08-23)
release: 1.55.0
commit-hash: 32da73ab19417aa89686e1d85c1440b72fdf877d
commit-date: 2021-08-23
traviscross commented 9 months ago

@rustbot labels +AsyncAwait-Triaged +WG-async

We reviewed this today in WG-async triage.

We believe this is a manifestation of what's being worked on and tracked in #110338.