操作的声明与应用
上一节末尾用一行操作式代码展开了链表节点,当时直接使用了现成的操作。本节说明这个工具从何而来:如何将一条健全的局部资源变换封装为操作(operation),用消耗/产出模式描述它,并在声明时卸除证明义务;应用操作时,又如何得到并提交一条完整状态定理。本节以已通过验证的 tutorial/10_reverse_op.ctutorial/10_reverse_op.c 为例。这个程序实现链表的原地反转,其中的每一次视图转换都用操作或通用状态变换函数表达。
上一节示例中的展开引理只需证明一次,此后便可反复使用;但应用引理所需的辅助工作仍要重复。在每个需要展开链表节点的程序点,用户都要完成以下步骤:
- 用当前指针和列表(
v_vv_v、l2l2)实例化引理参数; - 选中正确的资源——是
sll v_v l2sll v_v l2,而不是旁边的sll w_v l1sll w_v l1; - 根据分支中的事实卸除前提
~(v_v == 0i)~(v_v == 0i); - 为展开产生的存在变量确定稳定的名称;
- 把其余资源作为框架保留,并提交完整状态蕴含——若状态有多个分支,还须逐分支处理。
前向证明一节曾为一个程序点手写这套辅助代码。如果有十处需要展开,就要写十份这样的代码;引理的陈述一旦调整,这十处也都要修改。
一份操作模式可以概括为:
O = (name, inputs, consumes, facts, produce)
O = (name, inputs, consumes, facts, produce)
| 部分 | 回答的问题 | 展开非空链表时 |
|---|---|---|
namename |
用什么名称绑定操作、报告诊断信息? | unfold_sll_not_nullunfold_sll_not_null |
inputsinputs |
调用者提供什么? | ptpt |
consumesconsumes |
选择哪些资源? | sll pt lsll pt l |
factsfacts |
需要哪些纯前提? | ~(pt == 0i)~(pt == 0i) |
produceproduce |
选中的资源变成什么? | exists h t q. ...exists h t q. ... |
inputsinputs 与其余部分各有分工:调用者只提供自己已经知道的稳定参数(这里是一个地址);资源内部的未知数据,如逻辑列表和后继指针,则通过匹配从当前状态中取得。
操作用 decl_operationdecl_operation 声明。声明之前,须先定义一个证明回调,其名称为操作名加上 _proof_proof 后缀。下面是本部分一直使用的展开操作,与 10_reverse_op.c10_reverse_op.c 中的声明一致。回调使用后向分离逻辑策略,直接完成该模式所要求的证明:
PROOF thm unfold_sll_not_null_proof(const term goal_tm) { gnode root = gnode_new_with_ccl(goal_tm); gnode g = AUTO_INIT_SLTAC(root)[0]; gnode_list gns = CASES_TAC(g, `l:(int)list`, "P_l"); g = gns[0]; // l == []:与 ~(pt == 0i) 矛盾 g = CONV_WITH_ASMP_SLTAC(g, rewrite_conv, THM_LIST(sll_def)); CONTR_SLTAC(g, "H"); g = gns[1]; // l == a0 :: a1:展开并逐项匹配 g = CONV_WITH_ASMP_SLTAC(g, rewrite_conv, THM_LIST(sll_def)); g = AUTO_HANT_DESTRUCT_SLTAC(g)[0]; g = LIST_EXISTS_SLTAC(g, TERM_LIST(`a0:int`, `a1:(int)list`, `q:addr`)); g = CONV_SLTAC(g, simp_conv(thm_list_n(0))); g = AUTO_FRAME_SLTAC(g); return gnode_prove(root);}decl_operation(unfold_sll_not_null, TERM_LIST(`pt:addr`), term_list_n(1, `sll (KEY pt) (CAPTURE l)`), term_list_n(1, `~(pt == 0i)`), `exists h t q. fact(l == h :: t) ** data_at (field_addr pt Tlist Fhead) Tint h ** data_at (field_addr pt Tlist Ftail) Tptr q ** sll q t`)
PROOF thm unfold_sll_not_null_proof(const term goal_tm) { gnode root = gnode_new_with_ccl(goal_tm); gnode g = AUTO_INIT_SLTAC(root)[0]; gnode_list gns = CASES_TAC(g, `l:(int)list`, "P_l"); g = gns[0]; // l == []:与 ~(pt == 0i) 矛盾 g = CONV_WITH_ASMP_SLTAC(g, rewrite_conv, THM_LIST(sll_def)); CONTR_SLTAC(g, "H"); g = gns[1]; // l == a0 :: a1:展开并逐项匹配 g = CONV_WITH_ASMP_SLTAC(g, rewrite_conv, THM_LIST(sll_def)); g = AUTO_HANT_DESTRUCT_SLTAC(g)[0]; g = LIST_EXISTS_SLTAC(g, TERM_LIST(`a0:int`, `a1:(int)list`, `q:addr`)); g = CONV_SLTAC(g, simp_conv(thm_list_n(0))); g = AUTO_FRAME_SLTAC(g); return gnode_prove(root);}decl_operation(unfold_sll_not_null, TERM_LIST(`pt:addr`), term_list_n(1, `sll (KEY pt) (CAPTURE l)`), term_list_n(1, `~(pt == 0i)`), `exists h t q. fact(l == h :: t) ** data_at (field_addr pt Tlist Fhead) Tint h ** data_at (field_addr pt Tlist Ftail) Tptr q ** sll q t`)
这份模式从上到下依次指定:操作名为 unfold_sll_not_nullunfold_sll_not_null;调用者提供地址 ptpt;消耗一个资源——位于 ptpt 处的 sllsll,其逻辑列表被捕获为 ll;应用前须满足纯前提 ~(pt == 0i)~(pt == 0i);选中的资源变为结构分解事实 l == h :: tl == h :: t、两个字段单元和尾部链表。回调证明的义务本质上就是分离逻辑的后向证明中的展开引理,在声明时证明一次,此后在任何使用点都无须重新证明。
KEYKEY、CAPTURECAPTURE、DEFERDEFER 在逻辑中都是恒等函数,只对匹配过程有意义。构造健全性目标时,证明库先擦除它们:
erase(CAPTURE t) = erase(KEY t) = erase(DEFER t) = t
erase(CAPTURE t) = erase(KEY t) = erase(DEFER t) = t
设 consumesconsumes 为 ,factsfacts 为 ,produceproduce 为 ,生成的目标为:
forall FV. f1 ==> ... ==> fn ==> (erase(c1) ** ... ** erase(cm) |-- Q)
forall FV. f1 ==> ... ==> fn ==> (erase(c1) ** ... ** erase(cm) |-- Q)
其中,所有自由模式变量都由全称量词绑定。接口不保证量词顺序。回调因此通过 goal_tmgoal_tm 接收目标,并应证明收到的项,不能自行重建一个看起来相同的目标。
decl_operationdecl_operation 展开为一次 operation_newoperation_new 调用:它检查模式的合法性,以生成的目标为参数调用一次证明回调,再检查返回的定理。该定理不得携带未卸除的假设,结论必须与目标 α-等价。
折叠空链表既不需要输入参数,也不需要纯前提:
decl_operation(fold_sll_null, term_list_n(0), term_list_n(1, `emp`), term_list_n(0), `sll 0i []`)
decl_operation(fold_sll_null, term_list_n(0), term_list_n(1, `emp`), term_list_n(0), `sll 0i []`)
它表达局部资源变换 emp |-- sll 0i []emp |-- sll 0i []。初次了解操作值时,可以用这个简单例子观察其中的 inputsinputs / consumesconsumes / factsfacts / produceproduce 字段和 lemma=<proved>lemma=<proved> 标记。
consumesconsumes 中的每一项有两种作用:既是引理前件中的一个合取项,也是描述如何在分支中找到对应资源的匹配模式。匹配从左到右进行,使用三种标记。
假设分支中同时存在
sll w_v l1 ** sll v_v l2
sll w_v l1 ** sll v_v l2
而操作以 pt := v_vpt := v_v 调用。模式 sll (KEY pt) (CAPTURE l)sll (KEY pt) (CAPTURE l) 先尝试第一个候选 sll w_v l1sll w_v l1:这要求证明 v_v = w_vv_v = w_v。由于无法证明这个等式,该候选被排除。再尝试第二个候选时,v_v = v_vv_v = v_v 成立,于是选中 sll v_v l2sll v_v l2。
KEY tKEY t 通过可证明的等式匹配资源,而非比较字符串。证明库按以下顺序尝试建立等式:精确相等;两边都是整数时使用整数算术;最后是通过 set_normalization_rulesset_normalization_rules 配置的规范化规则。 KEYKEY 只筛选候选,并不保证唯一性;若多个资源都满足模式,选中的仍是最前面的那个。模式因此应以稳定的语义身份作为键:地址、端点、所有者。
选中 sll v_v l2sll v_v l2 之后,CAPTURE lCAPTURE l 产生绑定 l := l2l := l2。 CAPTURE xCAPTURE x 必须引入一个尚未绑定的模式变量;每个变量至多捕获一次,之后的消耗项可以直接使用它,或在 KEYKEY、DEFERDEFER 中引用它。捕获读取的是状态中已经存在的逻辑值,不会凭空引入例证。
设 pp 与 vv 都已绑定,模式
data_at (KEY p) Tint (DEFER v)
data_at (KEY p) Tint (DEFER v)
与 data_at p Tint ndata_at p Tint n 匹配时,先由 KEY pKEY p 定位资源;DEFER vDEFER v 只检查 vv 与 nn 的类型相容,并记录待处理的等式 v = nv = n。只有当所有消耗项都选择成功之后,证明库才尝试证明这些推迟的等式:先检查是否精确相等,再用 solve_eqsolve_eq 求解;仍无法证明的等式会登记为待证条件。
| 标记 | 选择阶段 | 主要用途 |
|---|---|---|
KEY tKEY t |
必须证明与候选相等 | 按语义身份筛选资源 |
CAPTURE xCAPTURE x |
把候选子项绑定给新变量 | 取得未知的逻辑数据或抽象模型 |
DEFER tDEFER t |
只要求类型相容 | 选择后再证明等式,必要时留下待证条件 |
普通变量也可以出现在模式中,但必须已经由输入参数或此前的捕获绑定;匹配时,只接受与其绑定值精确相等的项。
折叠一个非空节点需要三个资源,后面的模式使用前面的捕获结果:
decl_operation(fold_sll_not_null, TERM_LIST(`pt:addr`), term_list_n(3, `data_at (field_addr (KEY pt) Tlist Fhead) Tint (CAPTURE h)`, `data_at (field_addr (KEY pt) Tlist Ftail) Tptr (CAPTURE q)`, `sll (KEY q) (CAPTURE t)` ), term_list_n(1, `~(pt == 0i)`), `sll pt (h :: t)`)
decl_operation(fold_sll_not_null, TERM_LIST(`pt:addr`), term_list_n(3, `data_at (field_addr (KEY pt) Tlist Fhead) Tint (CAPTURE h)`, `data_at (field_addr (KEY pt) Tlist Ftail) Tptr (CAPTURE q)`, `sll (KEY q) (CAPTURE t)` ), term_list_n(1, `~(pt == 0i)`), `sll pt (h :: t)`)
匹配依次进行:以 ptpt 为键选中头字段,捕获头元素 hh;以 ptpt 为键选中尾字段,捕获该单元的当前值 qq;再以刚捕获的 qq 为键选中后继链表,捕获其内容 tt;产出 sll pt (h :: t)sll pt (h :: t)。它的证明回调与之前作用相同,只是这次证明的是列表引理的折叠方向。
每个消耗项都从尚未使用的顶层合取项中选择第一个匹配的资源;一旦选定,就不会重新选择:
consume_1 选中第一个匹配的资源 |consume_2 在剩余资源中继续选择 |后续失败不会让 consume_1 回头改选其他候选
consume_1 选中第一个匹配的资源 |consume_2 在剩余资源中继续选择 |后续失败不会让 consume_1 回头改选其他候选
这种确定性的贪心策略使匹配行为可以预测,也要求模式使用足以区分候选资源的 KEYKEY。
理解这三种匹配标记后,就可以列出 operation_newoperation_new 在声明时的全部检查:
inputsinputs中每一项都是 HOL 变量,并且彼此不同;- 从左到右扫描
consumesconsumes:CAPTURE xCAPTURE x必须引入新变量;普通变量以及KEYKEY、DEFERDEFER参数中的自由变量必须已经绑定;模式中不支持 λ-抽象; consumesconsumes、factsfacts、produceproduce中的每个自由变量都来自输入参数或捕获;- 证明回调返回不带假设、结论与生成目标 α-等价的定理。
声明时还没有具体的符号状态,因此不会检查某个程序点是否持有所需资源、某个分支是否满足所需事实;这些检查在应用阶段完成。
PROOF void apply_operation_st( const operation op, const term_list arguments);PROOF void apply_operation_named_st( const operation op, const term_list arguments, const term_list existential_names);
PROOF void apply_operation_st( const operation op, const term_list arguments);PROOF void apply_operation_named_st( const operation op, const term_list arguments, const term_list existential_names);
apply_operation_stapply_operation_st 把操作应用到当前状态的每一个分支。 apply_operation_named_stapply_operation_named_st 执行相同的变换,并按位置重命名产出断言最外层的存在量词前缀。
对每个分支,应用接口依次完成:
- 按位置把
argumentsarguments绑定到inputsinputs,检查数量与类型; - 为输入参数以外的模式变量选择不与该分支冲突的新名称,避免名为
ll的模式变量与状态中的同名变量冲突; - 按
consumesconsumes的顺序,从左到右在尚未使用的顶层合取项中贪心选择资源,并收集捕获结果; - 用
solve_eqsolve_eq处理各DEFERDEFER等式,使模式与实际资源对齐; - 用输入参数与捕获结果实例化操作携带的封闭引理;
- 根据该分支的顶层事实,先用命题推理、再用整数算术证明
factsfacts中的纯前提,并将其卸除; - 用
local_applylocal_apply提升得到的局部蕴含:未选中的资源作为框架保留,外层存在量词保持不变; - 把各分支的定理合并为一条完整状态蕴含,在所有分支都成功之后,只调用一次
set_symbolic_stateset_symbolic_state提交。
不同分支可以捕获不同的具体值、产生不同的后置状态,但操作必须能应用于每一个分支。
consumesconsumes 到 produceproduce 的局部变换;框架规则保留每个分支中未参与的资源;析取单调性合并各分支。最终提交的是一条封闭的完整状态蕴含,不会绕过证明直接修改符号状态。PROOF apply_operation_named_st( unfold_sll_not_null, TERM_LIST(`v_v:addr`), TERM_LIST(`hd:int`, `tl:(int)list`, `nxt:addr`));
PROOF apply_operation_named_st( unfold_sll_not_null, TERM_LIST(`v_v:addr`), TERM_LIST(`hd:int`, `tl:(int)list`, `nxt:addr`));
这里 v_vv_v 是用于实例化输入参数 ptpt 的实参,而 hdhd、tltl、nxtnxt 只是产出断言最外层三个存在变量的名称。它们的值来自操作产生的存在量词,并非用户提供的例证。名称向量的长度必须等于产出断言最外层存在量词绑定子的数量,各名称的类型须与相应绑定子的类型一致。各名称还须彼此不同,且不得捕获产出体中的自由变量;所有分支使用同一个名称向量。这些名称需要保持稳定,因为随后的 assert_fact_stassert_fact_st、abbrev_stabbrev_st 等证明代码会直接引用它们。证明块可以直接使用这些名称,但由编译器在 C 作用域中处理的标注不能直接使用,例如后续调用上的 [[cst::where]][[cst::where]] 或内层循环的不变式。使用这些标注前,需要先用 [[cst::given(`hd:int`, `nxt:addr`)]];[[cst::given(`hd:int`, `nxt:addr`)]]; 从当前状态中绑定这些名称,做法见函数契约、循环不变式与断言。
以下是已验证的 10_reverse_op.c10_reverse_op.c 中的完整函数(其中三个操作已在上文声明;另一个操作 unfold_sll_nullunfold_sll_null 与 fold_sll_nullfold_sll_null 对称,在前提 pt == 0ipt == 0i 下把 sll pt lsll pt l 转换为 fact(l == [])fact(l == [])):
PROOF void forget_facts_st(term_list facts) { for (int i = 0; i < vector_size(facts); i++) { forget_fact_st(facts[i]); }}struct list *reverse(struct list *pt) PARAM(`l:(int)list`) REQUIRE(`sll pt l`) ENSURE(`sll __return (REVERSE l)`){ PROOF set_default_types(TERM_LIST( `l:(int)list`, `l1:(int)list`, `l2:(int)list`, `w_v:int`, `v_v:int`, `pt__pre:int` )); struct list *w, *v; w = (void *)0; PROOF { // 产出已反转的前缀链表 `l1`(初始为 `[]`), // 由 `w_v`(初始为 NULL)指向 apply_operation_st(fold_sll_null, NULL); abbrev_st(`w_v = 0i`); abbrev_st(`l1 = []`); } v = pt; PROOF { // 为尚未处理的后缀链表命名 `l2`(初始为 `l`), // 由 `v_v`(初始为 `pt__pre`)指向 abbrev_occurrences_st( `v_v == pt__pre`, TERM_LIST( `data_at v__addr Tptr v_v`, `sll v_v l` )); abbrev_st(`l2 = l`); assert_fact_st( `l == (REVERSE l1) ++ l2`, simp_conv, THM_LIST(REVERSE_def, APPEND_def)); } PROOF forget_facts_st(TERM_LIST( `l2 == l`, `v_v == pt__pre`, `l1 == []`, `w_v == 0i` )); PROOF term loop_inv = get_symbolic_state(); while (v) INV(loop_inv) { PROOF apply_operation_named_st( unfold_sll_not_null, TERM_LIST(`v_v:addr`), TERM_LIST(`hd:int`, `tl:(int)list`, `nxt:addr`)); struct list *t = v->tail; v->tail = w; PROOF apply_operation_st( fold_sll_not_null, TERM_LIST(`v_v:addr`) ); PROOF forget_fact_st(`~(v_v == 0i)`); PROOF { assert_fact_st( `l == (REVERSE (hd :: l1)) ++ tl:(int)list`, simp_conv, THM_LIST(REVERSE_def, APPEND_def, gsym_rule(APPEND_ASSOC))); rename_hexists_st("[l1'/l1] [l2'/l2]"); abbrev_st(`l1 = hd :: l1':(int)list`); abbrev_st(`l2 = tl:(int)list`); } PROOF forget_facts_st(TERM_LIST( `l2 == tl`, `l1 == hd :: l1'`, `l == REVERSE l1' ++ l2'`, `l2' == hd :: l2` )); w = v; PROOF rename_locals_st(); v = t; PROOF rename_locals_st(); PROOF { substitute_st(sym_rule(assume_rule(`w_v == v_v'`))); substitute_st(sym_rule(assume_rule(`v_v == nxt`))); forget_facts_st(TERM_LIST(`w_v == v_v'`, `v_v == nxt`)); } } PROOF apply_operation_st(unfold_sll_null, TERM_LIST(`v_v:addr`)); PROOF assert_fact_st(`l1 == (REVERSE l):(int)list`, simp_conv, THM_LIST(APPEND_NIL, REVERSE_REVERSE) ); PROOF substitute_st(assume_rule(`l1 == (REVERSE l):(int)list`)); return w;}
PROOF void forget_facts_st(term_list facts) { for (int i = 0; i < vector_size(facts); i++) { forget_fact_st(facts[i]); }}struct list *reverse(struct list *pt) PARAM(`l:(int)list`) REQUIRE(`sll pt l`) ENSURE(`sll __return (REVERSE l)`){ PROOF set_default_types(TERM_LIST( `l:(int)list`, `l1:(int)list`, `l2:(int)list`, `w_v:int`, `v_v:int`, `pt__pre:int` )); struct list *w, *v; w = (void *)0; PROOF { // 产出已反转的前缀链表 `l1`(初始为 `[]`), // 由 `w_v`(初始为 NULL)指向 apply_operation_st(fold_sll_null, NULL); abbrev_st(`w_v = 0i`); abbrev_st(`l1 = []`); } v = pt; PROOF { // 为尚未处理的后缀链表命名 `l2`(初始为 `l`), // 由 `v_v`(初始为 `pt__pre`)指向 abbrev_occurrences_st( `v_v == pt__pre`, TERM_LIST( `data_at v__addr Tptr v_v`, `sll v_v l` )); abbrev_st(`l2 = l`); assert_fact_st( `l == (REVERSE l1) ++ l2`, simp_conv, THM_LIST(REVERSE_def, APPEND_def)); } PROOF forget_facts_st(TERM_LIST( `l2 == l`, `v_v == pt__pre`, `l1 == []`, `w_v == 0i` )); PROOF term loop_inv = get_symbolic_state(); while (v) INV(loop_inv) { PROOF apply_operation_named_st( unfold_sll_not_null, TERM_LIST(`v_v:addr`), TERM_LIST(`hd:int`, `tl:(int)list`, `nxt:addr`)); struct list *t = v->tail; v->tail = w; PROOF apply_operation_st( fold_sll_not_null, TERM_LIST(`v_v:addr`) ); PROOF forget_fact_st(`~(v_v == 0i)`); PROOF { assert_fact_st( `l == (REVERSE (hd :: l1)) ++ tl:(int)list`, simp_conv, THM_LIST(REVERSE_def, APPEND_def, gsym_rule(APPEND_ASSOC))); rename_hexists_st("[l1'/l1] [l2'/l2]"); abbrev_st(`l1 = hd :: l1':(int)list`); abbrev_st(`l2 = tl:(int)list`); } PROOF forget_facts_st(TERM_LIST( `l2 == tl`, `l1 == hd :: l1'`, `l == REVERSE l1' ++ l2'`, `l2' == hd :: l2` )); w = v; PROOF rename_locals_st(); v = t; PROOF rename_locals_st(); PROOF { substitute_st(sym_rule(assume_rule(`w_v == v_v'`))); substitute_st(sym_rule(assume_rule(`v_v == nxt`))); forget_facts_st(TERM_LIST(`w_v == v_v'`, `v_v == nxt`)); } } PROOF apply_operation_st(unfold_sll_null, TERM_LIST(`v_v:addr`)); PROOF assert_fact_st(`l1 == (REVERSE l):(int)list`, simp_conv, THM_LIST(APPEND_NIL, REVERSE_REVERSE) ); PROOF substitute_st(assume_rule(`l1 == (REVERSE l):(int)list`)); return w;}
(set_default_typesset_default_types 为列出的变量名声明默认的 HOL 类型,此后的引用项可以省去大部分类型标注。)
入口处的两个证明块通过状态变换构造循环不变式,无须显式写出不变式。最简单的操作 fold_sll_nullfold_sll_null 把 empemp 变为空的已反转前缀 sll 0i []sll 0i [];几次 abbrev_stabbrev_st 与 abbrev_occurrences_stabbrev_occurrences_st 调用为相关值引入逻辑名称 w_vw_v、l1l1、v_vv_v、l2l2;assert_fact_stassert_fact_st 用化简证明模型等式 l == (REVERSE l1) ++ l2l == (REVERSE l1) ++ l2;forget_facts_stforget_facts_st 再清除引入名称时使用的临时定义等式。剩下的正是循环要维护的状态,于是代码直接保存快照:
PROOF term loop_inv = get_symbolic_state();while (v) INV(loop_inv)
PROOF term loop_inv = get_symbolic_state();while (v) INV(loop_inv)
进入循环体后,循环条件提供了 fact(~(v_v == 0i))fact(~(v_v == 0i)),因此可以应用 unfold_sll_not_nullunfold_sll_not_null。它通过 KEYKEY 排除 sll w_v l1sll w_v l1,选中 sll v_v l2sll v_v l2,再根据该事实卸除前提,展开当前节点,并将新的存在变量命名为 hdhd、tltl、nxtnxt。符号执行随即可以检查读取语句。 struct list *t = v->tail;struct list *t = v->tail; 之后,引擎报告:
旧的模型事实原样保留;展开只增加了结构分解事实 l2 == hd :: tll2 == hd :: tl。存储语句 v->tail = w;v->tail = w; 是真正的程序动作。执行后,尾单元变为 data_at (field_addr v_v Tlist Ftail) Tptr w_vdata_at (field_addr v_v Tlist Ftail) Tptr w_v,其余状态保持不变。
随后,fold_sll_not_nullfold_sll_not_null 消耗头单元和尾单元,并捕获尾单元的当前值 w_vw_v,再以这个值为键消耗 sll w_v l1sll w_v l1。它检查 ~(v_v == 0i)~(v_v == 0i),产出 sll v_v (hd :: l1)sll v_v (hd :: l1),同时将 sll nxt tlsll nxt tl 作为框架原样保留。尾单元的当前值决定了折叠时使用哪条链表,因此同一份折叠模式既适用于这个循环,也适用于 PSI 一节的访问器胶囊。
循环体的其余部分负责维护纯模型和名称,这些簿记工作全部由通用状态变换函数完成:
forget_fact_st(~(v_v == 0i))forget_fact_st(~(v_v == 0i))移除不变式并不携带的前提;assert_fact_stassert_fact_st用REVERSEREVERSE/APPENDAPPEND引理化简证明新的模型等式l == (REVERSE (hd :: l1)) ++ tll == (REVERSE (hd :: l1)) ++ tl——这是嵌在操作式流程中的一个声明式步骤;rename_hexists_st("[l1'/l1] [l2'/l2]")rename_hexists_st("[l1'/l1] [l2'/l2]")将上一轮的列表改名,两个abbrev_stabbrev_st再用l1 = hd :: l1'l1 = hd :: l1'与l2 = tll2 = tl为新一轮的列表命名;forget_facts_stforget_facts_st随后清除临时等式;- 每次游标赋值之后,
rename_locals_strename_locals_st重新为 C 变量的当前值命名:为单元的新值引入新的w_vw_v或v_vv_v绑定子,并将原绑定子改名为v_v'v_v'等历史名称; - 两个
substitute_stsubstitute_st将其余资源中的历史名称替换为新的游标名称,最后一个forget_facts_stforget_facts_st清除改名等式。
w = v;w = v; 之后,引擎显示的资源已经具有不变式的形状,其中使用的是更新后的列表。sll v_v l1sll v_v l1 是加长后的前缀,sll nxt l2sll nxt l2 是缩短后的后缀:
游标与名称对齐之后,状态与保存的 loop_invloop_inv 匹配。这正是引擎在循环体末尾检查的那条蕴含。(块内局部变量 tt 的单元在代码块结束时由符号执行自动回收,不需要任何证明代码删除它。)
循环退出时,分支已知 v_v == 0iv_v == 0i,unfold_sll_nullunfold_sll_null 把 sll v_v l2sll v_v l2 转换为 fact(l2 == [])fact(l2 == [])。 assert_fact_stassert_fact_st 随后由 l == REVERSE l1 ++ l2l == REVERSE l1 ++ l2、l2 == []l2 == [] 和 REVERSE_REVERSEREVERSE_REVERSE 证明 l1 == REVERSE ll1 == REVERSE l;substitute_stsubstitute_st 把 sll w_v l1sll w_v l1 改写为 sll w_v (REVERSE l)sll w_v (REVERSE l),恰好是 return w;return w; 的后置条件。
l == (REVERSE (hd :: l1)) ++ tll == (REVERSE (hd :: l1)) ++ tl、l1 == REVERSE ll1 == REVERSE l 由用户陈述,交给 assert_fact_stassert_fact_st 证明。资源协议采用操作式写法,纯目标采用声明式写法,两种风格可以自由交错。如果一个资源动作在多个程序点反复出现,具有清晰且可证明的消耗/产出协议,需要从状态中捕获未知参数,并根据地址、端点、所有者等语义身份选择资源,那么它通常适合声明为操作。典型例子包括:数据结构节点的展开与折叠、数组片段的打开与关闭、抽象表示之间的转换、消耗线性令牌并产出其后继。
反过来,以下情形通常不需要新的操作:
- 一次性的局部蕴含——直接用
apply_hconv_stapply_hconv_st应用; - 全局的重写或代换——使用
rewrite_strewrite_st及其同类接口; - 只为整理状态而做的一次性存在变量引入——
abbrev_stabbrev_st; - 中间夹有 C 内存访问的多个资源阶段——它们永远不是一个操作。
| 现象 | 首先检查 |
|---|---|
| 声明失败 | 变量来源、消耗模式的结构,以及回调返回定理的假设与结论 |
| 实参数量或类型错误 | argumentsarguments 是否与 inputsinputs 逐项对应? |
| 找不到要消耗的资源 | KEYKEY 是否足够明确?资源是否为尚未使用的顶层合取项? |
| 匹配到非预期的资源 | 模式是否依赖了贪心选择的顺序,而实际需要添加一个按语义身份筛选的 KEYKEY? |
| 出现待证条件 | 纯前提或 DEFERDEFER 等式为何无法自动证明? |
| 重命名接口失败 | 名称的数量、类型、互异性与变量捕获 |
| 部分分支成功、整体失败 | 操作能否应用于每一个顶层分支? |
排查时依次回答三个问题:模式本身是否合法、是否带有封闭证明?每个分支能否完成参数实例化、资源匹配和前提检查?所有分支能否保留框架、合并并一次提交?
- 操作将一条已证明的局部资源变换与参数实例化、资源选择方式封装在一起;
inputsinputs来自调用者,捕获值来自状态,factsfacts陈述纯前提。 KEYKEY用可证明的等式筛选资源,CAPTURECAPTURE绑定未知量,DEFERDEFER推迟等式检查;消耗项从左到右贪心选择,绝不回溯。- 声明阶段检查模式合法性,并要求证明回调给出封闭、α-等价的引理——没有未经证明的后备路径;资源匹配发生在应用阶段,逐分支进行,整个状态只提交一次。
- 反转证明由四个操作加通用状态变换函数驱动,遵循“展开——程序访问——折叠——模型更新”的节奏;循环不变式也由同一批变换函数构造,再用
get_symbolic_stateget_symbolic_state保存为快照。