Rust 编译器与运行时揭秘
第1章 编译管线全景:从源码到机器码的完整旅程
Rust 编译器不是一条线性流水线,而是一个按需驱动的查询系统。这一章先把地图画出来:源码 → Token → AST → HIR → 类型检查 → MIR → 单态化 → LLVM IR → 机器码,看清每一站丢掉了什么、又新增了什么。
本章要点
- Rust 编译器不是一条线性流水线,而是一个基于查询的按需驱动系统
- 从源码到机器码,数据依次流经这些关键阶段:源码 → Token → AST → 名称解析/宏展开 → HIR → 类型检查 → MIR → 借用检查/MIR 优化 → 单态化收集 → LLVM IR → 机器码 → 链接
- 每个阶段承担什么职责、丢弃什么信息、新增什么信息
- 查询系统如何让编译器实现增量编译和并行执行
- 跟踪一个真实的
fn main() { println!("hello"); }走完编译器的每一站
1.1 为什么需要这张地图
当你写下一段 Rust 代码:
fn main() {
let s = String::from("hello");
let r = &s;
println!("{}", r);
}
然后执行 cargo build,几秒后得到一个可执行文件。这中间发生了什么?
大多数 Rust 教程会告诉你"编译器检查了所有权和借用",然后跳到"程序运行"。但编译器不是一个黑箱——它是一个由数十个 crate 组成的精密系统,每个 crate 都有明确的输入和输出。
理解这个系统的意义不在于学术——而在于实战:
- 当你遇到 lifetime 错误时,知道这个错误发生在 MIR 层的借用检查器中,就知道该从控制流图的角度去思考修复方案
- 当你想优化编译速度时,知道单态化和 LLVM 优化是最耗时的阶段,就知道该用
cargo check而非cargo build、该减少泛型实例化的数量 - 当你读 Tokio、Axum 等项目的源码时,知道
Pin<Box<dyn Future + Send + 'static>>在每一站分别意味着什么,就不会被类型签名吓住 - 当你写过程宏时,知道宏展开发生在 AST 层、与名称解析交替进行、在类型检查之前,就知道过程宏能操纵什么、不能操纵什么
本章将建立一张完整的地图。后续每一章都是这张地图上某个区域的深入探索。
1.2 编译管线全景
在深入每个阶段之前,先看一张全景图。这张图展示了从 .rs 源文件到最终可执行文件的完整数据流:
graph LR
A["源码<br/>.rs 文件"] --> B["词法分析<br/>rustc_lexer"]
B --> C["语法分析<br/>rustc_parse"]
C --> D["名称解析<br/>宏展开<br/>rustc_resolve<br/>rustc_expand"]
D --> E["AST → HIR<br/>rustc_ast_lowering"]
E --> F["类型检查<br/>rustc_hir_typeck<br/>rustc_hir_analysis"]
F --> G["HIR → MIR<br/>rustc_mir_build"]
G --> H["借用检查<br/>MIR 优化<br/>rustc_borrowck<br/>rustc_mir_transform"]
H --> I["单态化收集<br/>rustc_monomorphize"]
I --> J["代码生成<br/>rustc_codegen_ssa<br/>rustc_codegen_llvm"]
J --> K["链接<br/>最终二进制"]
style A fill:#64748b,color:#fff,stroke:none
style B fill:#7c3aed,color:#fff,stroke:none
style C fill:#7c3aed,color:#fff,stroke:none
style D fill:#6366f1,color:#fff,stroke:none
style E fill:#4f46e5,color:#fff,stroke:none
style F fill:#2563eb,color:#fff,stroke:none
style G fill:#0ea5e9,color:#333,stroke:none
style H fill:#10b981,color:#333,stroke:none
style I fill:#f59e0b,color:#333,stroke:none
style J fill:#dc2626,color:#fff,stroke:none
style K fill:#dc2626,color:#fff,stroke:none
| 阶段 | 负责 crate | 输入 | 输出 | 核心职责 |
|---|---|---|---|---|
| 词法分析 | rustc_lexer |
源码文本 | Token 流 | 将字符流切分为词法单元 |
| 语法分析 | rustc_parse |
Token 流 | AST (rustc_ast::Crate) |
构建语法树,检查语法正确性 |
| 名称解析与宏展开 | rustc_resolve + rustc_expand |
AST | 展开后的 AST + 解析结果 | 展开所有宏,解析所有名称到定义 |
| AST → HIR | rustc_ast_lowering |
展开后的 AST | HIR | 脱糖,消除语法糖 |
| 类型检查 | rustc_hir_typeck + rustc_hir_analysis |
HIR | 带类型的 HIR | 类型推导、trait 解析、一致性检查 |
| HIR → MIR | rustc_mir_build |
HIR | MIR | 控制流图化,模式匹配编译 |
| 借用检查与 MIR 优化 | rustc_borrowck + rustc_mir_transform |
MIR | 优化后的 MIR | NLL 借用检查,常量传播,内联等优化 |
| 单态化收集 | rustc_monomorphize |
MIR | 单态化实例集合 + codegen unit 分区 | 确定需要生成代码的所有泛型实例 |
| 代码生成 | rustc_codegen_ssa + rustc_codegen_llvm |
MIR + 单态化实例 | LLVM IR → 目标文件 | 翻译为 LLVM IR,LLVM 优化,生成机器码 |
| 链接 | 系统链接器 | 目标文件 | 可执行文件 / 库 | 符号解析,重定位,生成最终二进制 |
接下来我们逐一拆解每个阶段。
1.3 词法分析:从字符到 Token
编译的第一步是词法分析(Lexing),将源码文本切分为 Token 流。
// 源码
let x: i32 = 42 + y;
// Token 流(示意,略去空白 Token)
Keyword(Let) Ident("x") Colon Ident("i32") Eq Literal(42) Plus Ident("y") Semi
Rust 的词法分析器位于 rustc_lexer crate 中。这个 crate 有一个非常独特的设计——它是不依赖编译器其他任何 crate 的纯函数库,只用到 memchr、unicode-ident、unicode-properties 这几个外部小库(见 compiler/rustc_lexer/Cargo.toml)。它直接操作 &str,产生的 Token 只是一个类型标签加上在原始文本中的长度:
// 来自 compiler/rustc_lexer/src/lib.rs
/// Parsed token.
/// It doesn't contain information about data that has been parsed,
/// only the type of the token and its size.
pub struct Token {
pub kind: TokenKind,
pub len: u32,
}
TokenKind 枚举定义了所有可能的词法单元类型——标识符、字面量、生命周期、标点符号、注释、空白等。这一层并不区分关键字:let、fn 和普通标识符一样都是 Ident(源码注释写的是 "An identifier or keyword"),关键字要到 rustc_parse 里把标识符驻留成 Symbol、再和预置的 kw::Let 等比较时才被认出来。上面示意里写成 Keyword(Let) 只是为了好读。
词法分析的微妙之处
词法分析看似简单,但 Rust 的词法层有几个值得注意的细节:
- 生命周期与字符字面量的歧义:
'a是生命周期还是字符字面量?词法分析器通过后续字符来区分——如果'后面跟着标识符字符且没有闭合的',则是生命周期 - 原始字符串字面量:
r#"..."#需要计数#数量来确定边界,在LiteralKind中表示为RawStr { n_hashes: Option<u8> } - 文档注释区分:
///(Outer)和//!(Inner)在词法层就被区分为不同的DocStyle - 两层架构:
rustc_lexer产生"原始 Token",还需经过rustc_parse::lexer二次处理,转换为解析器使用的"宽 Token"。这一层处理 token 联合(如将>>合并为>>)等工作
1.4 语法分析:从 Token 到 AST
语法分析器(Parser)将 Token 流组织成抽象语法树(Abstract Syntax Tree, AST)。Rust 的解析器位于 rustc_parse crate 中,是一个手写的递归下降解析器——不使用 yacc、bison 等解析器生成工具。
解析器的入口
编译的起点在 rustc_interface::passes::parse 函数中。它根据输入类型(文件或字符串)创建 Parser 实例,调用 parser.parse_crate_mod() 产出 ast::Crate。几个关键细节:
StripTokens::ShebangAndFrontmatter—— 解析器自动去除 shebang 行和 frontmatterparse_crate_mod()是顶层解析入口,产出完整的 crate AST- 命令行注入的 crate 级属性(
-Z crate-attr)在解析后由rustc_builtin_macros::cmdline_attrs::inject加进 AST
AST 的结构
AST 忠实地保留了源码的语法结构,包括所有的语法糖:
// 源码
fn add(a: i32, b: i32) -> i32 {
a + b
}
// AST(简化表示)
FnDecl {
name: "add",
params: [
Param { name: "a", ty: Path("i32") },
Param { name: "b", ty: Path("i32") },
],
return_ty: Path("i32"),
body: Block {
stmts: [],
expr: BinOp(Add, Path("a"), Path("b")),
},
}
此时编译器还不知道 i32 是什么类型、a + b 是否合法——这些是后续阶段的事。AST 只关心语法结构是否合法。
错误恢复
Rust 的解析器有一个重要特性——错误恢复(Error Recovery)。当遇到语法错误时,解析器不会立即停止,而是尝试跳过错误部分继续解析,以便一次编译就能报告多个错误。解析器内部有一个 Recovery 机制来控制这个行为:
// 来自 compiler/rustc_parse/src/parser/mod.rs
pub enum Recovery {
Allowed,
Forbidden,
}
在正常编译中使用 Recovery::Allowed,而在解析命令行参数等场景中使用 Recovery::Forbidden(因为命令行输入不需要恢复,只需要精确诊断)。
1.5 名称解析与宏展开
AST 构建完成后,编译器进入一个交织的阶段——名称解析(Name Resolution)和宏展开(Macro Expansion)。这两个过程必须交替进行,因为宏可以引入新的名称,而名称解析需要知道哪些标识符是宏调用。
宏展开
宏展开发生在 AST 阶段。解析完成后,宏展开器遍历 AST,遇到 println!("{}", x) 这样的宏调用时,将其展开为一棵新的 AST 子树。展开后的代码再次经过语法分析,这个过程可能递归进行。
编译器通过 configure_and_expand 函数驱动整个过程。这个函数是编译器前端最复杂的入口之一,它协调了宏展开和名称解析的交替执行。关键流程:
- 注册内建宏:
rustc_builtin_macros::register_builtin_macros(resolver)注册println!、vec!等内建宏 - 标准库注入:
rustc_builtin_macros::standard_library_imports::inject自动添加extern crate std;和按 edition 选择的 prelude(2024 edition 是use std::prelude::rust_2024::*;;#![no_std]下换成core,#![no_core]则什么都不注入) - 宏展开:
ecx.monotonic_expander().expand_crate(krate)递归展开所有宏调用,有递归深度限制(recursion_limit) - AST 验证:
rustc_ast_passes::ast_validation::check_crate检查展开后的 AST 是否满足语法约束 - 名称解析:
resolver.resolve_crate(&krate)将所有名称绑定到它们的定义
名称解析的作用
名称解析器(rustc_resolve)将源码中的每个标识符映射到它所指向的定义——处理模块系统、use 语句、glob 导入、嵌套路径等复杂情况。它的输出(ResolverOutputs)分为 global_ctxt(全局解析结果)和 ast_lowering(为降级准备的信息)两部分。
在名称解析完成后,所有宏都已被展开,所有名称都被绑定到具体定义。后续阶段看到的是一棵完全展开、完全解析的 AST。
1.6 AST → HIR 降级:脱糖的艺术
HIR(High-level Intermediate Representation)是 Rust 编译器的第一层中间表示。从 AST 到 HIR 的转换叫做 lowering(降级),由 rustc_ast_lowering crate 负责。
这个阶段在编译器中通过查询系统注册:
// 来自 compiler/rustc_interface/src/passes.rs
providers.queries.hir_crate = rustc_ast_lowering::lower_to_hir;
脱糖:消除语法糖
降级的核心操作是脱糖(desugaring)——将语法糖转换为更基本的构造:
for 循环脱糖:
// 你写的
for item in collection {
process(item);
}
// HIR 中的真实形式
{
let result = match IntoIterator::into_iter(collection) {
mut iter => loop {
match Iterator::next(&mut iter) {
Some(item) => { process(item); }
None => break,
}
},
};
result
}
? 操作符脱糖:
// 你写的
let value = some_result?;
// HIR 中的真实形式
let value = match Try::branch(some_result) {
ControlFlow::Continue(v) => v,
ControlFlow::Break(e) => return FromResidual::from_residual(e),
};
async fn 脱糖:async fn fetch(url: &str) -> Response 变为 fn fetch<'a>(url: &'a str) -> impl Future<Output = Response> + 'a,编译器生成一个匿名的 Future 类型。
while 脱糖:while cond { body } 变为 loop { if cond { body } else { break } },while let 同理,只是条件换成一个 let 表达式(compiler/rustc_ast_lowering/src/expr.rs 的 lower_expr_while_in_loop_scope)。if let 则不再改写成 match:HIR 里它仍是 if,条件是一个 hir::ExprKind::Let 表达式。
AST 与 HIR 的关键区别
graph TB
subgraph "AST 保留的信息"
A1["for 循环语法糖"]
A2["? 操作符"]
A3["if let / while let"]
A4["async fn 语法"]
A5["括号表达式"]
A6["精确的 Span 信息"]
end
subgraph "HIR 新增/变更"
H1["for → loop + match"]
H2["? → match Try::branch"]
H3["while / while let → loop + if<br/>if let → if + let 表达式"]
H4["async fn → fn + impl Future"]
H5["括号被消除"]
H6["每个节点有唯一 HirId"]
end
A1 -->|脱糖| H1
A2 -->|脱糖| H2
A3 -->|脱糖| H3
A4 -->|脱糖| H4
A5 -->|消除| H5
A6 -->|转换| H6
style A1 fill:#6366f1,color:#fff,stroke:none
style A2 fill:#6366f1,color:#fff,stroke:none
style A3 fill:#6366f1,color:#fff,stroke:none
style A4 fill:#6366f1,color:#fff,stroke:none
style A5 fill:#6366f1,color:#fff,stroke:none
style A6 fill:#6366f1,color:#fff,stroke:none
style H1 fill:#2563eb,color:#fff,stroke:none
style H2 fill:#2563eb,color:#fff,stroke:none
style H3 fill:#2563eb,color:#fff,stroke:none
style H4 fill:#2563eb,color:#fff,stroke:none
style H5 fill:#2563eb,color:#fff,stroke:none
style H6 fill:#2563eb,color:#fff,stroke:none
脱糖之后,编译器面对的是一个更简单但更冗长的表示——语法糖被消除了,for、while、? 都变成了显式的 loop、if、match、break。这大大简化了后续类型检查的工作,因为类型检查器只需要处理少数几种基本构造,而不需要知道 for 循环或 ? 操作符的特殊语义。
1.7 类型检查与推导
类型检查是 Rust 编译器最复杂的阶段之一,涉及两个核心 crate:
rustc_hir_analysis:负责 well-formedness 检查、trait 一致性检查、impl 验证等crate 级别的分析rustc_hir_typeck:负责函数体内部的类型推导和检查
Hindley-Milner 风格的类型推导
Rust 的类型推导基于 Hindley-Milner 类型系统的变体。当你写下:
let mut map = HashMap::new();
map.insert("key", 42);
编译器的推导过程是:
- 看到
HashMap::new(),知道返回HashMap<K, V>,但K和V尚未确定,创建类型变量?K和?V - 看到
map.insert("key", 42),推断?K = &str,?V = i32(整数字面量默认推导为i32) - 回溯统一:
map的类型最终确定为HashMap<&str, i32>
这个过程使用统一算法(Unification)——当两个类型必须相同时,尝试找到一组类型变量的赋值使它们一致。如果找不到,就产生类型错误。
Trait 解析
当遇到 a + b 时,编译器需要确定调用的是哪个 Add trait 的实现。这个过程叫做 trait resolution,它需要:
- 以
a的类型作Self、b的类型作泛型参数,构造一条Add约束 - 在
Add的所有 impl 中选择匹配的实现 - 运算符不走自动解引用——不会因为
a的类型没有实现就去试解引用后的类型(compiler/rustc_hir_typeck/src/method/mod.rs的lookup_method_for_operator只构造这一条约束);&i32 + i32能通过,是因为标准库直接为引用类型写了Add实现 - 处理 trait 约束传播——如果在泛型上下文中,可能需要推迟到单态化时再解析
方法解析与自动解引用
当你写 foo.bar() 时,编译器会沿着自动解引用链(T → &T → &mut T → Deref::Target)逐层查找固有方法和 trait 方法。这解释了为什么 String 可以直接调用 &str 的方法——因为 String: Deref<Target = str>。
类型检查的输出
类型检查完成后,每个 HIR 表达式节点都被标记了它的具体类型。这些类型信息存储在 TypeckResults 中,通过查询系统按需获取。从这一步开始,编译器拥有了完整的类型信息,后续所有阶段都可以通过 TyCtxt(类型上下文)查询任何表达式的类型。
这一阶段的检查也包括在 run_required_analyses 中注册的全量类型分析:
// 来自 compiler/rustc_interface/src/passes.rs
rustc_hir_analysis::check_crate(tcx);
1.8 HIR → MIR 降级:构建控制流图
MIR(Mid-level Intermediate Representation)是 Rust 编译器最独特的创新之一。从 HIR 到 MIR 的转换由 rustc_mir_build crate 负责。
MIR 是一个基于控制流图(Control Flow Graph, CFG)的表示。每个函数被分解为一系列基本块(Basic Block),每个基本块包含一系列语句(Statement)和一个终结器(Terminator)。
// 源码
fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
// MIR(简化)
fn max(_1: i32, _2: i32) -> i32 {
let mut _0: i32; // 返回值
let mut _3: bool; // 比较结果
bb0: {
_3 = Gt(_1, _2);
switchInt(_3) -> [0: bb2, otherwise: bb1];
}
bb1: { // a > b 为 true
_0 = _1;
goto -> bb3;
}
bb2: { // a > b 为 false
_0 = _2;
goto -> bb3;
}
bb3: {
return;
}
}
从 HIR 到 MIR 中间还隔着一层 THIR(Typed HIR):thir_body 查询先把类型检查后的函数体转成 THIR(compiler/rustc_mir_build/src/thir/cx/),MIR 再从 THIR 构建(compiler/rustc_mir_build/src/builder/mod.rs 的 build_mir_inner_impl)。
MIR 与 HIR 的关键区别
| 特性 | HIR | MIR |
|---|---|---|
| 结构 | 树形(嵌套表达式) | 控制流图(基本块 + 跳转) |
| 变量 | 有名称 | 匿名编号(_0, _1, ...) |
| 表达式 | 可嵌套 | 完全展平,中间结果存入临时变量 |
| 控制流 | if/match/loop |
switchInt/goto/return/drop |
| 返回值 | 隐式(表达式的值) | 显式变量 _0 |
| Drop | 隐式(作用域结束) | 显式 Drop 终结器 |
MIR 的设计使得许多分析变得简单。例如,借用检查需要知道每个引用在哪些代码路径上是"活跃的"——在控制流图上,这就是一个标准的数据流分析问题。
模式匹配的编译
MIR 构建阶段还负责将复杂的 match 模式编译为一系列条件判断和跳转,形成决策树。这个过程还需要检查模式的穷尽性——确保所有可能的值都被覆盖(这项检查在 THIR 上做,见 compiler/rustc_mir_build/src/thir/pattern/check_match.rs)。
1.9 借用检查与 MIR 优化
借用检查在 MIR 上执行
这是 Rust 编译器最关键的设计决策之一:借用检查在 MIR 上执行,而不是在 AST 或 HIR 上。
在编译器的实际代码中,借用检查通过 par_hir_body_owners 对每个函数体执行。MIR_borrow_checking 阶段对每个函数体(def_id)依次执行以下检查:
- unsafe 检查(
check_unsafety):验证 unsafe 块的正确使用 - 借用检查(
mir_borrowck):核心的 NLL 借用检查 - transmute 检查(
check_transmutes):验证类型转换的安全性 - FFI unwind 检查(
has_ffi_unwind_calls):检测跨 FFI 边界的 unwind - 活跃性分析(
check_liveness):检测未使用的变量 - MIR 后续变换(
mir_drops_elaborated_and_const_checked):Drop 展开和常量检查
其中第 1–3 项只对 typeck 根执行(闭包这类子函数体随父函数一起检查),第 6 项只在需要生成代码或函数体处于常量上下文时执行(compiler/rustc_interface/src/passes.rs 的 run_required_analyses)。
开启并行前端时,这些检查由 par_hir_body_owners 分派到多个线程上并行执行(何时真正并行,见 §1.14)。
借用检查使用 NLL(Non-Lexical Lifetimes) 算法。MIR 的控制流图结构让借用检查器能够精确地追踪每个引用的活跃区间——一个引用从创建到最后一次使用之间的所有代码路径,而不是简单的词法作用域。我们将在第 3 章深入拆解 NLL 算法的完整工作流程。
MIR 优化
借用检查通过后,rustc_mir_transform 在 MIR 上执行一系列优化 pass:
- 常量传播(Constant Propagation):将编译期可确定的表达式替换为常量
- 死代码消除(Dead Code Elimination):删除永远不会执行的基本块
- 内联(Inlining):将小函数的 MIR 内联到调用点
- 简化控制流(SimplifyCfg):合并只有一个前驱/后继的基本块
- 副本传播(CopyProp):消除不必要的变量复制
- 引用消除(ReferencePropagation):消除不必要的引用/解引用对
- Drop 展开(ElaborateDrops):将高层的
Drop转换为具体的析构序列
这些优化在 LLVM 优化之前执行,可以显著减少传递给 LLVM 的 IR 量,从而加速编译。我们将在第 15 章详细拆解每个优化 pass 的工作方式。
1.10 单态化收集
在生成代码之前,编译器需要确定哪些泛型函数的哪些具体实例需要被编译。这个过程叫做单态化收集(Monomorphization Collection),由 rustc_monomorphize crate 负责。
fn identity<T>(x: T) -> T { x }
fn main() {
identity(42_i32); // 需要生成 identity::<i32>
identity("hello"); // 需要生成 identity::<&str>
identity(3.14_f64); // 需要生成 identity::<f64>
}
收集器从入口点(main 函数或库的公开 API)出发,递归地扫描所有被调用的函数,记录每个泛型函数被实例化的具体类型参数。在编译器中,collect_and_partition_mono_items 函数先通过 collector::collect_crate_mono_items 收集所有需要生成代码的实例(有 Eager 和 Lazy 两种策略),然后通过 partition 函数将它们分配到不同的 Codegen Unit 中,同时验证所有符号名的唯一性(assert_symbols_are_distinct)。
收集完成后,单态化的实例被分配到不同的 Codegen Unit(CGU)中。每个 CGU 会独立地被翻译为一个 LLVM 模块,最终编译为一个目标文件。CGU 的数量由 codegen-units 选项控制(默认值取决于是否开启增量编译,见 §1.14),这直接影响编译的并行度。
1.11 代码生成:MIR → LLVM IR → 机器码
代码生成阶段将优化后的 MIR 翻译为 LLVM IR,然后由 LLVM 优化并生成目标平台的机器码。
代码生成的入口
start_codegen 函数是代码生成的入口。它首先编码 crate 的 metadata(供其他依赖此 crate 的 crate 使用),然后调用 codegen_backend.codegen_crate() 启动实际的代码生成过程。代码生成是异步的——ongoing_codegen 是一个后台任务句柄,链接阶段会等待它完成。
MIR → LLVM IR
每个单态化的函数实例被翻译为一个 LLVM IR 函数:
// 源码
fn add(a: i32, b: i32) -> i32 {
a + b
}
; LLVM IR(release 构建,省略了 noundef 等属性,哈希部分为示意)
define i32 @_ZN7example3add17h1234567890abcdefE(i32 %a, i32 %b) {
start:
%_0 = add i32 %a, %b
ret i32 %_0
}
debug 构建默认开启溢出检查,同一个函数会变成调用 llvm.sadd.with.overflow.i32,再根据溢出标志跳到 panic 分支。
注意函数名被 name mangling 了——example::add 变成了 _ZN7example3add17h...E,包含了 crate 名、模块路径和一个哈希值,确保全局唯一。这是 legacy mangling 方案,stable 编译器的默认值;nightly 默认改用 v0(_RNv...),也可以用 -C symbol-mangling-version=v0 显式指定(见 Options::get_symbol_mangling_version)。
LLVM 优化与机器码生成
LLVM 收到 IR 后,根据优化级别(-O0 到 -O3)执行不同强度的优化——从基本的常量折叠、死代码消除(-O1),到循环优化、激进内联、向量化(-O2),再到循环展开和更激进的向量化(-O3)。LLVM 的优化与 Rust MIR 层的优化是独立的——MIR 优化面向 Rust 特有语义,LLVM 优化则是通用的底层优化。
优化完成后,LLVM 经过指令选择、寄存器分配、指令调度等步骤,最终输出目标文件(.o / .obj)。
1.12 链接:拼装最终二进制
编译的最后一步是链接(Linking)。链接器将多个目标文件和库合并为最终的可执行文件或动态库。
在编译器中,链接由 Linker 结构体协调。它的 link 方法首先通过 codegen_backend.join_codegen() 等待所有 codegen unit 完成编译,然后调用 codegen_backend.link() 驱动系统链接器。
链接器需要处理的工作包括:
- 符号解析:将每个符号引用绑定到它的定义
- 重定位:调整代码和数据中的地址引用
- 合并段:将多个目标文件的
.text、.data、.bss等段合并 - 处理动态链接:生成 PLT/GOT 表项
- 生成最终格式:ELF(Linux)、Mach-O(macOS)、PE/COFF(Windows)
Rust 默认通过系统的 C 编译器驱动调用链接器(Linux 上的 ld,macOS 上的 ld64),可以通过 -C linker=... / -C linker-flavor=... 指定替代链接器。自 Rust 1.90.0 起,x86_64-unknown-linux-gnu 已经默认使用随工具链分发的 rust-lld(rust-lang/rust#140525),链接阶段比 GNU ld 快。
1.13 查询系统:按需驱动,而非流水线驱动
到目前为止,我们一直在用"管线"(pipeline)的隐喻来描述编译过程。但事实上,Rust 编译器内部并不是一条线性流水线。它使用的是一个基于查询的按需驱动系统(Demand-Driven Query System)。
什么是查询系统
传统编译器是流水线式的——先执行所有的解析,再执行所有的类型检查,再执行所有的代码生成。每个阶段必须完整处理完所有代码,才能进入下一阶段。
Rust 编译器的查询系统则不同。它将编译器的每个计算步骤定义为一个"查询"(Query),每个查询有一个输入(key)和一个输出(value)。查询之间形成依赖关系——当一个查询需要另一个查询的结果时,它会"拉取"(pull)那个查询的执行。
graph TB
A["codegen_crate<br/>(后端入口方法,非查询)"] --> B["collect_and_partition_mono_items"]
B --> C["optimized_mir(fn_def_id)"]
C --> D["mir_borrowck(fn_def_id)"]
D --> E["mir_built(fn_def_id)"]
E --> F["typeck_root(fn_def_id)"]
F --> G["hir_crate"]
G --> H["resolver_for_lowering_raw"]
H --> I["crate_for_resolver"]
C --> J["type_of(def_id)"]
F --> K["adt_def(struct_id)"]
F --> L["impl_trait_header(impl_id)"]
style A fill:#dc2626,color:#fff,stroke:none
style B fill:#f59e0b,color:#333,stroke:none
style C fill:#10b981,color:#333,stroke:none
style D fill:#10b981,color:#333,stroke:none
style E fill:#0ea5e9,color:#333,stroke:none
style F fill:#2563eb,color:#fff,stroke:none
style G fill:#4f46e5,color:#fff,stroke:none
style H fill:#6366f1,color:#fff,stroke:none
style I fill:#7c3aed,color:#fff,stroke:none
style J fill:#2563eb,color:#fff,stroke:none
style K fill:#2563eb,color:#fff,stroke:none
style L fill:#2563eb,color:#fff,stroke:none
图里除最顶上的 codegen_crate 外都是 compiler/rustc_middle/src/queries.rs 里的真实查询名。codegen_crate 是 CodegenBackend trait 的方法,由 start_codegen 直接调用,它本身不是查询,只是整条拉取链的起点。另外几个名字要注意:源码里随处可见的 tcx.typeck(def_id)、tcx.resolver_for_lowering()、tcx.impl_trait_ref(impl_id) 在 1.96.0 都是 TyCtxt 上的包装方法,背后的查询分别是 typeck_root(compiler/rustc_middle/src/queries.rs:1228;typeck 先把闭包等子函数体映射到它的 typeck 根,见 compiler/rustc_middle/src/query/plumbing.rs:212-216)、resolver_for_lowering_raw(compiler/rustc_middle/src/queries.rs:184)和 impl_trait_header(compiler/rustc_middle/src/queries.rs:1110)。
查询系统的注册
在 DEFAULT_QUERY_PROVIDERS 中,编译器注册了所有的查询提供者——它是一个 LazyLock<Providers>,整个初始化闭包就在 rust-1.96.0/compiler/rustc_interface/src/passes.rs:880-918。每个 crate 通过 provide 函数注册自己提供的查询——rustc_borrowck::provide(rust-1.96.0/compiler/rustc_borrowck/src/lib.rs:107)注册借用检查查询,rustc_hir_typeck::provide(rust-1.96.0/compiler/rustc_hir_typeck/src/lib.rs:663)注册类型检查查询,rustc_monomorphize::provide(rust-1.96.0/compiler/rustc_monomorphize/src/lib.rs:49)注册单态化查询,等等。这个闭包里一共 25 条 provide 调用外加十来条内联赋值;查询本身的清单在 rust-1.96.0/compiler/rustc_middle/src/queries.rs:138 起的 rustc_queries! 宏里,1.96.0 共 321 个 query 条目。
当运行时某个查询被首次请求时,对应的 provider 函数才会被调用。
查询缓存与增量编译
查询系统的另一个关键特性是缓存。每个查询的结果都被自动缓存——如果同一个查询被多次请求,只有第一次会实际计算,后续请求直接返回缓存的结果。
这个缓存机制是增量编译的基础。当源码发生变化时,编译器可以:
- 检测哪些查询的输入(依赖)发生了变化
- 只重新计算那些受影响的查询
- 复用所有未受影响的查询结果
例如,如果你只修改了一个函数的实现,那么:
- 该函数的
typeck_root、mir_borrowck、optimized_mir需要重新计算 - 其他函数的查询结果可以复用
- 如果该函数的签名没变,依赖它的其他函数可能也不需要重新检查
全局上下文 TyCtxt
所有查询都通过 TyCtxt(Type Context)访问。TyCtxt<'tcx> 是编译器最核心的数据结构,几乎所有编译阶段都需要它。它在 create_and_enter_global_ctxt 函数中被创建(rust-1.96.0/compiler/rustc_interface/src/passes.rs:921),所有编译工作都在其闭包内通过 tcx 驱动。
TyCtxt 使用了 Rust 的生命周期系统来保证安全性——'tcx 生命周期确保所有通过 TyCtxt 获取的数据都在编译器的 arena 中存活。这是一个精妙的设计:gcx_cell(一个 OnceLock::new())、arena、hir_arena(两个 WorkerLocal::new(..))都在同一个栈帧中创建、再以 &gcx_cell / &arena / &hir_arena 的形式传进 TyCtxt::create_global_ctxt,使得它们的引用共享同一个 'tcx 生命周期(rust-1.96.0/compiler/rustc_interface/src/passes.rs:985-995)。源码就在这里挂了一段注释点题:"this function body is the origin point of the widely-used 'tcx lifetime"(rust-1.96.0/compiler/rustc_interface/src/passes.rs:974)——如果 GlobalCtxt 改在 create_global_ctxt 内部创建,拿到的引用类型就会是带两个生命周期的 &'a GlobalCtxt<'tcx>,而 TyCtxt 需要的是 &'tcx GlobalCtxt<'tcx>。
1.14 并行编译
Rust 编译器在多个粒度上实现了并行化。
函数级并行
许多分析可以对不同的函数并行执行。编译器通过三个关键原语实现并行:
par_fns(&mut [&mut || { ... }, &mut || { ... }])—— 将多个独立的分析任务并行执行tcx.par_hir_body_owners(|def_id| { ... })—— 对所有函数体并行执行某个分析tcx.par_hir_for_each_module(|module| { ... })—— 对所有模块并行执行某个检查
这是一种任务级并行——不同的检查任务在不同的线程上同时运行。不过前端并行默认是关着的:只有用 nightly 的 -Z threads=N(N > 1)才会打开(compiler/rustc_interface/src/interface.rs:393 据此设置 set_dyn_thread_safe_mode);默认 threads = 1 时,这三个原语都退化为在当前线程上顺序执行。
Codegen Unit 级并行
代码生成阶段的并行化通过 Codegen Unit 实现。每个 CGU 被独立地翻译为 LLVM IR,然后独立地由 LLVM 优化和编译。这是编译中最耗时的部分,也是并行化收益最大的地方。
graph LR
subgraph "分析阶段(共享 TyCtxt)"
A["类型检查<br/>per-function<br/>(并行需 -Z threads)"] --> B["借用检查<br/>per-function<br/>(并行需 -Z threads)"]
end
subgraph "单态化"
B --> C["收集所有<br/>mono items"]
C --> D["分区为<br/>Codegen Units"]
end
subgraph "代码生成(并行 per-CGU)"
D --> E1["CGU 1<br/>MIR → LLVM IR"]
D --> E2["CGU 2<br/>MIR → LLVM IR"]
D --> E3["CGU 3<br/>MIR → LLVM IR"]
D --> E4["CGU N<br/>MIR → LLVM IR"]
end
subgraph "LLVM 后端(并行 per-CGU)"
E1 --> F1["LLVM 优化<br/>+ 目标文件 1"]
E2 --> F2["LLVM 优化<br/>+ 目标文件 2"]
E3 --> F3["LLVM 优化<br/>+ 目标文件 3"]
E4 --> F4["LLVM 优化<br/>+ 目标文件 N"]
end
subgraph "链接"
F1 --> G["链接器<br/>生成最终二进制"]
F2 --> G
F3 --> G
F4 --> G
end
style A fill:#2563eb,color:#fff,stroke:none
style B fill:#10b981,color:#333,stroke:none
style C fill:#f59e0b,color:#333,stroke:none
style D fill:#f59e0b,color:#333,stroke:none
style E1 fill:#dc2626,color:#fff,stroke:none
style E2 fill:#dc2626,color:#fff,stroke:none
style E3 fill:#dc2626,color:#fff,stroke:none
style E4 fill:#dc2626,color:#fff,stroke:none
style F1 fill:#dc2626,color:#fff,stroke:none
style F2 fill:#dc2626,color:#fff,stroke:none
style F3 fill:#dc2626,color:#fff,stroke:none
style F4 fill:#dc2626,color:#fff,stroke:none
style G fill:#9333ea,color:#fff,stroke:none
并行编译的限制
Rust 编译器的并行化仍在持续改进中。主要限制包括:查询之间的依赖(类型检查必须先于借用检查)、部分全局数据结构需要 FreezeLock 协调、链接器通常是单线程的。
-C codegen-units=N 控制 CGU 数量,影响代码生成并行度。但 CGU 越多,LLVM 跨 CGU 内联的机会越少。rustc 的默认值是 16;开启增量编译时改为 256(见 Session::codegen_units,compiler/rustc_session/src/session.rs)。cargo 的 release profile 沿用这个默认值;想换取最佳运行时性能,可以显式写 codegen-units = 1,或开启 fat LTO,把各 CGU 的模块合并成一个再做优化。
1.15 错误恢复:编译器如何在错误后继续
一个优秀的编译器不会在第一个错误处停下——它会尽量继续分析,以便一次编译就能报告尽可能多的错误。Rust 编译器在多个层面实现了错误恢复。
ErrorGuaranteed 类型
Rust 编译器使用 ErrorGuaranteed 类型来标记"已经报告了错误"的状态。这是一个零大小类型(ZST),它的存在本身就是证明——如果你持有一个 ErrorGuaranteed,就意味着已经有至少一个错误被报告给了用户。
许多编译器函数返回 Result<T, ErrorGuaranteed>。当遇到错误时,它们报告错误并返回 Err(guar),调用者可以选择传播错误或进行恢复。
分阶段的错误检查
编译器在关键阶段之间插入错误检查点。例如在 analysis 函数中,run_required_analyses 完成后会检查是否有非 lint 错误——如果有,立即停止,不再执行后续的 lint 检查等分析。
在进入代码生成之前,编译器会做最后的检查:has_errors_or_delayed_bugs()。这确保了代码生成阶段永远不会收到有错误的输入,避免了因为错误数据导致的编译器内部崩溃(ICE)。
"中毒"机制
当类型检查发现一个表达式有错误时,它会将该表达式的类型标记为"错误类型"(TyKind::Error)。这个错误类型有一个特殊属性——它会"传染"所有涉及它的后续计算。例如,如果 x 的类型是 Error,那么 x + 1 的类型也是 Error,不会产生额外的"无法对 Error 类型执行加法"的错误信息。这就是所谓的"中毒"(poisoning)机制,它防止了一个根本错误导致大量衍生错误。
1.16 跟踪一个真实程序走完编译器的每一站
让我们跟踪一个具体的程序,看它在编译器的每一站变成了什么样子:
fn main() {
println!("hello");
}
第1站:词法分析
rustc_lexer 将源码切分为 Token 流:
Keyword(Fn) Ident("main") OpenParen CloseParen OpenBrace
Ident("println") Bang OpenParen Literal(Str("hello")) CloseParen Semi
CloseBrace
注意 println! 被分为 Ident("println") 和 Bang 两个 Token——词法分析器不知道这是宏调用。
第2站:语法分析
rustc_parse 将 Token 流构建为 AST:
Crate {
items: [
FnItem {
name: "main",
params: [],
return_ty: None, // 推导为 ()
body: Block {
stmts: [
MacroCall {
path: "println",
args: TokenStream["hello"],
}
]
}
}
]
}
此时 println!("hello") 仍然是一个未展开的宏调用。
第3站:名称解析与宏展开
rustc_expand 将 println! 展开为实际的代码。按 library/std/src/macros.rs 里 println! 的定义,展开后的 AST 大致等价于:
fn main() {
{
::std::io::_print(format_args_nl!("hello"));
}
}
println!("hello") 变成了对 std::io::_print 的调用。内层的 format_args_nl! 是内建宏,它的展开结果不是普通的 Rust 代码,而是一个专门的 AST 节点 ExprKind::FormatArgs(compiler/rustc_builtin_macros/src/format.rs),记录着模板 "hello\n"。这个展开过程递归进行——如果展开后的代码中还有宏调用,会继续展开。
同时,rustc_resolve 将名称绑定到定义:std::io::_print → 标准库中的具体函数定义。
第4站:AST → HIR 降级
rustc_ast_lowering 将 AST 转换为 HIR。在这个例子中没有 for、? 这类语法糖,主要变化是:
- 每个节点获得唯一的
HirId FormatArgs节点在这里才被翻译成fmt::Arguments的构造:整个格式串是纯字面量、没有占位符时,走<core::fmt::Arguments>::from_str("hello\n")这条捷径;有占位符时才走Arguments::new(见compiler/rustc_ast_lowering/src/format.rs)- 省略的返回类型在 HIR 里仍记作
FnRetTy::DefaultReturn,要到类型检查时才确定为()
第5站:类型检查
rustc_hir_typeck 推导并检查所有类型:
main的签名:fn() -> ()<::core::fmt::Arguments>::from_str("hello\n")—— 参数类型&'static str,返回Arguments<'_>::std::io::_print(...)—— 参数类型Arguments<'_>,返回()- 整个块表达式类型
(),与main的返回类型一致
第6站:HIR → MIR 降级
rustc_mir_build 将 HIR 转换为控制流图。main 函数的 MIR 很短:bb0 调用 Arguments::from_str 构造 fmt::Arguments,bb1 调用 _print,bb2 返回。函数调用是终结器,写成 -> [return: bb1, unwind continue]:正常返回跳到下一个基本块;这里没有需要 drop 的值,所以没有单独的 unwind 清理块,展开时直接继续向上传播。你可以用 cargo +nightly rustc -- -Zunpretty=mir 亲眼看到完整的 MIR,stable 上也可以用 cargo rustc -- --emit=mir(输出到 target/debug/deps/*.mir)。
第7站:借用检查与 MIR 优化
借用检查器分析 MIR 的控制流图。在这个简单例子中,没有借用冲突。
MIR 优化器随后简化代码——可能内联 Arguments::from_str,消除临时变量等。
第8站:单态化收集
收集器从 main 出发,确定需要生成代码的所有函数实例:
mainstd::rt::lang_start::<()>及其内部闭包——编译器为可执行文件生成的入口包装,它是泛型函数,要在本 crate 实例化core::fmt::Arguments::from_str(library/core/src/fmt/mod.rs:815)——标了#[inline],所以在本 crate 里生成一份副本- 若干配套小函数,如
FnOnce::call_once的适配函数、<() as Termination>::report
std::io::_print 不在其中:它既非泛型也没有 #[inline],已经在标准库里编译好,本 crate 的 LLVM IR 里只有一条对它的 declare,链接时再解析。
第9站:LLVM IR 生成
每个函数实例被翻译为 LLVM IR。LLVM 执行优化后生成目标平台的机器码。
第10站:链接
链接器将 main.o 和标准库链接在一起,生成最终的可执行文件。执行它:
$ ./target/debug/example
hello
1.17 动手验证:查看每个阶段的输出
你不需要相信本专栏的任何结论——你可以自己看。以下是查看每个阶段输出的命令:
# 查看宏展开后的代码(接近 AST 的文本表示)
cargo +nightly rustc -- -Zunpretty=expanded
# 查看 HIR
cargo +nightly rustc -- -Zunpretty=hir
# 查看 HIR(带类型标注)
cargo +nightly rustc -- -Zunpretty=hir,typed
# 查看 MIR(打印的是 optimized_mir,即跑完所有 pass 之后的 MIR)
cargo +nightly rustc -- -Zunpretty=mir
# 查看 MIR 的控制流图(Graphviz dot 格式,用 dot -Tsvg 渲染)
cargo +nightly rustc -- -Zunpretty=mir-cfg
# 想看某个 pass 前后的 MIR(含借用检查之前的版本),用 -Zdump-mir
cargo +nightly rustc -- -Zdump-mir=main
# 查看 LLVM IR(未优化)
cargo rustc -- --emit=llvm-ir
# 查看 LLVM IR(优化后,release 模式)
cargo rustc --release -- --emit=llvm-ir
# 查看最终的汇编
cargo rustc --release -- --emit=asm
# 查看类型的内存布局
cargo +nightly rustc -- -Zprint-type-sizes
# 查看编译时间的详细分解
cargo +nightly rustc -- -Ztime-passes
# 查看查询系统的依赖图
cargo +nightly rustc -- -Zdump-dep-graph
建议现在就在你的项目上试一试。写一个简单的函数,然后用这些命令看看编译器在每个阶段的产出。
特别推荐:写一个 for 循环用 -Zunpretty=hir 看脱糖结果;写一个泛型函数并用两种类型调用,用 --emit=llvm-ir 看单态化结果;写一个 async fn 用 -Zunpretty=hir 看它脱糖成的 async 块(真正的状态机要到 MIR 的 StateTransform 才生成,见第 9 章)。
1.18 实测:rustc 编译管线各阶段的工程量地图
§1.19 全景地图把编译管线分前端 / 中端 / 后端——把 §1.3-1.12 各阶段对应到 compiler/ 子 crate 实测——
前端 Frontend——
| 阶段 | crate | 行 | 章节实测出处 |
|---|---|---|---|
| §1.3 词法分析 | rustc_lexer |
1650 | — |
| §1.4 语法分析 | rustc_parse |
31859 | — |
| §1.5 名称解析 | rustc_resolve |
27663 | — |
| §1.5 宏展开 | rustc_expand |
13581 | ch14 §14.10.3 |
| §1.6 AST→HIR | rustc_ast_lowering |
12599 | — |
| §1.7 类型检查 | rustc_hir_typeck |
47604 | ch11 §11.14 含 expr_use_visitor |
| §1.7 trait 求解 | rustc_trait_selection |
47295 | ch08 §8.13 含 dyn_compatibility / vtable |
中端 Middle-end——
| 阶段 | crate | 行 | 章节实测出处 |
|---|---|---|---|
| §1.8 HIR→MIR | rustc_mir_build |
22451 | ch15 §15.13.5 |
| §1.9 借用检查 | rustc_borrowck |
34458 | ch04 §4.12 |
| §1.9 MIR 优化 | rustc_mir_transform |
34654 | ch15 §15.13.5 |
| 数据流分析共享 | rustc_mir_dataflow |
7241 | ch15 §15.13.5 |
后端 Backend——
| 阶段 | crate | 行 | 章节实测出处 |
|---|---|---|---|
| §1.10 单态化 | rustc_monomorphize |
4118 | ch06 §6.11 |
| §1.11 LLVM codegen | rustc_codegen_llvm |
29116 | ch16 §16.15 |
| §1.11 codegen 共享 SSA | rustc_codegen_ssa |
27042 | ch16 §16.15(含 rust-1.96.0/compiler/rustc_codegen_ssa/src/back/link.rs 3624 行) |
| §1.11 Cranelift 后端 | rustc_codegen_cranelift |
22148 | ch16 §16.15 |
横切基础设施——
| 子系统 | crate | 行 | 章节实测出处 |
|---|---|---|---|
| §1.13 查询系统 | rustc_query_impl |
2552 | ch17 §17.12.2 |
| §1.13 增量编译 | rustc_incremental |
2828 | ch17 §17.12.2 |
| 会话 / 配置 | rustc_session |
10939 | — |
| 驱动入口 | rustc_driver_impl |
2652 | — |
本节所列阶段实测合计 382,450 行(统计口径:find rust-1.96.0/compiler/<crate> -name '*.rs' | xargs cat | wc -l,rust 仓库 1.96.0 tag;上面每一行的数字都是这条命令在对应 rust-1.96.0/compiler/<crate>/ 目录上的直接输出)——
前端 Frontend: 1650+31859+27663+13581+12599+47604+47295 = 182,251 (48%)
中端 Middle-end: 22451+34458+34654+7241 = 98,804 (26%)
后端 Backend: 4118+29116+27042+22148 = 82,424 (22%)
横切基础设施: 2552+2828+10939+2652 = 18,971 (5%)
————————————————————————————————————————————————————————
本节实测合计: 382,450
括号里的百分比是各组占本节 382,450 行的比重。作为分母参照:rust-1.96.0/compiler/ 目录下全部 .rs 共 827,651 行——本节挑出的这 19 个 crate 占了其中约 46%,其余是 rustc_middle、rustc_ast、rustc_hir、rustc_target、rustc_metadata、rustc_codegen_gcc 等未单列的部分。
两条值得记住的物理事实——
- 前端 182,251 行 = 48% 是最重的部分——其中
rustc_hir_typeck47,604 +rustc_trait_selection47,295 = 94,899 行 占前端 52%——印证 §1.7 所说"类型检查"是 rustc 中最复杂的子系统之一;和 ch04 §4.12 测得的借用检查 34,458 行加起来约 12.9 万行——说明 Rust 的"类型 + 所有权双重检查" 是工程主体 - 后端 82,424 行(含单态化与三个 codegen crate)——不到前端的一半——印证 §1.19 全景地图的划分:Rust 的复杂度集中在前中端,后端主要是把已经验证过的 MIR 翻译给 LLVM——重活由 LLVM 自己那份不在本表里的代码承担
串联本专栏 ch04-ch17 各章实测——本节给出的是按整 crate 统计的俯视图,各章是只统计自选文件的剖面图;两种口径不同,各章的数字不能与本节相加,也不能拿来算占比。
1.19 全景地图:本专栏的导航
把所有阶段和 Rust 的核心语言特性对应起来,就得到了本专栏的全景地图:
graph TB
subgraph "前端 Frontend"
A["源码"] --> B["Token → AST"]
B --> C["名称解析 + 宏展开"]
C --> D["AST → HIR"]
D --> E["类型检查"]
end
subgraph "中端 Middle-end"
E --> F["HIR → MIR"]
F --> G["借用检查 + MIR 优化"]
end
subgraph "后端 Backend"
G --> H["单态化收集"]
H --> I["MIR → LLVM IR"]
I --> J["LLVM 优化 → 机器码"]
J --> K["链接"]
end
M1["宏系统<br/>第14章"] -.-> C
M2["类型系统 · Trait 解析<br/>第6-8章"] -.-> E
M3["所有权 · 借用检查 · NLL<br/>第2-5章"] -.-> G
M4["MIR 优化 Pass<br/>第15章"] -.-> G
M5["Async/Await 状态机<br/>第9-10章"] -.-> F
M6["泛型与单态化<br/>第6章"] -.-> H
M7["LLVM 代码生成<br/>第16章"] -.-> I
M8["增量编译 · 查询系统<br/>第17章"] -.-> K
style A fill:#64748b,color:#fff,stroke:none
style B fill:#7c3aed,color:#fff,stroke:none
style C fill:#6366f1,color:#fff,stroke:none
style D fill:#4f46e5,color:#fff,stroke:none
style E fill:#2563eb,color:#fff,stroke:none
style F fill:#0ea5e9,color:#333,stroke:none
style G fill:#10b981,color:#333,stroke:none
style H fill:#f59e0b,color:#333,stroke:none
style I fill:#dc2626,color:#fff,stroke:none
style J fill:#dc2626,color:#fff,stroke:none
style K fill:#9333ea,color:#fff,stroke:none
从下一章开始,我们将沿着这张地图,逐个区域深入。先从 Rust 最核心的创新开始——所有权系统在编译器中的实现。
本章回顾
本章建立了 Rust 编译器的全景地图:
- 词法分析(
rustc_lexer)将源码切分为 Token,零依赖的纯函数设计 - 语法分析(
rustc_parse)构建 AST,手写递归下降,支持错误恢复 - 名称解析与宏展开(
rustc_resolve+rustc_expand)交替进行,消除所有宏调用 - AST → HIR 降级(
rustc_ast_lowering)脱糖,消除语法糖 - 类型检查(
rustc_hir_typeck+rustc_hir_analysis)Hindley-Milner 风格推导,trait 解析 - HIR → MIR 降级(
rustc_mir_build)构建控制流图 - 借用检查(
rustc_borrowck)在 MIR 上执行 NLL 算法 - MIR 优化(
rustc_mir_transform)常量传播、内联、死代码消除等 - 单态化收集(
rustc_monomorphize)确定所有需要生成代码的泛型实例,分配到 Codegen Unit - 代码生成(
rustc_codegen_ssa+rustc_codegen_llvm)MIR → LLVM IR → 机器码 - 链接:系统链接器拼装最终二进制
整个系统由查询系统驱动,支持增量编译和并行执行。TyCtxt 是贯穿所有阶段的核心数据结构。