深度指南:命名空间、Rib 作用域栈与两阶段解析流程)
Rust 编译器名称解析Name Resolution深度指南命名空间、Rib 作用域栈与两阶段解析流程【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust名称解析Name Resolution是 rustc 把 AST 中每一个名字变量、类型、宏、生命周期等映射到其定义处的核心编译阶段它决定了x到底是类型还是值这个helper指的是函数还是闭包这类问题的答案。本文以 rustc 官方开发指南中《Name resolution》一章为主线结合 rustc_resolve 的源码实现系统讲解两阶段解析流程、三大命名空间、Rib 作用域栈机制、整体解析策略以及投机式 crate 加载Speculative Crate Loading等关键原理帮助你理解名字如何被连接到定义以及类型错误提示、未使用告警与导入建议从何而来。为什么需要名称解析名字与定义之间的桥梁在我们编写的程序中变量、类型、函数等都是以名字name来指代的而这些名字并不总是唯一的。考虑下面这段合法的 Rust 程序type x u32; let x: x 1; let y: x 2; // 这里的 x 仍然是类型第 3 行中的x到底是类型u32还是值1这类同名歧义正是由名称解析来解决的。在这个具体例子中名称解析规则规定类型名与变量名分别位于不同的命名空间namespace因此它们可以共存而不冲突。注意变量以小写字母开头、类型以大写字母开头只是一种惯例编译器并不依赖这种惯例——上面的代码是合法 Rust 代码会有告警名称解析靠的是命名空间的分离而非大小写约定。名称解析的产出是一张名字 → 定义位置的索引它把源码中所有名字链接到名字被引入的位置定义处同时生成高质量的错误信息例如拼写错误建议typo suggestions、可导入的 trait 建议traits to import以及未使用项unused items的 lint。一次成功的完整解析运行对应源码中的Resolver::resolve_crate会创建一份后续编译阶段可以查询的索引后续阶段通过hir::lowering的Resolver接口即ResolverAstLowering向它询问当前存在的名字。两阶段解析宏展开期与完整解析期名称解析是一个两阶段two-phase过程第一阶段宏展开期间的解析第一阶段运行在宏展开macro expansion过程中。此时编译器构建一棵模块树module tree并解析导入imports和宏名。之所以要在这个阶段就做部分解析是因为必须先知道要展开什么才能进行宏展开——如果连导入的宏都解析不了后续展开就无从谈起。这一阶段并不做完整名称解析只解析两类名字导入imports与宏macros。在源码层面这一阶段的核心逻辑位于rustc_resolve的build_reduced_graph.rs构建模块图与macros.rs宏解析等模块中。宏展开与名称解析之间通过ResolverAstLoweringExttrait 通信——这是一个使用#[extension(trait)]定义的扩展 trait由ResolverAstLowering提供底层数据供rustc_ast_lowering在 AST 下降lowering为 HIR 时使用。第二阶段完整名称解析第二阶段的输入是由解析器parser生成、再经过宏展开后的完整语法树AST。这一阶段发生在rustc_resolve::late见 late.rs中。与宏展开期不同在晚期解析late resolution中每个名字只需要尝试解析一次即可因为此时不会再新增任何名字——如果某个名字解析失败那它就是编译器错误compiler error。第二阶段产出所有名字与引入位置的链接并生成前文所述的各类诊断信息。一次成功的第二阶段运行所创建的索引可供编译的其余部分通过Resolver接口查询。resolve_crate完整解析的入口从源码看整个 crate 解析的入口是Resolver::resolve_crate它按顺序执行了多个被sess.time计时的子阶段finalize_imports完成导入的收尾解析导入需要递归的不动点解析见下文整体策略compute_effective_visibilities计算生效可见性effective visibilities并产生exported_ambiguitieslint_reexports对再导出reexport进行 lintfinalize_macro_resolutions完成宏解析的收尾late_resolve_crate执行完整的晚期名称解析resolve_main解析main入口check_unused检查未使用项resolve_report_errors汇总并报告错误resolve_postprocesscstore 后处理。解析完成后会调用freeze_cstore()此后不再允许改动 crate 存储cstore与稳定的 crate id 映射。命名空间类型、值与宏互不干扰不同种类的符号生活在不同的命名空间namespaces中——例如类型不会与变量冲突。Rust 名称解析中的命名空间指的不是模块层级而是类型type对值value对宏macro的区分。下面这段代码是合法的会有告警因为它利用了类型与值命名空间的分离type x u32; let x: x 1; let y: x 2; // 看这里的 x 仍然是类型在lib.rs中可以看到解析器对每个命名空间逐一处理fn per_nsF: FnMut(Self, Namespace)(self, mut f: F) { f(self, TypeNS); // 类型命名空间 f(self, ValueNS); // 值命名空间 f(self, MacroNS); // 宏命名空间 }由于不同命名空间的作用域规则也略有差异解析器为它们分别维护相互独立的解析结构包括各自独立的 Rib 栈见下节。作用域与 Rib名字可见性的抽象名字的可见性区域一个名字只在源码的特定区域内可见这构成了一个层级结构——但这个结构并不简单即使某个作用域包含在另一个作用域之中外层作用域可见的名字在内层不一定可见即便可见所指的也可能不是同一个东西。为了应对这一点编译器引入了Rib的概念——它是作用域scope的抽象。每当可见名字的集合可能发生变化时一个新的Rib就会被压入栈中。可能压入新 Rib 的位置包括显而易见的位置花括号包围的块、函数边界、模块引入let绑定这可能会遮蔽shadow另一个同名绑定宏展开边界用于处理宏卫生macro hygiene。查找名字时解析器会从最内层 Rib 开始向外遍历Rib 栈从而找到名字最近的未被其他任何东西遮蔽的含义。此外向外跨越某个 Rib 还可能会影响哪些名字可用——例如当存在嵌套的普通函数非闭包时内层函数不能访问外层函数的参数和局部绑定即使按普通作用域规则它们本应可见。Rib 的源码定义在 late.rs 中Rib 的定义如下节选/// A single local scope. /// /// A rib represents a scope names can live in. Note that these appear in many places, not just /// around braces. At any place where the list of accessible names (of the given namespace) /// changes or a new restrictions on the name accessibility are introduced, a new rib is put onto a /// stack. This may be, for example, a let statement (because it introduces variables), a macro, /// etc. #[derive(Debug)] pub(crate) struct Ribra, R Res { pub bindings: FxIndexMapIdent, R, pub patterns_with_skipped_bindings: UnordMapDefId, Vec(Span, OptionSpan, Result(), ErrorGuaranteed), pub kind: RibKindra, }每个 Rib 持有该作用域内引入的名字绑定bindings并带有一个kind字段类型为RibKind。RibKind决定了该 Rib 对哪些访问施加限制主要变体包括Normal不施加任何限制Block(OptionLocalModule)穿过一个ast::Block若块内含 item 则部分表现如同ModuleAssocItem穿过 impl 或 trait 进入其方法或关联类型只允许引用该 impl/trait 绑定的类型参数FnOrCoroutine穿过函数、闭包或协程签名禁止引用外层 labelItem(HasGenericParams, DefKind)穿过 item 作用域禁止引用 upvars捕获变量ConstantItem(...)位于常量 item 中不能引用动态内容且对泛型参数的使用有严格限制Module(LocalModule)穿过模块 itemMacroDefinition(DefId)穿过macro_rules!语句ForwardGenericParamBan(...)泛型参数默认值中的前向引用禁令ConstParamTy位于 const 参数的类型中不能引用任何参数InlineAsmSym位于sym内联汇编操作数中只能引用全局项。RibKind上还提供了contains_params()判断该 Rib 是否包含泛型参数与is_label_barrier()判断该 Rib 是否禁止引用外层 label等辅助方法。示例各作用域中的 Rib 压栈过程原文档给出了一个直观的示例展示了函数体内各构造如何触发新的 Rib行号注释对应压栈时机fn do_somethingT: Default(val: T) { // - 在类型与值命名空间同时压入新 rib (1) // val 可访问helper 函数也可访问 // T 可访问 let helper || { // 块上压入新 rib (2) // val 在这里可访问 }; // (2) 结束为 helper 压入新 rib (3) // val 可访问helper 变量遮蔽了 helper 函数 fn helper() { // - 在类型与值命名空间压入新 rib (4) // val 在这里不可访问(4) 对局部变量不透明 // T 在这里不可访问 } // (4) 结束 let val T::default(); // 压入新 rib (5) // 这里的 val 是变量不再是参数 } // (5)、(3)、(1) 结束要点在于第 (4) 个 Rib 是嵌套普通函数引入的它对局部变量不透明not transparent for locals因此函数内部的val参数与T泛型参数都不可见而闭包引入的 (2) 则可以访问val。由于不同命名空间的规则略有不同每个命名空间拥有各自独立的 Rib 栈这些栈并行构造。此外还有一个用于局部 label例如循环或块的名称的 Rib 栈它本身并不构成一个完整命名空间。整体策略分阶段遍历与特殊情形自顶向下遍历要对整个 crate 执行名称解析编译器会自顶向下遍历语法树并解析遇到的每个名字。这对大多数名字都有效因为在名字的使用点该名字通常已经被引入到 Rib 层级中即在压栈时已注册。三类例外但存在三类例外情形它们无法在遍历到使用点时立即解析Items项项可以在被遇到之前就被使用前向引用因此每个块都需要先扫描其中包含的 item以填充其 RibImports导入需要递归的不动点解析recursive fixed-point resolution因为导入可能互相引用、形成环Macros宏必须在处理其余代码之前被解析并展开。正因如此解析被划分为多个阶段执行——这正对应上一节resolve_crate中依次调用finalize_imports、late_resolve_crate等子阶段的设计。投机式 crate 加载为诊断而预加载它解决什么问题当名字解析失败时为了给出有用的错误信息rustc 会建议把某些路径导入到当前作用域import suggestions。要做到这一点它必须遍历每个 crate 的每个模块寻找可能匹配的名字——这甚至包括尚未被加载的 crate预先eagerly加载尚未加载的 crate 来生成导入建议被称为投机式 crate 加载speculative crate loading。之所以叫投机是因为这些加载是 rustc 自己决定的为了诊断而非用户显式要求的——因此它在此过程中遇到的任何错误都不应该被报告出来。在源码中负责这项工作的函数是lookup_import_candidates它位于rustc_resolve::diagnostics即 diagnostics/impls.rs。从实现看该函数会从 crate 根graph_root出发在模块内查找候选者遍历extern_prelude外部 crate 预导表对每个候选外部 crate 通过maybe_process_path_extern触发加载若同名 item 已在作用域内还会用::前缀生成全局路径来消歧disambiguation通过filter_fn谓词过滤候选只接受符合预期类型如 trait的定义查找范围横跨所有 crateThe lookup spans across all crates。如何区分投机加载与用户发起的加载为了区分投机加载与用户发起的加载rustc_resolve会传递一个record_used参数当加载是投机性的时record_used为false不需要记录使用、不需要报告错误。在 build_reduced_graph.rs 的注释中还能看到并行解析时代码对投机解析的特别处理在投机解析期间两个线程可能同时创建完全相同的模块因此需要对外部模块映射表extern_module_map加锁避免重复创建模块FIXME 注释提到将来可能只对def_id加锁以缩小锁粒度。另外在 ident.rs 中可以确认被遮蔽的声明shadowed decls不需要被标记为已使用或以非投机方式加载而在投机解析或晚期解析中也不需要对歧义错误进行报告——因为这些情形下所有导入与展开已经完成。小结名称解析是 rustc 中连接名字与定义的关键编译阶段可以从四个层面理解它两阶段流程宏展开期只解析导入与宏保证知道展开什么完整 AST 就绪后rustc_resolve::late执行一次性完整解析失败即报错命名空间隔离类型、值、宏分属不同命名空间TypeNS/ValueNS/MacroNS各自拥有独立的解析结构解决了同名不同类型的冲突Rib 作用域栈每次可见名字集合可能变化时块、函数、let、宏展开边界等压入新的Rib查找时自内向外遍历每个命名空间并行维护各自的 Rib 栈分阶段整体策略与诊断辅助item 需预先扫描填充 Rib导入需不动点解析宏需先解析后展开而当解析失败时rustc_resolve::diagnostics中的lookup_import_candidates会通过投机式 crate 加载遍历所有 crate 的所有模块借助record_usedfalse区分投机加载从而给出跨 crate 的导入建议与拼写建议。值得一提的是rustc 开发指南的原文作者也明确提示由于这是对代码库的第一遍学习文档可能在某些细节上不够完整或不够精确例如 Rib 命名的出处、该阶段产出的索引如何被后续阶段发布与消费、是否有独立测试等。本仓库中 late.rs、lib.rs 与 diagnostics/impls.rs 是继续深入研究这些细节的最佳起点——读者可以直接在源码中验证本文所述的两阶段入口、Rib 栈压栈时机与候选查找逻辑。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考