Rust源码学习 - Lint 执行流程

简介: - 时间:2022.9.19- 撰稿:[张正](https://github.com/He1pa)@[KusionStack开发组](https://github.com/KusionStack/kusion)- 收录于:[rust-code-book](https://github.com/awesome-kusion/rust-code-book)- 源码学习系列 + [Lint 与

前面两篇介绍了 LintLintPassCombinedLintPass 几个结构的实现,并以这些结构写了一个 Lint 的伪代码实现。

impl ast_visit::Visitor for Linter {
    fn visit_crate(a: ast:crate){
        combinedlintpass.check_crate(a);
        walk_crate(a);
    }
    fn visit_stmt(a: ast:stmt){
        combinedlintpass.check_stmt(a)
        walk_stmt(a);
    }
    ...
}

let linter = Linter::new();

for c in crates{
    linter.visit_crate(c);
}

本文继续介绍 Rustc 中 Lint 的实现,包含 Lint 在编译流程中执行的流程,上述伪代码在 Rustc 中的对应实现,以及编译过程中的一个特殊参数 no_interleave_lints

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 中控制了编译流程的各个阶段:

fn run_compiler(...) -> interface::Result<()> {
    ...
    interface::run_compiler(config, |compiler| {
        let linker = compiler.enter(|queries| {
            ...
            queries.parse()?;   // lexer parse
            ...
            queries.expansion()?; // resolver
            ...
            queries.prepare_outputs()?;
            ...
            queries.global_ctxt()?; // ast -> hir
            ...
            queries.ongoing_codegen()?;
            ...
            }
}

前面介绍过,Rustc 中的 Lint 包含 early 和 late 两种,它们分别在 AST -> HIR 和 HIR -> THIR 两个阶段执行。这里我们同样以 WhileTrue 这个例子去看 Lint 从定义、到注册,最后执行的完整的流程。同时,WhileTrue 是 builtin 的 early lint 其中的一种,被包含在 BuiltinCombinedEarlyLintPass 之中。

定义

首先是 WhileTrue的 lint 和对应的 lintpass 的定义,它们被定义在 rustc_lint/src/builtin.rs

declare_lint! {
    /// The `while_true` lint detects `while true { }`.
    ///
    /// ### Example
    ///
    /// ```rust,no_run
    /// while true {
    ///
    /// }
    /// ```
    ///
    /// {{produces}}
    ///
    /// ### Explanation
    ///
    /// `while true` should be replaced with `loop`. A `loop` expression is
    /// the preferred way to write an infinite loop because it more directly
    /// expresses the intent of the loop.
    WHILE_TRUE,
    Warn,
    "suggest using `loop { }` instead of `while true { }`"
}

declare_lint_pass!(WhileTrue => [WHILE_TRUE]);

impl EarlyLintPass for WhileTrue {
    fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &ast::Expr) {
      ...
    }
}

与前面的介绍一样:

  1. declare_lint 宏声明一个 lint:WHILE_TRUE
  2. declare_lint_pass 宏声明一个lintpass:WhileTrue
  3. WhileTrue 实现 EarlyLintPass 中对应的检查方法,因为此 lintpass 只检查 Expr 节点,所以只需要实现 check_expr()函数即可。

注册

注册是指编译过程中将 Lint 加入到 LintStore 的过程。WhileTrue 不需要单独的注册和执行,它的检查方法通过宏扩展的方式展开到 BuiltinCombinedEarlyLintPass 中。BuiltinCombinedEarlyLintPass 的注册和执行都发生在 queries.expansion() 函数中。

pub fn expansion(
    &self,
) -> Result<&Query<(Rc<ast::Crate>, Rc<RefCell<BoxedResolver>>, Lrc<LintStore>)>> {
    tracing::trace!("expansion");
    self.expansion.compute(|| {
        let crate_name = self.crate_name()?.peek().clone();
        // 注册
        let (krate, lint_store) = self.register_plugins()?.take(); 
        let _timer = self.session().timer("configure_and_expand");
        let sess = self.session();
        let mut resolver = passes::create_resolver(
            sess.clone(),
            self.codegen_backend().metadata_loader(),
            &krate,
            &crate_name,
        );
        let krate = resolver.access(|resolver| {
            // 执行
            passes::configure_and_expand(sess, &lint_store, krate, &crate_name, resolver)
        })?;
        Ok((Rc::new(krate), Rc::new(RefCell::new(resolver)), lint_store))
    })
}

注册的过程会生成定义的 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中。

if no_interleave_lints {
    pre_expansion_lint_passes!(register_passes, register_pre_expansion_pass);
    early_lint_passes!(register_passes, register_early_pass);
    late_lint_passes!(register_passes, register_late_pass);
    late_lint_mod_passes!(register_passes, register_late_mod_pass);
} else {
    store.register_lints(&BuiltinCombinedPreExpansionLintPass::get_lints());
    store.register_lints(&BuiltinCombinedEarlyLintPass::get_lints());
    store.register_lints(&BuiltinCombinedModuleLateLintPass::get_lints());
    store.register_lints(&BuiltinCombinedLateLintPass::get_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。

pub fn configure_and_expand(
    sess: &Session,
    lint_store: &LintStore,
    mut krate: ast::Crate,
    crate_name: &str,
    resolver: &mut Resolver<'_>,
) -> Result<ast::Crate> {
    pre_expansion_lint(sess, lint_store, resolver.registered_tools(), &krate, crate_name);
    ...
    sess.time("early_lint_checks", || {
        let lint_buffer = Some(std::mem::take(resolver.lint_buffer()));
        rustc_lint::check_ast_node(
            sess,
            false,
            lint_store,
            resolver.registered_tools(),
            lint_buffer,
            rustc_lint::BuiltinCombinedEarlyLintPass::new(),
            &krate,
        )
    });
}

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)。

if no_interleave_lints {
    pre_expansion_lint_passes!(register_passes, register_pre_expansion_pass);
    early_lint_passes!(register_passes, register_early_pass);
    late_lint_passes!(register_passes, register_late_pass);
    late_lint_mod_passes!(register_passes, register_late_mod_pass);
} else {
    store.register_lints(&BuiltinCombinedPreExpansionLintPass::get_lints());
    store.register_lints(&BuiltinCombinedEarlyLintPass::get_lints());
    store.register_lints(&BuiltinCombinedModuleLateLintPass::get_lints());
    store.register_lints(&BuiltinCombinedLateLintPass::get_lints());
}
pub fn check_ast_node<'a>(...) {
    if sess.opts.debugging_opts.no_interleave_lints {
        for (i, pass) in passes.iter_mut().enumerate() {
            buffered =
                sess.prof.extra_verbose_generic_activity("run_lint", pass.name()).run(|| {
                    early_lint_node(
                        sess,
                        !pre_expansion && i == 0,
                        lint_store,
                        registered_tools,
                        buffered,
                        EarlyLintPassObjects { lints: slice::from_mut(pass) },
                        check_node,
                    )
                });
        }
    } else {
        buffered = early_lint_node(
            sess,
            !pre_expansion,
            lint_store,
            registered_tools,
            buffered,
            builtin_lints,
            check_node,
        );
        ...
    }
}

总结

至此,我们就分析了 Rustc 中一个 Lint 定义、实现对应的检查(LintPass)、注册、最终执行的完整流程。我们也可以利用这些宏,去定义新的Lint和LintPass(Clippy 中也是以相似的方式)。当然,Rustc 中关于 Lint 的部分远远不止这些,我只是分享了其中我能理解的一小部分,希望能够对大家有所帮助。
除此之外,我们在 KCLVM 这个项目中,也有对这部分内容的应用与实践,可以在这个 IssuePR 看到更为详细的设计方案和具体实现,包含了visitor模式,lint、lintpass、combinedlintpass的定义,在resolver阶段调用lint检查等实现,欢迎批评指正。

Ref

相关文章
|
2月前
|
存储 Rust 网络协议
【Rust学习】10_定义枚举
在这一章我们学习 枚举(enumerations),也被称作 enums。枚举允许你通过列举可能的 成员(variants) 来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 Option,它代表一个值要么是某个值要么什么都不是。然后会讲到在 match 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后,我们将学习 if let 结构,另一个简洁方便处理代码中枚举的结构。
45 7
|
3月前
|
Rust 算法 安全
学习Rust
【10月更文挑战第13天】学习Rust
64 8
|
3月前
|
Rust 安全 算法
Rust的学习
【10月更文挑战第12天】Rust的学习
31 2
|
3月前
|
Rust 算法 安全
如何学习Rust编程?
【10月更文挑战第12天】如何学习Rust编程?
63 1
|
4月前
|
Rust 索引
【Rust学习】08_使用结构体代码示例
为了了解我们何时可能想要使用结构体,让我们编写一个计算长方形面积的程序。我们将从使用单个变量开始,然后重构程序,直到我们改用结构体。
106 2
|
3月前
|
Rust API
【Rust学习】09_方法语法
结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,允许您指定结构体的实例具有的行为。 但是结构体并不是创建自定义类型的唯一方式:让我们转向 Rust 的 enum 功能,将另一个工具添加到你的工具箱中。
24 0
|
4月前
|
存储 Rust 编译器
【Rust学习】07_结构体说明
**struct**或 ***structure***是一种自定义数据类型,允许您命名和包装多个相关的值,从而形成一个有意义的组合。如果您熟悉面向对象的语言,那么**struct**就像对象中的数据属性。在本章中,我们将比较和对比元组与结构体,在您已经知道的基础上,来演示结构体是对数据进行分组的更好方法。
36 1
|
5月前
|
存储 Rust 安全
【Rust学习】06_切片
所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
32 1
|
4月前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习
|
6月前
|
存储 Rust 安全
【Rust学习】04_所有权
所有权是 Rust 最独特的特性,对语言的其余部分有着深远的影响。它使 Rust 能够在不需要垃圾收集器的情况下保证内存安全,因此了解所有权的运作方式非常重要。在本章中,我们将讨论所有权以及几个相关功能:借用、切片以及 Rust 如何在内存中布局数据。
36 1