The Rust compiler, usually invoked as rustc, is the official compiler for the Rust programming language. It is developed by the Rust Project and released under a dual Apache 2.0 and MIT License.
rustc translates Rust source code into native machine code. It is itself written in Rust and is self-hosting: each new version is built using the previous stable release. Most developers do not call rustc directly. They use Cargo, Rust's standard build tool and package manager, which invokes the compiler with the right options for a given project.
rustc is best known for two features: a type system that enforces memory and thread safety at compile time, and error messages that point at the offending source span and often suggest a fix.
History
Rust began in 2006 as a personal project of Graydon Hoare, then an engineer at Mozilla. Mozilla began sponsoring the project shortly before its public announcement in 2010. The earliest compiler was written in OCaml. A version of rustc written in Rust replaced it in 2011, after which the compiler has built itself.
The first stable release, Rust 1.0, shipped on 15 May 2015. From that point on, the project committed to backward compatibility: code that compiled on a given stable release would continue to compile on later ones, and new language features were added through opt-in editions rather than breaking changes.
In August 2020, Mozilla laid off a number of staff who had been working on Rust, as part of broader layoffs at the company. The Rust Foundation was established in February 2021 to host the trademarks and infrastructure that had previously sat with Mozilla, with several large technology companies as founding members. Day-to-day work on the compiler continued under the existing volunteer-led teams.
Usage and basic invocation
rustc can compile a single Rust source file without Cargo. A minimal source file named contains a function:
fn main() { println!("Hello, world!"); }
The file can be compiled directly from a shell:
rustc hello.rs
This produces an executable named on Unix-like systems, or on Windows, using the default edition and optimisation settings. Direct invocation is useful for small experiments, compiler tests and build systems that call rustc themselves.
For ordinary projects, Cargo is the usual entry point. A typical workflow creates a package, builds it, runs it, and then executes its tests:
cargo new hello cd hello cargo build cargo run cargo test
Cargo stores build output under , resolves dependencies from crates.io, and forwards the appropriate command-line options to rustc.
Editions
An edition is an opt-in revision of the Rust language that is implemented by rustc. Each crate declares which edition it targets in its manifest, and rustc applies the corresponding parsing, name-resolution and lint rules for that crate. Crates on different editions can still be linked together in the same program, which lets the ecosystem move forward without splitting in two.
Four editions have been released:
2015 — the baseline edition from Rust 1.0. In rustc this is effectively the compatibility mode for older crates and tools. 2018 — introduced the first large migration wave, including module-system path changes and non-lexical lifetimes (NLL). rustc provides edition lints and automated suggestions to help migrate 2015 code. 2021 — changed several defaults and trait-resolution edges, including array behaviour and closure-capture adjustments. rustc applies these changes only when a crate opts into 2021. 2024 — continues the same model with narrower semantic fixes (for example temporary-scope and capture-corner cases) while preserving cross-edition compatibility through rustc's per-crate edition handling.
An edition is selected per crate in its manifest:
[package] name = "myapp" version = "0.1.0" edition = "2021"
When invoking rustc directly, the equivalent is the flag. Omitting the field falls back to the 2015 edition for backward compatibility. For Cargo-based projects, edition migration is typically guided by rustc diagnostics and .
Compilation process
The architecture of rustc follows a multi-stage pipeline that progressively lowers the abstraction level of the source code. In simplified terms, rustc first checks what the program means, then checks whether it obeys Rust's safety rules, and only then asks a backend to generate machine code.
Frontend and intermediate representation
The compilation process begins with the frontend parsing the raw source code into an Abstract Syntax Tree (AST). During this phase, the compiler expands macros and resolves module imports. Rather than translating the AST directly to machine code, rustc converts it through multiple language-specific intermediate representations (IR). The AST is first lowered into a High-Level Intermediate Representation (HIR), where the compiler performs name resolution, type inference, and trait checking. It is then transformed into the Mid-Level Intermediate Representation (MIR). MIR is a simplified control-flow graph specifically designed to execute the language's borrow checker and run Rust-specific optimizations, such as removing dead code, before passing the program to the backend.
Parsing and macro expansion
The compiler first parses the source text into an abstract syntax tree (AST). Once the AST exists, macros declared in the program are expanded. Expansion can introduce new code, which then has to be parsed in turn, so this step runs to a fixed point before the next stage begins. Module imports are resolved alongside this work.
High-Level Intermediate Representation
The AST is then lowered into the High-Level Intermediate Representation, or HIR. The HIR is closer to the language's semantics than to its surface syntax: it has no macro calls, no shorthand forms, and a single canonical shape for each construct. The compiler performs name resolution, type inference and trait resolution on the HIR.
Mid-Level Intermediate Representation
After type checking, rustc lowers the HIR into the Mid-Level Intermediate Representation, or MIR. MIR is a simple control-flow graph in which each basic block contains a short list of statements and ends in a single terminator. It was introduced in 2016 to give the compiler a representation that was easier to analyse than the HIR but still preserved enough information about Rust's semantics for safety checks.
The MIR is the level at which the borrow checker runs. A small set of Rust-specific optimisations also runs on MIR before code generation, including constant evaluation (), removal of dead code and inlining of small functions.
Borrow checker
The borrow checker is the part of rustc that enforces Rust's ownership rules — strict memory safety rules checked at compile time. A value has exactly one owner at a time. References are either a single mutable borrow or any number of shared, read-only borrows, but never both at once. For every value in the program it tracks which part of the code currently owns the value, and which parts hold references ("borrows") to it. By tracking lifetimes and ownership within the MIR, the compiler prevents common memory bugs such as dangling pointers and data races without relying on a runtime garbage collector. These restrictions also rule out use-after-free, double-free and most data races of the kind common in C and C++. When a program breaks the rules, the compiler refuses to build it and prints an explanation that points at the offending code.
The original borrow checker worked on lexical scopes: a borrow was treated as live until the end of the block it was declared in, which sometimes rejected code that was in fact safe. Non-lexical lifetimes (NLL), which let the compiler end a borrow as soon as it is no longer used, were stabilised in the 2018 edition. A follow-up project, Polonius, reformulates the analysis as a Datalog-style set of rules and aims to accept further safe programs that the current checker still rejects. As of 2026 it remains experimental.
A short example shows what the checker rejects. The following program tries to use a value after it has been moved into another binding:
fn main() { let s = String::from("hello"); let t = s; // ownership of the String moves into `t` println!("{}", s); // error: `s` no longer owns the value }
rustc rejects the program with diagnostic (borrow of moved value), pointing at the call and at the earlier line where ownership was transferred.
Backend and code generation
After the Mid-Level Intermediate Representation (MIR) is validated and optimized, the compiler invokes a backend to generate the final object code. By default, rustc uses LLVM as its primary backend framework. The compiler translates the MIR into LLVM IR, relying on LLVM to handle aggressive architecture-specific optimizations and target-specific code generation. Finally, a linker — typically the system linker, or LLD on some targets — combines the resulting object files into a single executable image. To improve compile times during development and expand the language's platform coverage, the rustc architecture supports pluggable alternative backends based on GCC and Cranelift.
Incremental and parallel compilation
Internally, rustc is organised around a memoised query system. Each fact the compiler needs about the program — say, the type of an expression or the set of impls available for a given trait — is computed on demand by a query, and the results are cached on disk between runs. This is what makes incremental compilation work: when a file changes, the compiler only re-runs the queries whose inputs actually changed.
The query system is implemented as a dependency graph, called the . Every query — , , and many others — records what it read and what it produced. When a source file changes, rustc hashes the new AST/HIR fingerprint, marks stale queries and reuses cached results for unaffected parts of the crate. As a result, a second after a small change typically completes much faster than the first.
Cargo turns this on by default for dev builds: the profile sets , so dependency crates and a project's own code get incremental artifacts under (and the query cache lives alongside it in the build directory). Release builds normally skip incremental compilation because the extra bookkeeping does not pay off for a one-off optimised compile.
Parallelism is handled separately. Code generation already splits work through — each unit is lowered to LLVM in parallel and linked at the end. The newer push is the parallel frontend on nightly: type-checking, borrow-checking and MIR opts can run with (via or ), though it stays off by default because threading bugs still show up occasionally. Parsing and macro expansion are still mostly serial as of the current compiler guide.
For incremental compilation to be reliable, queries must behave as pure functions of their inputs. If a query could depend on hidden global state, cached results could be reused incorrectly after a rebuild; this constraint shapes many internal rustc refactors.
Compiler driver and crate structure
The compiler is itself organised as a workspace of small crates in the namespace, tied together by an entry point called . Each phase of the pipeline lives in roughly its own crate, which makes it easier for tools such as Clippy and rust-analyzer to reuse parts of the compiler without taking on the full pipeline.
Some of the more visible crates are:
— wires the phases together and parses the command line. and — tokenise the source and produce the AST. and — define AST types and run macro expansion. — resolves names and modules. and — define the HIR and perform type and trait checking ( replaced the earlier crate). — runs the borrow checker on MIR. and — build MIR from HIR and run MIR-level optimisations. — central types and the query engine shared across phases. and — code generation, with LLVM as the default backend.