Making use of F#'s math libraries together with Z3
A short note by Byron Cook
Recent work on F#'s math libraries, together with the latest release of
Z3 make for a pretty powerful mixture. In particular I find it interesting that its so easy
to combine F#'s polymorphic matrix code together with the power of Z3.
I recently used F#'s new matrix syntax and the new Z3 release in order
to re-implement the rank function synthesis engine used within
TERMINATOR. The result turned out to be so concise that I thought it would be interesting to the larger F# community. I expect that, in the future,
Don will probably pick up
this example and use it as an F# sample. Thus, if you're looking for an up-to-date version of this example check the F# distribution.
At the high-level we're going to build a tool that takes in a mathematical relation represented as the conjunction of linear inequalities. As an example consider "x>0 and x' = x-1 and y'>y", which is a relation stating that the new value of x is always one less than the old value of x, that x is always positive, and that y goes up. We're out to automatically prove that this relation is well-founded, meaning that if you apply it pointwise to any infinite sequence of pairs (x0,y0),(x1,y1),......... that the relation will eventually not hold on a pair. See recent lecture notes
(lecture 1,
lecture 2,
and lecture 3) for more information.
The underlying algorithm that we'll implement is given in a paper by Podelski and
Rybalchenko
called
"A complete method for the synthesis of linear ranking functions".
The crux of the paper is in Fig. 1:
In short, the paper encourages us to think of a relation R as a matrix of
coefficients applied to the pre- and post-variables. Think of A as the coefficients that effect the pre-variables in R, and A' the coefficients that effect the post-variables (i.e. the variables with 's) . The paper says that if we can find a couple of vectors (lambda 1 and lambda 2) that meet some constraints, then we have proved the relation well-founded (i.e., the program that the relation represents will terminate under all conditions).
Consider a simple example of a loop like "while(x>0 and y>0) { x := x-1; y := y + posnum(); }". As a mathematical relation we can think of this as
"x>0 and x'=x-1 and y>0 and y'>=y + 1". R is matrix representing this relation:
let R = [ [-1;0;0;0;1] // x > 0
; [-1;0;1;0;1] // x'>= x-1
; [1;0;-1;0;-1] // x'<= x-1
; [0;-1;0;0;1] // y > 0
; [0;1;0;-1;1] // y' > y
]
Each line in R represents an inequality. The first line, in this example,
represents "(x * -1) + (y * 0) + (x' * 0) + (y'* 0) + 1 <= 0", i.e. "x>0". We would like to build a tool that takes relations like R and proves them well-founded via the synthesis of a ranking function and
bound. For example, imagine we have:
let run (R:int list list) =
begin match synthesis R with
| None -> Printf.printf "Not well-founded\n"
| Some(rf,b) -> Printf.printf "Rank function: %s\n" (rf.ToString());
Printf.printf "Bound: %s\n" (b.ToString())
end;
Printf.printf "\n"
If we execute this function run on our relation R from above, we'd like for our procedure to print something like:
Rank function: [1;0]
Bound: 0
Where the rank function [1;0] represents the coefficients on the variables (x and y). In this case the ranking function is (x * 1) + (y * 0). That is, on every
iteration of R to pairs of points on an infinite sequence we know
that (x * 1) + (y * 0) would go down and that (x * 1) + (y * 0) would still
be larger than or equal to the bound 0. For this reason we know that R is
well-founded.
Implementation
We now define a working implementation of the algorithm. To get things
started we need symbolic matrices.
Thus we use generic matrices of type Matrix<z3e>, where z3e are the symbolic expressions used in Z3. This is where things get a little bit interesting, as
the Matrix<T> library
expects that T is an arithmetic type. Thus we need to instantiate
a Z3 type such that operations like + and * are defined on it. The code to get Z3 loaded up, and to declare its type to be arithmetic is as follows:
#light
#I "c:/Program Files/Microsoft Research/Z3-1.1/bin"
#r @"Microsoft.Z3.dll"
open Microsoft.Z3
open Microsoft.FSharp.Math
open Matrix.Generic
// Load up Z3. Note: we need to dispose of z3 at the end.
let p = new Config()
p.SetParamValue("MODEL","true")
let z3 = new Context(p)
p.Dispose()
// We make a type z3e
type z3e = Z of nativeint
let z3ePtr (Z p) = p
let integer (i:int) = Z (z3.MkNumeral(i,z3.MkRealType()))
let zero = integer 0
let one = integer 1
let add (Z a) (Z b) = Z(z3.MkAdd(a,b))
let sub (Z a) (Z b) = Z(z3.MkSub(a,b))
let mul (Z a) (Z b) = Z(z3.MkMul(a,b))
let neg z = mul (integer (-1)) z
// Register arithmetic on z3e, allowing me to use overloaded +, *, etc
let FormNumerics =
{ new INumeric<_> with
member ops.Zero = zero
member ops.One = one
member ops.Add(a,b) = add a b
member ops.Subtract(a,b) = sub a b
member ops.Multiply(a,b) = mul a b
member ops.Negate(a) = neg a
// The remainder of these ops are left unfinished for now.
member ops.Abs(a) = failwith "no abs"
member ops.Sign(a) = failwith "no sign"
member ops.ToString((x:z3e),fmt,fmtprovider) = "formula"
member ops.Parse(s,numstyle,fmtprovider) = failwith "no parsing"
}
Math.GlobalAssociations.RegisterNumericAssociation (FormNumerics)
To call Z3 on z3e queries we simply call the following function
let solve (Z q) =
z3.Push();
z3.AssertCnstr(q) ;
let model = ref null in
let ans = z3.CheckAndGetModel(model) in
z3.Pop();
match ans with
| LBool.True -> Some (!model)
| LBool.False -> None
| _ -> failwith "Error!\n"
Before we implement the algorithm we also need a few helper functions on z3e expressons:
let make_var (name:string) = Z (z3.MkConst(name,z3.MkRealType()))
let make_const (x:int) = integer x
let conjunction xs = Z(z3.MkAnd (Array.of_list (List.map (fun (Z x) -> x) xs))) ;;
let make_constraint f (v:RowVector<z3e>) =
let vl = Vector.Generic.fold (fun x (Z y) -> Z y::x) [] (v.Transpose) in
let cs = List.map f vl in
conjunction cs
// Some infix comparison operators that take vectors * integers
let (=*) v (k:int) = make_constraint (fun (Z x) ->
Z(z3.MkEq(x,z3.MkNumeral(k,z3.MkRealType())))) v
let (>=*) v (k:int) = make_constraint (fun (Z x) ->
Z(z3.MkGe(x,z3.MkNumeral(k,z3.MkRealType())))) v
let (<*) v (k:int) = make_constraint (fun (Z x) ->
Z(z3.MkLt(x,z3.MkNumeral(k,z3.MkRealType())))) v
We also need some code for generating fresh variables:
// naturals = 0,1,2.......
let naturals = Seq.unfold (fun i -> Some (i,i+1I)) 0I
// vars = v0,v1,v2,v3,...
let vars = { for i in naturals -> make_var (sprintf "v%O" i) }
let varEnumerator = vars.GetEnumerator()
let fresh_var () = assert(varEnumerator.MoveNext()); varEnumerator.Current
From there we can almost literally implement the algorithm as it's found in the figure in the paper. Note especially that the definition
of query comes directly from Fig 1.
let synthesis R =
// Cut the input relation into its components
// (as described in Podelski & Rybalchenko)
// R defined as "coefs (X/X') + b <= 0" where
// coefs is a r*c matrix
let coefs = Matrix.Generic.of_list R in
let r,c = coefs.Dimensions in
let b = - coefs.[ 0 .. r-1 , c-1 .. c-1 ] in
let A = coefs.[ 0 .. r-1 , 0 .. (c-2)/2 ] in
let A' = coefs.[ 0 .. r-1 , (c-2)/2 +1 .. c-2 ] in
// Make z3e versions of matrices A, A', and b
// NOTE: the current version of F#'s generic map
// doesn't work. See below for my version
let b_symb = Matrix.Generic.map make_const b in
let A_symb = Matrix.Generic.map make_const A in
let A_symb' = Matrix.Generic.map make_const A' in
// Create z3e-vectors lambda1 and lambda2
let lambda1 = RowVector.Generic.init r (fun i -> fresh_var ())
let lambda2 = RowVector.Generic.init r (fun i -> fresh_var ())
// Construct the query as described in Podelski & Rybalchenko, Fig. 1
let query = conjunction [ lambda1 >=* 0
; lambda2 >=* 0
; lambda1 * A_symb' =* 0
; (lambda1 - lambda2) * A_symb =* 0
; lambda2 * (A_symb + A_symb') =* 0
; lambda2 * b_symb <* 0
]
in
// Search for a model via Z3
match solve query with
// Case: relation not proved well-founded, thus no ranking
// function is returned
| None -> None
// Case: relation is well-founded, construct a ranking function and bound
| Some (m as model) ->
// Get concrete instance for lambda1 and lambda2
let get_val (Z x) = m.GetNumeralValueInt(m.Eval(x)) in
let lambda1_inst = RowVector.Generic.map get_val lambda1
let lambda2_inst = RowVector.Generic.map get_val lambda2
m.Dispose();
// Compute ranking function "r" and bound "delta_0" as in
// Podelski & Rybalchenko, Fig. 1
let r = lambda2_inst * A' in
let delta_0 = - lambda1_inst * b in
Some(r,delta_0)
Examples
Now for some examples:
// Simple example. R0 represents "x'=x-1 && x>0"
let R0 = [ [-1;0;0] // x >= 0
; [-1;1;1] // x'>= x-1
; [1;-1;-1] // x'<= x-1
]
// Example, slide 17 from in
// http://research.microsoft.com/TERMINATOR/l2.pps
// note: relation is A'A in the slides, here it is AA'
let R1 = [ [-1;0;0;0;1] // x > 0
; [-1;0;1;0;1] // x'>= x-1
; [1;0;-1;0;-1] // x'<= x-1
; [0;-1;0;0;1] // y > 0
; [0;1;0;-1;1] // y' > y
]
// Example 1 from Podelski and Rybalchenko.
// R2 represents loop "while (i-j>=1) do (i,j) := (i-Nat(),j+Pos()); od"
// where "Nat()" could return any natural number and
// "Pos()" could return any positive number
let R2 = [ [-1;1;0;0;1] // -i + j + 1 <= 0
; [-1;0;1;0;0] // -i + i'+ 0 <= 0
; [0;1;0;-1;1] // j - j' + 1 <= 0
]
// Example of termination bug. Shouldn't be well-founded.
// Represents "x'=x+1 && x>0"
let R3 = [ [-1;0;1] // x > 0
; [-1;1;-1] // x'>= x+1
; [1;-1;1] // x'<= x+1
]
When we call the sequence run R0;; run R1;; run R2;; run R3;; we
get the following output:
Rank function: rowvec [1]
Bound: rowvec [0]
Rank function: rowvec [1;0]
Bound: rowvec [1]
Rank function: rowvec [1;-1]
Bound: rowvec [1]
Not well-founded
One final thing
If you're wanting to make this example work today using the
current version of F# you'll need to use this code:
// #'s Generic Matrix map is not general enough in type
// Additions to library -- lifted out of example
module Matrix =
module Generic =
let map f (m:Matrix<'a>) = Matrix.Generic.init m.NumRows m.NumCols
(fun r c -> f m.[r,c])
module RowVector =
module Generic =
let map f (rv:RowVector<'a>) = RowVector.Generic.init rv.Length
(fun i -> f rv.[i])
Acknowledgements
Nikolaj Bjorner,
James Margetson,
Leonardo de Moura,
and
Don Syme
all looked at previous versions of the code above and made helpful suggestions.