Posting to internals forum on recommendation from quindot here.
I posted about a question about ?Move backwards compatibility on Reddit and didn't get a good answer. So I'm going to apply Cunningham's_Law and assume I'm right.
I'll assume P: !Move + !Unpin and U: Move + Unpin for the remainder of this post.
Summary
?Move is backwards compatible just like Pin is. The definition used by [1] and [2] both work for this explanation, but I'll go with: T: !Move means "safe code may never move T and unsafe code may only move T if the Move type's address has not been witnessed.
The short description is:
- Replace all instances of
Pin<Ptr>withPtrand!Movetrait as needed. Replace all instances with-> Pwhich is later pinned toPin<&mut Pwith-> UwhereUhas a method later used to initializePwith an out parameter (e.g.into_future<P: ?Move + Future>(self) -> super P[3]). - Add two new
Dereftraits corresponding toDerefandDerefMutwhereTarget: ?Move.
Not breaking
[2:1] argues that Move is unusable because of backward incompatibility. Most examples given don't actually present issues though.
Traits which can present a problem must have the following characteristics:
- Returns a type which mentions an associated type without moving the associated type (like
&Por&mut P).- If the associated type is moved by value, it can't be
!Moveanyway. - This eliminates the arithmetic operators which return associated type by value.
- This eliminates
Iteratorwhich return associated type by value.
- If the associated type is moved by value, it can't be
- The type doesn't interact with or use
Pin.- This proposal replaces
Pinwith theMovetrait, so traits which explicitly rely onPinwill be substituted with equivalent?Movewhere needed. - This eliminates the normal
FutureandIntoFuturetypes.
- This proposal replaces
- The trait is implemented on
Pincurrently.- Traits like
Index{,Mut}aren't implemented forPinshowing a clear lack of need for, or inability to use, stable addresses. As{Mut,Ref}are not implemented forPinand instead are inherent impls onPin. Normal reference reborrowing on&Tand&mut T(with the addition of the two new traits) can replace these.- This eliminates all traits that aren't implemented for
Pin<Ptr>wherePtr'sTarget: !UnpinleavingDeref.
- Traits like
To fix the inability to use Deref and DerefMut, create two new traits DerefNotMove and DerefMutNotMove (bikeshed) which are the same as Deref and DerefMut but their associated type is ?Move. These new traits will be added to deref coercion.
- The traits could be made so that implementing
DerefpreventsDerefNotMoveandDerefNotMove, is implemented for allDeref, and only theDeref{,Mut}is special to the compiler. - Or
DerefandDerefMutcould become aliases toDerefNotMove<Target: Move>and a limited specialization would allow implementing both depending onT: Move/T: !Move. - Or they could simply be additional special traits.
Comparison to Pin
Any api which wishes to take advantage of the pinned typestate and stable addresses must either accept the parameter as Pin or bound by ?Move, but both require a change. For example if the api takes an argument &mut T it doesn't support a T with a stable address and can't rely on T having a stable address. If T: !Unpin is pinned then it can't be used in api's that require &mut T. This is the same as with Move.
In the world of Pin if T is the type you return from a function, you need a new type Pin<&mut T> with T: Future to poll it.
In the world of Move if T is the type you return from a function, you need a new type P: Future to poll it. The difference is how T is converted to the new type. Pin requires a new_unchecked after custom setup code for T. Move requires putting that custom code in a into_future<T: IntoFuture, P: ?Move + Future>(T) -> super P.
Using Index as an example, to use stable addresses with IndexMut requires IndexMut<Output = Pin<&mut P>> and method signature becomes index(&self, index: Idx) -> &mut Pin<&mut P>. To do the same with Index<Output = &mut P> is also allowed (since &mut P: Move) and the signature becomes index(&self, index: Idx) -> &mut &mut P which is the same thing as the Pin version.
Function traits are also the same. You can't return P from a function without using super which changes the function signature. This is the same as Pin<&mut T>. You can't return, by regular move, a type T after its pinned. To return a &P or &mut P is not problem since &P: Move and &mut P: Move, just like Pin<&P> and Pin<&mut P>.
New traits
If Iterator took self by Pin instead, this would require a new trait. This is what's happening with the AsyncIterator/Stream proposal. If Iterator bounded self with ?Move this would require a new trait. Both approaches require a new trait.
A similar argument can be made for every new trait which takes a Pin.
Migrating
The following implementations can ease transitioning to Move based APIs.
// It is safe to pin or remove a pin on an immovable type. The type can't move so the `Pin` does nothing.
impl<Ptr> Pin<Ptr>
where
Ptr: core::ops::Deref,
<Ptr as DerefNotMove>::Target: !Move,
{
/// Constructs a new `Pin<Ptr>` around a pointer to some data of a type that doesn't implement `Move`.
fn new_immovable(ptr: Ptr) -> Pin<Ptr> {
unsafe { Pin::new_unchecked(ptr) }
}
/// Unwraps this Pin<Ptr>, returning the underlying pointer.
/// Doing this operation safely requires that the data pointed at by this pinning pointer doesn't implement `Move` so that we can ignore the pinning invariants when unwrapping it.
fn into_inner_immovable(pin: Pin<Ptr>) -> Ptr {
unsafe { Pin::into_inner_unchecked(pin) }
}
}
#[repr(transparent)]
struct PinFutureFromMoveFuture<T: !Move>(T);
impl<T: core::mov::Future> core::future::Future for PinFutureFromMoveFuture<T> {
type Output = T::Output;
fn poll(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output> {
// Safety: T: !Move so `poll` can't move out of the `Pin`.
let me = unsafe { self.get_unchecked_mut() };
me.0.poll(cx)
}
}
#[repr(transparent)]
struct MoveFutureFromPinFuture<'a, T>(Pin<&'a mut T>);
impl<'a, T: core::future::Future> core::mov::Future for MoveFutureFromPinFuture<T> {
type Output = T::Output;
// Required method
fn poll(
self: &mut Self,
cx: &mut Context<'_>
) -> Poll<Self::Output> {
self.0.as_mut().poll(cx)
}
}
async fn desugaring
Like in [1:1], an async fn returns an IntoFuture<IntoFuture: ?Move> instead of a Future. The IntoFuture itself is Move. .await first calls into_future and then polls.
Stable rust out parameters.
Also there's a way to implement a usable desugaring of -> super on stable Rust today.
[3:1] describes most of the process. The bit that's missing is that .assume_init() which moves the value, is not necessary (though it would be much better). Instead use an owned reference (&own) similar to the Stackbox type and cast the ptr/reference as needed. After all, what else can an owned !Move value be used for beyond delayed Drop or reference? This gets even messier (but still possible?) with enums since you can't easily get the address of the variant's fields while the out pointer is uninitialized.
Some code related to this idea is here in case it helps understand what I'm saying.
The thing I would like feedback on the most is if this idea is not crazy, and whether ?Move is in fact not necessarily backward incompatible.