Safe Made Easy Pt.1: Single Ownership is (Not) Optional | Of Rabbits and HolesSafe Made Easy Pt.1: Single Ownership is (Not) Optional<br>2026-05-26<br>Intro<br>What it promises and what it doesn’t<br>Motivating example<br>The proposal<br>Linear drops<br>The formal background<br>The rules so far<br>Conclusion<br>Intro<br>This post introduces an approach to memory safety that I believe is more practical and more ergonomic than the available alternatives.<br>It all started way back when, and was inspired by things I read and wrote:<br>Attempts to bolt linear types on top of Rust (1), (2)<br>Leakpocalypse<br>Verdagon’s post on Vale design and higher RAII<br>A lot, and I mean A LOT, of TypeScript<br>Three years of development later, I believe I finally got it. The proposal is complete.<br>Moreover, I have implemented it in my own programming language I intend to release soon-ish, and I want to share the design decisions and the entire path from “huh, why not” to “omg it’s live”.<br>So, TL;DR: linear types (which are dropped exactly once) + abstract interpretation + a bunch of tricks allows us to eliminate the same classes of bugs as Rust does (at least in non-concurrent environments) plus memory leaks, and we can extend the approach to also cover concurrent environments, all the while being more ergonomic and less restrictive.<br>Sounds fun? Let’s dig in.<br>What it promises and what it doesn’t<br>It is safe - it completely eliminates entire classes of bugs, such as:<br>Double-free<br>Use-after-free<br>Dangling pointers<br>Null pointer dereferences<br>Buffer overflows<br>Out-of-bounds accesses<br>Iterator invalidation<br>Uninitialized memory access<br>Memory leaks<br>Single ownership enables linearity - each value is dropped exactly once - and prohibits ownership cycles. Together with the flow-sensitive type system built to enforce it, these eliminate most of the above. Buffer overflows and OOB accesses are covered separately, but the mechanics of the rest of the system make dealing with these easy and efficient.
It is sound - I will demonstrate over the course of this series that the claims hold for arbitrary inputs. There are no holes that can be used to break the guarantees provided from inside the system.
It is NOT simple - there is a fairly large number of primitives working together so that the whole system can uphold the safety guarantees promised.
It is NOT concerned with concurrency - though the “fearless concurrency” guarantees are a natural extension to the proposed system, it has not been implemented in a complete enough way to demonstrate the viability of the approach. I will expand on this in a future post once I get it up and running.
It is NOT claiming to be “zero cost”, though it keeps runtime overhead to the minimum - it introduces runtime checks (a single branch per indeterminate access) if the compiler cannot statically prove availability.
Motivating example<br>Consider this pseudocode:<br>var x: T = new T;<br>if random() > 0.5 {<br>drop x;<br>print(x);What this code does is it conditionally consumes a value.<br>There are two ways this could go in a real language. C++ doesn’t particularly care and will happily compile this code:<br>#include<br>#include<br>int main() {<br>int *i = new int(42);
if ((double)rand() / RAND_MAX > 0.5) {<br>delete i;
printf("i=%d\n", *i);<br>return 0;<br>Which will then proceed to invoke UB in about 50% of runs. A modern C++ developer would reach for std::unique_ptr and std::optional here - and they would help, partially. RAII via smart pointers eliminates the manual delete, and optional gives you a way to represent “maybe moved.” But unique_ptr only manages heap-allocated objects, and the type system does not enforce the optional check - operator* on an empty optional is undefined behavior, and even .value() only gives you a runtime exception instead of a compile-time error. It is still on you to remember.<br>In Rust, though, this code does not compile at all:<br>fn main() {<br>let x = Box::new(42);<br>if rand::random::f64>() > 0.5 {<br>drop(x);<br>println!("{}", x); // error[E0382]: borrow of moved value: `x`<br>Rust takes a very different approach. The compiler tracks moves through control flow - it sees that x might have been moved in the if branch, and rejects the program outright. Rust’s ownership model requires that every variable’s move state is statically known at every point in the program - a conditionally-moved value violates that requirement, so the program is rejected. You can wrap the value in Option yourself and .take() it manually, but Rust won’t do that for you - the burden is on the developer to restructure the code upfront.<br>So, what if there was a third way between these two?<br>The proposal<br>The proposed solution is straightforward:<br>var x: T = new T;<br>if rand() > 0.5f {<br>drop x;<br>// The type of the value is now control-flow-dependent - the compiler evaluates it as it goes through the program, widening it each time control flow diverges to accommodate for both possibilities. Then it...