Move Prover 良基递归引理应用:decreases 终止度量的设计与实现 Move Prover 良基递归引理应用decreases 终止度量的设计与实现【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core导读本文围绕 Move Provermove-prover规范语言中引理lemma递归应用所缺失的良基well-founded终止检查展开完整讲解decreases声明子句的设计动机、默认终止度量规则、递归组recursion group的构建方式以及证明义务的展开形式。读者将理解为什么一个「自我应用」的假引理曾经能够通过验证掌握decreases exp、decreases (e1, ..., en)的书写规范并能够根据仓库中 move-model 的源码实现复现每一个检查点与错误诊断。本文以仓库设计文档 third_party/move/move-prover/doc/dev/lemma_well_foundedness.md 为骨架结合其对应的实现与测试如 module_builder.rs、spec_translator.rs、lemma_wf.move 及基线 lemma_wf.exp逐层印证。问题背景未被守卫的递归引理Move Prover 的证明语言允许在一个引理的 proof 块内apply另一个引理包括应用它自己。翻译器中的expand_lemma_apply见 spec_translator.rs对每一次应用都做同样的展开assert requires(args); // 先断言前置条件 assume ensures(args); // 再假定后置条件问题在于当这个apply发生在引理L自己的证明内部时展开会在正在证明L的过程中直接假定L的结论并且没有任何约束把args与L的参数关联起来。换言之有守卫的递归guarded recursion是真正的归纳法因为递归调用发生在某个参数确实变小且有下界的路径上循环递归circular recursion则被静默接受——证明器既不报错也没有任何路径条件约束递归参数。设计文档给出了两个今天确实能通过验证的反例lemma probe_same(base: num, exp: num) { ensures base 2 exp 0 math_pow(base, exp) 1; // false } proof { apply probe_same(base, exp); } // verifies lemma probe_ascending(base: num, exp: num) { ensures base 2 exp 0 math_pow(base, exp) 1; // false } proof { apply probe_ascending(base, exp 1); } // verifies两个假引理都能通过验证分别对应「参数不变」和「参数反而增大」两种无守卫情形而一个声称相同结论、但没有 proof 的控制引理control lemma会被拒绝——这说明检查器本身是正常工作的唯一被放过的就是未加守卫的递归。而假引理一旦被接受就可以被应用到任何地方从而击穿整个证明体系soundness。合法用法真正的归纳递归应用本身是证明器唯一的归纳机制且非常常见。文档给出的合法示例是lemma math_pow_pos(base: num, exp: num) { ensures base 1 exp 0 math_pow(base, exp) 1; } proof { if (exp 0) { apply math_pow_pos(base, exp - 1); } }这里exp - 1严格小于exp且守卫条件exp 0提供了下界是货真价实的结构归纳。仓库测试 lemma_wf.move 中的pow_pos引理与math_pow函数exp 0时返回 1否则递归调用math_pow(base, exp - 1) * base正是这个模式的完整可运行版本。设计目标Dafny 式终止度量限定在既有证明语句内设计方案非常明确每个递归引理都有一个终止度量termination measure要么显式声明要么使用默认值同一递归组内每次引理应用都必须让度量严格下降。这正是 Dafny 的处理方式但被限制在证明语言已有的语句apply/forall ... apply之上因此后端无需任何改动。表面语法decreases子句lemma L(params) { decreases e; // 度量单个整数表达式或 decreases (e1, ..., en); // 元组按字典序lexicographic比较 requires ...; ensures ...; } proof { ... }几个关键语法约束decreases是已有的ConditionKind即ConditionKind::Decreases。当前实现中def_ana_spec_block_member对除引理外的所有上下文仍然拒绝它——在 module_builder.rs 中可以读到if matches!(kind, ConditionKind::SucceedsIf) || (kind ConditionKind::Decreases !self.is_lemma_context(context)) { self.parent.error(loc, condition kind is not supported); return; }即decreases在引理的条件块中变为合法在其他上下文函数、结构体等中维持原有的 condition kind is not supported 拒绝。解析器在decreases之后已经接受一个表达式因此多分量度量写成元组表达式即可每个分量必须是整数类型。每个引理最多一个decreases。实现中对decreases条件的收集与去重见 module_builder.rs将条件按ConditionKind::Decreases划分出来若多于一条则报错 at most onedecreasesclause per lemma。从 module_builder.rs 的类型检查代码还可以看到decreases表达式的具体处理单表达式视为单分量元组表达式拆成多个分量至少需要一个分量每个分量都必须有整数类型。默认度量整数参数元组没有写decreases的递归引理会得到 Dafny 风格的默认度量按声明顺序排列的所有整数类型参数num、u8..u256。这在 ast.rs 的LemmaDecl::measure中实现优先返回声明的decreases否则过滤出所有is_number()的参数作为临时表达式measure_arity返回有效度量的分量个数。is_number覆盖U8/U16/U32/U64/U128/U256/I8/I16/I32/I64/I128/I256/Num全部整数类型见 ty.rs。对上面的math_pow_pos默认度量是(base, exp)而递归应用math_pow_pos(base, exp - 1)确实使它下降base不变exp - 1 exp且守卫条件给出0 exp。因此最常见的exp - 1递归模式无需任何注解。默认规则的两个边界情形在非首个变化的参数上递归例如L(n, i)在i上递归而n随之改变默认度量(n, i)不下降检查失败。诊断信息会打印默认的度量元组作者据此知道应写decreases i。没有任何整数参数默认度量为空必须显式声明decreases否则报模型错误。在同一递归组内未写decreases的成员使用各自默认值后续的「元数一致」规则作用于最终元组因此默认度量与显式声明混用是允许的只要元数一致。递归组模块内引理调用图的强连通分量递归组的构建方式为模块内每个引理建立节点对每个引理 proof 中的Proof::Applyapply L(args)与Proof::ForallApplyforall ... apply L(args)添加边L - L边收集必须穿透IfElse、Block、Post、Split等复合 proof 语句Proof::Calc、Proof::Assert、Proof::Assume、Proof::Let等不含应用见collect_lemma_appliesmodule_builder.rs用强连通分量SCC算法实现使用petgraph的tarjan_sccpetgraph已是 workspace 依赖见 move-model/Cargo.toml求出递归组。一个引理是递归的当且仅当它所在的组多于一个成员或存在自环self-edge。跨模块的引理不可能同组proof 只能 apply 已经声明的引理而模块之间构成 DAG。证明义务assert path_cond m(args) _lex m(params)设引理L的度量为m(params) (e1, ..., en)在L的 proof 中出现apply L(args)且L与L同组、度量为m。递归组按一个统一的度量形状检查组内每个成员必须声明相同元数的decreases互递归的通常约定。在应用点、现有的 requires 断言之前发射assert path_cond m(args) _lex m(params)其中_lex是num上的字典序通过给「下降的那个分量」加下界来保证良基在应用点展开为(e1 e1 0 e1) || (e1 e1 (e2 e2 0 e2)) || ...其中ei是外层引理的分量ei是应用引理在args处的分量。每个严格下降步都发生在「步进前至少为 0」的分量上因此任何分量都不能无限下降而某个靠后的分量下降时它之前的所有分量都相等——这正是字典序。对应的展开代码mk_lexicographic_decrease在 spec_translator.rs 中逐分量生成Lt、Le(0, ·)与相等比较的合取/析取。三种错误形态错误类型触发条件诊断示例空度量模型错误递归引理无decreases且无整数参数recursive lemmaLneeds adecreasesclause全称应用模型错误组内成员出现在forall ... apply中forall ... applycannot apply a lemma from the same recursion group; useapplyat a decreasing instance元数不一致模型错误组内成员的度量分量数不同lemmaLhas a measure of N component(s), but its recursion group uses M全称应用被禁止的原因量化形式会在每个绑定处实例化引理不存在「单个递减实例」可供检查在度量下展开它正是该检查要排除的无界递归。而组外引理的全称应用不受影响。此外非递归引理上写decreases会被接受但忽略警告可选。证明义务的落点与诊断该义务与 requires 检查一样是ProofAction::Assert因此Boogie 后端与字节码流水线完全无需改动诊断直接出现在apply处lemma application does not decrease the measure (base, exp)打印的是声明或默认的度量元组。实现细节spec_translator.rs通过self.enclosing_lemma()spec_translator.rs拿到外层引理——引理 proof 是以FunctionKind::Lemma函数形式验证的find_lemma_by_name已能把该函数映射回LemmaDecl若enclosing.module_id qid.module_id且两个recursion_group相同则计算next被应用引理在args处的度量分量与current外层引理的度量分量生成字典序下降断言并用path_cond作守卫后压入 proof action。注意解析时的翻译流程是先处理 requires断言再处理 ensures假定以避免条件声明顺序颠倒时在检查前提之前就假定结论。递归组分析实现要点递归组在每个模块的所有引理 proof 分析完成后一次性计算analyze_lemma_recursion见 module_builder.rs在模块分析末尾 module_builder.rs 处调用并存储在模块数据上。def_ana_lemma在分析时所有引理名都已注册decl_ana 阶段完成因此互递归天然可表示module_builder.rs。对每个 SCC 组的处理逻辑组内成员数 1 或存在自环 → 判定为递归组以组内声明顺序最靠前的成员确定组的度量元数measure_arity对每个成员度量元数为 0 → 报「需要 decreases」模型错误元数与组不一致 → 报元数错误对组内成员的每个应用点若是forall ... apply且目标在组内 → 报全称应用模型错误递归组的编号与每个引理的有效度量保存在LemmaDecl.recursion_group与LemmaDecl.decreases字段上ast.rs。与实现同步的测试基线仓库测试 lemma_wf.move需--language-version2.4完整覆盖了设计文档描述的各个场景其期望基线 lemma_wf.exp 记录了 4 处错误测试引理场景结果pow_pos默认度量(base, exp)上的归纳通过pow_pos_declared显式decreases exp通过circular_sameapply circular_same(n)度量不变错误measure does not decreasecircular_ascendingapply circular_ascending(n 1)度量增大错误measure does not decreaseunboundedapply unbounded(n - 1)无下界错误measure does not decreasesecond_param_defaultapply second_param_default(n 1, i - 1)默认度量(n, i)不下降错误measure does not decreasesecond_param_declared显式decreases i通过lexdecreases (n, i)字典序i重置而n下降通过even_pos/odd_pos互递归各自默认度量下降通过其中值得注意的两点无下界也会失败unbounded(n - 1)虽然是「变小」但字典序展开要求0 n证明器无法从上下文推出n的下界因此同样报错——这体现了良基性检查的完整语义不只是「值变小」。字典序实例lex展示了decreases (n, i)在i可重置、由n承担总体下降时的用法验证了多分量度量的实用价值。设计文档还规划了lemma_induction、lemma_circular、lemma_mutual、lemma_default_wrong_param、lemma_no_measure、lemma_forall_self等一组带.exp基线的测试文件位于move-prover/tests/sources/functional/分别对应默认度量归纳、循环递归报错、互递归与元数不匹配报错、默认度量错参、空度量模型错误、全称自应用模型错误。实现计划与影响范围设计文档给出约 250 行 Rust 及基线文件的实现计划无需任何后端改动解析器parse_lemma_spec_member接受引理块中的decreasesmodule_builder.rs 在引理上下文接受ConditionKind::Decreases、函数上下文保持拒绝拒绝重复声明。模块分析末尾收集各 proof 的引理应用、计算 SCC、填充默认度量后做组级元数检查把lemma_groups与每个引理的有效度量存到模块上。spec_translator.rs 中expand_lemma_apply通过fun_env查找外层引理若目标同组则发射下降断言expand_forall_lemma_apply对组内目标报错。sourcifier.rs支持打印decreases子句保证源码往返round-trip一致——该分支已存在于 sourcifier.rs。上述测试与基线。文档更新用户指南的引理章节以及move-inf技能文本其当前声称「没有引理可以通过归纳证明性质」的说法需要修正。仓库中该功能已按此设计落地decreases的接受与类型检查module_builder.rs、递归组分析与校验module_builder.rs、应用点下降断言spec_translator.rs三处核心代码均可在上述路径中查阅。备选方案对比设计文档还比较了三种被否决的替代方案理解它们有助于把握最终设计的分寸仅显式decreases更易解释但让最常见的exp - 1模式被迫写一个签名本已表达的注解。默认度量覆盖该模式其唯一代价是一个必须打印默认元组的诊断实现已做到。语法守卫检查要求递归apply位于if (p 0)之下且实参为p - k只能覆盖math_pow_pos这一类模式向量上的结构递归、互递归、字典序下降都需要语义检查语法方案无法胜任。完全禁止递归会失去证明器唯一的归纳机制——设计文档特别提到pow 实验的第 4 轮运行在闭式 abort 条件上正需要递归。结语良基递归引理应用检查把 Move Prover 从「任何自我应用的引理都能通过」的脆弱状态收紧为「递归必须沿良基度量下降」的严格证明义务同时以 Dafny 风格的默认度量保住exp - 1这类高频归纳模式零注解的体验。对于希望在自己的 Move 模块中安全使用递归引理的开发者本文给出的decreases语法、默认度量规则、字典序展开与错误形态配合 lemma_wf.move 中的正反示例可以直接作为编写与排查递归引理的手册。【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考