Reading Rust

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.

40 minnotional machinepretesting effectPOE
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.

Unlearn thiscarried over from TypeScript, Java
You think

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.

Wrong here

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.

Instead

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.

Predict firstcommit in writing, then look
fn main() {
    let a = [1u8, 2, 3, 4];
    let b = vec![1u8, 2, 3, 4];
    println!("{} {}", size_of_val(&a), size_of_val(&b));
}
Predict firstcommit in writing, then look
fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("{first}");
}
Predict firstcommit in writing, then look
fn show<T: std::fmt::Display>(x: T) { println!("{x}"); }

fn main() {
    show(1u8);
    show("hi");
    show(2.5f64);
}
Predict firstcommit in writing, then look
async fn work() { println!("working"); }

fn main() {
    let fut = work();
    println!("done");
}
Predict firstcommit in writing, then look

Inside an unsafe block, which of the following stop being checked?

  1. The borrow checker
  2. Type checking
  3. Dereferencing a raw pointer
  4. Reading a static mut
  5. Calling a function marked unsafe
Predict firstcommit in writing, then look

A Rust programmer says: “You can’t do that, orphan rule.” What have you probably just tried to do?

Score yourself honestlywrite it — thinking it does not count

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.

In the wildgo read the real thing now, while it is fresh
  • rustc dev guide — the compiler's own overviewSkim the pipeline diagram only. You are not learning compiler internals; you are confirming that borrow checking and trait solving are genuinely separate phases with separate jobs, which is the claim this lesson makes.
  • Rust API guidelines — namingTen minutes. This is the third machine written down: shared conventions that carry meaning. Once you know that `into_` consumes and `as_` is cheap, you read a lot of signatures without opening the docs.
Scheduled for recall3 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.