Lint 的执行流程
前面两篇介绍了 Lint
,LintPass
和 CombinedLintPass
几个结构的实现,并以这些结构写了一个 Lint 的伪代码实现。
1 | impl ast_visit::Visitor for Linter { |
Rustc 中 Lint 的执行阶段
Rustc 的设计与经典编译器的设计基本无异,包含词法分析、语法分析、语义分析、生成IR、IR优化和代码生成等流程,但针对 Rust 的语言特性,还加入了一些特有的流程,如借用检查。对应的,代码在整个编译流程中的中间表示也有一定的扩展:
- Token stream:Lexer 将源代码的字符流转化为词法单元(token) 流,这些词法单元被传递给下一个步骤,即语法分析。
- Abstract Syntax Tree(AST):Parser 将 Token 流转换为抽象语法树(AST),抽象语法树几乎可以完全描述源代码中所写的内容。在 AST 上,Rustc 还执行了宏扩展、 early lint 等过程。
- High-level IR(HIR):这是一种脱糖的 AST。它仍与源代码中的内容非常接近,但它包含一些隐含的东西,例如一些省略的生命周期等。这个 IR 适合类型检查。late lint也在类型检查之后进行。
- Typed HIR(THIR):THIR 与 HIR 类似,但它携带了类型信息,并且更加脱糖(例如,函数调用和隐式的间接引用都会变成完全显式)。
- Middle-level IR(MIR):MIR 基本上是一个控制流图(Control-Flow Graph)。CFG 是程序执行过程的抽象表现,代表了程序执行过程中会遍历到的所有路径。它用图的形式表示一个过程内所有基本块可能流向。Rustc 在 MIR 上除了基础的基于 CFG 的静态分析和 IR 优化外,还进行了 Rust 中所有权的借用检查。
- LLVM IR:Rustc 的后端采用了 LLVM,因此,Rustc 会将 MIR 进一步转化为 LLVM IR 并传递给 LLVM 做进一步优化和代码生成的工作。
以上 Rust 代码的中间表示的转化流程也反映了 Rust 整个编译的流程,总结为一张图:
Rustc 中的 rustc_driver::lib.rs
中控制了编译流程的各个阶段:
1 | fn run_compiler(...) -> interface::Result<()> { |
前面介绍过,Rustc 中的 Lint 包含 early 和 late 两种,它们分别在 AST -> HIR 和 HIR -> THIR 两个阶段执行。这里我们同样以 WhileTrue
这个例子去看 Lint 从定义、到注册,最后执行的完整的流程。同时,WhileTrue
是 builtin 的 early lint 其中的一种,被包含在 BuiltinCombinedEarlyLintPass
之中。
定义
首先是 WhileTrue
的 lint 和对应的 lintpass 的定义,它们被定义在 rustc_lint/src/builtin.rs
中
1 | declare_lint! { |
与前面的介绍一样:
declare_lint
宏声明一个 lint:WHILE_TRUE
declare_lint_pass
宏声明一个lintpass:WhileTrue
- 为
WhileTrue
实现EarlyLintPass
中对应的检查方法,因为此 lintpass 只检查 Expr 节点,所以只需要实现check_expr()
函数即可。
注册
注册是指编译过程中将 Lint 加入到 LintStore 的过程。WhileTrue
不需要单独的注册和执行,它的检查方法通过宏扩展的方式展开到 BuiltinCombinedEarlyLintPass
中。BuiltinCombinedEarlyLintPass
的注册和执行都发生在 queries.expansion()
函数中。
1 | pub fn expansion( |
注册的过程会生成定义的 lint 的结构并添加到 LintStore 中。Lint 整体上被分为4个种类:pre-expansion, early, late, late-module。尽管 Lint 对应的 LintPass 在编译流程中执行的阶段不同,但注册都是发生在同一个阶段。
Lint 注册过程的函数调用链路如下:
- rustc_driver::lib::run_compiler()
- rustc_interface::queries::Queries.expansion()
- rustc_interface::queries::Queries.register_plugins()
- rustc_lint::lib::new_lint_store()
- rustc_lint::lib::register_builtins()
在这里,默认的编译流程会执行 else{} 分支中的语句,BuiltinCombinedEarlyLintPass::get_lints() 会生成 WHILE_TRUE
并添加到 LintStore中。
1 | if no_interleave_lints { |
执行
不同的 LintPass 的执行过程发生在编译过程的不同阶段,其中,BuiltinCombinedEarlyLintPass
执行过程的函数调用链路如下:
- rustc_driver::lib::run_compiler()
- rustc_interface::queries::Queries.expansion()
- rustc_interface::passes::configure_and_expand()
- rustc_lint::early::check_ast_node()
- rustc_lint::early::early_lint_node()
首先,在 configure_and_expand() 函数中,执行了 pre-expansion 和 early 两种 lintpass。注册时使用了 BuiltinCombinedEarlyLintPass::get_lints() 方法生成 lints,而这里用 BuiltinCombinedEarlyLintPass::new() 方法生成了 lintpass。
1 | pub fn configure_and_expand( |
Lint 的执行最终发生在 rustc_lint::early::early_lint_node()
函数中。比较 early_lint_node()
函数和 CombinedLintPass
一节最后的伪代码:
它们之间有以下的对应关系:
- 参数 pass 是 configure_and_expand() 函数中新建的 BuiltinCombinedEarlyLintPass,它对应 combinedlintpass。
- EarlyContextAndPass 将 pass 与 context 信息组合在一起,并且实现了 visitor,它对应 Linter。
- check_node.check(cx) 调用了 cx.pass.check_crate() 进行 lint 检查,根据 BuiltinCombinedEarlyLintPass 的定义, 这个函数中会调用所有 builtin early lint 的 check_crate() 方法,然后执行 ast_visit::walk_crate() 遍历子节点,它对应了 visit_crate()。
no_interleave_lints
虽然 Rustc 中考虑性能因素,将 LintPass 组合成 CombinedLintPass,但提供了一些编译参数去配置 Lint。其中,Lint 的注册和执行过程中都用到了 no_interleave_lints 参数。这个参数默认为 false,表示是否单独执行每一个 lint。编译时将这个修改这个参数就可以单独注册每一个 lint 以及单独执行 lintpass,这样的设计提供了更好的灵活性和自定义的能力(比如,可以对每一个 lint 单独做 benchmark)。
1 | if no_interleave_lints { |
1 | pub fn check_ast_node<'a>(...) { |
总结
至此,我们就分析了 Rustc 中一个 Lint 定义、实现对应的检查(LintPass)、注册、最终执行的完整流程。我们也可以利用这些宏,去定义新的Lint和LintPass(Clippy 中也是以相似的方式)。