ScalaZ3 Integrating SMT and Programming Ali Sinan Köksal, Viktor Kuncak, Philippe Suter École Polytechnique Fédérale de Lausanne
Efficient SMT solver from Microsoft Research Supports many theories through DPLL(T) and Nelson-Oppen combination SMT-LIB standard input format, as well as C, .NET, OCaml, and Python bindings “Scalable programming language” Blending of functional and object-oriented programming Runs on the Java Virtual Machine (and .NET) Rich type system (generics, type classes, implicit conversions, etc.) Now used by over 100’000 developers (incl. Twitter, UBS, LinkedIn) - Z3 just won most of the SMT-COMP divisions
~$ ./demo I’m going to start by showing how it feels to use the system One part of Scala^Z3 mirrors the functions in the C API. We will come back to that later, for now I will focus on some of the things that we added as part of the integration.
C and Scala side-by-side #include "z3.h" … Z3_config cfg = Z3_mk_config(); Z3_set_param_value(cfg, "MODEL", "true"); Z3_context z3 = Z3_mk_context(cfg); Z3_sort intSort = Z3_mk_int_sort(z3); Z3_func_decl f = Z3_mk_func_decl(z3, Z3_mk_string_symbol(z3,"f"), 1, &intSort, intSort); Z3_ast x = Z3_mk_const(z3, Z3_mk_string_symbol(z3,"x"), intSort); Z3_ast y = Z3_mk_const(z3, Z3_mk_string_symbol(z3,"y"), intSort); Z3_ast ineq = Z3_mk_not(z3, Z3_mk_eq(z3, x, Z3_mk_app(z3, f, 1, &y))); Z3_assert_cnstr(z3, ineq); Z3_model m; if(Z3_check_and_get_model(z3, &m)) { printf("%s", Z3_model_to_string(z3, m)); } import z3.scala._ … val cfg = new Z3Config cfg.setParamValue("MODEL", "true") val z3 = new Z3Context(cfg) val intSort : Z3Sort = z3.mkIntSort val f : Z3FuncDecl = z3.mkFuncDecl( z3.mkStringSymbol("f"), List(intSort), intSort) val x : Z3AST = z3.mkConst( z3.mkStringSymbol("x"), intSort) val y : Z3AST = z3.mkConst( z3.mkStringSymbol("y"), intSort) val ineq : Z3AST = z3.mkNot( z3.mkEq(x, z3.mkApp(f, y))) z3.assertCnstr(ineq) z3.checkAndGetModel match { case (Some(true), m) ⇒ println(m) case _ ⇒ ; } One thing this tool is: a mapping of the C functions into Scala. Almost one-to-one correspondence; uses object-oriented style and syntax standards differ
def choose[A,B](p: (Val[A],Val[B])⇒Tree[BoolSort]) : (A,B) def find[A,B](p: (Val[A],Val[B])⇒Tree[BoolSort]) : Option[(A,B)] def findAll[A,B](p: (Val[A],Val[B])⇒Tree[BoolSort]) : Iterator[(A,B)]
Anatomy of an Inline Invocation import z3.scala._ import dsl._ ... choose((y: Val[Int⇒Int], x: Val[Int], y: Val[Int]) ⇒ !(x === f(y))) imports and Val[_]s only manifestations of the library Desired return types (actual Scala types). Domain specific language of formulas resembles Scala expressions. Returned values are Scala values (including functions).
for Comprehensions Can you find positive x, y, z such that 2x + 3y ≤ 40, xz = 3y2, and y is prime? for((x,y) ← findAll((x: Val[Int], y: Val[Int]) ⇒ x > 0 && y > x && x * 2 + y * 3 <= 40); if(isPrime(y)); z ← findAll((z: Val[Int]) ⇒ z * x === 3 * y * y)) yield (x, y, z) Generators: sequences, computed eagerly or lazily. Filter: arbitrary boolean expression. Returned expression. (1,2,12), (1,3,27), (1,5,75), (1,7,147), (1,11,363), (3,11,121), (3,5,25), (3,7,49)
Implementation Aspects
Automatic conversions between the two kinds. Bottom Top IntSort BoolSort … SetSort Basic representation: class Z3AST(ptr : Long) Tree hierarchy as part of the DSL: abstract class Tree[+T >: Bottom <: Top] { ... def <(other : Tree[_ <: IntSort]) : Tree[BoolSort] = ... } implicit def ast2Tree(ast : Z3AST) : Tree[Bottom] implicit def tree2AST(tree : Tree[_]) : Z3AST Two kinds of trees: the basic kind, and the typed kind. Conversions between the two is automatic. The typed kind of trees will catch some compile-time errors. Automatic conversions between the two kinds. Soft typing for the DSL Trees.
DSL in Action import z3.scala._ … val cfg = new Z3Config cfg.setParamValue("MODEL", "true") val z3 = new Z3Context(cfg) val intSort : Z3Sort = z3.mkIntSort val f : Z3FuncDecl = z3.mkFuncDecl( z3.mkStringSymbol("f"), List(intSort), intSort) val x : Z3AST = z3.mkConst( z3.mkStringSymbol("x"), intSort) val y : Z3AST = z3.mkConst( z3.mkStringSymbol("y"), intSort) val ineq : Z3AST = z3.mkNot( z3.mkEq(x, z3.mkApp(f, y))) import z3.scala._ … val z3 = new Z3Context("MODEL“ -> true) val intSort = z3.mkIntSort val f = z3.mkFuncDecl( "f", intSort, intSort) val x = z3.mkConst("x", intSort) val y = z3.mkConst("y", intSort) val ineq : Z3AST = !(x === f(y)) One thing this tool is: a mapping of the C functions into Scala. Almost one-to-one correspondence; uses object-oriented style and syntax standards differ
Z3 Sorts Int BV[32] Bool Array[A,B] … Scala Types Int Boolean Set[A] A⇒B … Different function calls to create constants and to retrieve the models: Users should be able to ignore the underlying representation: ctx.getBool(m.eval(tree)) ctx.getNumeralInt(m.eval(tree)) m.getArrayValue(tree)... m.evalAs[Boolean](tree) m.evalAs[Int](tree) m.evalAs[Int⇒Int](tree)
def evalAs[T : Evaluator](tree : Z3AST) : T def evalAs[T](tree : Z3AST) : T def evalAs[T](tree : Z3AST)(implicit e : Evaluator[T]) : T @implicitNotFound(“No known model evaluator for type ${T}.”) trait Evaluator[T] { def eval(model : Z3Model, tree : Z3AST) : T } implicit object IntEvaluator extends Evaluator[Int] { def eval(model : Z3Model, tree : Z3AST) : Int = ... } implicit def lift2Set[T : Evaluator] = new Evaluator[Set[T]] { def eval(model : Z3Model, tree : Z3AST) : Set[T] = ... } In fact, the signature is slightly more complicated. That : Evaluator reads as “evalAs is defined for any type T such that T has an Evaluator” It’s short for the long form with implicit So what is an evaluator? It’s something we define as part of the library: simply an interface parametrized by a type. The interface says “I know how to extract a value of type T from a Z3Model” Now what we do is that we define evaluators for the types we know how to handle. The implicit keywords mean that we don-t actually need to pass the argument: the Scala compiler fills them in by scanning for an implicit value of the right type. Hence, when we write, it automatically is expanded into … we injected type-dependent code, but the user doesn’t have to know! Now, I could define Evaluators for Ints, Bools, etc. What about type constructors? We can handle them too! Now, instead of one Evaluator, we provide a rule to build an Evaluator from another one. This reallys reads as Prolog: if T has an evaluator, then Set[T] has one too, given as follows. m.evalAs[Int](t) m.evalAs[Int](t)(IntEvaluator) m.evalAs[Set[Set[Int]]](t) m.evalAs[...](t)(lift2Set(lift2Set(IntEvaluator)))
Procedural Attachments val z3 = new Z3Context(“MODEL” -> true) val stringTheory = new ProceduralAttachment[String](z3) { val concat = function((s1,s2) ⇒ s1 + s2) val substr = predicate((s1,s2) ⇒ s2.contains(s1)) val evenLength = predicate(_.length % 2 == 0) } import stringTheory._ val s = z3.mkConst(“s”, stringTheory.sort) z3.assertCnstr(s === “world” || s === “moon”) z3.assertCnstr(evenLength(s)) z3.check > Some(true) z3.assertCnstr(substr(concat(“hello”, s), “low”)) > Some(false) So far, we’ve seen how to communicate mostly from Scala to Z3.
Applications MUNCH: Decision procedure for multisets and sets R. Piskac, V. Kuncak, IJCAR 2010 Z3 extension for sets with cardinality constraints P. Suter, R. Steiger, V. Kuncak, VMCAI 2011 Leon: Verifier for functional programs P. Suter, A.S. Köksal, V. Kuncak, SAS 2011, http://lara.epfl.ch/leon/ Kaplan: Constraint programming in Scala Ali Sinan Köksal’s Master Thesis Other users at ETH Zürich, KU Leuven
(Tested on Windows and Linux, on 32 and 64 bit architectures.) Availability http://lara.epfl.ch/w/ScalaZ3 https://github.com/psuter/ScalaZ3 - Note that it’s probably also a good starting point for a Java wrapper. (Tested on Windows and Linux, on 32 and 64 bit architectures.)
Thank you.