news 2026/9/10 21:36:35

Rust 编译器常量求值(Constant Evaluation)完全指南:从 const_eval 查询到 ValTree

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rust 编译器常量求值(Constant Evaluation)完全指南:从 const_eval 查询到 ValTree

Rust 编译器常量求值(Constant Evaluation)完全指南:从 const_eval 查询到 ValTree

【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust

常量求值(constant evaluation,CTFE)是 Rust 编译器在编译期执行代码、计算值的过程:static的初始化器、数组长度、枚举判别式(discriminant)乃至模式匹配的排他性检查都依赖它,同时它也支撑着 const 泛型等类型系统特性,并让开发者得以把复杂计算"搬"到编译期以减少运行时开销。本文将基于 rustc-dev-guide 的 const-eval.md 文档,结合rustc_middlerustc_const_eval的实际源码,梳理常量求值的触发时机、const_eval_*系列入口、GlobalId寻址机制、ValTree 与 MIR 常量值两种结果表示,以及底层的查询调用链,帮助你完整理解 rustc 中"编译期计算"这一核心管线的设计。

常量求值是什么:编译期计算值

常量求值(constant evaluation)指的是在编译期(compile time)计算值的全过程。对某个具体的条目(item)——常量(const)、静态变量(static)、数组长度(array length)——而言,该过程发生在其MIR 完成借用检查(borrow-check)与优化之后。一个重要的连带效应是:在很多情况下,尝试对某个条目做常量求值会首次触发该条目 MIR 的生成,即 const eval 是 MIR 构造的下游消费者。这意味着const_eval相关查询与mir_builtmir_borrowckmir_promoted等 MIR 管线存在显式的依赖关系,当读者在编译日志中看到 "const-evaluating + checking" 的描述时,说明对应的 MIR 已经就绪。

从用户视角看,常量求值最常见、最典型的用例包括:

  • static的初始化器(initializer):静态变量的初始值必须在编译期确定,并以固定的内存布局固化进最终产物。
  • 数组长度(array length)[T; N]中的N必须在编译期已知,因为编译器需要据此在栈上或堆上预留空间。
  • 枚举变体判别式(enum variant discriminants):判别式的值必须已知,以保证任意两个变体不会拥有相同的 discriminant。
  • 模式(patterns):模式中的常量必须已知,以便检查模式是否重叠(overlapping patterns)。

除了上述"不得不做"的场景,常量求值还能被主动用来削减运行时的工作量与二进制体积:把复杂运算在编译期预先算好,只把结果存进产物,运行时直接读取。典型如const声明的大表、const fn参与的编译期计算等。

文档将这些使用场景归纳为两大类:

  1. 影响类型系统(influencing the type system):数组长度、枚举判别式、const 泛型参数等——它们的结果直接参与类型检查与泛型实例化。
  2. 仅为预计算运行时表达式(precompute expressions to be used at runtime):把编译期算好的结果嵌入到运行时代码中,与类型系统无关。

这一分类直接决定了结果表示方式的不同:类型系统常量要求结果能被编译器进一步"审视"(因此求值为 ValTree),而运行时预计算只需要最终值(因此求值为 MIR 常量值)。

const_eval_* 入口:TyCtxt上的三个包装函数

常量求值通过TyCtxtconst_eval_*系列函数触发,它们是底层const_eval查询(query)的包装(wrapper)。当前仓库中,这些函数的实现位于 compiler/rustc_middle/src/mir/interpret/queries.rs。文档给出的三个核心入口如下:

入口函数结果类型用途
const_eval_global_id_for_typeck求值为valtree类型检查期间使用,结果可被编译器进一步检查(如 const 泛型、数组长度)
const_eval_global_id求值为包含最终值的opaque blobConstValue供 codegen 后端与 CTFE 求值引擎自身使用
eval_static_initializer求值为 static 初始化器的内存分配仅用于 static;其他所有函数都无法正确表示 static,并带有防止误用 static 的断言

三个入口各自对应一条底层查询。在 compiler/rustc_middle/src/queries.rs 中可以看到这些查询的正式定义:

// 底层查询:求值为内存分配(MIR 常量值的原始形态) query eval_to_allocation_raw(key: ty::PseudoCanonicalInput<'tcx, GlobalId<'tcx>>) -> EvalToAllocationRawResult<'tcx> { desc { "const-evaluating + checking `{}`", key.value.display(tcx) } cache_on_disk } // 底层查询:求值为 MIR 常量值(opaque blob) query eval_to_const_value_raw(key: ty::PseudoCanonicalInput<'tcx, GlobalId<'tcx>>) -> EvalToConstValueResult<'tcx> { desc { "simplifying constant for the type system `{}`", key.value.display(tcx) } depth_limit cache_on_disk } // 底层查询:求值为类型级常量(ValTree) query eval_to_valtree(key: ty::PseudoCanonicalInput<'tcx, GlobalId<'tcx>>) -> EvalToValTreeResult<'tcx> { ... }

值得注意的细节:

  • 三个查询的键(key)统一为PseudoCanonicalInput<GlobalId>,即"规范化后的GlobalId+ 求值环境",便于查询缓存去重。
  • eval_to_const_value_raweval_to_valtree都标注了"Do not call this directly"警告,要求调用方必须经由TyCtxt的包装函数(如const_eval_polyconst_eval_resolveconst_eval_instanceconst_eval_global_id)间接调用,从而保证 span 信息、region 擦除、规范化等前置处理被正确执行。
  • eval_to_const_value_raw额外带有depth_limit,说明它受求值深度限制保护;eval_to_allocation_raweval_static_initializer均带cache_on_disk,允许跨编译会话缓存结果。

入口函数的实现细节

const_eval_global_id为例,它在调用底层查询前做了两件关键工作(见 queries.rs):

pub fn const_eval_global_id( self, typing_env: ty::TypingEnv<'tcx>, cid: GlobalId<'tcx>, span: Span, ) -> EvalToConstValueResult<'tcx> { // Const-eval 不应依赖生命周期,擦除后能提升查询缓存命中率 let inputs = self.erase_and_anonymize_regions( typing_env.with_post_analysis_normalized(self).as_query_input(cid), ); if !span.is_dummy() { // 查询本身不知道在何处被调用,需要修正 span self.at(span).eval_to_const_value_raw(inputs).map_err(|e| e.with_span(span)) } else { self.eval_to_const_value_raw(inputs) } }
  1. 生命周期无关化:常量求值不应依赖任何生命周期,因此通过erase_and_anonymize_regions擦除并匿名化 region,使查询键可被稳定缓存;
  2. span 修正:若调用方提供了非 dummy 的 span,则用self.at(span)把 span 附加到求值错误上,让报错能指向实际使用常量的位置。

const_eval_global_id_for_typeck(见 queries.rs)则更复杂:它调用eval_to_valtree后,还需把ValTreeCreationError分类处理——NonSupportedType交给调用方决定,NodesOverflow(valtree 节点数超限)、InvalidConstCyclicConst则会发出对应的诊断错误。此外它还会在成功求值后检查"常量是否非法依赖了泛型参数"(仅在未启用generic_const_exprs且目标是 anon const 时),并发出const_evaluatable_unchecked相关的 future-compat 警告(lint),对应 queries.rs 中一段带有详细注释的逻辑。

环境参数:从 ParamEnv 到 TypingEnv

原文档描述求值环境时提到ParamEnv,并链接了 typing-parameter-envs.md。需要说明的是,在当前仓库的源码中,这一概念已经演进为ty::TypingEnv:三个入口函数的签名均接收typing_env: ty::TypingEnv<'tcx>,且各 provider 中会出现crate::assert_typing_mode(key.typing_env.typing_mode())之类的模式断言(见 compiler/rustc_const_eval/src/const_eval/eval_queries.rs)。从语义上看,该环境描述了常量被求值时所处的上下文——例如常量被使用的那个函数——从而决定 trait 求解、泛型解析等行为,这与文档所述"常量在其中被求值的环境(e.g. the function within which the constant is used)"一致。阅读旧文档或旧版本代码时,可把ParamEnv视作TypingEnv的前身。

GlobalId:常量求值的"寻址"单元

const_eval_*函数接收一个GlobalId来唯一定位"要求值什么"。其定义位于 compiler/rustc_middle/src/mir/interpret/mod.rs:

/// 唯一标识以下二者之一: /// - 一个常量(constant) /// - 一个静态变量(static) pub struct GlobalId<'tcx> { /// 对于常量或 static,是该条目自身的 `Instance`; /// 对于 promoted global,则是其所属函数的 `Instance`。 pub instance: ty::Instance<'tcx>, /// promoted global 在其所属函数 `mir::Body` 的 promoted 表中的索引。 pub promoted: Option<mir::Promoted>, }

GlobalId由两部分构成:

  • 一个Instance,它要么引用一个常量或 static(此时promotedNone),要么引用一个函数(此时promotedSome(index),索引指向该函数 MIR 中Promoted表内的某一项);
  • 可选的分支:promoted: Option<mir::Promoted>promoted即"提升常量"——被提升到'static的临时表达式(例如&42&format!(...)的某些中间值),它们不是独立条目,而是挂在所属函数的 MIR 上,因此需要"函数 Instance + promoted 索引"二元组寻址。

GlobalId派生了一整套编码/哈希 trait(Copy, Clone, Debug, Eq, PartialEq, Hash, TyEncodable, TyDecodable, StableHash, TypeFoldable, TypeVisitable),说明它可以作为查询键在跨会话缓存(on-disk cache)与增量编译中被序列化使用。

为了构造GlobalId,编译器内部提供了若干辅助函数(同样位于 queries.rs):

  • const_eval_poly(def_id):在不提供任何泛型参数的情况下求值一个常量,适用于 const 条目、枚举判别式等不含泛型的场景。它用GenericArgs::identity_for_item构造恒等泛型参数,再通过Instance::new_raw构造实例;若常量内部真的用到了泛型参数,会得到ErrorHandled::TooGeneric
  • const_eval_resolve(typing_env, ct, span):解析并求值一个mir::UnevaluatedConst,支持 trait 上的关联常量(如<A as B>::C)。解析失败时刻意不指向常量使用处,而是返回ErrorHandled::TooGenericErrorHandled::Reported
  • const_eval_resolve_for_typeck(typing_env, ct, span):typeck 专用版本,接收ty::AliasConstProjection/InherentImpl/Free/Anon等别名常量变体),最终走到const_eval_global_id_for_typeck并附加上述 lint 检查。
  • const_eval_instance(typing_env, instance, span):对给定的Instance直接求值,等价于GlobalId { instance, promoted: None }const_eval_global_id

此外,这些函数都会拒绝包含推断变量(inference variable)的常量,代码中通过has_non_region_infer()检查并bug!报错;需要处理推断变量时,应改用Infcx::const_eval_resolve路径。

求值结果:ValTree 与 MIR 常量值

常量求值返回的结果类型与"目标消费者"严格绑定。文档明确:类型系统常量返回EvalToValTreeResult运行时预计算返回EvalToConstValueResult。二者的类型别名定义于 compiler/rustc_middle/src/mir/interpret/error.rs:

pub type EvalToConstValueResult<'tcx> = Result<ConstValue, ErrorHandled>; pub type EvalToValTreeResult<'tcx> = Result<ValTree<'tcx>, ValTreeCreationError<'tcx>>;

MIR 常量值(MIR constant value):mir::ConstValue

mir::ConstValue是求值结果的底层字节级表示。rustc-dev-guide 的 mir/index.md 一节说明:求值产生的是按单个字节组织、存放在内存中的低层表示,称为indirect(间接)常量mir::ConstValue::Indirect)。但"一切都放内存"效率太低,因此ConstValue为可以直接写成字面量的常见值提供了优化变体:整数、浮点数、charbool,乃至"string literals"b"byte string literals"都有避免完整内存表示开销的专用表示。文档将其称为包含最终值的 "opaque blob",因为它只对 codegen 后端与 CTFE 引擎本身有意义,编译器其他部分不会去"拆解"它。

ValTree:类型系统常量的规范表示

类型系统常量(type system constant)求值得到的是valtreety::ValTree)。根据 mir/index.md 的说明,ty::ValTree可以表示:

  • 数组(arrays)
  • 许多结构体(structs)
  • 元组(tuples)
  • 枚举(enums)
  • 大多数基本类型(primitives)

最核心的规则是唯一表示性(unique representation):每个值只能有一种合法的ValTree表示。例如两个整数的数组只有一种表示方式——Branch([Leaf(first_int), Leaf(second_int)]);尽管理论上[u32; 2]可以塞进一个u64变成Leaf(bits_of_two_u32),但那不是合法的ValTree构造。正是这种唯一性,使得ValTree可以直接用于比较、哈希与 dedup——例如数组长度是否相等、const 泛型参数是否匹配、模式是否重叠等判断都因此变得可靠。

唯一性也意味着有些值无法表示

  • **联合体(union)**不能出现在类型级常量中,因为其活跃变体(active variant)未知,无法确定表示;
  • **裸指针(raw pointer)**无法表示,因为地址在编译期未知;
  • 引用(reference)反而可以表示:引用的相等性由其所指的值决定,因此求值时忽略地址、只看背后承载的值。&42被编码成与42完全相同的 valtree;从 valtree 转回 MIR 常量值时再重新引入实际的间接(indirection),而 codegen 阶段这些地址是否合并、如何合并,完全取决于任意优化选择。

因此,对ValTree所有解码都必须先匹配类型再做决定——脱离类型,值本身不携带任何有用信息(见 mir/index.md)。这也是为什么const_eval_global_id_for_typeck中会存在ValTreeCreationError::NonSupportedType这类"类型不支持 valtree 化"的错误分支:遇到 union、裸指针等类型时无法构造 valtree,只能把类型交还给调用方(typeck)决定如何处理。

底层调用链:从包装函数到解释器

将包装函数与 provider 拼接起来,可以得到完整的求值调用链:

TyCtxt::const_eval_global_id / const_eval_global_id_for_typeck / const_eval_poly ... └─> eval_to_const_value_raw / eval_to_valtree / eval_to_allocation_raw(查询) └─> eval_to_const_value_raw_provider / eval_to_valtree / eval_to_allocation_raw_provider └─> eval_in_interpreter(InterpCx + CompileTimeMachine,见 compiler/rustc_const_eval/src/const_eval/eval_queries.rs)

在 compiler/rustc_const_eval/src/const_eval/eval_queries.rs 中可以看到:

  • eval_to_const_value_raw_provider先做 trivial 常量快捷路径(tcx.trivial_const直接返回),否则回退到eval_to_allocation_raw,再把分配结果turn_into_const_valueConstValue;其中还有一个retry_codegen_mode_with_postanalysis的重试机制,用于处理 typing mode 相关的差异;
  • eval_static_initializer_providertcx.is_static断言开头,用Instance::mono构造单态化实例后直接调用eval_in_interpreter
  • eval_to_allocation_raw_provider的关键断言是promoted.is_some() || !tcx.is_static(...)——即该查询不允许直接求值 static,因为 static 在概念上是"位置(place)"而非"值",直接求值会破坏指针同一性(pointer identity)。这与turn_into_const_valueeval_to_const_value_raw不应被用于 static 的断言("theeval_to_const_value_rawquery should not be used for statics")相互印证。

static 的特殊性:为什么必须单独处理

文档特别强调:statics 是特殊的(Statics are special),除eval_static_initializer之外的所有函数都无法正确表示 static,因此这些函数内部带有阻止其用于 static 的断言。结合源码,原因可以总结为三点:

  1. static 是位置而非值:static 拥有稳定的内存地址与指针身份,其"值"在编译期尚未完全定型(例如&STATIC的地址要等链接期确定)。eval_to_allocation_raw_provider的注释明确写道:"statics are conceptually places, not values — so what we do here could break pointer identity"。
  2. 初始化器单独求值:static 的初始化器由eval_static_initializer查询专门计算,返回EvalStaticInitializerRawResult(即初始化的内存分配),见 compiler/rustc_middle/src/queries.rs。该查询还标注了separate_provide_externfeedable,允许跨 crate 提供与外部喂入。
  3. 访问权限不同:求值器在访问 static 时会区分"能否访问可变全局"(CanAccessMutGlobal),mk_eval_cx_to_read_const_val中以CanAccessMutGlobal::from(is_static)构造求值上下文(见 eval_queries.rs)。

小结:常量求值在 rustc 中的位置

常量求值处于 MIR 管线(借用检查与优化之后)与代码生成(codegen)之间的咽喉位置:它消费已优化的 MIR,产出两种面向不同消费者的结果——面向类型系统的ValTree(唯一表示、可比较可哈希)与面向后端的ConstValue(低层字节表示)。GlobalId统一了"常量/static/提升常量"三种求值目标的寻址方式,const_eval_*包装函数负责 region 擦除、span 修正与错误分类,底层查询则承载了缓存、深度限制与磁盘持久化。理解这条管线,是深入 rustc 类型检查、const 泛型、模式检查乃至 codegen 的前提;相关的更多细节还可以继续阅读 typing-parameter-envs.md(求值环境)、mir/index.md(ValTree 与 MIR 常量值)、mir/index.md 的 promoted constants 一节(提升常量),以及rustc_const_evalcrate 的 const_eval 模块 与 valtrees 模块 的实际实现。

【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

管式土壤墒情监测仪:从数据采集到灌溉决策的全流程落地指南

干了这么多年农业物联网&#xff0c;我见过太多“装完就吃灰”的墒情监测项目。设备花几万块往地里一插&#xff0c;手机APP上数据天天跳&#xff0c;但真正拿这些数据去做灌溉决策、生产指挥的人却少得可怜。多数情况是数据归数据、经验归经验&#xff0c;两套系统长期并行&am…

作者头像 李华
网站建设 2026/9/10 21:30:47

30分钟本地跑通Qbot:从克隆代码到第一次回测

30分钟本地跑通Qbot&#xff1a;从克隆代码到第一次回测 【免费下载链接】Qbot [&#x1f525;updating ...] AI 自动量化交易机器人(完全本地部署) AI-powered Quantitative Investment Research Platform. &#x1f4c3; online docs: https://ufund-me.github.io/Qbot ✨ :n…

作者头像 李华
网站建设 2026/9/10 21:29:13

Starship Catppuccin Powerline 预设完整指南:从安装到调色板定制

Starship Catppuccin Powerline 预设完整指南&#xff1a;从安装到调色板定制 【免费下载链接】starship ☄&#x1f30c;️ The minimal, blazing-fast, and infinitely customizable prompt for any shell! 项目地址: https://gitcode.com/GitHub_Trending/st/starship …

作者头像 李华
网站建设 2026/9/10 21:29:03

期末高效学习工具与应急技巧全攻略

1. 期末周生存指南&#xff1a;那些真正能救命的学习工具 每到期末周&#xff0c;图书馆总是人满为患&#xff0c;咖啡消耗量直线上升。作为一名经历过无数次期末洗礼的老学长&#xff0c;我深刻理解那种被deadline追着跑的窒息感。今天要分享的不是什么高大上的学习方法&#…

作者头像 李华