Part 1 Β· The runtime machine
Values, places, and bindings
The unlearning that everything else depends on. A Rust variable is not a label pointing at a value β it is a place that owns one.
What this lesson is trying to break (4)
- A variable is a name that refers to a value living somewhere else.
- Assignment creates a second way to reach the same value.
- `mut` makes the value mutable.
- A move is an expensive deep copy.
This is a threshold concept: troublesome, transformative, and irreversible. You should expect to be genuinely stuck here, and being stuck is the mechanism rather than a sign that you are failing. Budget several sessions. Do not move on because you are bored β move on when you can explain it out loud without notes.
Lesson 0.1 showed you a three-line program that did not compile, and β more importantly β showed that you did not doubt it. This lesson replaces the model that failed.
It is the most important lesson on the site. Everything in Parts 2 and 4 is a consequence of what follows.
A variable is a name that refers to a value. The value lives somewhere β
usually the heap β and the variable is a label pointing at it. So let t = s
gives you a second label for the same thing, and both labels are equally
good ways to reach it.
In Rust, the value does not live somewhere else. It lives at the binding.
A let does not create a pointer to a value; it creates a place β a named
slot of memory β and puts the value in it. The binding does not refer to
the value. It owns it.
Read let t = s; as: move the value out of the place called s and into
the place called t. Afterwards, there is still exactly one value and
exactly one place holding it. s has not become a second route to the
value β s has been emptied, and the compiler will refuse to let you read
from it.
The official Rust term for this is a place expression β an expression that
denotes a location rather than a value. Local variables, struct fields, array
elements and *ptr are all place expressions. Nearly no tutorial uses this
word, which is a shame, because once you have it, assignment, borrowing and
pattern matching stop being three separate topics.
What a move actually does
The word βmoveβ makes people imagine something expensive. It is the opposite.
Take let s = String::from("hello"). A String is a three-word struct:
place `s` ββββββββββββββββ¬βββββββββ¬βββββββββββ
(on stack) β ptr β len: 5 β cap: 5 β
ββββββββ¬ββββββββ΄βββββββββ΄βββββββββββ
β
βΌ
heap βββββ¬ββββ¬ββββ¬ββββ¬ββββ
β h β e β l β l β o β
βββββ΄ββββ΄ββββ΄ββββ΄ββββ
Now let t = s;. Here is the whole operation:
place `s` β β β β β β β ββ¬β β β β β¬β β β β β β
(dead) (bytes may still be here, but the
β β compiler will not let you read them) β
place `t` ββββββββββββββββ¬βββββββββ¬βββββββββββ
(on stack) β ptr β len: 5 β cap: 5 β
ββββββββ¬ββββββββ΄βββββββββ΄βββββββββββ
β
βΌ
heap βββββ¬ββββ¬ββββ¬ββββ¬ββββ β untouched, never copied
β h β e β l β l β o β
βββββ΄ββββ΄ββββ΄ββββ΄ββββ
Three words of memcpy, and a note in the compilerβs records that s is now
dead. The heap buffer is not copied, not reference-counted, not scanned. In
optimised builds the copy itself usually disappears β the compiler just decides
both names refer to the same stack slot.
A move is mostly a compile-time bookkeeping act. That is why Rust can afford to make it the default.
Given the diagram above: why would it be unsafe for Rust to let you keep using
s after the move, even though the bytes are almost certainly still sitting in
sβs stack slot?
Think about what happens at the end of the function. Two sentences.
The answer is Drop, and it is worth stating plainly because it is the load-bearing
reason for the entire ownership system: when a place goes out of scope, the value
in it is destroyed, and for a String that means freeing the heap buffer. If both
s and t were live, both would try to free the same buffer at the end of the
function. That is a double free, and it is one of the classic memory-safety bugs.
Rustβs answer is not to detect double frees. It is to make it impossible to have two owners in the first place. One value, one place, one free.
mut is about the binding, not the value
Second piece of unlearning, and this one trips up people who are otherwise doing fine.
Varies: only where mutability is allowed β the value 5 is identical in every row
| Code | Legal? | What is happening |
|---|---|---|
let x = 5; x = 6; | No β E0384 | The place x is not declared mutable, so it cannot be assigned again. |
let mut x = 5; x = 6; | Yes | The place is mutable. The old value is overwritten. |
let x = 5; let x = 6; | Yes | Shadowing. A second, separate place that reuses the name. The first still exists. |
let x = 5; let x = "five"; | Yes | Shadowing again β a new place can have a new type. |
let mut x = 5; x = "five"; | No β E0308 | Assignment writes into the same place, so the type cannot change. |
Every row stores the same value. What varies is whether you are writing into an existing place or creating a new one β which is the entire distinction between mutation and shadowing.
Read that table before the explanation below it. The rows are constructed so that exactly one thing changes at a time; that is what makes the critical feature visible.
Now the explanation. mut is a property of the path you use to reach a
value, not of the value itself. The same String can be reachable through a
&mut in one part of a program and a & in another. Nothing is stamped onto
the bytes.
This is why the following is not a contradiction:
let s = String::from("hi"); // s is not mutable
let mut s = s; // moved into a mutable place
s.push('!'); // fine
The value never changed. It moved from an immutable place to a mutable one.
fn main() {
let x = 5;
{
let x = x * 2;
println!("inner: {x}");
}
println!("outer: {x}");
}inner: 10
outer: 5The inner let created a new place inside the inner scope. It read the outer
x to compute its initial value, then shadowed the name for the rest of that
block. When the block ended, the inner place went away and the name x referred
to the outer place again.
If you thought βouter: 10β, you were reading let x = x * 2 as an assignment.
It is not β it is a declaration that happens to reuse a name.
Note also that i32 is Copy, so reading x to compute x * 2 did not move
anything. With a String the same code would move, and the outer name would be
dead. That difference is 1.3.
Ownership is a tree
One more consequence, and then we stop.
Because every value has exactly one owning place, and places can be nested (a struct field is a place inside a structβs place), ownership in a Rust program forms a tree. Not a graph β a tree.
struct Server {
name: String, // the Server owns this String
routes: Vec<Route>, // and this Vec, which owns each Route
}
Dropping the Server drops the String and the Vec; dropping the Vec drops
every Route. The whole subtree is released, deterministically, in a defined
order, with no runtime bookkeeping at all.
This is the real reason Rust does not need a garbage collector, and it is worth being precise about it in conversation. It is not that Rust is βcareful with memoryβ. It is that the ownership structure is a tree known at compile time, so the compiler can insert the frees itself.
The natural follow-up question β what if my data really is a graph? β is
correct, important, and deliberately deferred to
2.6. Rustβs answer is Rc, RefCell and
friends, and they exist precisely because the tree model is not always enough.
Keep the question.
Explain to an imaginary colleague, in three sentences, why Rust does not need a garbage collector. You may not use the words βsafeβ or βfastβ.
If you find yourself writing βbecause the borrow checkerβ β that is machine 2, and this question is about machine 1. Try again.
What you should now be able to say
- A
letbinding creates a place that owns a value. let t = smoves: a shallow copy of the valueβs representation, plus a compile-time mark thatsis dead. The heap is untouched.- Moves are cheap. The expense people imagine is
Clone, which is a different operation. mutdescribes a place, not a value.- Shadowing makes a new place; mutation writes into an existing one.
- Ownership forms a tree, which is what makes deterministic destruction possible without a collector.
Next: 1.2 β Stack, heap, and fat pointers, where we look at what is actually in these places.