用 SMT 求解纯目标
上一节最后通过纯化得到了普通逻辑目标,包括 reversereverse 循环头部的列表等式 l = REVERSE [] ++ ll = REVERSE [] ++ l;完成数组示例后,还会得到 clearclear 中的索引边界与 replace_inthreplace_inth 等式。等式、整数运算以及涉及这些运算的量化事实,正是 SMT 求解器擅长处理的内容。本节将 C* 的证明层连接到 Z3,用 SMT_TACSMT_TAC 解决这些目标,并将三个步骤组合为完整的流水线。在两个示例的程序点上,只需调用这条流水线就能完成证明。
SMT 用户库通过 Z3 的 C API 调用求解器,因此需要安装带 C 头文件与链接库的 Z3;安装方式因平台而异。安装后,在仓库根目录的 cstar.tomlcstar.toml 中将构建系统指向 Z3 的安装位置:
cflags = "-I/path/to/z3/include"link_flags = "-L/path/to/z3/lib -lz3"
cflags = "-I/path/to/z3/include"link_flags = "-L/path/to/z3/lib -lz3"
/path/to/z3/path/to/z3 需要替换为本机上的实际路径。如果 Z3 位于编译器的默认搜索路径中,只需设置 link_flags = "-lz3"link_flags = "-lz3"(策略示例采用的就是这种配置)。cstarccstarc 只会将自身的头文件目录、库目录以及项目根目录加入编译器的搜索路径。如果 Z3 的安装前缀不在随附 clangclang 的默认搜索范围内,比如从源码编译后安装到 /opt/z3/opt/z3,就需要显式给出 -I-I 与 -L-L 路径。要快速检查环境,可以验证完整的示例:
cstarc verify strategy_demo/examples/reverse.ccstarc verify strategy_demo/examples/clear.c
cstarc verify strategy_demo/examples/reverse.ccstarc verify strategy_demo/examples/clear.c
PROOF gnode SMT_TAC(gnode gn);
PROOF gnode SMT_TAC(gnode gn);
SMT_TACSMT_TAC 把当前的普通 HOL 目标交给已初始化的 SMT 用户库。如果 Z3 判定目标有效,SMT_TACSMT_TAC 就会求解当前 gnodegnode;如果翻译失败、求解器无法判定,或目标本身无效,目标就会保持开放,由用户继续处理。
有了 PURIFY_TACPURIFY_TAC、POST_PURIFY_TACPOST_PURIFY_TAC 与 SMT_TACSMT_TAC 这三个策略,就可以将本部分的流水线封装为一个可复用的函数:
PROOF thm solve_vc_auto(term vc) { gnode root = gnode_new_with_ccl(vc); gnode g = PURIFY_TAC(root); g = POST_PURIFY_TAC(g); SMT_TAC(g); return gnode_prove(root);}
PROOF thm solve_vc_auto(term vc) { gnode root = gnode_new_with_ccl(vc); gnode g = PURIFY_TAC(root); g = POST_PURIFY_TAC(g); SMT_TAC(g); return gnode_prove(root);}
在需要重建不变式或建立后置条件状态的程序点上,都可以先读取符号状态,再构造蕴含,最后提交证明得到的定理:
PROOF { term st = cst_get_symbolic_state(); term vc = `${st:hprop} |-- ${loop_inv:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc));}
PROOF { term st = cst_get_symbolic_state(); term vc = `${st:hprop} |-- ${loop_inv:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc));}
完整示例 strategy_demo/examples/reverse.cstrategy_demo/examples/reverse.c 中的函数与 reverse_clean.creverse_clean.c 相同,只是删去了那条很长的断言。它在三个位置调用 solve_vc_autosolve_vc_auto:循环前建立不变式,循环体末尾重建不变式,循环结束后将状态转换为 returnreturn 所需的形式:
PROOF { term st = cst_get_symbolic_state(); term return_ready = `exists t_v v_v w_v. data_at p__addr Tptr p__pre ** data_at w__addr Tptr w_v ** data_at v__addr Tptr v_v ** data_at t__addr Tptr t_v ** sll w_v (REVERSE l) `; term vc = `${st:hprop} |-- ${return_ready:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc));}
PROOF { term st = cst_get_symbolic_state(); term return_ready = `exists t_v v_v w_v. data_at p__addr Tptr p__pre ** data_at w__addr Tptr w_v ** data_at v__addr Tptr v_v ** data_at t__addr Tptr t_v ** sll w_v (REVERSE l) `; term vc = `${st:hprop} |-- ${return_ready:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc));}
这里仍然通过定理更新状态:solve_vc_autosolve_vc_auto 返回一条蕴含定理,再由 cst_set_symbolic_statecst_set_symbolic_state 提交,整个过程不会直接修改状态。
SMT 求解器擅长处理等式、整数运算和量词,但 Z3 的内建知识并不包含 REVERSEREVERSE 的 HOL 定义或 REVERSE_REVERSEREVERSE_REVERSE 定理。 smt_add_axiomsmt_add_axiom 可以将这些已经证明的定理作为背景知识,供后续 SMT 调用使用。reverse.creverse.c 在加载时将它们一次性注册:
PROOF static int install_smt(void) { smt_init(); smt_register_seq_handler(); type_pair_list int_inst = (type_pair_list)vector_create(); vector_add(&int_inst, ((type_pair){`:int`, `:A`})); thm reverse_def_int = gen_all_rule( inst_type_rule(int_inst, get_REVERSE())); thm reverse_reverse_int = gen_all_rule(inst_type_rule( int_inst, get_theorem_by_name("REVERSE_REVERSE"))); smt_add_axiom(reverse_def_int); smt_add_axiom(reverse_reverse_int); return 0;}PROOF static int _install_smt = install_smt();
PROOF static int install_smt(void) { smt_init(); smt_register_seq_handler(); type_pair_list int_inst = (type_pair_list)vector_create(); vector_add(&int_inst, ((type_pair){`:int`, `:A`})); thm reverse_def_int = gen_all_rule( inst_type_rule(int_inst, get_REVERSE())); thm reverse_reverse_int = gen_all_rule(inst_type_rule( int_inst, get_theorem_by_name("REVERSE_REVERSE"))); smt_add_axiom(reverse_def_int); smt_add_axiom(reverse_reverse_int); return 0;}PROOF static int _install_smt = install_smt();
核心调用是 smt_add_axiomsmt_add_axiom:其参数是一条定理,定理的结论将成为求解器可用的背景知识。定理的假设会先被卸除,但自由项变量不会被全称化,因此需要显式调用 gen_all_rulegen_all_rule。还需要一个技术步骤:单态化(monomorphization)。 HOL 列表定理是多态的,其中的元素类型 AA 尚未固定。当前的翻译过程不支持 HOL 多态,因此需要先用 inst_type_ruleinst_type_rule 将 AA 固定为此处实际使用的 intint,再将定理交给求解器。
SMT_TACSMT_TAC 位于 userlib/smt/smt.cuserlib/smt/smt.c,其核心做法是递归检查 HOL 项的结构,构建语义上对应的 Z3 AST。 term_to_z3term_to_z3 的主体可以概括为:
Z3_ast term_to_z3(Z3_context ctx, term tm) { if (is_int(tm)) return Z3_mk_int(ctx, dest_int(tm), Z3_mk_int_sort(ctx)); if (is_forall(tm)) { /* 翻译绑定变量的类型,递归进入主体,构建 Z3 forall */ } if (is_exists(tm)) { /* 类似地,构建 Z3 exists */ } if (is_var(tm)) { /* 自由变量变为同名、同类型的 Z3 常量 */ } if (is_const(tm) || is_comb(tm)) return smt_app_to_z3(ctx, tm); return NULL;}
Z3_ast term_to_z3(Z3_context ctx, term tm) { if (is_int(tm)) return Z3_mk_int(ctx, dest_int(tm), Z3_mk_int_sort(ctx)); if (is_forall(tm)) { /* 翻译绑定变量的类型,递归进入主体,构建 Z3 forall */ } if (is_exists(tm)) { /* 类似地,构建 Z3 exists */ } if (is_var(tm)) { /* 自由变量变为同名、同类型的 Z3 常量 */ } if (is_const(tm) || is_comb(tm)) return smt_app_to_z3(ctx, tm); return NULL;}
函数应用(application)按头部常量分发:==>==> 变为 Z3_mk_impliesZ3_mk_implies,/\/\ 变为 Z3_mk_andZ3_mk_and,整数 ++、--、**、<<、<=<= 映射到相应的 Z3 运算。类型可翻译的其他函数则作为未解释函数(uninterpreted function)处理;smt_add_axiomsmt_add_axiom 添加的定理用来约束它们的行为。 smt_register_seq_handlersmt_register_seq_handler 注册的处理器还会将列表相关的符号映射到 Z3 的序列理论:NILNIL 映射为空序列,CONSCONS 与 APPENDAPPEND 映射为序列拼接,inthinth 映射为序列索引,类型 (A)list(A)list 映射为序列类型。因此,列表目标中的这些符号在 Z3 中都有对应的解释。
最后,求解器断言背景知识与目标的否定。如果 Z3 报告不可满足(unsatisfiable),则反模型(countermodel)不存在,目标有效,SMT_TACSMT_TAC 据此求解该 gnodegnode。
数组示例 strategy_demo/examples/clear.cstrategy_demo/examples/clear.c 把 00 写入数组的每一个元素。上一节的策略已经能够处理其中的空间推理,剩下的纯目标涉及 replace_inthreplace_inth,也就是 int_array_with_holeint_array_with_hole 规则引入逻辑列表中的函数。下面三条引理已在 strategy_demo/array/lemmas.cstrategy_demo/array/lemmas.c 中一次性证明,准确刻画了一次更新对读取结果和列表长度的影响:
|- forall i v:int. forall l:(int)list. 0i <= i ==> i < ilength l ==> inth i (replace_inth i v l) = v|- forall i j v:int. forall l:(int)list. 0i <= i ==> i < ilength l ==> 0i <= j ==> j < ilength l ==> ~(j = i) ==> inth j (replace_inth i v l) = inth j l|- forall i v:int. forall l:(int)list. ilength (replace_inth i v l) = ilength l
|- forall i v:int. forall l:(int)list. 0i <= i ==> i < ilength l ==> inth i (replace_inth i v l) = v|- forall i j v:int. forall l:(int)list. 0i <= i ==> i < ilength l ==> 0i <= j ==> j < ilength l ==> ~(j = i) ==> inth j (replace_inth i v l) = inth j l|- forall i v:int. forall l:(int)list. ilength (replace_inth i v l) = ilength l
clear.cclear.c 安装数组策略,并将这三条引理注册为 SMT 背景知识,然后用两个证明块完成验证:一个位于循环体末尾,另一个位于循环之后,将状态转换为后置条件的形式:
PROOF static int _strategy_demo_array = install_strategy_demo_array();PROOF static int install_clear_smt_axioms(void) { smt_add_axiom(replace_inth_at()); smt_add_axiom(replace_inth_other()); smt_add_axiom(replace_inth_length()); return 0;}PROOF static int _clear_smt_axioms = install_clear_smt_axioms();void clear(int *to, int len) [[cst::require(`exists l. fact(0i <= len && len <= 100i) ** fact(ilength l == len) ** int_array to 0i len l `)]] [[cst::ensure(`exists l. fact(forall i. 0i <= i && i < len ==> inth i l = 0i) ** int_array to 0i len l `)]]{ int i = 0; PROOF term loop_inv = `exists i_v l. fact(0i <= i_v && i_v <= len__pre && 0i <= len__pre && len__pre <= 100i && ilength l == len__pre) ** fact(forall j. 0i <= j && j < i_v ==> inth j l = 0i) ** data_at to__addr Tptr to__pre ** data_at len__addr Tint len__pre ** data_at i__addr Tint i_v ** int_array to__pre 0i len__pre l `; while (i < len) [[cst::invariant(loop_inv)]] { to[i] = 0; i = i + 1; PROOF { term st = cst_get_symbolic_state(); term vc = `${st:hprop} |-- ${loop_inv:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc)); } } PROOF { term st = cst_get_symbolic_state(); term post_ready = `exists i_v l. fact(forall i. 0i <= i && i < len__pre ==> inth i l = 0i) ** data_at to__addr Tptr to__pre ** data_at len__addr Tint len__pre ** data_at i__addr Tint i_v ** int_array to__pre 0i len__pre l `; term vc = `${st:hprop} |-- ${post_ready:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc)); }}
PROOF static int _strategy_demo_array = install_strategy_demo_array();PROOF static int install_clear_smt_axioms(void) { smt_add_axiom(replace_inth_at()); smt_add_axiom(replace_inth_other()); smt_add_axiom(replace_inth_length()); return 0;}PROOF static int _clear_smt_axioms = install_clear_smt_axioms();void clear(int *to, int len) [[cst::require(`exists l. fact(0i <= len && len <= 100i) ** fact(ilength l == len) ** int_array to 0i len l `)]] [[cst::ensure(`exists l. fact(forall i. 0i <= i && i < len ==> inth i l = 0i) ** int_array to 0i len l `)]]{ int i = 0; PROOF term loop_inv = `exists i_v l. fact(0i <= i_v && i_v <= len__pre && 0i <= len__pre && len__pre <= 100i && ilength l == len__pre) ** fact(forall j. 0i <= j && j < i_v ==> inth j l = 0i) ** data_at to__addr Tptr to__pre ** data_at len__addr Tint len__pre ** data_at i__addr Tint i_v ** int_array to__pre 0i len__pre l `; while (i < len) [[cst::invariant(loop_inv)]] { to[i] = 0; i = i + 1; PROOF { term st = cst_get_symbolic_state(); term vc = `${st:hprop} |-- ${loop_inv:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc)); } } PROOF { term st = cst_get_symbolic_state(); term post_ready = `exists i_v l. fact(forall i. 0i <= i && i < len__pre ==> inth i l = 0i) ** data_at to__addr Tptr to__pre ** data_at len__addr Tint len__pre ** data_at i__addr Tint i_v ** int_array to__pre 0i len__pre l `; term vc = `${st:hprop} |-- ${post_ready:hprop}`; cst_set_symbolic_state(solve_vc_auto(vc)); }}
循环体执行一次时,证明按以下步骤推进:
- 符号执行准备执行
to[i] = 0to[i] = 0,需要目标单元的data_atdata_at; - 数组规则
00从int_arrayint_array中切出该单元,留下int_array_with_holeint_array_with_hole。由于不变式中的事实给出了0 <= i_v0 <= i_v与i_v < len__prei_v < len__pre,规则的两条inferinfer检查都能通过; - 执行写入后,该单元的值变为
00; - 重建不变式时,规则
11把左侧的空洞与右侧的目标int_arrayint_array匹配,将目标改写为一个存在变量vv、索引ii处的data_atdata_at,以及等式l2 = replace_inth (i - x) v l1l2 = replace_inth (i - x) v l1; - 通用
data_atdata_at策略将刚写入的单元与这个data_atdata_at一同消去,并实例化v = 0v = 0; - 如果状态两侧都直接包含同一区段的
int_arrayint_array,就可以用规则22消去空间谓词,留下列表等式; POST_PURIFY_TACPOST_PURIFY_TAC生成普通逻辑目标,SMT_TACSMT_TAC再利用replace_inthreplace_inth引理,证明已清零的前缀增加了一个单元,长度与边界事实仍然成立。
循环入口不需要证明块:注册策略后,引擎可以自行建立不变式。后置条件则需要单独的证明块。循环结束后,状态中仍然包含不变式中的 i_vi_v,以及退出循环时的事实 i_v >= len__prei_v >= len__pre。引擎内置的求解器不会自行将不变式中的 forall j. … j < i_v …forall j. … j < i_v … 转换为后置条件中的 forall i. … i < len__pre …forall i. … i < len__pre …,因此这条蕴含会作为尚未解决的验证条件保留下来。第二个证明块用 post_readypost_ready 表示后置条件所需的状态,再调用 solve_vc_autosolve_vc_auto,由 SMT_TACSMT_TAC 完成这一步量词推理。
SMT_TACSMT_TAC将普通 HOL 目标连同已注册的背景定理翻译到 Z3,通过断言目标的否定,以不可满足性判定目标有效,从而解决目标。求解过程有超时限制;求解器的判定结果被直接信任,判定有效的目标列入报告的axiomsaxioms数组。smt_add_axiomsmt_add_axiom把已证明的 HOL 定理转化为求解器的背景知识;多态的列表定理在提交给求解器之前,先用inst_type_ruleinst_type_rule单态化,并用gen_all_rulegen_all_rule全称化。term_to_z3term_to_z3根据项的结构递归构建 Z3 AST,按头部常量分发函数应用,将可翻译但含义未知的函数保留为未解释函数;序列处理器将列表相关的符号映射到 Z3 的序列理论。solve_vc_autosolve_vc_auto依次执行纯化、后纯化和 SMT 求解,在reversereverse的三个程序点和clearclear的两个程序点上卸除了全部剩余义务。上一节的策略与本节的求解器共同完成了整个证明。- 策略内部的
inferinfer检查与SMT_TACSMT_TAC处于不同层次:前者在 QCP 内部判断规则在局部是否适用,后者在 C* 证明层解决整个纯目标。