Rust 编译器与运行时揭秘

第1章 编译管线全景:从源码到机器码的完整旅程

作者 杨艺韬 · 10,920 字 · 发布于 · 更新于

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。几个关键细节:

  1. StripTokens::ShebangAndFrontmatter —— 解析器自动去除 shebang 行和 frontmatter
  2. parse_crate_mod() 是顶层解析入口,产出完整的 crate AST
  3. 命令行注入的 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 函数驱动整个过程。这个函数是编译器前端最复杂的入口之一,它协调了宏展开和名称解析的交替执行。关键流程:

  1. 注册内建宏:rustc_builtin_macros::register_builtin_macros(resolver) 注册 println!、vec! 等内建宏
  2. 标准库注入:rustc_builtin_macros::standard_library_imports::inject 自动添加 extern crate std; 和按 edition 选择的 prelude(2024 edition 是 use std::prelude::rust_2024::*;;#![no_std] 下换成 core,#![no_core] 则什么都不注入)
  3. 宏展开:ecx.monotonic_expander().expand_crate(krate) 递归展开所有宏调用,有递归深度限制(recursion_limit)
  4. AST 验证:rustc_ast_passes::ast_validation::check_crate 检查展开后的 AST 是否满足语法约束
  5. 名称解析: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);

编译器的推导过程是:

  1. 看到 HashMap::new(),知道返回 HashMap<K, V>,但 K 和 V 尚未确定,创建类型变量 ?K 和 ?V
  2. 看到 map.insert("key", 42),推断 ?K = &str,?V = i32(整数字面量默认推导为 i32)
  3. 回溯统一:map 的类型最终确定为 HashMap<&str, i32>

这个过程使用统一算法(Unification)——当两个类型必须相同时,尝试找到一组类型变量的赋值使它们一致。如果找不到,就产生类型错误。

Trait 解析

当遇到 a + b 时,编译器需要确定调用的是哪个 Add trait 的实现。这个过程叫做 trait resolution,它需要:

  1. 以 a 的类型作 Self、b 的类型作泛型参数,构造一条 Add 约束
  2. 在 Add 的所有 impl 中选择匹配的实现
  3. 运算符不走自动解引用——不会因为 a 的类型没有实现就去试解引用后的类型(compiler/rustc_hir_typeck/src/method/mod.rs 的 lookup_method_for_operator 只构造这一条约束);&i32 + i32 能通过,是因为标准库直接为引用类型写了 Add 实现
  4. 处理 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)依次执行以下检查:

  1. unsafe 检查(check_unsafety):验证 unsafe 块的正确使用
  2. 借用检查(mir_borrowck):核心的 NLL 借用检查
  3. transmute 检查(check_transmutes):验证类型转换的安全性
  4. FFI unwind 检查(has_ffi_unwind_calls):检测跨 FFI 边界的 unwind
  5. 活跃性分析(check_liveness):检测未使用的变量
  6. 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 函数才会被调用。

查询缓存与增量编译

查询系统的另一个关键特性是缓存。每个查询的结果都被自动缓存——如果同一个查询被多次请求,只有第一次会实际计算,后续请求直接返回缓存的结果。

这个缓存机制是增量编译的基础。当源码发生变化时,编译器可以:

  1. 检测哪些查询的输入(依赖)发生了变化
  2. 只重新计算那些受影响的查询
  3. 复用所有未受影响的查询结果

例如,如果你只修改了一个函数的实现,那么:

  • 该函数的 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 出发,确定需要生成代码的所有函数实例:

  • main
  • std::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 等未单列的部分。

两条值得记住的物理事实——

  1. 前端 182,251 行 = 48% 是最重的部分——其中 rustc_hir_typeck 47,604 + rustc_trait_selection 47,295 = 94,899 行 占前端 52%——印证 §1.7 所说"类型检查"是 rustc 中最复杂的子系统之一;和 ch04 §4.12 测得的借用检查 34,458 行加起来约 12.9 万行——说明 Rust 的"类型 + 所有权双重检查" 是工程主体
  2. 后端 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 编译器的全景地图:

  1. 词法分析(rustc_lexer)将源码切分为 Token,零依赖的纯函数设计
  2. 语法分析(rustc_parse)构建 AST,手写递归下降,支持错误恢复
  3. 名称解析与宏展开(rustc_resolve + rustc_expand)交替进行,消除所有宏调用
  4. AST → HIR 降级(rustc_ast_lowering)脱糖,消除语法糖
  5. 类型检查(rustc_hir_typeck + rustc_hir_analysis)Hindley-Milner 风格推导,trait 解析
  6. HIR → MIR 降级(rustc_mir_build)构建控制流图
  7. 借用检查(rustc_borrowck)在 MIR 上执行 NLL 算法
  8. MIR 优化(rustc_mir_transform)常量传播、内联、死代码消除等
  9. 单态化收集(rustc_monomorphize)确定所有需要生成代码的泛型实例,分配到 Codegen Unit
  10. 代码生成(rustc_codegen_ssa + rustc_codegen_llvm)MIR → LLVM IR → 机器码
  11. 链接:系统链接器拼装最终二进制

整个系统由查询系统驱动,支持增量编译和并行执行。TyCtxt 是贯穿所有阶段的核心数据结构。