Overview
rustc is the compiler nobody invokes directly — cargo drives it — but whose pipeline
explains both Rust's famous error messages and its famous compile times. Source is
lowered through HIR (post-macro-expansion AST) to MIR (a control-flow-graph IR), where
borrow checking and drop elaboration run, then monomorphized generics feed LLVM for
optimization and codegen. Knowing roughly where each phase's cost and each error's
origin lies is what turns "the compiler is slow/angry" into an actionable model.
Key points
-
The pipeline: parse → macro expansion → HIR (type checking, trait resolution) → MIR (borrow check, drop elaboration, const eval) → monomorphization → LLVM IR → object code. The borrow checker is a MIR pass — exactly the analysis miri later interprets.
flowchart TB SRC[Source] --> PA[Parse] PA --> ME[Macro expansion] ME --> HIR["HIR: type checking, trait resolution"] HIR --> MIR["MIR: borrow check, drop elaboration, const eval"] MIR --> MO["Monomorphization: one copy per concrete instantiation, the compile-time and binary-size cost"] MO --> IR["LLVM IR: optimized in parallel codegen units"] IR --> OBJ[Object code] -
Monomorphization is the cost model: every concrete instantiation of a generic function gets its own copy — zero-cost dispatch at runtime, paid for in compile time and binary size;
dyn Traitis the escape valve. -
Codegen units: each crate is split into parallel LLVM units (16 in release by default); fewer units = better optimization, slower builds — the
codegen-units = 1+ LTO release recipe lives in profiles. -
Incremental compilation caches per-query results in
target/incremental— on by default for dev, off for release; the reason the secondcargo checkis fast. -
Error codes are documentation: every
E0502has a full explanation viarustc --explain E0502— the error-message culture is a deliberate, resourced project value. -
Lint levels are compiler-native:
#[allow]/#[warn]/#[deny]per item, crate or command line; clippy plugs its lints into this same machinery. -
RUSTFLAGSand-Cflags:-C target-cpu=native,-C opt-level,-C debuginfo— usually reached through cargo profiles, not directly. -
No stable ABI: rlibs and Rust dylibs only link against artifacts from the same compiler version — the deep reason cargo rebuilds the world per toolchain and artifact formats look the way they do.
Examples
rustc --explain E0382 # use of moved value, chapter and verse
cargo build --timings # HTML report: which crate dominates the build
RUSTFLAGS="-C target-cpu=native" cargo build --release
cargo rustc -- -Zself-profile # (nightly) per-pass compiler profiling
Related
- The Rust toolchain — parent catalog.
- Rust compilers & the language definition — rustc as the single implementation, and the outliers.
- Cargo in depth — profiles: where the codegen knobs are actually set.
- miri — the MIR interpreter reusing this pipeline for UB detection.
- cargo-expand — inspecting the macro-expansion phase's output.