| I've been looking into this, and I suspect that one actually needs surprisingly little to interoperate safely with Rust. TL;DR: The lowest common denominator between Rust and any other memory-safe language is a borrow-less affine type. The key insight is that Rust is actually several different mechanisms stacked on top of each other. To illustrate, imagine a program in a Rust-like language. Now, refactor it so you don't have any & references, only &mut. It actually works, if you're willing to refactor a bit: you'll be storing a lot of things in collections and referring to them by index, and cloning even more, but nothing too bad. Now, go even further and refactor the program to not have any &mut either. This requires some acrobatics: you'll be temporarily removing things from those collections and moving things into and out of functions like in [2], but it's still possible. You're left with something I refer to as "borrowless affine style" in [1] or "move-only programming" in [0]. I believe that's the bare minimum needed to interoperate with Rust in a memory safe way: unreference-able moveable types. The big question then becomes: if our language has only these moveable types, and we want to call a Rust function that accepts a reference, what then? I'd say: make the language move the type in as an argument, take a temporary reference just for Rust, and then move-return the type back to the caller. The rest of our language doesn't need to know about borrowing, it's just a private implementation detail of the FFI. These weird moveable types are, of course, extremely unergonomic, but they serves as a foundation. A language could use these only for Rust interop, or it could go further: it could add other mechanisms on top such as & (hard), or &mut (easy), or both (like Rust), or a lot of cloning (like [3]), or generational references (like Vale), or some sort of RefCell/Rc blend, or linear types + garbage collection (like Haskell) and so on. (This is actually the topic of the next post, you can tell I've been thinking about it a lot, lol) [0] "Move-only programming" in https://verdagon.dev/grimoire/grimoire#the-list [1] "Borrowless affine style" in https://verdagon.dev/blog/vale-memory-safe-cpp [2] https://verdagon.dev/blog/linear-types-borrowing [3] https://web.archive.org/web/20230617045201/https://degaz.io/... |
> We wish to establish type soundness in such a setting, where there are two languages making foreign calls to one another. In particular, we want a notion of convertibility, that a type τA from language A is convertible to a type τB from language B, which we will write τA ∼ τB , such that conversions between these types maintain type soundness (dynamically or statically) of the overall system
> ...the languages will be translated to a common target. We do this using a realizability model, that is, by up a logical relation indexed by source types but inhabited by target terms that behave as dictated by source types. The conversions τA ∼ τB that should be allowed, are the ones implemented by target-level translations that convert terms that semantically behave like τA to terms that semantically behave like τB (and vice versa)
I've toyed with this approach to formalize the FFI for TypeScript and Pyret and it seemed to work pretty well. It might get messier with Rust because you would probably need to integrate the Stacked/Tree Borrows model into the common target.
But if you can restrict the exposed FFI as a Rust-sublanguage without borrows, maybe you wouldn't need to.
[0] (PDF Warning): https://wgt20.irif.fr/wgt20-final23-acmpaginated.pdf