Rust 增量编译依赖图(Dep-Graph)调试与测试指南
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
导读
rustc 的增量编译依赖图(dep-graph)记录了每个编译查询节点之间的依赖边,是增量编译正确性与性能的核心数据结构。当你在修改编译器代码后遇到"改了foo却错误地重编了bar"这类问题,或想为新功能补充依赖图测试时,需要一套专门的工具来观测、过滤和断言这张图。本文基于 rustc-dev-guide 的incrcomp-debugging章节,系统讲解依赖图的三种调试手段——#[rustc_if_this_changed]/#[rustc_then_this_would_need]注解测试、-Z dump-dep-graph图形导出与过滤、RUST_FORBID_DEP_GRAPH_EDGE非法边追踪,并结合本仓库源码(compiler/rustc_incremental/src/assert_dep_graph.rs、compiler/rustc_middle/src/dep_graph/debug.rs 等)深入讲解其底层原理与实战用法。
一、测试依赖图:注解断言机制
1.1 两个核心注解
依赖图测试最简单的方式是在tests/ui的测试源码中直接使用两个 rustc 内部属性注解:
#[rustc_if_this_changed]:标注"改变源头"。它标记某个函数(或 trait 项、impl 项、结构体字段)作为依赖路径的起点,等价于声明"如果我改了这里……"。#[rustc_then_this_would_need(...)]:标注"预期受影响的目标",参数是目标依赖节点(如TypeckTables),等价于声明"……那么那里的 xxx 就需要重算"。
一个经典的成对用法如下:
#[rustc_if_this_changed] fn foo() { } #[rustc_then_this_would_need(TypeckTables)] //~ ERROR OK fn bar() { foo(); }这段代码的语义是:如果foo改变了,那么bar的TypeckTables就需要重算。测试会检查依赖图中确实存在一条从Hir(foo)到TypeckTables(bar)的路径;路径存在时,编译器在//~ ERROR OK所在行输出字符串OK到 stderr,与 UI 测试框架的预期注释匹配,测试通过。
反过来,可以断言"不应该存在依赖路径":
#[rustc_then_this_would_need(TypeckTables)] //~ ERROR no path fn baz() { }语义是:如果foo改变了,baz的TypeckTables不需要重算。此时宏必须产生一条错误,且错误消息必须包含no path,对应源码中的诊断定义(见 compiler/rustc_incremental/src/diagnostics.rs):
no path from `foo` to `TypeckTables`注意:
//~ ERROR OK和//~ ERROR no path从被测试的 Rust 代码角度看是普通注释,但对 UI 测试框架而言是必须匹配的预期输出(关于 UI 测试的通用机制,可参考 tests/ui 章节)。
1.2 源码级原理:断言如何被收集与校验
这两个注解的解析与校验逻辑位于 compiler/rustc_incremental/src/assert_dep_graph.rs。核心入口是assert_dep_graph(tcx)函数,它通过IfThisChangedvisitor 遍历 crate 内所有 item、trait 项、impl 项与字段定义,收集两类注解:
rustc_if_this_changed的完整形式是#[rustc_if_this_changed("foo")],其中"foo"用于指定源节点标签,省略时默认使用Hir(对应DepKind::hir_owner);rustc_then_this_would_need则通过DepNode::from_label_string将字符串解析为具体的DepNode标签。
随后check_paths对每个源节点调用query.transitive_predecessors(source_dep_node)求出其传递前驱集合,再逐一检查目标节点是否在该集合中:在则报告OK,不在则报告NoPath错误。也就是说,断言校验的是整条路径的存在性(传递闭包),而不只是单条边。
同时源码中有两处前置约束值得注意:
- 使用这两个注解必须开启
-Z query-dep-graph,否则直接报错(assert_dep_graph内对应断言); rustc_attrsfeature 必须启用,注解才会被识别。
从 assert_dep_graph.rs 的模块注释可知:这些检查在代码生成之后运行,观察的是依赖图的最终状态;而persist::clean中还有类似的断言用于检查刚从磁盘加载依赖图时的初始状态,二者配合可覆盖增量编译的全生命周期。
1.3 真实测试用例解读
仓库中的 tests/ui/dep-graph/dep-graph-caller-callee.rs 是文档点名的示例,它验证了一个非常本质的增量编译性质——改动x只应重算直接调用者y的函数体,而不应波及z(y的调用者,其签名未变):
//@ incremental //@ compile-flags: -Z query-dep-graph #![feature(rustc_attrs)] #![allow(dead_code)] mod x { #[rustc_if_this_changed] pub fn x() { } } mod y { use crate::x; // These dependencies SHOULD exist: #[rustc_then_this_would_need(typeck_root)] //~ ERROR OK pub fn y() { x::x(); } } mod z { use crate::y; // These are expected to yield errors, because changes to `x` // affect the BODY of `y`, but not its signature. #[rustc_then_this_would_need(typeck_root)] //~ ERROR no path pub fn z() { y::y(); } }该测试同时使用incremental与-Z query-dep-graph两个编译标志。注意这里的目标标签用的是typeck_root而非TypeckTables,说明标签字符串可以由源码中的DepNode定义灵活指定。tests/ui/dep-graph/目录下还有 dep-graph-trait-impl.rs、dep-graph-struct-signature.rs、dep-graph-type-alias.rs、dep-graph-variance-alias.rs、dep-graph-assoc-type-codegen.rs 等大量同类用例,分别覆盖 trait 实现、结构体签名、类型别名、variance、关联类型等多个维度的依赖路径断言,是编写新测试时的绝佳模板。
二、调试依赖图:导出与过滤
2.1 导出完整图:-Z dump-dep-graph
编译器支持把依赖图导出到磁盘,方便人工检视。使用方法:
rustc -Z dump-dep-graph your_crate.rs默认会在当前目录生成两个文件:
dep_graph.txt:纯文本边列表,每行形如Source -> Target;dep_graph.dot:GraphViz 格式,可交给dot工具渲染成可视化图形。
文件名可通过环境变量RUST_DEP_GRAPH覆盖,例如:
RUST_DEP_GRAPH=mygraph rustc -Z dump-dep-graph your_crate.rs # 生成 mygraph.txt 与 mygraph.dot对应实现见 assert_dep_graph.rs 的dump_graph:txt文件直接按{source:?} -> {target:?}逐行写出所有边;dot文件则通过rustc_graphviz渲染,节点标签为DepKind的 Debug 输出。
两处前置条件同样来自源码:
-Z dump-dep-graph是[UNTRACKED]的 unstable 选项,其官方描述为"将依赖图以文本文件和 GraphViz dot 文件两种形式导出到$RUST_DEP_GRAPH(默认./dep_graph.{dot,txt})",定义于 compiler/rustc_session/src/options.rs;- 实测同样要求
-Z query-dep-graph开启,dep-graph-dump.rs 测试 即验证了在未开启时输出can't dump dependency graph without '-Z query-dep-graph'的错误。因此完整命令应为:
rustc -Z query-dep-graph -Z dump-dep-graph your_crate.rs2.2 过滤图:RUST_DEP_GRAPH_FILTER
全量依赖图通常非常庞大,难以直接阅读。编译器支持通过环境变量RUST_DEP_GRAPH_FILTER过滤,过滤模式有三种:
source_filter // 从 source_filter 出发可达的所有节点(出边方向) -> target_filter // 能到达 target_filter 的所有节点(入边方向) source_filter -> target_filter // 位于 source_filter 与 target_filter 之间的节点其中source_filter与target_filter是用&分隔的字符串列表。一个节点只要其标签中同时包含这些子串,即视为匹配(对应 dep_graph/debug.rs 中DepNodeFilter::test的实现:对{node:?}的 Debug 字符串按&切分后逐一做contains检查)。
典型用法:
# 选出所有 TypeckTables 节点的全部前驱 RUST_DEP_GRAPH_FILTER='-> TypeckTables' # 只选名字含 bar 的函数的 TypeckTables 节点的前驱 RUST_DEP_GRAPH_FILTER='-> TypeckTables & bar' # 找出从 Hir(foo) 到 TypeckTables(bar) 之间的所有节点 # 用于排查"改 foo 却重算 bar"的错误边来源 RUST_DEP_GRAPH_FILTER='Hir & foo -> TypeckTables & bar'第三行是最常用的排错姿势:当你发现改动foo后编译器不得不重新类型检查bar,而你认为这不该发生时,用它导出Hir(foo)到TypeckTables(bar)之间的全部中间节点,就能顺藤摸瓜找到那条多余的边。
从源码看,EdgeFilter::new要求格式严格为source -> target两个部分,否则报错expected a filter like 'a&b -> c&d';过滤逻辑则在assert_dep_graph.rs的filter_nodes中分三种情况处理:只有源则沿OUTGOING方向 BFS 遍历、只有目标则沿INCOMING方向遍历、两者都有则执行walk_between双向搜索并借助三态标记(Undecided/Deciding/Included/Excluded)处理环。
2.3 追踪错误边:RUST_FORBID_DEP_GRAPH_EDGE
有时候你导出的图中存在一条"不应存在"的路径,但不知道它从哪来。在开启 debug 断言的编译器构建中(即debug_assertions下),可以设置环境变量RUST_FORBID_DEP_GRAPH_EDGE来定位:
RUST_FORBID_DEP_GRAPH_EDGE='Hir & foo -> Collect & bar' RUST_BACKTRACE=1 rustc ...编译器在创建每一条依赖边时都会用该过滤器做匹配,一旦命中立即触发bug!并给出崩溃与回溯,配合RUST_BACKTRACE=1即可看到创建这条边时的完整调用栈。
其语法与RUST_DEP_GRAPH_FILTER相同(同样基于EdgeFilter),但有两点本质差异:
- 它作用于每一条边(source 端匹配
DepNodeFilter、target 端匹配DepNodeFilter),不处理图中的长路径,见 graph.rs 中CurrentDepGraph::new对forbidden_edge的解析; - 该逻辑被
#[cfg(debug_assertions)]包裹,release 构建无效,必须使用带 debug 断言的编译器。
2.4 实战排错流程
把上面的工具串起来,一个完整的"追查错误边"流程如下:
- 复现现象:改动
foo后bar被重新类型检查,而你不认为需要。 - 导出局部图:
RUST_DEP_GRAPH_FILTER='Hir & foo -> TypeckTables & bar' \ rustc -Z query-dep-graph -Z dump-dep-graph your_crate.rs - 阅读
dep_graph.txt,假设看到:Hir(foo) -> Collect(bar) Collect(bar) -> TypeckTables(bar) - 锁定可疑边:
Hir(foo) -> Collect(bar)这条边看起来是多余的。 - 设置禁令过滤器并重新编译(debug 构建):
RUST_FORBID_DEP_GRAPH_EDGE='Hir & foo -> Collect & bar' \ RUST_BACKTRACE=1 \ rustc -Z query-dep-graph -Z dump-dep-graph your_crate.rs - 编译器在创建该边时立即
bug!并打印回溯,根据调用栈定位到创建这条边的查询代码,修复后问题消失。
三、总结:三件套工具的分工
| 工具 | 作用对象 | 关键参数 | 适用场景 |
|---|---|---|---|
#[rustc_if_this_changed]/#[rustc_then_this_would_need] | 依赖图路径(传递闭包) | -Z query-dep-graph+rustc_attrsfeature | 写自动化回归测试,断言/否定路径存在 |
-Z dump-dep-graph+RUST_DEP_GRAPH_FILTER | 图导出与节点过滤 | -Z query-dep-graph(必需) | 人工检视局部依赖图,定位错误边 |
RUST_FORBID_DEP_GRAPH_EDGE | 单条边的创建 | debug 断言构建 +RUST_BACKTRACE=1 | 确认错误边来源,获取回溯 |
三个工具共享同一套DepNode标签与过滤器语法(&连接子串、->分隔源与目标),底层实现在 compiler/rustc_middle/src/dep_graph/debug.rs 与 compiler/rustc_incremental/src/assert_dep_graph.rs。掌握它们,无论是为增量编译的新改动编写依赖图回归测试,还是排查"过度重算"的性能与正确性 bug,都能事半功倍。更多相关背景可继续阅读仓库中增量编译章节的配套文档(rustc-dev-guide 目录下incrcomp-*系列)。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考