rust-lang/rust
Frontend: parse, expand, resolve, lower
The "frontend" is everything that turns text into HIR: lex, parse, expand macros, resolve names, and lower the AST. Each step is a separate crate (or two), and each has a well-defined output that the next step consumes.
Pipeline
graph LR
Bytes["source bytes"] --> Lexer[rustc_lexer]
Lexer --> Parser[rustc_parse]
Parser --> AST[(AST)]
AST --> Validate[rustc_ast_passes]
Validate --> Expand[rustc_expand]
Expand -.invokes.-> Builtins[rustc_builtin_macros]
Expand -.invokes.-> ProcMac[rustc_proc_macro]
Expand --> Resolve[rustc_resolve]
Resolve --> Lower[rustc_ast_lowering]
Lower --> HIR[(HIR)]Lexer
compiler/rustc_lexer/ is intentionally minimal — it produces a flat stream of tokens with no whitespace or comments dropped (those are kept for diagnostics and rustfmt). It's published as a separate crate on crates.io (rustc-lexer) so that rust-analyzer and other tools can use the same tokenization that rustc does.
It takes raw &str and emits Tokens. Higher-level wrapping (interning into Symbol, attaching spans) happens in rustc_parse.
Parser
compiler/rustc_parse/ implements the full Rust grammar. Output: an ast::Crate. Highlights:
- Recovery — the parser tries hard to keep going after a syntax error so users get diagnostics for the rest of the file
Spannedeverything — every node carries a span pointing back to source- Macro inputs are kept as token streams (
TokenStream) until expansion runs
The AST itself lives in compiler/rustc_ast/ and the format!-string mini-grammar in compiler/rustc_parse_format/.
AST validation
compiler/rustc_ast_passes/ runs immediately after parsing. It checks things that are syntactically valid but semantically forbidden — e.g., pub on a struct field within a pub union, ill-formed feature usage, where clauses on the wrong items. These checks are simpler when done on the AST than on the HIR.
Macro expansion
compiler/rustc_expand/ is the macro engine. It walks the AST, finds mac!() invocations, and asks for their expansion:
macro_rules!macros — expanded purely in the engine itself, by pattern-matching token trees- Built-in macros (
format_args!,cfg!,derive(Debug),panic!,include_str!, …) — implemented as Rust code incompiler/rustc_builtin_macros/ #[proc_macro]macros — invoked over the proc-macro server viacompiler/rustc_proc_macro/, which talks to a sandboxed user-defined process or DLL
Expansion is fixed-point: a macro can expand to code containing more macros, and the engine keeps going until no invocations remain.
The proc_macro crate (library/proc_macro/) is the user-facing API; the bridge to rustc is in rustc_proc_macro.
Name resolution
compiler/rustc_resolve/ maps every path in the AST to a definition. This includes:
- Resolving
usedeclarations - Resolving paths in expressions, types, and patterns
- Tracking visibility (
pub,pub(crate),pub(in some::path)) - Building the module tree (which is interleaved with macro expansion: a macro can expand to
mods) - Resolving labels and lifetimes (the lifetime resolver lives in
rustc_resolve::late::lifetimes)
Output: Resolutions, attached to the AST so AST lowering can read them.
AST lowering
compiler/rustc_ast_lowering/ is the "AST → HIR" step. HIR (compiler/rustc_hir/) is more uniform and decision-friendly than the AST:
- All paths are pre-resolved to
DefIds/Res - Sugar is desugared (e.g.,
forloops becomeloop { match it.next() { … } },?becomes branching,if letandwhile letare lowered) async fnis lowered to a synchronous function returning a coroutine- Closures keep enough info for borrowck and codegen but lose source-level pretty-print fidelity
Lowering is the last "syntactic" step — everything after it operates on HIR (or further lowered IRs).
Why these are separate crates
- Encapsulation — the parser doesn't know about types; the lowerer doesn't know about source bytes; etc.
- Compile time — heavy crates like
rustc_parsechange rarely; isolating them keeps incremental rebuilds fast. - Reusability —
rustc_lexeris published to crates.io asrustc-lexerfor use in tools.
Key abstractions
| Type | Lives in | What it represents |
|---|---|---|
Token |
rustc_lexer::Token |
A single token with its kind |
TokenStream |
rustc_ast::tokenstream |
A token tree fed to / from a macro |
ast::Crate |
rustc_ast::ast |
A full parsed crate |
ast::Item |
rustc_ast::ast |
A top-level item |
Span |
rustc_span::Span |
Source location |
Symbol |
rustc_span::Symbol |
An interned identifier string |
Ident |
rustc_span::Ident |
Symbol + Span + hygiene info |
hir::Crate |
rustc_hir |
Lowered crate |
Res |
rustc_hir::def::Res |
The resolution of a path |
DefId |
rustc_hir::def_id |
Stable identifier of a definition |
Hygiene
Macro hygiene is handled by expansion contexts attached to spans. Every identifier carries a SyntaxContext that records which macro expansion it was introduced from; resolution uses this to decide whether two foo identifiers refer to the same binding. The implementation lives in rustc_span::hygiene and rustc_resolve::macros.
Entry points for modification
- Adding a new built-in macro →
rustc_builtin_macros, then register inlib.rs - Adding a parser recovery →
rustc_parse— find the relevantparse_*method and add a recovery branch - Lowering a new sugar →
rustc_ast_lowering(search for an existing example likelower_for_loop) - New name-resolution rule →
rustc_resolve - See Diagnostics for how to wire user-facing errors
See also
- Type checking — what runs on HIR after lowering
- Diagnostics — the diagnostic machinery used throughout the frontend
- rustc-dev-guide: parsing
- rustc-dev-guide: macro expansion
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.