You'll learn: the loop you'll run a few hundred times over the rest of this
course — cargo check, cargo test — and the one skill that decides whether
Rust feels like a collaborator or a gatekeeper: reading a compiler diagnostic all
the way to the end.
Why this is stage 01 and not an appendix
Here is the thing nobody says on day one, so it lands as a personal failing instead of a fact about the tool:
Not compiling is the normal state of writing Rust. Experienced Rust programmers don't write code that compiles first try. They write code, get rejected, read the rejection, adjust — dozens of times an hour.
Rust front-loads work other languages defer. C compiles a program that segfaults
next Tuesday; Python runs right up to the AttributeError in production. Rust
moves both to the one moment they're cheap to fix — while you're looking at it.
The price is a compiler that talks back, which at first feels like being told off.
It isn't. A diagnostic is closer to a code review than a rejection letter: it
names the line, underlines the expression, says what it expected and what it
found, and often writes the fix out for you. Beginners who stall are rarely the
ones who can't write Rust — they're the ones who read the first line, feel bad,
and start guessing. The ones who get fluent read the whole thing, help:
included. So this stage is deliberately about failure: your file ships
broken, and the job is to satisfy the compiler six times in a row.
The loop
Everything in this course is one cargo crate, sqlkit, and two commands.
cargo check # type-check only, no machine code — the fast loop while a file is red
cargo test # build, then run every #[test] in the crate — the real verdict
cargo test prints a line per test and a summary:
running 5 tests
test warmup::tests::magic_is_sixteen_bytes ... ok
test warmup::tests::short_slice_is_not_sqlite ... ok
test warmup::tests::notadb_is_not_sqlite ... ok
test warmup::tests::tiny_db_is_sqlite ... ok
test warmup::tests::describe_reports_kind_and_length ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Look twice: those lines are also the evidence this course is graded on. What the grader reads is what your run printed — so a claim you never asserted is one nobody, including you, knows is true.
One thing this course never stops to do, and it is better said now than
discovered: it does not teach you how a variable, a function or a loop is
written. It teaches the Rust that is peculiar to Rust — ownership, match,
Result, lifetimes, traits — and takes the rest the way you already read it in
whatever language you use today. Where a spelling is genuinely new — &[u8],
String against &str, format!, the #[cfg(test)] block that will live at
the bottom of every file you write — Rust, spelled out
gives it four lines and one sentence on what is different, and nothing more.
Keep it open beside this lab. It is not graded and never will be.
Anatomy of a diagnostic
Two of them, both verbatim rustc 1.97.1 output. The first is from a two-line file written for this section, so that every part of the format is on screen at once:
let n = bytes.length();
and what rustc says about it:
error[E0599]: no method named `length` found for reference `&[u8]` in the current scope
--> src/lib.rs:2:19
|
2 | let n = bytes.length();
| ^^^^^^
|
help: there is a method `len` with a similar name
|
2 - let n = bytes.length();
2 + let n = bytes.len();
|
Five parts, each doing a job:
error[E0599]— severity, then a stable code, identical on every machine and in every Rust version. It's what you search for and what you look up.- The message, read as a sentence: no method named
lengthfound for reference&[u8]. It names both halves — your name and its type. --> src/lib.rs:2:19— file, line, column. (Yours will differ.)- The span — the source line with
^^^^^^under the exact expression the compiler couldn't accept. help:— a concrete suggestion, sometimes a-/+diff you can apply verbatim, sometimes only a pointer, and sometimes absent entirely. Which of this stage's six are generous and which are not is in the project's What goes wrong first notes.note:ishelp:'s quieter sibling: context rather than a fix.
Now one with two underlines — and this one is verbatim from the file you are
about to fix, cargo check run from your db/ directory:
error[E0308]: mismatched types
--> sqlkit/src/warmup.rs:21:18
|
21 | let n: u32 = bytes.len();
| --- ^^^^^^^^^^^ expected `u32`, found `usize`
| |
| expected due to this
|
help: you can convert a `usize` to a `u32` and panic if the converted value doesn't fit
|
21 | let n: u32 = bytes.len().try_into().unwrap();
| ++++++++++++++++++++
The carets are on the expression rustc could not accept; the --- is on the
thing that created the expectation — here the type annotation immediately to its
left, which says expected due to this in as many words. That's the shape nearly
every type error takes — the carets say what, the second underline says why —
and the second underline is often nowhere near the first: a function's return
type, an earlier argument, a let twenty lines up. Reading both is the
difference between fixing it in five seconds and staring at the carets.
Read that help: twice, though. It compiles, and it is the wrong fix: it would
make a function whose job is to describe any slice panic on a file bigger than
four gigabytes. A suggestion is the compiler's guess at what you meant, and the
compiler has not read your doc comment. Which is the project's rule in advance —
the right fix is the one that keeps the function doing its job.
Two habits: errors print in the order rustc found them, not in line order, so the top one isn't necessarily earliest in the file; and fix one, then re-run, because a later error is often a consequence of an earlier one.
rustc --explain
Every E…. code has a page, offline, on your machine right now:
rustc --explain E0382
You get prose, a short example that triggers the error, and the same example fixed — and cargo reminds you at the bottom of a failing build. Run it for each of this stage's six codes, including the ones you fix easily: that's how a code becomes a word you recognise instead of a number you squint at.
Fixing errors uncovers errors
One behaviour surprises everyone once, so let's get it out of the way:
The error count can go up after a fix. That's progress, not regression.
rustc works in phases. It type-checks a function body, and only if that body type-checked does it go on to borrow-check it — the pass that enforces ownership and mutability. While a function holds a type error, rustc has no usable picture of that body and reports none of its ownership problems. They're still there, just not visible yet.
Measured on the file you're about to fix, rustc 1.97.1: the first run reports
four errors and every one of them is type-level — E0425, E0599, E0308,
E0061. Fix those four and the second run reports two, E0384 and E0382,
the mutability and move errors that were in the same function all along.
The six you're about to meet
| Code | What rustc is telling you |
|---|---|
E0308 |
mismatched types — this value's type isn't the one this position requires |
E0384 |
cannot assign twice to an immutable variable — bindings are immutable unless you say mut |
E0425 |
cannot find that name in this scope — a typo, or something never declared |
E0599 |
no method of that name on this type — usually a near-miss name, or the wrong type |
E0061 |
this function takes N arguments but M were supplied |
E0382 |
a value used (or borrowed) after it was moved — Rust's ownership rule, showing up early |
The first five are ordinary mistakes any language catches somehow. E0382 is the
Rust-specific one: assigning a non-Copy value to a second binding moves it,
and the first binding stops being usable. Stage 03 gives that its own lab; for
now, recognise it — and know .clone() is the sledgehammer, not the answer.
The file you're about to recognise
This course builds a reader for the SQLite file format — the single-file database inside browsers, phones and most applications on your machine. It all starts with one question: is this even a SQLite file?
The format answers in the first sixteen bytes. Every valid SQLite database begins
with the UTF-8 string SQLite format 3 plus a nul terminator — sixteen bytes
exactly, 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 in hex. It's called
the magic header string, and this is the whole of it:
pub const SQLITE_MAGIC: [u8; 16] = *b"SQLite format 3\0";
b"…" is a byte-string literal — not a &str but a &'static [u8; 16],
a reference to a fixed-size array of raw bytes. The leading * dereferences it,
so the constant is the array itself rather than a reference to one (byte arrays
are Copy, which is what makes that legal in a const). Checking for it is a
sixteen-byte comparison with one trap: a file shorter than sixteen bytes
exists, and must answer "no" rather than crash. That trap is your project.
The shape of what's ahead: eleven stages grow one program from that constant to a tool that walks a b-tree and prints a table's rows. Nothing gets thrown away and nothing gets a dependency — the crate's
[dependencies]is empty now and stays empty. What changes fastest isn't your Rust; it's how long it takes you to read an error and know what to do.