名称解析

在上一个章节,我们看到了如何在展开所有宏的情况下构建 AST。这个过程需要用名称解析(name resolution)来解析导入和宏的名字。在本章中,我们将展示这是如何实现的。

事实上,我们在展开宏的过程中并不做完全的名称解析 -- 我们只解析导入和宏在这个过程中。这要求知道什么是需要展开。在这之后,在获得整个 AST 之后,我们将执行全名解析来解析 crate 中的所有名称。这发生在 rustc_resolve::late 中。与宏展开不同,在这个后期展开中,我们只需要尝试解析一个名称一次,因为没有新增的名字,如果失败了,那么将抛出一个编译错误。

名称解析可能很复杂。这里有几个不同的命名空间(例如,宏,值,类型,生命周期)和名称可能在不同(嵌套的)范围。此外,针对不同类型的名称解析,其解析失败的原因也可能会不同。例如,在一个模块的作用域内,模块中存在尚未展开的宏和未解析的 glob 导入会导致解析失败。另一方面,在函数内,在我们所在的 block 中,外部作用域和全局作用域中都没有该名称会导致解析失败。

基础

在我们的程序中,我们可以通过给一个变量名,来引用 变量,类型,函数等等。这些名字并不总是唯一的。例如,下面是一个有效 Rust 程序:


#![allow(unused)]
fn main() {
type x = u32;
let x: x = 1;
let y: x = 2;
}

我们是如何知道 第三行 x 是一个类型 (u32) 还是 数值 (1)。这个冲突将在名称解析中被解析。在这个特殊栗子中,名称解析定义 类型名称(type names) 和 变量名称(variable names) 在独立的命名空间,所以他们可以共存。

Rust 中的名称解析分为两个阶段。在第一阶段,将运行宏展开,我们将构建一个模块的书结构和解析导入。宏展开和名字解析通过ResolverAstLowering 特性(trait)来通信。

输入的第二阶段的输入是语法树,它通过解析输入文件和展开宏。这个阶段将 从链接源文件中所有的名称到相关关联的地方(即名称被引用的地方)。它还会生成有用的错误信息,如输入错误建议,要导入的特性(trait)或 lints 关于未使用的项。(or lints about unused items)

成功运行第二阶段(Resolver::resolve_crate) 将创建一个索引,剩余的编译部分可以使用它来查询当前的名称(通过 hir::lowering::Resolver 接口)

名称解析在 rustc_resolve crate 中,部分内容位于 lib.rs 中,其他模块中有一些帮助程序或 symbol-type logic 。

命名空间

不同类型的符号存在于在不同的命名空间中。例如 类型的名称 不与 变量的名称 冲突。这通常不会发生,因为变量以小写字母开头,而类型以大写字母开头。但这仅仅是一种约定。以下 Rust 代码是合法的,并通过编译(带有 warnings):


#![allow(unused)]
fn main() {
type x = u32;
let x: x = 1;
let y: x = 2; // See? x is still a type here.
}

为了应对这种情况,并使用这些名称的作用域规则少有不同,解析起将它们分开,并为他们创建不同的机构。

也就是说,当(code)代码谈到命名空间,它并不代表模块的结构层次,它是 类型 vs 值 vs 宏。

作用域和 ribs

名称只在源代码的特定区域可见。这形成了一个层次结构,但不一定是简单的-如果一个作用域是另一个作用域的一部分,这不意味在外部可见的名称也是在内部可见的,或指的是同一样内容。

为了处理这种情况,编译器引入一个 Ribs 概念。这是一种抽象作用域。每每次可见名称可能发生变化时,一个新的 rib 被推入栈中。可能发生这种情况的地方包括:

  • 明显的地方 - 花括号包围块,函数边界,模块。
  • 通过 let 绑定引入 - shodow 另一个同名变量。
  • 宏展开边缘 - 因对宏的卫生(hygiene)

ribs 栈会由内到外的搜索名称。这有助于找到名称最近的意思(这个名称不会被其他任何东西覆盖)。向外过渡 rib 也可能会改则未使用名称的规则 - 如果这里又一个嵌套的函数(非闭包),内层作用域内不能访问参数和进行本地绑定外层作用域的内容,即使他们在常规作用域规则中应该可见。一个例子:


#![allow(unused)]
fn main() {
fn do_something<T: Default>(val: T) { // <- New rib in both types and values (1)
    // `val` is accessible, as is the helper function
    // `T` is accessible
    let helper = || { // New rib on `helper` (2) and another on the block (3)
        // `val` is accessible here
    }; // End of (3)
    // `val` is accessible, `helper` variable shadows `helper` function
    fn helper() { // <- New rib in both types and values (4)
        // `val` is not accessible here, (4) is not transparent for locals)
        // `T` is not accessible here
    } // End of (4)
    let val = T::default(); // New rib (5)
    // `val` is the variable, not the parameter here
} // End of (5), (2) and (1)
}

因为对于不同作用域的规则有所不同,每个作用域会有他自己独立的与命名空间并行构造的 rib 栈。此外,也有对于那些没有完整命名空间的 local lables 也有一个 rib 栈(例如 loops 或者 blocks 的名称)。

总体策略

为了执行整个 crate 的名称解析,自上而下的遍历语法树,并解析每个遇到的名称。这适用于大多数类型的名称,因为在使用名称时,已经在 Rib 层次结构中引入了该名称。

这里有一些例外,一些会有些棘手,因为它们甚至可以在遇到之前就可以使用 - 因此需要扫描每一个项去填满 Rib。

其他甚至更有问题的导入,需要递归的定点解析和宏,需要在处理剩下代码之前进行解析和展开。

因此,名称解析是在多个阶段执行的。

Speculative crate loading

为了给出有用的错误,rustc 建议将未找到的路径导入作用域中。它是怎么做到的呢?他会检查每一个 crate 的每一个模块,并寻找可能的匹配项。这甚至包括还没有加载的 crate。

为尚未加载的导入的提供导入建议被称为_speculative crate loading_,因为不应报告任何遇到的错误:决定去加载这些导入的并非用户。执行此功能的函数是在 rustc_resolve/src/diagnostics.rs 中的lookup_import_candidates

为了 speculative loads 和用户的加载,解析通过传递一个 record_used 参数,当 speculative loads 时候,值为 false。

TODO:

这是第一遍学习代码的结果。绝对是不完整的,不够详细的。在某系地方也可能不准确。不过,它可能在将来能提供有用的帮助。

  • 它究竟链接到什么?后续的编译阶段如何发布和使用该链接? 谁调用它以及如何实际使用它。
  • 它是通过,然后仅使用结果,还是可以递增计算(例如,对于RLS)?
  • 总体策略描述有点模糊。
  • Rib这个名字来自哪里?
  • 这东西有自己的测试,还是仅作为某些端到端测试的一部分进行测试?