news 2026/9/12 3:17:13

Rust 增量编译依赖图(Dep-Graph)调试与测试指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rust 增量编译依赖图(Dep-Graph)调试与测试指南

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改变了,那么barTypeckTables就需要重算。测试会检查依赖图中确实存在一条从Hir(foo)TypeckTables(bar)的路径;路径存在时,编译器在//~ ERROR OK所在行输出字符串OK到 stderr,与 UI 测试框架的预期注释匹配,测试通过。

反过来,可以断言"不应该存在依赖路径":

#[rustc_then_this_would_need(TypeckTables)] //~ ERROR no path fn baz() { }

语义是:如果foo改变了,bazTypeckTables不需要重算。此时宏必须产生一条错误,且错误消息必须包含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错误。也就是说,断言校验的是整条路径的存在性(传递闭包),而不只是单条边。

同时源码中有两处前置约束值得注意:

  1. 使用这两个注解必须开启-Z query-dep-graph,否则直接报错(assert_dep_graph内对应断言);
  2. rustc_attrsfeature 必须启用,注解才会被识别。

从 assert_dep_graph.rs 的模块注释可知:这些检查在代码生成之后运行,观察的是依赖图的最终状态;而persist::clean中还有类似的断言用于检查刚从磁盘加载依赖图时的初始状态,二者配合可覆盖增量编译的全生命周期。

1.3 真实测试用例解读

仓库中的 tests/ui/dep-graph/dep-graph-caller-callee.rs 是文档点名的示例,它验证了一个非常本质的增量编译性质——改动x只应重算直接调用者y的函数体,而不应波及zy的调用者,其签名未变)

//@ 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_graphtxt文件直接按{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.rs

2.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_filtertarget_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.rsfilter_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),但有两点本质差异:

  1. 它作用于每一条边(source 端匹配DepNodeFilter、target 端匹配DepNodeFilter),不处理图中的长路径,见 graph.rs 中CurrentDepGraph::newforbidden_edge的解析;
  2. 该逻辑被#[cfg(debug_assertions)]包裹,release 构建无效,必须使用带 debug 断言的编译器。

2.4 实战排错流程

把上面的工具串起来,一个完整的"追查错误边"流程如下:

  1. 复现现象:改动foobar被重新类型检查,而你不认为需要。
  2. 导出局部图
    RUST_DEP_GRAPH_FILTER='Hir & foo -> TypeckTables & bar' \ rustc -Z query-dep-graph -Z dump-dep-graph your_crate.rs
  3. 阅读dep_graph.txt,假设看到:
    Hir(foo) -> Collect(bar) Collect(bar) -> TypeckTables(bar)
  4. 锁定可疑边Hir(foo) -> Collect(bar)这条边看起来是多余的。
  5. 设置禁令过滤器并重新编译(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
  6. 编译器在创建该边时立即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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:15:38

DFS与BFS算法解析:岛屿问题的双解法实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:15:35

台阶爆破SPH-FEM耦合数值模拟:爆堆堆积与参数标定实践

做台阶爆破数值模拟,如果只让我留一个关键词,我会选“堆积”。很多工程师用ANSYS/LS-DYNA做三维台阶抛掷爆破模拟,模型建得漂亮,炸药参数也对得上,结果跑完一看,岩石像花瓣一样四散飞去,爆堆完全…

作者头像 李华
网站建设 2026/9/12 3:15:09

Python Socket多线程聊天室:基于MySQL持久化的完整C/S项目实战

简介:基于Python和MySQL实现的简易聊天室系统完整源码,面向Python网络编程初学者、课程设计与毕业设计开发者,结合Socket多线程通信、MySQL数据持久化与图形界面交互,完整演示了从登录注册到群聊私聊的桌面聊天工具开发流程。压缩…

作者头像 李华
网站建设 2026/9/12 3:14:47

Java线程间通信机制详解与实战应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:14:35

日期时间数据处理全攻略:从Excel到SQL再到Pandas

做数据分析这些年,我越来越觉得“日期时间数据”是个被严重低估的数据类型。很多人做数据分析项目时,一开始关注的是销售额、用户量、转化率这些指标数字,却忽略了背后真正撑起分析框架的时间字段。等到做同环比、留存、漏斗、生命周期分析的…

作者头像 李华
网站建设 2026/9/12 3:14:24

光伏充电站V2G技术优化与动态电价策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华