Part 0 · Orientation
The three machines
Rust does not have one execution model, it has three. Almost every moment of confusion is running the wrong one — plus a pretest you are meant to fail.
What this lesson is trying to break (3)
- A programming language has one execution model.
- The type system is annotation that gets erased before anything runs.
- Community idioms are decoration on top of the real language.
Here is the organising idea of this course, and it is not one you will find in The Book.
Rust does not have one execution model. It has three, and they answer different questions. Most of the time someone is confused about Rust, they are running the wrong machine in their head.
Machine 1 — the runtime machine
This is the one you already expect a language to have. It answers:
- Where does this value live — stack, heap, or inside another value?
- What does this line actually cost?
- When is this memory released, and who releases it?
Rust’s runtime machine is unusual mostly for being knowable. There is no garbage collector deciding things later, and no hidden allocation. A value has one owner; when the owner’s scope ends, the value is dropped. Every allocation is visible in the source.
This is why people say Rust is predictable. It is not a claim about speed. It is a claim that you can answer “when is this freed?” by reading.
Machine 2 — the compile-time machine
This is the one that has no equivalent in JavaScript, and only a weak one in
Java and Go. It answers a completely different question: what does rustc
know, and what will it therefore refuse?
It has two halves that people constantly confuse:
The borrow checker is a static analysis over your function’s control-flow graph. It computes, for each reference, the set of program points where that reference must still be valid, and then checks that nothing invalidates it during that set. That set is what a lifetime actually is. Hold that thought — it is the single most misunderstood idea in Rust and it gets a whole lesson (2.3).
The trait solver is a second evaluator. When you write T: Display + Send,
you are writing constraints for a small logic engine that runs before your
program does. It picks implementations, resolves associated types, and then
monomorphisation generates a separate concrete copy of your function for
every type you used it with.
That last part deserves emphasis, because it is where TypeScript intuition misleads badly.
Types are annotations. They help the tooling, they catch mistakes, and then they are erased. Generic code is one function that works for many types.
In Rust, types are not erased and generic code is not one function. Types determine memory layout, decide which function is called, and control when destructors run. A generic function you call with three different types becomes three different functions in the binary.
Treat Rust’s type system as a program that runs before your program. It has inputs (your annotations and impls), it computes (trait resolution), and it emits output (concrete machine code). This is why Rust builds are slow, why binaries are large, and why “just make it generic” is a decision with a real cost — all three are the same fact, and being able to say so is a tech-lead-level observation.
Machine 3 — the social machine
The third machine is not in the language specification at all, and it is the one most courses skip entirely. It answers: what is this code’s author signalling?
When you read Arc<Mutex<Vec<Job>>>, the language tells you it is an atomically
reference-counted pointer to a mutex containing a vector. The social machine
tells you something more useful: this is shared mutable state across threads,
the author expects contention to be low, and a reviewer will ask whether a
channel would have been better.
When you see a struct with a private field of type PhantomData<*const ()>, the
language says “zero-sized marker”. The social machine says “the author is
deliberately making this type not Send”.
For your stated goal — discussing Rust with Rust programmers — machine 3 is not optional garnish. It is most of the conversation. Part 6 is dedicated to it.
The diagnostic question
This frame earns its keep when you are stuck. Ask: which machine am I asking about?
| When you are asking… | The machine | Where to look |
|---|---|---|
| Why is this slower than I expected? | Runtime | Allocation, cloning, dynamic dispatch |
| Why won’t this compile? | Compile-time | The error code, the borrow, the missing impl |
Why does .clone() fix it, and is that bad? |
Both | Borrow checker complained; clone costs runtime |
| Why did they write it this way? | Social | Idiom, convention, API guidelines |
| When is this freed? | Runtime | Owner’s scope end, drop order |
| Why does this generic bound exist? | Compile-time | Trait solver needs it to pick an impl |
A worked example of the confusion. A newcomer writes:
let first = &v[0];
v.push(4);
println!("{first}");
…and gets an error. They ask “why does Rust think this is unsafe? Nothing is
freed here.” That question mixes machines. The runtime machine is not the one
complaining. The compile-time machine noticed that first must stay valid
across the push, and push takes &mut v, and those two cannot overlap. The
compiler is not predicting a crash — it is enforcing a rule about references
that happens to also prevent a crash.
Getting this distinction right is what separates “Rust is fighting me” from “I know which of Rust’s two brains I am arguing with”.
Pretest — one question per part
Now the part you will not enjoy.
Below are six questions, one from each remaining part of the course. You have not been taught any of it. Do not look anything up. Do not open the Playground. Guess, commit, and rate your confidence honestly.
This feels pointless. It is not. Being tested on material before studying it measurably improves how much you learn from the later study — including for the items you answer wrong. The effect is called pretesting, and the leading explanation is that a failed retrieval attempt marks the gap, so that when the real answer arrives it lands somewhere prepared rather than sliding past.
The wrong answers you are about to give are doing work.
fn main() {
let a = [1u8, 2, 3, 4];
let b = vec![1u8, 2, 3, 4];
println!("{} {}", size_of_val(&a), size_of_val(&b));
}4 24 on a 64-bit machine.
The array is its four bytes. The Vec is a three-word header — pointer,
length, capacity — and the bytes live somewhere else entirely, on the heap.
size_of_val measured the header, not the contents.
Covered in 1.2.
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("{first}");
}No — E0502. &v[0] is a shared borrow of v that is still live at the
println!. v.push(4) needs &mut v. Shared and unique borrows cannot overlap.
The interesting part is why the rule exists, and it is not the reason most
people give. push may reallocate, which would leave first pointing at freed
memory. But Rust rejects this even when it provably would not reallocate,
because the rule is about references, not about predicting allocation.
Covered in 2.1.
fn show<T: std::fmt::Display>(x: T) { println!("{x}"); }
fn main() {
show(1u8);
show("hi");
show(2.5f64);
}Three. One per concrete type, generated by monomorphisation.
If you said “one, generics are erased”, you brought that from Java or
TypeScript. If you said “one, it’s dynamic dispatch”, you were thinking of
dyn Display, which is the other choice and has different costs.
This single fact explains Rust’s compile times, its binary sizes, and why
dyn exists at all.
Covered in 3.3.
async fn work() { println!("working"); }
fn main() {
let fut = work();
println!("done");
}It prints only done. working never appears.
Calling an async fn does not run it. It builds a state machine and hands it to
you, inert. Nothing happens until an executor polls it — and here nothing ever
does, so fut is dropped without ever starting. (The compiler warns you about
exactly this.)
If you came from JavaScript, this is the single biggest trap in the language.
const p = work() in JS starts the work. Rust’s future is a value describing
work that has not begun.
Covered in 4.3.
Inside an unsafe block, which of the following stop being checked?
- The borrow checker
- Type checking
- Dereferencing a raw pointer
- Reading a
static mut - Calling a function marked
unsafe
Only 3, 4 and 5 — and “stop being checked” is the wrong description even for
those. unsafe does not disable the borrow checker or the type checker. They
run exactly as before.
What unsafe does is unlock five extra abilities that are otherwise
forbidden, and shift the burden of proving a specific set of invariants from the
compiler to you.
Most people believe unsafe is an escape hatch from the borrow checker. It is
not, and saying so in a conversation is an immediate signal you have read about
this rather than used it.
Covered in 5.1.
A Rust programmer says: “You can’t do that, orphan rule.” What have you probably just tried to do?
Implement someone else’s trait for someone else’s type — for example
impl Display for Vec<u8> in your own crate, where both Display and Vec
belong to std.
Coherence requires that a given trait/type pair has exactly one implementation across the whole program. The orphan rule enforces this locally: at least one of the trait or the type must be defined in your crate. The standard workaround is the newtype pattern — wrap it in your own struct.
This is a good example of machine 3. The phrase “orphan rule” is a two-word compression of that whole paragraph, and knowing it saves five minutes in a design discussion.
How many of the six did you get right? More usefully: on how many were you confident and wrong?
Write down the ones where your confidence was 3 or 4 and the answer was no. Those are not gaps in knowledge — those are places where a model you trust is actively producing wrong output. They are the highest-value part of this course for you, and you now have a list of them.
Check Calibration now to see your baseline. Come back to it after Part 2 and after Part 4. The number that matters is not your accuracy — it is the distance between your confidence and your accuracy.
Next: 1.1 — Values, places, and bindings, where we replace the model that failed you in 0.1.