Memory safety has been a huge concern lately, for example roughly 70% of vulnerabilities in Microsoft are memory issue.
- Memory leaks
- Dangling pointers
- Use after free and more.
All of them have been issue programmers tried to mitigate by all means. They made tools (Valgrind, Address Sanitizer, Tracy, etc), they made conventions, they made patterns and abstraction and ultimately came to one conclusion:
"Developers can't be trusted with memory safety, we need a way to enforce safety a make the developer work with it."
This gave birth to Garbage collection, born with Lisp, Smalltalk and popularized by Java.
The pitch was simple: "Build your software without thinking about how memory is managed, we handle that for you."
No more dangling pointers, no more use after free (you could not more free something yourself), so it seemed like the problem was solved.
Yet low-level programmer still weren't satified.
The failure of GCs
Early garbage collectors had a glaring flaw: they were slow. Back then, reclaiming memory required complex heap scans, object tracing, cycle detection algorithms, and the infamous stop-the-world pauses to guarantee thread safety during collection.
This is just a small part of the problem but it already sets the tone.
GC was inadapted for performance-critical systems, and low level programmers had to go back into the arms of C/C++.
Garbage collection has, however, evolved over time, notably with ARC (Automatic Reference Counting) or Nim's ORC (which handle reference cycle) that allows deterministic automatic memory management and do the heavy lifting during compilation.
But programmers wanted more, as always and decided to go even further. The question was "Can we have memory safety without GC ?"
Rust
Rust is a programming language introduced in 2015 by Mozilla, and was a shockwave for the software industry.
Reading the description felt like a fever dream: "Memory safety without garbage collection".
How on earth did they pull it off ? How can we have memory safety without that GC ? Our program just have embedded cleanup AI within them ?
Skepticism was rising, the huge hype train left the station, Rust quickly became the most loved, and at times the most polarizing, language in tech.
Yet fundamentally we may just ask "Is this stuff real? Have we touched the graal ?"
So what's the secret ?
I see you coming here, ready to spell B-O-R-R-O-W C-H-E-C-K-I-N-G, but the answer is deeper than that and more simple that it seems.
It's about 3 fundamental concepts:
- Affine types
- Move semantics
- Borrow semantics
Affine type
Affine type is about an object that should be used at most 1 type.
If you have the variable a <- 1 and then you do b <- a, you used a and now it's no more possible to use the value it previously had.
This is the core of the memory safety as it guarantee that each value can only have one owner.
You exactly know when to free an object (exactly when it's used) and you can easily statically ensure a correctness.
This set up the concept of Ownership used in Rust.
So we have already achieved both memory safety and thread safety (1 owner means one sync point)
However it's really quite cumbersome to use like:
a <- 1
b <- a + 1
some_function(b) # `b` is consumed here
c <- b + 3 # Error: No more `b` exist
This lead to the introduction of move semantics to make this more usable.
Move semantics
Here we redefine what use means.
We no more consider an object dead after an use but when his owner drop him or die (because of scoping rules).
Object change owners at each assignment, if we have a <- 1, b <- a move the ownership from a to b and makes a undefined but b <- a + 1 doesn't kill a as it still keeps ownership of it's value.
But even with that it's still cumbersome:
a <- 1
some_function(a) # Need to modify `a`, so it takes ownership of it and will kill it at the end of the function
a <- a + 1 # Error: No more `a` exist
It's clear that affine types hate being shared!
In order to solve that, Rust introduced his famous Borrow semantics
Borrow semantics
Borrow semantics allows you to create references to an object known as borrows.
So in our previous example you could do:
a <- 1
some_function(&mut a) # Need to modify `a`, we lend it a mutable reference to a
a <- a + 1 # `a` still exist!
But as any sharing mecanism (especially with references) it comes with 2 problems:
- Lifetimes: Making sure the references doesn't outlive the owner.
- Thread-safety: Making sure there is only one sync point, even though there are shared reference. Solved with the mighty rule aliasing xor mutability, multiple immutable ref, only one mutable ref.
Those 2 constraints are then ensured by a mechanism known as Borrow checker which make sure that at any points in the program, aliasing rules are respected and lifetimes... oh lifetimes... this is a whole kind of hell
The hell out of lifetimes
Ensuring borrows don't outlive the owner is actually what makes borrow checking hard.
It gives birth to all sorts of absurd situations.
An objects that keeps a references to another object also need to keep it's lifetime to statically know when it's no more alive an avoid dangling pointers
struct MyType<'a> {
field: &'a String,
}
Types quickly becomes a torrent of static lifetimes annotations as each time you want to share a reference to something, the object that will keep it also need to keep track of the lifetimes
And if your struct aggregates multiple references with different lifetimes, the shortest lifetime restricts the validity of the entire object:
struct MyType<'a, 'b> {
f1: &'a String,
f2: &'b String, // The shortest lifetime dictates how long the whole struct can live
}
At times, lifetimes becomes so much of a problem that Rust, the language that promised safety without GC, introduced a Rc type, which stands for Reference Counting and Arc (our Automatic Reference Counting mentioned above) which are GCs, just to allow things to be easily shared.
And that's not even getting into infamous edge cases, like self-referential or cyclic data structures, which are virtually impossible to express cleanly using standard borrows.
Rust: The Hype and the Cult
Rust became notoriously famous for its steep learning curve. The internet flooded with articles and videos about "fighting the borrow checker", developers locked in combat with aliasing rules and lifetime annotations.
To many outsiders, the culture felt almost like a cult. When newcomers complained that fighting the compiler shouldn't be a normal day-to-day workflow, their frustration was routinely dismissed.
"You just don't understand the concept."
"You need to embrace the borrow checker."
"Think of it as a friendly teacher, not an adversary."
Except software engineering isn't kindergarten, and many engineers grew weary of the babysitting.
Yet, what choice did they have? The core promises were undeniably real:
- Memory safety without (a heavy mandatory) garbage collector
- "Fearless concurrency"
Developers could rage all they wanted, but the hype train had already left the station. Major enterprises began rewriting core systems in Rust: Microsoft, Amazon, Cloudflare, Meta.
So, embracing the borrow checker was the right path after all... right?
As research progressed and the initial euphoria settled, a nagging question emerged: Was all this complexity actually necessary? Was the early skepticism truly just skill issue and stubbornness?
With time and retrospective insight, the answer turns out to be no. Something really was off.
And to understand it we need to go back to the root
Another path to memory safety
We need to go back to the core.
Remember when I said that affine types were enough to have memory and thread safety but were just cumbersome to use ?
We will go back from that point and take another path, Mutable Value Semantics
Mutable Value Semantics
This works a bit like value semantics except that when a variable is assigned to another one, we don't move it, we copy it
a <- 1
some_function(a) # `a` is copied and passed to the function, once it's no more used, it's freed
a <- a + 1 # `a` still exist!
b <- a # a is copied into b
Here we kept the fundamentals of affine types, there is a single owner, and at each assignment or when we need a borrow we make a copy. a itself is still mutable and can be used at will (the "Mutable" part of it.)
And here it is. We have achieved memory and thread safety and the framework is usable without need for any lifetimes.
Don't thank me... well you saw the flaw I guess.
This works but induces a lots of copies in the process and that's where we need another concept to complete it and reduce copies... borrows, but not like Rust's ones.
Borrows
Here, borrows aren't to be used for variables but only for functions
When calling a function with some parameters and that function need to modify the value, we temporarily lend it to the function and give it the ability to modify the variable.
The same
This can fully be specified in the function's definition:
function add_in_place(mut a: int, b: int) {
a += b
}
a <- 1
b <- 5
add_in_place(a, b) # `a` and `b` are lended to the function, 0 copy
This is already a powerful result as data can fully be processed in a functional style without making tons of copies.
This can be even optimized further with Implicit moves by statically analyzing how variables are used and transforming copies into moves
a <- 1
b <- a # Last use of `a`, it's value is moved into `b`, not copied
c <- b + 1
We may be tempted to stop here, after all we have already greatly reduced copied (not to 0 as Rust does it) and achieved the same safety guarantees, however there are still circumstance where we can't avoid the hard encounters with lifetimes... notably with slices of data
Dealing with slices
It may be easy to just say "make a copies" but that would incur huge performances penalties, so we need to be able to references a part of an object.
In order to solve that specific problem, we indeed need a simple local borrow checker.
It's work is simple, making sure the variable owning the data doesn't change during the lifetime of the slice nor outlive it.
So will we need complex lifetime to know when the slice will be dropped ?
The answer is no, thanks to MVS.
When taking a slice to some data by reference, if we try passing that references to another object directly, it will induce a copy of the slice, not the reference to preserve the single owner rule and avoid the annotations
a <- [0, 1, 2, 3]
b <- &a[0..2] # Taking a ref to slice of an object
c <- b # Not the ref, but the slice is copied, c is now the distinct sequence [0, 1, 2]
a.push(4) # Error: Can't modify `a` while it's borrowed by `b`
b[0] += 1
Because references cannot be stored into arbitrarily surviving heap objects without triggering a copy or moving into an explicit projection scope, lifetime analysis remains strictly local. This eliminates global lifetime annotations entirely and drastically speeds up compilation times.
Sharing Data
The challenge of sharing data isn't fundamentally a borrow checker problem, itβs an inherent constraint of affine types. By stating that a value cannot be used more than once, affine logic inherently prevents a value from residing in multiple locations simultaneously. This makes classic patterns like graphs, doubly-linked lists, or self-referential structures notoriously difficult to express.
Rust attempted to bridge this gap primarily through complex borrow semantics and lifetime shenaningans.
For Mutable Value Semantics, we take a far more direct approach: use a targeted GC for shared data.
Instead of forcing every single allocation into a unified lifetime model, we draw a clear line between value types and reference types:
- Value types operate strictly under Mutable Value Semantics (zero lifetimes, local borrows, deterministic cleanup).
- Reference types rely on a lightweight, deterministic collector (such as ARC or Nimβs ORC) to manage shared, cyclic, or graph-like relationships.
Interestingly, Rust ended up doing the exact same thing by providing Rc and Arc in its standard library. The difference? Rust tried to solve everything with borrow semantics first, only to admit that for truly shared graph topologies, compile-time borrow checking simply isn't enough.
Comparison of approach
We make a simple program that returns the longest string between 2 strings:
Rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
MVS
# x and y are implicit references
function longest(x: string, y: string): &string {
if x.len() > y.len() { x } else { y } # Will return an implicit ref
}
Conclusion: The Lie About Borrow Checking
In recent years, Rust was framed as the ultimate pinnacle of memory safety. Even skeptics felt pressured to adopt it, with many coming to believe it was the only viable path to zero-cost, garbage-collector-free safety.
The steep learning curve was routinely portrayed as the inevitable price of admission for building reliable software. Enduring the pain of lifetime management became a rite of passage, an initiation required to "elevate one's mind" to the elegance of borrow checking.
Today, we've seen that this was never the whole truth.
The friction, the constant fighting with the compiler, the mental gymnastics... these were never user errors. They were warning signs that something was fundamentally off with the language's design.
Rust is an impressive achievement, but it tried to force every programming paradigm into the strict box of compile-time borrow semantics. In doing so, it introduced immense cognitive overhead for developers.
If you ever felt frustrated or alienated by Rust, rest assured: you were right all along. It was never a "skill issue." It was a design flaw rebranded as a feature.
Languages like Swift, Mojo, and Nim are already proving that Mutable Value Semantics can deliver thread safety and high performance without forcing developers to wrestle with global lifetime parameters.
So the next time someone tells you that "you just don't understand the borrow checker", remind them that perhaps they haven't yet explored the alternatives.
Top comments (0)