Orientation: Rust makes the compiler keep the contract
The C++ track ended on a bargain: undefined behavior is the price of zero overhead, and the contract — never break these rules — is kept by nothing but the programmer's promise. The Go track answered a different question (what does complexity cost a team?) and kept the contract with a run-time system instead. This series asks the third question: could the compiler keep the contract, at C++'s price? Before any syntax, one claim: Rust is what you get when you take that question seriously and follow its consequences one forced step at a time. This lesson states the promise, shows why the design space collapses, installs a five-question habit, and draws the 23-lesson chain in which every lesson solves exactly the problem the previous one created.
New idea: state the promise — safe code has no undefined behavior — and the budget — no garbage collector, no run-time system, no cost you cannot see — and show that together they leave exactly one place for the proof: compile time.
Forces next: Suppose safe code must never exhibit undefined behavior, with no garbage collector and no run-time checking beyond the cheap ones. "Undefined behavior" is a long list of different bugs, and a compiler can only reject what it can recognize. What exactly must be ruled out, and what does each rule need to know?
1 · Three answers to one problem
The C++ track's last lesson made a claim worth rereading: undefined behavior is not a defect in the standard, it is the mechanism by which zero overhead is implemented. The compiler may assume you never break a short list of rules — never index out of bounds, never touch freed memory, never race — so it emits no check that would guard against you doing so. You keep your half of the contract by discipline, by review, by running sanitizers in test. If you break it, the standard owes you nothing at all.
Any language that lets you touch memory has to decide who keeps that contract. There are only three candidates: the programmer, the run-time system, and the compiler. The three language tracks in this library are the three answers. ("Run-time system" here means whatever runs beside the program to enforce the contract: a collector, a scheduler, checks inserted everywhere. Code the compiler emits that enforces nothing, such as the bookkeeping for a panic's unwinding in Lesson 10, is not one.)
| C++ · trusts you | Go · constrains you | Rust · verifies you | |
|---|---|---|---|
| Who keeps the safety contract | You — it is a promise (Lesson 19 of that track) | The run-time system: bounds checks, nil panics, a garbage collector | The compiler — the contract is a proof it must be able to check |
| What it costs at run time | Nothing: no defensive checks are emitted | A bounded but real bill: a collector, a scheduler, bounds checks | A few cheap checks; no collector, no scheduler |
| When a mistake shows up | At run time as undefined behavior — or never | At run time as a panic; a data race only if you remembered to run the race detector | At compile time, as an error you can read |
| What you give up | The safety net | A little speed, control over when memory is released, and some type-system power | Some safe programs the checker cannot prove safe: you restructure them, or you write unsafe and prove it yourself |
Nothing here says one column is right. It says they are three different places to spend the same scarce thing: someone's attention. C++ spends the programmer's. Go spends the machine's. Rust spends the compiler's — and yours, up front, learning to write programs the compiler can verify. Whether that is a good trade is a question about your project. What this series can do is show you exactly what the trade is, by deriving the language from the promise instead of listing its features.
2 · The promise and the budget
Two sentences define the whole design. Everything else in this series is a consequence of holding both at once.
unsafe — no program can exhibit undefined behavior. Every operation whose behavior the language would otherwise leave undefined is either rejected at compile time or checked at run time.Budget. No garbage collector. No run-time system the program did not ask for. No abstraction that costs more than the code you would write by hand. (There is one honest concession, and §3 makes you find it.)
Read the promise as a statement about programs, not about programmers: it does not say your code is correct, it says your code cannot be undefined. The distinction is the whole point. A wrong answer is a bug you can find with a test; undefined behavior is a bug the language has agreed not to describe — it can corrupt unrelated memory, be exploited, and vanish when you add a print statement (the C++ track's Lesson 19 showed the optimizer deleting a null check that came after the bug).
Here is the smallest program that shows what "the compiler keeps the contract" feels like. It takes a name for the first element of a vector, then grows the vector — which may move its buffer — then uses the name. In C++ this compiles, runs, and is undefined behavior. In Rust:
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // a name for the first element
v.push(4); // may reallocate: moves the buffer
println!("{first}"); // ...then uses the stale name
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0];
| - immutable borrow occurs here
4 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ----- immutable borrow later used here
Notice what the message is made of: three places. Where a name was created, where something conflicting happened, and where the name is used afterwards. That triple — creation, conflict, later use — is the anatomy of nearly every borrow error you will ever read, and Lesson 07 shows it is not a coincidence but the definition of what the checker computes. Move the println! above the push and the same statements compile: nothing changed except that the old name is no longer used after the buffer moves. What the message cannot tell you is which repair you want: finishing with first before the push, cloning, or restructuring are all legal edits, and it only says where to look.unsafe code it relies on is sound — the standard library's Vec is built on such code, and Lessons 19–20 are about who checks it — and it describes the language as designed; a compiler is a program with its own bugs (Lesson 22 takes stock of the soundness holes still open).Both sentences are now on the table. Which designs can keep the first without breaking the second?
3 · Why the design space collapses
You can argue that the promise and the budget together force the design. Not by decree — by elimination. The C++ track's catalog of undefined behavior has seven families (out-of-bounds access, use-after-free, signed overflow, uninitialized reads, null dereference, data races, type punning). For each, ask: which mechanisms can actually rule it out, and what do they cost?
Now make the choice yourself. The slider is your run-time budget; the button says whether the safety must be a guarantee (the promise) or a hope (C++). A filled dot means "this mechanism really does rule that family out"; it turns hollow when your constraints forbid paying for it. The bottom row counts how many designs are left for each family. Start at the top of the budget and drag down.
What you should have found. With the guarantee required and the budget at "cheap checks", the readout lists five families with exactly one design left. Three have a cheap one: a local rule for signed overflow and for type punning, a compare for out-of-bounds access. The other two — use-after-free / dangling and data race — are left with a static proof about which names can reach which memory, and when. Not because someone preferred it, but because the alternatives cost a collector (which the budget forbids, and which does not help with races anyway), a counter per access (which the budget forbids for every access), or the sanitizers' slowdown (roughly 2–10× in the C++ track). Drop the budget to zero and a different family goes red — out-of-bounds access — and that is the honest concession: even Rust's promise costs one compare per checked index. Everything the promise needs beyond that is free of run-time cost, because it happens before the program runs.
The two families that collapse are not arbitrary. Every other family is a problem with a single operation — this index, this read, this cast — and a local rule can rule it out. These two are problems with a relationship between two names over time: one name frees or changes a place while another name still expects it to be there. No local rule can see that. The next lesson makes it precise, and the rest of the series is the machinery that makes it checkable without a run-time system. First, though, a habit that finds such relationships in any line of code.
4 · The habit: five questions for any line of code
The C++ track installed five questions about cost; the Go track installed five about complexity. This one installs five about proof. By the end you will ask them reflexively, in any language, and usually know the answer:
Send/Sync, fallibility: the contract is written in the type, so a caller can rely on it without reading the body. (Lessons 08–14, 16.)RefCell's borrow flag, a bounds check and an Arc's atomic counter each buy a guarantee while the program runs. Name the price. (Lessons 02, 15, 17.)unsafe have to promise here, and who audits that promise? The answer marks the boundary between what the compiler knows and what a human vouches for. (Lessons 19–21.)None of these is Rust-specific. They are what a careful reviewer asks of any C++ pointer, any Go channel, any Java shared list. Rust is the language that makes you answer them in the source, checked by a machine — which is how they become reflex. The order in which the series answers them is its spine.
5 · The 23-lesson spine is a derivation, not a topic list
Each lesson opens with the problem the previous one created and closes by creating the next. That relay is written down: the sentence that ends lesson N is the same sentence that begins lesson N+1, and the series index shows all of them in one table. The six movements: state the promise (I); derive the ownership-and-borrowing law that keeps it (II); build types that carry proofs, so abstraction does not cost the guarantee (III); handle the cases where one owner is not enough (IV); mark the boundary where a human takes over from the checker (V); and ship it, with an honest bill (VI).
| # | Lesson | The one move it makes |
|---|---|---|
| I · The promise | ||
| 00 | Orientation | state the promise and the budget; the only place left for the proof is compile time |
| 01 | The promise, made precise | factor the seven bug families into cheap local fixes plus one law about names and places |
| 02 | Places, values, and the stack | fix the vocabulary; bounds, initialization and overflow become local rules |
| II · Ownership and borrowing — the law | ||
| 03 | Ownership | one owner per value; its scope end is the one drop |
| 04 | Move | assignment transfers ownership; the source is statically dead |
| 05 | Borrowing | look without owning, under the law: shared readers or one exclusive writer |
| 06 | Lifetimes as regions | a borrow is a region and must fit inside its owner's |
| 07 | The borrow checker | regions are liveness on the control-flow graph |
| 08 | Lifetimes in signatures | write the input-to-output borrow relationship down, so checking is modular |
| III · Types that carry proofs | ||
| 09 | Enums, Option, match | absence and choice become types, with an exhaustiveness proof |
| 10 | Errors: Result, ?, panic | failure is a value with a one-character propagation operator; bugs panic |
| 11 | Traits | behavior as a named, checked, globally-unique contract |
| 12 | Generics and monomorphization | one definition, checked once, erased at zero run-time cost |
| 13 | Trait objects | dynamic dispatch as an explicit, priced choice |
| 14 | Closures and iterators | the same three ownership modes, and abstraction that compiles away |
| IV · When one owner is not enough | ||
| 15 | Smart pointers and interior mutability | the same law, checked at run time, by choice |
| 16 | Send and Sync | the law across threads, derived structurally by the compiler |
| 17 | Concurrent programs | move over channels, lock to share, scope to borrow |
| 18 | Async | a future is a compiler-generated state machine; Pin makes it sound |
| V · The trust boundary | ||
| 19 | unsafe | extra powers, explicit obligations, and soundness as the standard |
| 20 | Building a safe abstraction | invariants plus privacy turn an unsafe core into a safe API |
| VI · Shipping, and the honest bill | ||
| 21 | Crates, Cargo, and privacy | make promises portable across teams |
| 22 | Capstone and the honest costs | assemble, price, and choose deliberately |
Before the first step, price the destination: what thinking this way buys, and what it costs.
6 · What it buys, what it costs
- Whole bug classes that never reach a test. Use-after-free, iterator invalidation and data races in safe code are compile errors, not incidents (Lessons 05–07, 16).
- Abstraction with little tax. Generics, closures and iterators, in many cases, compile to what you would write by hand (Lessons 12, 14); dynamic dispatch is the priced exception (Lesson 13).
- Contracts you can read. A signature says who borrows what, for how long, and whether it may cross a thread (Lessons 08, 16).
- A vocabulary for every language. Ownership, aliasing and lifetime questions arise in Go, Java and C++ too; here they are checked instead of hoped for.
- An up-front learning cost. You are learning to write programs a checker can verify; some correct programs are rejected until you restructure them (Lessons 07, 22).
- Some shapes fight the law. Cyclic graphs, self-referential structs and shared mutable state need indices,
Rc/RefCell, orunsafe(Lessons 15, 19–20). - Compile time and complexity. Build time is a recurring complaint in Rust's own surveys, and signatures say more than Go's do (Lessons 12, 22).
- Async is a second dialect. Waiting costs bytes instead of threads, and comes with pinning and function colouring (Lesson 18).
7 · Misconceptions to drop now
Arc's atomic counter, a RefCell's borrow flag, a dynamic dispatch. The series prices each one.Checkpoint exercise
ConcurrentModificationException, a data race, a stale cache entry. Write down, in one line each: the two names for the same thing, what one of them did (wrote? freed? moved?), and when the other used it. If you cannot find two names, your bug was a wrong answer rather than undefined behavior, and §2 says the promise does not cover it. Then try to state, in one sentence, the rule that would have made that bug impossible. Do not worry if the sentence is clumsy; you will write the exact version in Lesson 01, and you will find that the same sentence covers bugs that look nothing alike.Where this points next
We have a promise, a budget, and the argument that together they leave only compile time for the proof. But "compile time" is not yet a design: a compiler can reject only what it can recognize, and "undefined behavior" is a long, heterogeneous list. Lesson 01 does the work that turns the slogan into a specification. It takes the seven families apart, shows that most yield to a small local rule, and shows that the two dangerous ones are really the same bug in different clothes — one law about names and places. Once you can state that law in a sentence, the rest of the series is how to make a machine check it. First, then: what exactly must be ruled out, and what does each rule need to know?
unsafe underneath). The portable skill is five questions — who owns it, who else can reach it and how, what does the signature promise, is it proven or checked, and where does the proof stop — and the 23-lesson spine is a derivation in six movements that turns each into a reflex.Interview prompts
- State Rust's safety promise and its cost budget, and say what each one rules out. (§2 — safe code has no undefined behavior; no garbage collector, no run-time system, no hidden cost. Together they exclude designs that pay for safety at run time.)
- Why does removing the garbage collector and heavy run-time checking force a compile-time analysis for use-after-free and data races? (§3 — every other mechanism that guarantees those families needs a collector, a per-access counter or sanitizer-style instrumentation (roughly 2–10× in the C++ track), all outside the budget; a GC does not address races at all.)
- How do C++, Go and Rust differ in who keeps the safety contract? (§1 — C++: the programmer, by promise; Go: the run-time system, by checks and a collector; Rust: the compiler, by proof at build time.)
- What does safe Rust not prevent? (§2 — deadlocks, leaks, overflow to a wrong-but-defined answer, logic errors; and the promise is conditional on the soundness of the
unsafecode it relies on.) - Why is the bounds check the "honest concession" in Rust's zero-overhead claim? (§3 — no free mechanism can guarantee an index is in range, so the promise costs one compare per checked index.)
- Name the five questions the series trains you to ask of any line of code. (§4 — who owns it and when does it end; who else can reach it, shared or exclusive, for how long; what does the signature promise; is it proven or checked and at what price; where does the proof stop.)
- What is the anatomy of a borrow-checker error message? (§2 — three points: where a name was created, where a conflicting action happens, and where the name is used afterwards.)
Companion reads: C++ · 19 Undefined behavior (the bargain this lesson starts from), C++ · 05 RAII (why a collector does not end files and locks), and Go · 03 Pointers, the heap and the collector (the run-time system's answer, priced in its own terms).