Tech News
← Back to articles

The inconceivable types of Rust: How to make self-borrows safe (2024)

read original related products more articles

One of the first things any Rust programmer learns is that you can’t pass an object and a reference to that object around at the same time. It’s impossible to do, even indirectly. This limitation has been the subject of countless questions on Stack Overflow and posts on Reddit and the Rust forums and anywhere else where Rust programmers might ask for help. It’s so well-known that most people treat it like an axiom, not just a limitation of Rust as it currently exists, but an inherent limitation of borrow checking in general.

However, that’s not the case. In fact, with the right perspective, the way to support it is obvious. In this post, I’ll walk through the steps and show how self-borrows and much more could be supported in a hypothetical alternate or future version of Rust.

But first, some obligatory disclaimers

To be clear, when I say something can’t be done in Rust, what I mean is that it can’t be done in a safe, zero-cost way. As an army of internet commenters are no doubt rushing to observe, any limitation of a static type system can be bypassed by using unsafety or runtime checks instead (e.g. “lol, just wrap everything in Arc> ” or “lol, just build your own memory management on top of Vec indices”). And the fact that a less safe or efficient workaround exists is of great interest to people who just need to solve a problem quickly. But from a language design perspective, the pertinent fact is that Rust’s type system has gaps which make certain common tasks impossible to do in a way that lets Rust be Rust, and not just a glorified C or Javascript.

Lastly, this post will discuss changes purely from a type checking perspective without regard to how hard they’d actually be to implement. In a major real-world language like Rust, any change, no matter how trivial, has a huge cost in terms of ecosystem, documentation, tooling, backwards compatibility, etc. But I’m a language design hobbyist, not a Rust compiler engineer, so language design is the part I’ll speak about.

So how do you type-check self borrows? The trick is actually to adopt an even more ambitious goal, safe async functions.

A brief history of Rust

Rust 1.0 shipped with no support whatsoever for non-movable types. The fact that any value of any type could be arbitrarily memcpy’d around and still work was a core assumption of the language.

However, it didn’t take long before people realized that non-movable types are actually very useful. In particular, async functions nearly always produce non-movable future types, so you can’t have async Rust without support in some fashion for non-movable types.

Sadly, it was too late to do things properly (i.e. a Move auto-trait), but they were able to at least hack in partial support via the Pin type, added in Rust 1.33.0.

... continue reading