Getting Started¶
Install¶
Your first proof¶
from provably import verified
@verified(
pre=lambda x, y: (x >= 0) & (y >= 0),
post=lambda x, y, result: result >= 0,
)
def add(x: int, y: int) -> int:
return x + y
proof = add.__proof__
proof.verified # True
proof.solver_time_ms # 1.1
proof.status # Status.VERIFIED
str(proof) # "[Q.E.D.] add"
At import time, provably:
- Parses the function body into a Python AST.
- Translates the AST into Z3 expressions.
- Asserts the negation of the postcondition under the precondition.
- Z3 answers
unsat-- no input violates the contract.
A stronger example¶
from provably import verified
@verified(
pre=lambda a, b: b > 0,
post=lambda a, b, result: (result * b <= a) & (a < result * b + b),
)
def floor_div(a: int, b: int) -> int:
return a // b
floor_div.__proof__.verified # True
floor_div.__proof__.solver_time_ms # ~2ms
str(floor_div.__proof__) # "[Q.E.D.] floor_div"
The postcondition result * b <= a < result * b + b is the complete characterisation
of floor division when b > 0. Z3 proves it for all integers a and all positive b.
Counterexamples¶
When a contract is wrong, Z3 produces the exact input that breaks it:
from provably import verified
@verified(
pre=lambda a, b: b > 0,
post=lambda a, b, result: result * b == a, # wrong: floor division != exact
)
def bad_div(a: int, b: int) -> int:
return a // b
cert = bad_div.__proof__
cert.verified # False
cert.status # Status.COUNTEREXAMPLE
cert.counterexample # {'a': 1, 'b': 2, '__return__': 0}
# 1 // 2 = 0, but 0 * 2 != 1. The contract is wrong, not the function.
Fix the postcondition to match what floor division actually guarantees:
@verified(
pre=lambda a, b: b > 0,
post=lambda a, b, result: (result * b <= a) & (a < result * b + b),
)
def safe_div(a: int, b: int) -> int:
return a // b
safe_div.__proof__.verified # True
str(safe_div.__proof__) # "[Q.E.D.] safe_div"
While loops¶
Bounded while loops are unrolled up to 256 iterations, just like for i in range(N):
from provably import verified
@verified(
pre=lambda n: (n >= 0) & (n <= 10),
post=lambda n, result: result == 0,
)
def countdown(n: int) -> int:
while n > 0:
n = n - 1
return n
countdown.__proof__.verified # True
The loop body is unrolled symbolically. If the bound cannot be determined statically
or exceeds 256 iterations, provably raises TranslationError.
Walrus operator¶
The := (named expression) operator is supported in all expression contexts:
from provably import verified
@verified(
post=lambda x, result: (result >= 0) & ((result == x) | (result == -x)),
)
def abs_walrus(x: float) -> float:
return (neg := -x) if x < 0 else x
abs_walrus.__proof__.verified # True
The walrus binding is visible to subsequent Z3 constraints in the enclosing scope.
Match/case¶
match/case statements (Python 3.10+) are desugared to if/elif/else:
from provably import verified
@verified(
pre=lambda direction: (direction >= 0) & (direction <= 3),
post=lambda direction, result: (result >= -1) & (result <= 1),
)
def direction_to_dx(direction: int) -> int:
match direction:
case 0:
return 1
case 1:
return -1
case _:
return 0
direction_to_dx.__proof__.verified # True
Literal values, singletons, wildcards, and guard clauses are supported.
Structural and class patterns raise TranslationError.
What Q.E.D. means¶
__proof__.verified == True means the Z3 SMT solver determined that the verification
condition (VC) is unsatisfiable -- no assignment of input values satisfies the
precondition while violating the postcondition. This is a mathematical proof.
For function \(f\) with precondition \(P\) and postcondition \(Q\):
provably checks that \(\neg\,\text{VC}\) is unsatisfiable:
If unsat: the implication holds universally. If sat: Z3 returns a concrete counterexample.
What the proof covers
VERIFIED covers all possible inputs satisfying the precondition. For integer
arithmetic, this is exact. For float, Z3 reasons over mathematical reals, not
IEEE 754 -- see Soundness.
Next¶
- Contracts -- pre/post lambda syntax
- Refinement types -- constraints in type annotations
- Soundness -- what the proof does and does not cover
- Pytest integration -- proofs in CI