The rustc_driver
is essentially rustc
's main()
function. It acts as
the glue for running the various phases of the compiler in the correct order,
managing state such as the CodeMap
(maps AST nodes to source code),
Session
(general build context and error messaging) and the TyCtxt
(the "typing context", allowing you to query the type system and other cool
stuff). The rustc_driver
crate also provides external users with a method
for running code at particular times during the compilation process, allowing
third parties to effectively use rustc
's internals as a library for
analysing a crate or emulating the compiler in-process (e.g. the RLS).
For those using rustc
as a library, the run_compiler()
function is the main
entrypoint to the compiler. Its main parameters are a list of command-line
arguments and a reference to something which implements the CompilerCalls
trait. A CompilerCalls
creates the overall CompileController
, letting it
govern which compiler passes are run and attach callbacks to be fired at the end
of each phase.
From rustc_driver
's perspective, the main phases of the compiler are:
- Parse Input: Initial crate parsing
- Configure and Expand: Resolve
#[cfg]
attributes, name resolution, and expand macros - Run Analysis Passes: Run trait resolution, typechecking, region checking and other miscellaneous analysis passes on the crate
- Translate to LLVM: Translate to the in-memory form of LLVM IR and turn it into an executable/object files
The CompileController
then gives users the ability to inspect the ongoing
compilation process
- after parsing
- after AST expansion
- after HIR lowering
- after analysis, and
- when compilation is done
The CompileState
's various state_after_*()
constructors can be inspected to
determine what bits of information are available to which callback.
For a more detailed explanation on using rustc_driver
, check out the
stupid-stats guide by @nrc
(attached as Appendix A).
Warning: By its very nature, the internal compiler APIs are always going to be unstable. That said, we do try not to break things unnecessarily.
The Rust compiler is a fairly large program containing lots of big data
structures (e.g. the AST, HIR, and the type system) and as such, arenas and
references are heavily relied upon to minimize unnecessary memory use. This
manifests itself in the way people can plug into the compiler, preferring a
"push"-style API (callbacks) instead of the more Rust-ic "pull" style (think
the Iterator
trait).
For example the CompileState
, the state passed to callbacks after each
phase, is essentially just a box of optional references to pieces inside the
compiler. The lifetime bound on the CompilerCalls
trait then helps to ensure
compiler internals don't "escape" the compiler (e.g. if you tried to keep a
reference to the AST after the compiler is finished), while still letting users
record some state for use after the run_compiler()
function finishes.
Thread-local storage and interning are used a lot through the compiler to reduce
duplication while also preventing a lot of the ergonomic issues due to many
pervasive lifetimes. The rustc::ty::tls
module is used to access these
thread-locals, although you should rarely need to touch it.