Reading Rust

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.

45 minThreshold conceptrefutation textPOEvariation theorynotional machine
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.
Liminality warning

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.

Unlearn thiscarried over from JavaScript, TypeScript, Java, Go
You think

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.

Wrong here

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.

Instead

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.

Explain it to yourselfwrite it β€” thinking it does not count

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

CodeLegal?What is happening
let x = 5; x = 6;No β€” E0384The place x is not declared mutable, so it cannot be assigned again.
let mut x = 5; x = 6;YesThe place is mutable. The old value is overwritten.
let x = 5; let x = 6;YesShadowing. A second, separate place that reuses the name. The first still exists.
let x = 5; let x = "five";YesShadowing again β€” a new place can have a new type.
let mut x = 5; x = "five";No β€” E0308Assignment 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.

Predict firstcommit in writing, then look
fn main() {
    let x = 5;
    {
        let x = x * 2;
        println!("inner: {x}");
    }
    println!("outer: {x}");
}

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.

Say it out loudwrite it β€” thinking it does not count

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 let binding creates a place that owns a value.
  • let t = s moves: a shallow copy of the value’s representation, plus a compile-time mark that s is dead. The heap is untouched.
  • Moves are cheap. The expense people imagine is Clone, which is a different operation.
  • mut describes 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.

In the wildgo read the real thing now, while it is fresh
  • std::mem::swap β€” sourceSearch the page for `pub fn swap`. Ask yourself why this function needs `&mut` on both arguments, and why it cannot be written with plain assignment. The answer is entirely about places and ownership.
  • The Rust Reference β€” place expressionsThis is the official name for the idea in this lesson: expressions are either place expressions or value expressions. Almost no tutorial uses this vocabulary, and it makes assignment, borrowing and pattern matching all click at once.
Scheduled for recall4 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course β€” not grouped by topic. Sorting out which idea applies is the part that transfers.