0. Intro
本文介绍如何为Python中的函数进行auto-active verification。主要依赖于Hoare Logic的自动推导和SMT Solver。一时兴起写的小玩具(mini-dafny),随缘添加更多支持。
项目地址(Work-In-Progress):https://github.com/AD1024/veripygithub.com
为了提高食用体验,建议:了解Hoare Logic (Partial Correctness)
了解Program Verification with Hoare Logic
对Python没有生理性排斥反应(笑
1. 目标效果
@verify(
inputs=[('n', types.TINT)],
requires=['n >= 0'],
ensures=['x == n'])
def test_func(n):
y = n
x = 0
while y > 0:
invariant('x + y == n')
invariant('y >= 0')
x = x + 1
y = y - 1
return x
如上代码,我们想要实现verify这个装饰器。使用时给出输入的类型,对于输入的假设和输出满足的条件,然后验证这个函数是否满足我们的条件。对于loop invariant,我们用一个假的function call代替,在做AST mapping的时候处理。
主要流程如下:用inspect和ast两个库拿到对应函数的AST (Python AST)
将Python的AST转换到我们定义的AST(方便做Hoare Logic Inference)。实现一个Parser,能够将手动标注的表达式Parse到我们自己定义的AST (见transformer.py | parser/parser.py | parser/syntax.py)
跑weakeast_precondition(stmt, Q),其中stmt是转换好的AST,Q是用户提供的postcondition。目前这里的实现是返回一个
,其中
是我们的weakeast precondition,
是一个断言(side conditions)的集合。(verify.py)
利用Z3检查以下式子是否valid (若formula
满足
称其为valid): (其中
是用户提供的precondition)
(verify.py| transformer.py)
简单来说就是,通过transform AST得到weakest precondition然后通过rule of consequence检查hoare logic是否valid。一个简单的变换:因为我们用的是SMT Solver,只能检查是否存在一个model M满足我们的constraints,所以这里需要取反(
),然后检查我们的constraint是否unsat即可。
2. Syntax
为了方便计算weakest precondition,我们需要定义一个语法(maybe subset of Python)。最基础的我实现了这几个:赋值
While循环
If-then-else
Assert / Assume
Skip (No-op)
隐藏AST: Seq,连接两个Statement。
大致语法是这样(示意)
stmt ::= Skip
| Assert e
| Assume e
| If e then stmt else stmt
| While e do stmt
| Seq stmt stmt
e ::= e + e | e - e | e * e | e / e
| e and e
| e or e
| not e
| e ==> e
| e <==> e
Weakest Precondition
定义一个函数wp(s, Q),接受一个statement和一个postcondition,返回s的weakest precondition P使得P ==> Q。
这里给出weakest percondition的计算方法:
Skip: Trivial
Assume: 如果我们在程序中间(其实一般是在开始的时候assume输入值的一些性质)assume了一个e,那么我们至少要保证e => Q。
Assignment:对于x = y的wp,显然如果post condition是Q,那么最弱的需要被满足的precondition就是将y替换到Q中对x的断言。
Assert: 如果我们在程序执行过程中断言了e,那么若程序最终step到了Skip,说明这个assertion没有被打破,因此最弱的P是
Seq: 因为是wp,所以我们要从最后一个statement往前推,因此:
If e then s1 else s2: 因为当 e为True时我们会执行s1,反之执行s2,所以对于整个If语句,我们至少要满足:
While e s: 这里我们会用到side conditions那个set。While的wp本质就是loop invariant,但是除了满足invariants以外,我们还要保证 ,这里I是loop invariant,
是s的wp。s的wp也很容易,因为I自始至终都是被满足的,因此
然后我们将计算s的wp时的side conditions和上面的两个条件合并作为While的side conditions即可。
使用例
考虑以下求和程序,我们验证用这个While求和本质和使用求和公式一样
@verify(
inputs=[
('n', types.TINT)
],
requires=[
'n >= 0'
],
ensures=[
'ans == ((n + 1) * n) // 2'
]
)
def Summation(n):
ans = 0
i = 0
while i <= n:
ans = ans + i
i = i + 1
return ans
我们在不标注任何loop invariant的情况下进行验证,会有这样的报错
Exception: VerificationViolated on
Not(Implies(And(True, Not(i <= n)), ans == ((n + 1)*n)/2))
Model: [i = 1, ans = -1, n = 0]
Side condition violated
能看出来,这是说在循环结束之后,我们的precondition推不出我们的结论(ans == ((n + 1) * n) // 2)
OK,因为我们什么都没有标,invariant是True.....
我们知道循环条件是i <= n,那么一个很明显的invariant就是i <= n + 1。另外,我们还需要找到一个invariant建立ans和i的联系。
@verify(
inputs=[
('n', types.TINT)
],
requires=[
'n >= 0'
],
ensures=[
'ans == ((n + 1) * n) // 2'
]
)
def Summation(n):
ans = 0
i = 0
while i <= n:
invariant('i <= n + 1')
invariant('ans == i * (i - 1) // 2')
ans = ans + i
i = i + 1
return ans
找invariant就是纯靠经验(直觉)了...直觉告诉我,track上一个iteration的i就可以(小声
这时候再跑verification,嗯,verified.
更多例子可以看https://github.com/AD1024/veripy/tree/master/examplesgithub.com
之后的工作
可能有些人已经发现,这个weakest precondition的计算方法和想象中不太一样。是的...这样是比较偷懒的写法。常规操作是要unroll while loop,然后havoc loop variable。
这我只能说
在做了在做了
TODOs:Array
Havoc
Function call
Quantifiers
更多Python语法的支持