Description
With the following files:
Cargo.toml
:
[package]
name = "future_pin_issue"
version = "0.1.0"
authors = ["Joe Doyle <[email protected]>"]
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