Skip to content

The compiler loop

You build understanding of a codebase by changing it and seeing what happens. In Rust, the compiler is the first thing that happens, and it is unusually helpful. This lesson is the tight inner loop you’ll run hundreds of times: edit, ask the compiler, read what it says, fix, repeat.

  • Run cargo check to get a compile signal in seconds, and say how it differs from a full cargo build.
  • Read a Rust diagnostic (its error code, its location, and its suggested fix) and act on it.
  • Order the inner loop (check → test → fmt → clippy) and pick the smallest gate that covers a change.
  • Explain why Clinker runs cargo clippy once without --all-targets before the full pass.
  • Distinguish the Rust compiler’s diagnostics (about your code) from Clinker’s own run-time diagnostic codes (about a pipeline).

New terms in this lesson (each is named here before it appears below):

  • cargo check: type-check without building a binary; the fastest signal.
  • diagnostic, the compiler’s structured error report (code + location + suggested fix).
  • clippy: Rust’s lint tool, run as a required gate.
  • -D warnings, which promotes every warning to a hard error.
  • exit code: the integer a run returns; 0 is clean, non-zero names a failure kind.

You met the {tee} typo idea informally above. Now predict the compiler’s exact reaction before you cause it.

Rust’s compiler doesn’t only say “no”; it explains what’s wrong and usually how to fix it. Run this, then break it on purpose:

rust // editable

It compiles and prints two lines. Now change {t} to {tee} and Run again. You won’t get a vague crash; you’ll get the E0425 diagnostic you just predicted: a precise location, a code you can look up, and a suggested fix. Reading these well is the single highest-leverage skill for working in a Rust codebase.

The inner loop: check → test → fmt → clippy

Section titled “The inner loop: check → test → fmt → clippy”

Clinker’s own command guide documents the gates; reach for the smallest one that covers your change and broaden as you go.

clinker ·50_TESTING_AND_COMMANDS.md doc @19acdcb4

cargo check, the fastest signal. A cargo check type-checks without generating a binary, so it’s much quicker than a full build. This is your every-few-seconds command:

Terminal window
cargo check -p clinker-record # check one crate
cargo check --workspace # check everything

cargo test, to prove behavior. Run the tests at the boundary you touched, not the whole suite:

Terminal window
cargo test -p clinker-record # one crate's tests
cargo test -p clinker-exec value -- --exact # a targeted test by name

cargo fmt --all --check, for formatting. Reports formatting drift (drop --check to fix it).

cargo clippy, for lints. Clippy catches what compiles but shouldn’t ship. Clinker runs it twice, and the order matters:

Terminal window
cargo clippy --workspace -- -D warnings
cargo clippy --workspace --all-targets -- -D warnings

The first pass deliberately omits --all-targets. Why? So that code which is only referenced from tests still trips the dead-code lint. It’s a small trick that keeps unused production code from hiding behind the test suite, and a good first taste of how seriously this project treats its lints: -D warnings makes every warning a hard error.

Three rungs, each centered on the edit→check loop and reading the diagnostic. Run each in the Playground; the compiler is the answer key.

Here is a fully worked break-and-fix. The snippet below is broken on purpose with a move error: it uses a value after handing it away. Run it, read the diagnostic (annotated for you), then read the fix.

rust // editable

cargo check refuses this with a diagnostic shaped like:

error[E0382]: borrow of moved value: `tier`
--> src/main.rs:4:16
|
3 | let owned = tier;
| ---- value moved here
4 | println!("{tier}");
| ^^^^ value borrowed here after move

Read it the way the predict-first taught: a code (E0382), two locations (where the value moved, where you tried to use it after), and a description of the conflict. The fix is to not use the moved-out name: print owned, or borrow instead of moving. Here’s the borrow fix, which keeps both names usable:

let owned = &tier; // borrow, don't move
println!("{tier}"); // tier is still yours
println!("{owned}");

You’ll meet move-vs-borrow properly in Data & Representation; for now the point is the loop: edit → cargo check → read the named diagnostic → apply the fix it points at.

Completion: finish the break, predict the code

Section titled “Completion: finish the break, predict the code”

This snippet is almost a clean type error. One arm of the match is missing, so the match isn’t exhaustive, a thing the Rust compiler refuses. Add the missing arm so it compiles, and predict the error code the incomplete version would have produced before you reveal it.

rust // editable
💡 Hint 1

A match in Rust must cover every variant of the enum; that’s the exhaustiveness guarantee. Count the variants in Tier, then count the arms. The compiler names the one you left out.

The missing arm, and the diagnostic you'd see without it
Tier::Inactive => "inactive", // the third arm

Left incomplete, cargo check reports:

error[E0004]: non-exhaustive patterns: `Tier::Inactive` not covered

E0004 is the exhaustiveness error: the compiler proves at type-check time that you handled every case. It even tells you which variant is missing, so you don’t have to hunt. This is the same exhaustiveness that will keep your Option/Result handling honest later in the track.

Faded: cause and read a diagnostic of your own

Section titled “Faded: cause and read a diagnostic of your own”

Now with no scaffolding. Take this clean, compiling snippet and introduce one error of your choosing: a typo in a name, a wrong type, a missing match arm, a use after move. Run cargo check (or the Playground), read the diagnostic, then fix it back to green.

rust // editable
What a good break-and-read looks like

Any of these breaks the build with a named diagnostic you can read and reverse:

// rename Value::Text -> Value::Txt in one place -> error[E0599] no variant `Txt`
// change `v: &Value` to `v: Value` and keep `&cells` -> type-mismatch on the call
// delete the Value::Text arm -> error[E0004] non-exhaustive patterns

The exercise trains a reflex, not a specific break: when cargo check prints a diagnostic, read the code, the location, and the suggested fix before touching anything. That reflex is the whole inner loop, and you now own it.

There are two diagnostic systems, and they get conflated often. The Rust compiler checks your code (the E04xx/E05xx codes above). Clinker, at run time, checks your pipelines, and it has its own diagnostic codes. When a run fails with something like E105, ask the engine to explain it:

Terminal window
cargo run -p clinker -- explain --code E105

That explain --code is the run-time twin of the compiler’s error code: a stable identifier you can look up. And the run itself reports outcome through its exit code: 0 clean, and distinct non-zero codes for a config/schema error, records sent to the DLQ, a CXL evaluation failure, or an I/O error. A script calling Clinker can branch on those, the same way you branch on whether cargo check came back clean.

Keep the two straight: a Rust error[E0425] means your source won’t compile; a Clinker E105 means a well-compiled engine rejected a pipeline at run time. Different layer, different fix.

💡 Hint 1

A quick way to provoke a clear diagnostic: use a value after moving it, or reference a name that doesn’t exist (like the {tee} typo above). The compiler will name the problem and often point at the fix.

What a good loop looks like

Edit → cargo check -p <crate> (seconds) → repeat until it compiles → cargo test -p <crate> <name> for the behavior you changed → cargo fmt --all and cargo clippy --workspace -- -D warnings before you call it done. Run the full workspace gates only when your change crosses crate boundaries.

You now have a feedback loop measured in seconds and, more importantly, the reflex to read a diagnostic’s code, location, and suggested fix instead of guessing. Next: how to find your way around the thirteen crates that make up the engine.

From the pipeline author’s side (optional, one-directional; Clinker’s run-time diagnostics and the DLQ, from the pipeline author’s side rather than the compiler’s):