Satisfiability Modulo Recursive Programs Philippe Suter Ali Sinan
Satisfiability Modulo Recursive Programs Philippe Suter, Ali Sinan Köksal, Viktor Kuncak École Polytechnique Fédérale de Lausanne, Switzerland
Leon(Online) • A verifier for Scala programs. • The programming and specification languages are the same purely functional subset. def content(t: Tree) = t match { case Leaf ⇒ Set. empty case Node(l, v, r) ⇒ (content(l) ++ content(r)) + e } def insert(e: Int, t: Tree) = t match { case Leaf ⇒ Node(Leaf, e, Leaf) case Node(l, v, r) if e < v ⇒ Node(insert(e, l), v, r) case Node(l, v, r) if e > v ⇒ Node(l, v, insert(e, r)) case _ ⇒ t } ensuring( res ⇒ content(res) == content(t) + e)
~$. /demo
…of quantifier-free formulas in a decidable base theory… Satisfiability Modulo Recursive Programs …pure, total, deterministic, firstorder and terminating on all inputs…
Satisfiability Modulo Recursive Programs • Semi-decidable problem of great interest: – applications in verification, testing, theorem proving, constraint logic programming, … • Our algorithm always finds counter-examples when they exist, and will also find assume/guarantee style proofs. • It is a decision procedure for infinitely and sufficiently surjective abstraction functions. • We implemented it as a part of a verification system for a purely functional subset of Scala.
Proving/Disproving Modulo Recursive Programs
Proving by Inlining def size(lst: List) = lst match { case Nil ⇒ 0 case Cons(_, _) ⇒ 1 } ensuring(res ⇒ res ≥ 0) Convert pattern-matching, negate. (if(lst = Nil) 0 else 1) < 0 Unsatisfiable.
Proving by Inlining def size(lst: List) = lst match { case Nil ⇒ 0 case Cons(x, xs) ⇒ 1 + size(xs) } ensuring(res ⇒ res ≥ 0) Convert pattern-matching, negate, treat functions as uninterpreted. (if(lst = Nil) 0 else 1 + size(lst. tail)) < 0 Satisfiable. (e. g. for size(Nil) = -2) Inline post-condition. (if(lst = Nil) 0 else 1 + size(lst. tail)) < 0 ∧ size(lst. tail) ≥ 0 Unsatisfiable.
Disproving by Inlining def prop(lst: List) = { size(lst) ≥ 0 } ensuring(res ⇒ res) def size(lst: List) = lst match { case Nil ⇒ -1 case Cons(x, xs) ⇒ 1 + size(xs) } Negate, treat functions as uninterpreted. size(lst) < 0 Satisfiable. (e. g. lst = [0], size([0]) = -1) Inline definition of size(lst) < 0 ∧ size(lst) = (if(lst = Nil) -1 else 1 + size(lst. tail)) Satisfiable. (e. g. lst = [0, 1], size([1]) = -2)
Disproving with Inlining size(lst) ? size(lst. tail) ? … There always unknown branches in the evaluation tree. We can never be sure that there exists no smaller solution.
Branch Rewriting size(lst) = if(lst = Nil) { -1 } else { 1 + size(lst. tail) } size(lst) = ite 1 ∧ p 1 ⇔ lst = Nil ∧ p 1 ⇒ ite 1 = -1 ∧ ¬p 1 ⇒ ite 1 = 1 + size(lst. tail) … ∧ p 1 size(lst) p 1 ? ¬p 1 • Satisfiable assignments do not depend on unknown values. • Unsatisfiable answers have two possible meanings.
Algorithm (φ, B) = unroll(φ, _) while(true) { solve(φ ∧ B) match { case “SAT” ⇒ return “SAT” Some literals in B may be implied by φ : no need to unroll what they guard. case “UNSAT” ⇒ solve(φ) match { case “UNSAT” ⇒ return “UNSAT” case “SAT” ⇒ (φ, B) = unroll(φ, B) } Inlining must be fair } } “I’m feeling lucky” Inlines some postconditions and bodies of function applications that were guarded by a literal in B, returns the new formula and new set of guards B.
Completeness for Counter-Examples • Let a 1, … an be the free variables of φ, and c 1, …, cn be a counter-example to φ. • Let T be the evaluation tree of φ[a 1→c 1, …, an→cn]. ? T: ? ? ? • Eventually, the algorithm will reach a point where T is covered.
( ) Pattern-Matching Exhaustiveness • We generate a formula that expresses that no case matches, and prove it unsatisfiable. def zip(l 1: List, l 2: List) : Pair. List = { require(size(l 1) == size(l 2)) l 1 match { case Nil ⇒ PNil case Cons(x, xs) ⇒ l 2 match { case Cons(y, ys) ⇒ PCons(P(x, y), zip(xs, ys)) } } } size(l 1) == size(l 2) ∧ l 1 ≠ Nil ∧ l 2 == Nil Unsatisfiable.
Termination for Sufficiently Surjective Abstraction Functions
Sufficiently Surjective Abstractions • The algorithm terminates, and is thus a decision procedure for the case of infinitely and sufficiently surjective abstraction functions. def α(tree: Tree) : C = tree match { case Leaf() ⇒ empty case Node(l, v, r) ⇒ combine(α(l), v, α(r)) } [ P. Suter, M. Dotta, V. Kuncak, Decision Procedures for Algebraic Data Types with Abstractions, POPL 2010 ]
« « » » If one tree is mapped by α into an element, then many other trees are mapped to the same one. There are only finitely many trees for which this does not hold. content ∅ 5 5 5 1 1 5 … content { 1, 5 }
def content(tree: Tree) : Set[Int] = tree match { case Leaf() ⇒ Set. empty case Node(l, v, r) ⇒ (content(l) ++ content(r)) + v } def all. Even(list: List) : Boolean = list match { case Nil() ⇒ true case Cons(x, xs) ⇒ (x % 2 == 0) && all. Even(list) } def max(tree: Tree) : Option[Int]= tree match { case Leaf() ⇒ None case Node(l, v, r) ⇒ Some(max(l), max(r)) match { case (None, None) ⇒ Some(v) case (Some(lm), None) ⇒ max(lm, v) m is h p r o m a t a c case (None, Some(rm))…⇒ s any well av) asmax(rm, ! in a m case (Some(lm), Some(rm)) ⇒ (max(lm, rm), v) o d e it in f a s to p a m t a h t })}
« Sufficiently Surjective Abstractions in the Wild In the context of a project to reason about a domain specific language called Guardol for XML message processing applications […the] procedure has allowed us to prove a number of interesting things […], so I'm extremely grateful for it. » Mike Whalen (University of Minnesota & Rockwell Collins)
A Tale of Two Techniques φ ≡ … α(t) … t≡ t≡ t≡ ∨ ψ(α(t)) • For catamorphisms, unrolling α corresponds to enumerating potential counter-examples by the size of their tree. • When all required trees have been considered, a formula at least as strong as ψ(α(t)) is derived by the solver.
Verification System
Leon(Online) www . scala compiler - prunes out rewrites functions generate VCs solve VCs (generate testcases) final report
Leon(Online) • Proves that all match expressions are exhaustive. • For each function, proves that the pre-condition implies the post-condition. • Proves that all function invocations satisfy the preconditions. ( Bonus features: ) • Can generate test-cases satisfying complex preconditions • Outputs to Isabelle format for manual proofs
(Some) Experimental Results Benchmark Lo. C #Funs. #VCs. Time (s) List. Operations 107 15 27 0. 76 Associative. List 50 5 11 0. 23 Insertion. Sort 99 6 15 0. 42 117 11 24 3. 73 Propositional. Logic 86 9 23 2. 36 Amortized. Queue 124 14 32 3. 37 94 11 23 0. 79 677 71 155 11. 66 Red. Black. Trees VSComp Functional correctness properties of data structures: red-black trees implement a set and maintain height invariants, associative list has read-over-write property, insertion sort returns a sorted list of identical content, amortized queue implements a balanced queue, etc.
Related Work • SMT Solvers + axioms – can be very efficient for well-formed axioms – in general, no guarantee than instantiations are fair – cannot in general conclude satisfiability (changing…) • Interactive verification systems (ACL 2, Isabelle, Coq) – very good at building inductive proofs – could benefit greatly from counter-example generation (as feedback to the user but also to prune out branches in the search) • Sinha’s Inertial Refinement technique – similar use of “blocked paths” – no guarantee to find counter-examples
Related Work, continued • DSolve (aka. “Liquid Types”) – because the proofs are based on type inference rules, cannot immediately be used to prove assertions relating different functions • Bounded Model Checking – offer similar guarantees on the discovery of counter-examples – reduces to SAT rather than SMT – unclear when it is complete for proofs • Finite Model Finding (Alloy, Paradox) – lack of theory reasoning, no proofs
Satisfiability Modulo Recursive Programs • Semi-decidable problem of great interest: – applications in verification, testing, theorem proving, constraint logic programming, … • Our algorithm always finds counter-examples when they exist, and will also find assume/guarantee style proofs. • It is a decision procedure for infinitely and sufficiently surjective abstraction functions. • We implemented it as a part of a verification system for a purely functional subset of Scala.
Try it online http: //lara. epfl. ch/leon/
Thank you.
- Slides: 29