资讯动态

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

发布时间:2026/9/10 21:36:42 来源:尧图企业网站定制
Rust 编译器常量求值Constant Evaluation完全指南从 const_eval 查询到 ValTree【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust常量求值constant evaluationCTFE是 Rust 编译器在编译期执行代码、计算值的过程static的初始化器、数组长度、枚举判别式discriminant乃至模式匹配的排他性检查都依赖它同时它也支撑着 const 泛型等类型系统特性并让开发者得以把复杂计算搬到编译期以减少运行时开销。本文将基于 rustc-dev-guide 的 const-eval.md 文档结合rustc_middle与rustc_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_built、mir_borrowck、mir_promoted等 MIR 管线存在显式的依赖关系当读者在编译日志中看到 const-evaluating checking 的描述时说明对应的 MIR 已经就绪。从用户视角看常量求值最常见、最典型的用例包括static的初始化器initializer静态变量的初始值必须在编译期确定并以固定的内存布局固化进最终产物。数组长度array length[T; N]中的N必须在编译期已知因为编译器需要据此在栈上或堆上预留空间。枚举变体判别式enum variant discriminants判别式的值必须已知以保证任意两个变体不会拥有相同的 discriminant。模式patterns模式中的常量必须已知以便检查模式是否重叠overlapping patterns。除了上述不得不做的场景常量求值还能被主动用来削减运行时的工作量与二进制体积把复杂运算在编译期预先算好只把结果存进产物运行时直接读取。典型如const声明的大表、const fn参与的编译期计算等。文档将这些使用场景归纳为两大类影响类型系统influencing the type system数组长度、枚举判别式、const 泛型参数等——它们的结果直接参与类型检查与泛型实例化。仅为预计算运行时表达式precompute expressions to be used at runtime把编译期算好的结果嵌入到运行时代码中与类型系统无关。这一分类直接决定了结果表示方式的不同类型系统常量要求结果能被编译器进一步审视因此求值为 ValTree而运行时预计算只需要最终值因此求值为 MIR 常量值。const_eval_* 入口TyCtxt上的三个包装函数常量求值通过TyCtxt的const_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::PseudoCanonicalInputtcx, GlobalIdtcx) - EvalToAllocationRawResulttcx { desc { const-evaluating checking {}, key.value.display(tcx) } cache_on_disk } // 底层查询求值为 MIR 常量值opaque blob query eval_to_const_value_raw(key: ty::PseudoCanonicalInputtcx, GlobalIdtcx) - EvalToConstValueResulttcx { desc { simplifying constant for the type system {}, key.value.display(tcx) } depth_limit cache_on_disk } // 底层查询求值为类型级常量ValTree query eval_to_valtree(key: ty::PseudoCanonicalInputtcx, GlobalIdtcx) - EvalToValTreeResulttcx { ... }值得注意的细节三个查询的键key统一为PseudoCanonicalInputGlobalId即规范化后的GlobalId 求值环境便于查询缓存去重。eval_to_const_value_raw与eval_to_valtree都标注了Do not call this directly警告要求调用方必须经由TyCtxt的包装函数如const_eval_poly、const_eval_resolve、const_eval_instance、const_eval_global_id间接调用从而保证 span 信息、region 擦除、规范化等前置处理被正确执行。eval_to_const_value_raw额外带有depth_limit说明它受求值深度限制保护eval_to_allocation_raw与eval_static_initializer均带cache_on_disk允许跨编译会话缓存结果。入口函数的实现细节以const_eval_global_id为例它在调用底层查询前做了两件关键工作见 queries.rspub fn const_eval_global_id( self, typing_env: ty::TypingEnvtcx, cid: GlobalIdtcx, span: Span, ) - EvalToConstValueResulttcx { // 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) } }生命周期无关化常量求值不应依赖任何生命周期因此通过erase_and_anonymize_regions擦除并匿名化 region使查询键可被稳定缓存span 修正若调用方提供了非 dummy 的 span则用self.at(span)把 span 附加到求值错误上让报错能指向实际使用常量的位置。const_eval_global_id_for_typeck见 queries.rs则更复杂它调用eval_to_valtree后还需把ValTreeCreationError分类处理——NonSupportedType交给调用方决定NodesOverflowvaltree 节点数超限、InvalidConst、CyclicConst则会发出对应的诊断错误。此外它还会在成功求值后检查常量是否非法依赖了泛型参数仅在未启用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::TypingEnvtcx且各 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 GlobalIdtcx { /// 对于常量或 static是该条目自身的 Instance /// 对于 promoted global则是其所属函数的 Instance。 pub instance: ty::Instancetcx, /// promoted global 在其所属函数 mir::Body 的 promoted 表中的索引。 pub promoted: Optionmir::Promoted, }GlobalId由两部分构成一个Instance它要么引用一个常量或 static此时promoted为None要么引用一个函数此时promoted为Some(index)索引指向该函数 MIR 中Promoted表内的某一项可选的分支promoted: Optionmir::Promoted。promoted即提升常量——被提升到static的临时表达式例如42、format!(...)的某些中间值它们不是独立条目而是挂在所属函数的 MIR 上因此需要函数 Instance promoted 索引二元组寻址。GlobalId派生了一整套编码/哈希 traitCopy, Clone, Debug, Eq, PartialEq, Hash, TyEncodable, TyDecodable, StableHash, TypeFoldable, TypeVisitable说明它可以作为查询键在跨会话缓存on-disk cache与增量编译中被序列化使用。为了构造GlobalId编译器内部提供了若干辅助函数同样位于 queries.rsconst_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::TooGeneric或ErrorHandled::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.rspub type EvalToConstValueResulttcx ResultConstValue, ErrorHandled; pub type EvalToValTreeResulttcx ResultValTreetcx, ValTreeCreationErrortcx;MIR 常量值MIR constant valuemir::ConstValuemir::ConstValue是求值结果的底层字节级表示。rustc-dev-guide 的 mir/index.md 一节说明求值产生的是按单个字节组织、存放在内存中的低层表示称为indirect间接常量mir::ConstValue::Indirect。但一切都放内存效率太低因此ConstValue为可以直接写成字面量的常见值提供了优化变体整数、浮点数、char、bool乃至string literals与bbyte 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_interpreterInterpCx 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_value成ConstValue其中还有一个retry_codegen_mode_with_postanalysis的重试机制用于处理 typing mode 相关的差异eval_static_initializer_provider以tcx.is_static断言开头用Instance::mono构造单态化实例后直接调用eval_in_interpretereval_to_allocation_raw_provider的关键断言是promoted.is_some() || !tcx.is_static(...)——即该查询不允许直接求值 static因为 static 在概念上是位置place而非值直接求值会破坏指针同一性pointer identity。这与turn_into_const_value中eval_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 的断言。结合源码原因可以总结为三点static 是位置而非值static 拥有稳定的内存地址与指针身份其值在编译期尚未完全定型例如STATIC的地址要等链接期确定。eval_to_allocation_raw_provider的注释明确写道statics are conceptually places, not values — so what we do here could break pointer identity。初始化器单独求值static 的初始化器由eval_static_initializer查询专门计算返回EvalStaticInitializerRawResult即初始化的内存分配见 compiler/rustc_middle/src/queries.rs。该查询还标注了separate_provide_extern与feedable允许跨 crate 提供与外部喂入。访问权限不同求值器在访问 static 时会区分能否访问可变全局CanAccessMutGlobalmk_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.mdValTree 与 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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价