Ergonomic Self-Referential Types for Rust
— 2024-07-01

  1. motivating example
  2. self-referential lifetimes
  3. constructing types in-place
  4. converting into immovable types
  5. immovable types
  6. motivating example, reworked
  7. phased initialization
  8. migrating from pin to move
  9. making immovable types movable
  10. further reading
  11. conclusion

I've been thinking a little about self-referential types recently, and while it's technically possible to write them today using Pin (limits apply), they're not at all convenient. So what would it take to make it convenient? Well, as far as I can tell there are four components involved in making it work:

  1. The ability to write 'self lifetimes.
  2. The ability to construct types from functions in fixed memory locations.
  3. A way to mark types as "immovable" in the type system.
  4. The ability to safely initialize self-references in structs without going through an option-dance.

It's only once we have all four of these components that writing self-referential types can become accessible to most regular Rust programmers. And that seems important, because as we've seen with async {} and Future: once you start writing sufficiently complex state machines, being able to track references into data becomes incredibly useful.

Speaking of async and Future: in this post we'll be using that as a motivating example for how these features can work together. Because if it seems realistic that we can make that a case as complex as that work, other simpler cases should probably work too.

Oh and before we dive in, I want to give a massive shout-out to Eric Holk. We've spent several hours working through the type-system implications of !Move together, and worked through a number of edge cases and issues. I can't be solely credited for the ideas in this post. However any mistakes in this post are mine, and I’m not claiming to speak for the both of us.

Disclaimer: This post is not a fully-formed design. It is an early exploration of how several features could work together to solve a broader problem. My goal is primarily to narrow down the design space to a tangible list of features which can be progressively implemented, and share it with the broader Rust community for feedback. I'm not on the lang team, nor do I speak for the lang team.

Motivating example

Let's take async {} and Future as our examples here. When we borrow local variables in an async {} block across .await points, the resulting state machine will store both the concrete value and a reference to that value in the same state machine struct. That state machine is what we call self-referential, because it has a reference which points to something in self. And because references are pointers to concrete memory addresses, there are challenges around ensuring they are never invalidated as that would result in undefined behavior. Let's look at an example async function:

async fn give_pats() {
    let data = "chashu tuna".to_string();       // ← Owned value declared
    let name = data.split(' ').next().unwrap(); // ← Obtain a reference
    pat_cat(&name).await;                       // ← `.await` point here
    println!("patted {name}");                  // ← Reference used here
} 

async fn main() {
    give_pats().await; // Calls the `give_pats` function.
}

This is a pretty simple program, but the idea should come across well enough: we declare an owned value in-line, we call an .await function, and later on we reference the owned value again. This keeps a reference live across an .await point, and that requires self-referential types. We can desugar this to a future state machine like so:

enum GivePatsState {
    Created,       // ← Marks our future has been created
    Suspend1,      // ← Marks the first `.await` point
    Complete,      // ← Marks the future is now done
}

struct GivePatsFuture {
    resume_from: GivePatsState,
    data: Option<String>,
    name: Option<&str>,  // ← Note the lack of a lifetime here
}

impl GivePatsFuture {
    fn new() -> Self {
        Self {
            resume_from: GivePatsState::Created,
            data: None,
            name: None,
        }
    }
}

impl Future for GivePatsFuture {
    type Output = ();
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { ... }
}

The lifetime of GivePatsFuture::name is unknown, mainly because we can't name it. And because the desugaring happens in the compiler, it doesn't have to name the lifetime either. We'll talk more about that later in this post. Because this generates a self-referential state machine, this future will need to be fixed in-place using Pin first. Once pinned, the Future::poll method can be called in a loop until the future yields Ready. The desugaring for that will look something like this:

let mut future = IntoFuture::into_future(GivePatsFuture::new());
let mut pinned = unsafe { Pin::new_unchecked(&mut future) };
loop {
    match pinned.poll(&mut current_context) {
        Poll::Ready(ready) => break ready,
        Poll::Pending => yield Poll::Pending,
    }
}

And finally, just for reference, here is what the traits we're using look like today. The main bit that's interesting here for the purpose of this post is that Future takes a Pin<&mut Self>, which we'll be explaining how it can be replaced with a simpler system throughout the remainder of this post.

pub trait IntoFuture {
    type Output;
    type IntoFuture: Future<Output = Self::Output>;
    fn into_future(self) -> Self::IntoFuture;
}

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Now that we've taken a look at how self-referential futures are desugared by the compiler today, let's take a look at how we can incrementally replace it with a safe, user-constructible system.

Self-referential lifetimes

In our motivating example we showed the GivePatsFuture which has the name field that points at the data field. It's a clearly a reference, but it does not carry any lifetime:

struct GivePatsFuture {
    resume_from: GivePatsState,
    data: Option<String>,
    name: Option<&str>, // ← Note the lack of a lifetime
}

The reason this doesn't have a lifetime is not inherent, it's because we can't actually name the lifetime here. It's not 'static because it isn't valid for the remainder of the program. In the compiler today I believe we can just omit the lifetime because the codegen happens after the lifetimes have already been checked. But say we wanted to write this by hand today as-is; we would need a concept for an "unchecked lifetime", something like this:

struct GivePatsFuture {
    resume_from: GivePatsState,
    data: Option<String>,
    name: Option<&'unsafe str>, // ← Unchecked lifetime
}

Being able to write a lifetime that isn't checked by the compiler would be the first-click stop to enable writing self-referential structs by hand. It would just require a lot of unsafe and anxiety to get right. But at least it would be possible. I believe folks on T-compiler are already working on adding this, which seems like a great idea.

But even better would be if we could describe checked lifetimes here. What we actually want to write here is a lifetime which is valid for the duration of the value - and it would always be guaranteed to be valid. Adding this lifetime would come with additional constraints we'll get into later in this post (e.g. the type wouldn't be able to move), but what we really want is to be able to write something like this:

struct GivePatsFuture {
    resume_from: GivePatsState,
    data: Option<String>,
    name: Option<&'self str>, // ← Valid for the duration of `Self`
}

To sidetrack slightly; adding a named 'self lifetime could also allow us to remove the where Self: 'a boilerplate when using lifetimes in generic associated types. If you've ever worked with lifetimes in associated types, then you'll likely have run into the "missing required bounds" error. Niko suggested in this issue using 'self as one possible solution for the bounds boilerplate. I think its meaning would be slightly different than when used with self-references? But I don't believe using this would be ambiguous either. And all in all I think it looks rather neat:

// A lending iterator trait as we have to write it today:
trait LendingIterator {
    type Item<'a>
    where
        Self: 'a;
    fn next(&mut self) -> Self::Item<'_>;
}

// A lending iterator trait if we had `'self`:
trait LendingIterator {
    type Item<'self>;
    fn next(&mut self) -> Self::Item<'_>;
}

Constructing types in-place

In order for 'self to be valid, we have to promise our value won't move in memory. And before we can promise a value won't move, we have to first construct it somewhere we can be sure it can stay and not be moved further. Looking at our motivating example, the way we achieve this using Pin is a little goofy. Here's that same example again:

impl GivePatsFuture {
    fn new() -> Self {
        Self {
            resume_from: GivePatsState::Created,
            data: None,
            name: None,
        }
    }
}

async fn main() {
    let mut future = IntoFuture::into_future(GivePatsFuture::new());
    let mut pinned = unsafe { Pin::new_unchecked(&mut future) };
    loop {
        match pinned.poll(&mut current_context) {
            Poll::Ready(ready) => break ready,
            Poll::Pending => yield Poll::Pending,
        }
    }
}

In this example we're seeing GivePatsFuture being constructed inside of the new function, be moved out of that, and only then pinned in-place using Pin::new_unchecked. Even if GivePatsFuture: !Unpin, the Unpin trait only affects types once they are held inside of a Pin structure. And we can't just return Pin from new, because the function's stack frames are discarded the moment the function returns.

It would be better if we enabled types to describe how they can construct themselves in-place. That means no more external Pin::new_unchecked calls; but exclusively internally provided constructors. This enables us to make self-referential types entirely self-contained, with internally provided constructors replacing the external pin dance. Here's how we could rewrite GivePatsFuture::new to use an internal constructor instead:

impl GivePatsFuture {
    fn new(slot: Pin<&mut MaybeUninit<Self>>) {
        let slot = unsafe { slot.get_unchecked_mut() };
        let this: *mut Self = slot.as_mut_ptr();
        unsafe { 
           addr_of_mut!((*this).resume_from).write(GivePatsState::Created);
           addr_of_mut!((*this).data).write(None);
           addr_of_mut!((*this).name).write(None);
        };
    }
}

If you don't like this: that's understandable. I don't think anyone does. But bear with me; we're on a little didactic sausage-making journey here together. I'm sorry about the sights; let's quickly move on.

In a recent blog post I posited we might be able to think of parameters like these as "spicy return". James Munns pointed out that in C++ this feature has a name: out-pointers. And Jack Huey made an interesting connection between this and the super let design. Just so we don't have to look at a pile of unsafe code again, let's pretend we can combine these into something coherent:

impl GivePatsFuture {
    fn new() -> super Pin<&'super mut Self> {
        pin!(Self { // just pretend this works
            resume_from: GivePatsState::Created,
            data: None,
            name: None,
        })
    }
}

I know, I know - I'm showing syntax here. I'm personally meh about the way it looks and we'll talk more later about how we can improve this, but I hope we can all agree that the function body itself is approximately 400% more legible than the pile of unsafe we were working with earlier. We'll also get to how we can entirely remove Pin from the signature, so please don't get too hung up on that either.

I'm asking you to play along here for a second, and pretend we might be able to do something like this for now, so we can get to the part later on this post where we can actually fix it. In maybe state this more clearly: this post is less about proposing concrete designs for a problem, but more about how we can tease apart the problem of "immovable types" into separate features we can tackle independently from one another.

Converting into immovable types

Alright, so we have an idea of how we might be able to construct immovable types in-place, provided as a constructor defined on a type. Now while that's nice, we've also lost an important property with that: whenever GivePatsFuture is constructed, it needs to have a fixed place in memory. Where before we could freely move it around until we started .awaiting it.

One of the main reasons why async is useful is because it enables ad-hoc concurrent execution. That means we want to be able to take futures and pass them to concurrency operations to enable concurrency through composition. We can't move futures which have a fixed location in memory, so we need a brief moment where futures can be moved before they're ready to be kept in place and polled to completion.

The way Pin works with this today is that a type can be !Unpin - but that only becomes relevant once it's placed inside of a Pin structure. With futures that typically doesn't happen until it begins being polled, usually via .await, and so we get the liberty of moving !Unpin futures around until we start .awaiting them. That's why !Unpin doesn't mark: "A type which cannot be moved", it marks: "A type which cannot be moved once it has been pinned". This is definitely confusing, so don't worry if it's hard to follow.

fn foo<T: Unpin>(t: &mut T);   // Type is not pinned, type can be moved.
fn foo<T: !Unpin>(t: &mut T);  // Type is not pinned, type can be moved.
fn foo<T: Unpin>(t: Pin<&mut T>);  // Type is pinned, type can be moved.
fn foo<T: !Unpin>(t: Pin<&mut T>); // Type is pinned, type can't be moved.

If we want "immovability" to be unconditionally part of a type, we can't make it behave the same way Unpin does. Instead it seems better to separate the movable / immovable requirements into two separate types. We first construct a type which can be freely moved around - and once we're ready to drive it to completion, we convert it to a type which is immovable and we begin calling that. This maps perfectly to the separation between IntoFuture and Future we already use.

Let's take a look at our first example again, but modify it slightly. What I'm proposing here is that rather than give_pats returning an impl Future, it should instead return an impl IntoFuture. This type is not pinned, and can be freely moved around. It's only once we're ready to .await it that we call .into_future to obtain the immovable future - and then we call that.

struct GivePatsFuture { ... } 
impl GivePatsFuture {
    fn new() -> super Pin<&'super mut Self> { ... } // suspend belief pls
}

struct GivePatsIntoFuture;
impl IntoFuture for GivePatsIntoFuture {
    type Output = ();
    type IntoFuture = GivePatsFuture;

    // We call the `Future::new` constructor which gives us a
    // `Pin<&'super GivePatsFuture>`, and then rather than writing
    // it into the current function's stack frame we write it in the
    // caller's stack frame.
    //
    // (keep belief suspended a little longer)
    fn into_future(self) -> super Pin<&'super mut GivePatsFuture> {
        GivePatsFuture::new() // create in caller's scope
    }
}

Just like we can keep returning values from functions to pass them further up the call stack, so should we be able to use out-pointers / emplacement / spicy allocate in a stack frame further up the call stack. Though even if we didn't support that out of the gate, we could probably in-line the GivePatsFuture::new into GivePatsIntoFuture::into_future and things would still work. And with that, our .await desugaring could then look something like this:

async fn main() {
    let into_future: GivePatsIntoFuture = give_pats();
    let mut future: Pin<&mut GivePats> = GivePatsIntoFuture.into_future();
    loop {
        match future.poll(&mut current_context) {
            Poll::Ready(ready) => break ready,
            Poll::Pending => yield Poll::Pending,
        }
    }
}

To re-iterate why this section exists: we can get the same functionality Pin + Unpin provide today by creating two separate types. One type which can be freely moved around. And another type which once constructed will not move locations in memory.

So far the only framing of "immovable types" I've seen so far is a single types which have both these properties - just like Unpin does today. What I'm trying to articulate here is that we can avoid that issue if we choose to create two types instead, enabling one to construct the other, and make them provide separate guarantees. I think that's a novel insight, and one I thought was important to spend some time on.

Immovable types

Alright, I've been asking folks to suspend belief that we can in fact perform in-place construction of a Pin<&mut Self> type somehow and that would all work out the way we want it to. I'm not sure myself, but for the sake of the narrative of this post it was easier if we just pretended we could for a second.

The real solution here, of course, to get rid of Pin entirely. Instead types themselves should be able to communicate whether they have a stable memory location or not. The simplest formulation for this would be to add a new built-in auto-trait, Move, which tells the compiler whether a type can be moved or not.

auto trait Move {}

This is of course not a new idea: we've known about the possibility for Move since at least 2017. That's before I started working on Rust. There were some staunch advocates for that in the Rust community in favor of Move, but ultimately that wasn't the design we ended up going with. I think in hindsight most of will acknowledge that the downsides of Pin are real enough that revisiting Move and working through its limitations seems like a good idea 1. To explain what the Move trait is: it would be a language-level trait which governs access to the following capabilities:

1

For anyone looking to assign blame here or pull skeletons out of closets: please don't. The higher-order bit here is that we have Pin today, it clearly doesn't work as well as was hoped at the time, and we'd like to replace it with something better. I think the most interesting thing to explore here is how we can move forward and do better.

Conversely, when a type implements !Move they would not have access to any of these capabilities - making it so they cannot be moved once they have a fixed memory location. And by default we would assume in all bounds that types are Move, except for places that explicitly opt-out by using + ?Move. Here are examples of things that constitute moves:

// # examples of moving

// ## swapping two values
let mut x = new_thing();
let mut y = new_thing();
swap(&mut x, &mut y);

// ## passing by value
fn foo<T>(x: T) {}
let x = new_thing();
foo(x);

// ## returning a value
fn make_value() -> Foo {
    Foo {
        x: 42
    }
}

// ## `move` closure captures
let x = new_thing();
thread::spawn(move || {
    let x = x;
})

And here are some things that do not constitute moves:

// # things that are not moves

// ## passing a reference
fn take_ref<T>(x: &T) {}
let x = new_thing();
take_ref(&x);

// ## passing mutable references is also okay,
//    but you have to be careful how you use it
fn take_mut_ref<T>(x: &mut T) {}
let mut x = new_thing();
take_mut_ref(&mut x);

Passing types by-value will never be compatible with !Move types because that’s what a move is. Passing types by-reference will always be compatible with !Move types because they are immutable 2. The only place with some ambiguity is when we work with mutable references, as things like mem::swap allow us to violate the immovability guarantees.

2

Yes yes, we'll get to internal mutability in a second here.

If a function wants to take a mutable reference which may be immovable, they will have to add + ?Move to it. If a function does not use + ?Move on their mutable reference, then a !Move type cannot be passed to it. In practice this will work as follows:

fn meow<T>(cat: T);            // by-value,   can't pass `!Move` values
fn meow<T>(cat: &T);           // by-ref,     can pass `!Move` values
fn meow<T>(cat: &mut T);       // by-mut-ref, can't pass `!Move` values
fn meow<T: ?Move>(cat: &mut T) // by-mut-ref, can pass `!Move` values

By default all cat: &mut T bounds would imply + Move. And only where we opt-in to + ?Move could !Move types be passed. In practice it seems likely most places will probably be fine adding + ?Move, since it's far more common to write to a field of a mutable reference than it is to replace it whole-sale using mem::swap. Things like interior mutability are probably also largely fine under these rules, since even if accesses go through shared references, updating the values in the pointers will have to interact with the earlier rules we've set out - and those are safe by default.

To be entirely accurate we also have to consider internal mutability. That allows us to mutate values through shared references - but only by being able to conditionally convert it to &mut references at runtime. Just because we allow casting &T to &mut T at runtime, doesn't mean that the rules we've applied to the system don't still work. Say we held an &mut T: !Move inside of a Mutex. If we tried to call the deref_mut method, we'd get a compile-error because that bound hasn't yet declared that T: ?Move. We could probably add that, but because it doesn't work by default we'd have an opportunity to validate its soundness before adding it.

Anyway, that's enough theory about how this should probably work for now. Let's try and update our earlier example, replacing Pin with !Move. That should be as simple as adding a !Move impl on GivePatsFuture.

struct GivePatsFuture {
    resume_from: GivePatsState,
    data: Option<String>,
    name: Option<&'self str>,
}
impl !Move for GivePatsFuture {}

And once we have that, we can change our constructors to return super Self instead of super Pin<&'super mut Self>. We already know that emplacement using something like super Self (not actual notation) to write to fixed memory locations seems plausible. All we then need to do is add an auto-trait which tells the type-system that further move operations aren't allowed.

struct GivePatsFuture { ... } 
impl !Move for GivePatsFuture {}
impl GivePatsFuture {
    fn new() -> super Self { ... } // create in caller's scope
}

struct GivePatsIntoFuture;
impl IntoFuture for GivePatsIntoFuture {
    type Output = ();
    type IntoFuture = GivePatsFuture;
    fn into_future(self) -> super GivePatsFuture {
        GivePatsFuture::new() // create in caller's scope
    }
}

I probably should have said this sooner, but I'll say it now: in this post I'm intentionally not bothering with backwards-compat. The point, again, is to break the complicated design space of "immovable types" into smaller problems we can tackle one-by-one. Figuring out how to bridge Pin and !Move is something we will want to figure out at some point - but not now.

As far as async {} and Future are concerned: this should work! This allows us to freely move around async blocks which desugar into IntoFuture. And only once we're ready to start polling them do we call into_future to obtain an impl Future + !Move. A system like that is equivalent to the existing Pin system, but does not need Pin in its signature. For good measure, here's how we would be able to rewrite the signature of Future with this change:

// The current `Future` trait
// using `Pin<&mut Self>`
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

// The `Future` trait leveraging `Move`
// using `&mut self`
pub trait Future {
    type Output;
    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

That would also mean: no more Pin-projections. No more incompatibilities with Drop. Because it's an auto-trait that governs language behavior, as long as the base rules are sound, the interaction with all other parts of Rust would be sound.

Also, most relevant for me probably, this would make it possible to write future state machines using methods and functions, rather than the current status quo where we just lump everything into the poll function body. After having written a ridiculous amount of futures by hand over the past six years, I can't tell you how much I'd love to be able to do that.

Motivating example, reworked

Now that we've covered self-referential lifetimes, in-place construction, understand async {} should return IntoFuture, and have seen !Move, we're ready to bring these features together to rework our motivating example. This is what we started with using regular async/.await code:

async fn give_pats() {
    let data = "chashu tuna".to_string();
    let name = data.split(' ').next().unwrap();
    pat_cat(&name).await;
    println!("patted {name}");
} 

async fn main() {
    give_pats().await;
}

And here is what that, with these new capabilities, here is what async fn give_pats would be able to desugar to. Note the 'self lifetime, the !Move impl for the future, the omission of Pin everywhere, and the in-place construction of the type.

enum GivePatsState {
    Created,
    Suspend1,
    Complete,
}

struct GivePatsFuture {
    resume_from: GivePatsState,
    data: Option<String>,
    name: Option<&'self str>,        // ← Note the `'self` lifetime
}
impl !Move for GivePatsFuture {}     // ← This type is immovable
impl Future for GivePatsFuture {
    type Output = ();
    fn poll(&mut self, cx: &mut Context<'_>)  // ← No `Pin` needed
        -> Poll<Self::Output> { ... }
}

struct IntoGivePatsFuture {}
impl IntoFuture for IntoGivePatsFuture {
    type Output = ();
    type IntoFuture: GivePatsFuture
    fn into_future(self)
        -> super GivePatsFuture {  // ← Writes to a stable addr
        Self {
            resume_from: GivePatsState::Created,
            data: None,
            name: None,
        }
    }
}

And finally, we can then desugar the give_pats().await call to concrete types we construct and call to completion:

let into_future = IntoGivePatsFuture {};
let mut future = into_future.into_future(); // ← Immovable without `Pin`
loop {
    match future.poll(&mut current_context) {
        Poll::Ready(ready) => break ready,
        Poll::Pending => yield Poll::Pending,
    }
}

And with that, we should have a working example of async {} blocks, desugared to concrete types and traits that don't use Pin anywhere at all. Accessing fields within it wouldn't go through any kind of pin projection, and there would no longer be any need for things like stack-pinning. Immovability would just be a property of the types themselves, constructed when we need them in the place where we want to use them.

Oh and I guess just to mention it: functions working with these traits would always want to use T: IntoFuture rather than T: Future. That's not a big change, and actually something people should already be doing today. But I figured I'd mention it in case people are confused about what the bounds should be for concurrency operations.

Phased initialization

We didn't show this in our example, but there is one more aspect to self-referential types worth covering: phased initialization. This is when you initialize parts of a type at separate points in time. In our motivating example we didn't have to use that, because the self-references lived inside of an Option. That means that when we initialized the type we could just pass None, and things were fine. However, say we did want to initialize a self-reference, how would we go about that?

struct Cat {
    data: String,
    name: &'self str,
}
impl !Move for Cat {}

impl Cat {
    fn new(data: String) -> super Self {
        Cat {
            data: "chashu tuna".to_string(),
            name: /* How do we reference `self.data` here? */
        }
    }
}

Now of course because String is heap-allocated, its address is actually stable and so we could write something like this:

struct Cat {
    data: String,
    name: &'self str,
}
impl !Move for Cat {}

impl Cat {
    fn new(data: String) -> super Self {
        let data = "chashu tuna".to_string();
        Cat {
            name: data.split(' ').next().unwrap(),
            data, 
        }
    }
}

That's clearly cheating, and not what we want people to have to do. But it does point us at how the solution here should probably work: we first need a stable address to point to. And once we have that address, we can refer to it. We can't do that if we have to build the entire thing in a single go. But what if we could do it in multiple phases? That's what Niko's recent post on borrow checking and view types went into. That would allow us to change our example to instead be written like this:

struct Cat {
    data: String,
    name: &'self str,
}
impl !Move for Cat {}

impl Cat {
    fn new(data: String) -> super Self {
        super let this = Cat { data: "chashu tuna".to_string() }; // ← partial init
        this.name = this.data.split(' ').next().unwrap();         // ← finish init
        this
    }
}

We initialize the owned data in Cat first. And once we have that, we can then initialize the references to it. These references would be 'self, we sprinkle in a super let annotation to indicate we're placing this in the caller's scope, and everything should subsequently check out.

Migrating from Pin to Move

What we didn't cover in this post is any migration story from the existing Pin-based APIs to the new Move-based system. If we want to move off of Pin in favor of Move, the only path plausible way I see is by minting new traits that don't carry Pin in its signature, and providing bridging impls from the old traits to the new traits. A basic conversion with an explicit method could look like this, though blanket impls could also be a possibility:

pub trait NewFuture {
    type Output;
    fn poll(&mut self, ...) { ... }
}

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, ...) { ... }

    /// Convert this future into a `NewFuture`.
    fn into_new_future(self: Pin<&mut Self>) -> NewFutureWrapper<&mut Self> { ... }
}

/// A wrapper bridging the old future trait to the new future trait.
struct NewFutureWrapper<'a, F: Future>(Pin<&'a mut F>);
impl !Move for NewFutureWrapper {}
impl<'a, F> NewFuture for NewFutureWrapper<'a, F> { ... }

I've been repeating this line for at least three years now: but if we want to fix the problems with Pin, the first step we need to take is to not make the problem worse. If the stdlib needs to fix the Future trait once, that sucks but it's fine and we'll find a way to do it. But if we tie Pin up into a number of other traits, the problems will compound and I'm no longer sure whether we can rid ourselves of Pin. And that's a problem, because Pin is broadly disliked and we actively want to get rid of it.

Compatibility with self-referential types is not only relevant for iteration; it's a generalized property which ends up interacting with nearly every trait, function, and language feature. Move just composes with any other trait, and so there's no need for a special PinnedRead or anything. A type would instead just implement Read + Move, and that would be enough for a self-referential reader to function. And we can repeat that for any other combination of traits.

In-place construction of course does change the signature of traits. But in order to support that in a backwards-compatible way, all we'd need to do is enable traits to opt-in to "can perform in-place construction". And being able to gradually roll-out capabilities like that is exactly why we're working on effect generics.

pub trait IntoFuture {
    type Output;
    type IntoFuture: Future<Output = Self::Output>;

    // Marked as compatible with in-place construction, with
    // implementations being able to decide whether they want
    // to use it or not.
    fn into_future(self) -> #[maybe(super)] Self::IntoFuture;
}

If we want self-referential types to be generally useful, they need to practically compose with most other features we have. And so really, the first step to getting there is stop stabilizing any new traits in the stdlib which use Pin in its signature.

Making immovable types movable

So far we’ve been talking a lot about self-referential types and how we need to make sure they cannot be moved, because moving them would be bad. But what if we did allow them to be moved? In C++ this is possible using a feature called "move constructors", and if we supported self-referential types in Rust, it doesn't seem like a big leap to support that too.

Before we go any further I want to preface this: I've heard from people who have worked with move constructors in C++ that they can be rather tricky to work with. I haven't worked with them, so I can't speak from experience. Personally I don't really have any uses where I feel like I would have wanted move constructors, so I'm not particularly in favor or against supporting them. I'm writing this section mostly out of academic interest, because I know there will be people wondering about this. And the rules for how this should work seem fairly straightforward.

Niko Matsakis recently wrote a two-parter on the Claim trait (first, second), proposing a new Claim trait to fill the gap between Clone and Copy. This trait would be for types which are "cheap" to clone, such as the Arc and Rc types. And using autoclaim, the compiler would automatically insert calls to .claim as needed. For example when a move || closure captures a type implementing Claim but it is already in use somewhere else - it would automatically call .claim so it would compile.

Enabling immovable types to relocate would work much the same as auto-claiming would. We would need to introduce a new trait, which we'll call Relocate here, with a method relocate. Whenever we tried to move an otherwise immovable value, we would automatically call .relocate instead. The signature of the Relocate trait would take self as a mutable reference. And return an instance of Self, constructed in-place:

trait Relocate {
    fn relocate(&mut self) -> super Self;
}

Note the signature of self here: we take it by mutable reference - not owned nor shared. That is because what we're writing is effectively the immovable equivalent to Into, but we can't take self by-value - so we have to take it by-reference instead and tell people to just mem::swap away. Applying this our earlier Cat example, we would be able to implement it as follows:

struct Cat {
    data: String,
    name: &'self str,
}
impl Cat {
    fn new(data: String) -> super Self { ... }
}
impl Relocate for Cat {
    fn relocate(&mut self) -> super Self {
        let mut data = String::new(); // dummy type, does not allocate
        mem::swap(&mut self.data, &mut data); // take owned data
        super let cat = Cat { data }; // construct new instance
        cat.name = cat.data.split(' ').next().unwrap(); // create self-ref
        cat
    }
}

We're making one sketchy assumption here: we need to be able to take the owned data from self, without running into the issue where the data can't be moved because it is already borrowed from self. This is a general problem we need to solve, and one way we could for example work around this is by creating dummy pointers in the main struct to ensure the types are always valid - but we invalidate the types:

struct Cat {
    data: String,
    dummy_data: String, // never initialized with a value
    name: &'self str,
}

impl Relocate for Cat {
    fn relocate(&mut self) -> super Self {
        self.name = &self.dummy_data; // no more references to `self.data`
        let data = mem::take(&mut self.data); // shorter than `mem::swap`
        super let cat = Cat { data };
        cat.name = cat.data.split(' ').next().unwrap();
        cat
    }
}

In this example the Cat would implement Move even if it has a 'self lifetime, because we can freely move around. When a type is dropped after having been passed to Relocate, it should not call its Drop impl. Because semantically we're not trying to drop the type - all we're doing is updating its location in memory. Under these rules access to 'self in structs would be available both if Self: !Move OR Self: Relocate.

I want to again emphasize that I'm not directly advocating here for the introduction of move constructors to Rust. Personally I'm pretty neutral about them, and I can be convinced either way. I mainly wanted to have at least once walked through the out the way move constructors could work, because it seems like a good idea to know a gradual path here should be possible. Hopefully that point is coming across okay here.

Further reading

The Pin RFC is an interesting read as it describes the system for immovable types we ended up going with today. Specifically the comparison between Pin and Move, and section on drawbacks are interesting to read back up on. Especially when we compare it with the pin docs, and see what was not present in the RFC - but later turned out to be major practical issues (e.g. pin projections, interactions with the rest of the language).

Tmandry presented an interesting series (blog 1, blog 2, talk) on async internals. Specifically he covers how async {} blocks desugar into Future-based state machines. This post uses that desugaring as its motivating example, so for those keen to learn more about what happens behind the scenes, this is an excellent resource. The section on .await desugaring in the Rust reference is also a good read, as it captures the status quo in the compiler.

More recently Miguel Young de la Sota's (mcyoung) talk and crate to support C++ move constructors is interesting to read up on. Something I haven't fully processed yet, but is interesting, is the New trait it exposes. This can be used to construct types in-place on both the stack and the heap, which ideally something like the super let/super Type notation could support too. You can think of C++'s move constructors as further evolution of immovable types, so it's no surprise that there are lots of shared concepts.

Two years ago I tried formulating a way we could leverage view types for safe pin projection (post), and I came up short in a few regards. In particular I wasn't sure how to deal with the interactions with #[repr(packed)], how to make Drop compatible, and how to mark Unpin as unsafe. There might be a path for the latter, but I'm not aware of any practical solutions for the first two issues. This post is basically a sequel to that post, but changing the premise from: "How can we fix Pin?" to "How can we replace Pin?".

Niko's series on view types is also well worth reading. His first post discusses what view types are, how they'd work, and why they're useful. And in one of his most recent posts he discusses how view types fall into a broader "4-part roadmap for the borrow checker" (aka: "the borrow checker within"). In his last post he directly covers phased initialization using view types as well, which is one of the features we discuss in this post in relation to self-referential types.

Finally I'd suggest looking at the ouroboros crate. It enables safe, phased initialization for self-referential types on stable Rust by leveraging macros and closures. The way it works is that fields using owned data are initialized first. And then the closures are executed to initialize fields referencing the data. Phased initialization using view types as described in this post emulates that approach, but enables it directly from the language through a generally useful feature.

Conclusion

In this post we've deconstructed "self-referential types" into four constituent parts:

  1. 'unsafe (unchecked) and 'self (checked) lifetimes which will make it possible to express self-referential lifetimes.
  2. A moral equivalent to super let / -> super Type to safely support out-pointers.
  3. A way to backwards-compatibly add optional -> super Type notations.
  4. A new Move auto-trait which governs access to move operations.
  5. A view types feature which will make it possible to construct self-referential types without going through an Option dance.

The final insight this post provides is that today's Pin + Unpin system can be emulated with Move by creating Move wrappers which can return !Move types. In the context of async, the pattern would be to construct an impl IntoFuture + Move wrapper, which constructs an impl Future + !Move future in-place via an out-pointer.

People generally dislike Pin, and as far as I can tell there is broad support for exploring alternative solutions such as Move. Right now the only trait that uses Pin in the stdlib is Future. In order to facilitate a migration off of Pin to something like Move, we would do well not to further introduce any Pin-based APIs to the stdlib. Migrating off of a single API will take effort, but seems ultimately doable. Migrating off of a host of APIs will take more effort, and makes it more likely we'll forever be plagued with the difficulties of Pin.

The purpose of this post has been to untangle the big scary problem of "immovable types" into its constituents parts so we can begin tackling them one-by-one. None of the syntax or semantics in this post are meant to be concrete or final. I mainly wanted to have at least once walked through everything required to make immovable types work - so that others can dig in, think along, and we can begin refining concrete details.