0

I wrote a benchmark below to generate cross product of two lists. Does z3 have some sort of max recursive bound? For some reason it can reason about lists of size 1 but not size 2. Or perhaps I have a mistake somewhere in my formalization?

(declare-datatypes ((MyList 1)) ((par (T) ((cons (head T) (tail (MyList T))) (nil)))))
(declare-datatypes (T2) ((Pair (pair (first T2) (second T2)))))

; list functions for lists of ints
(define-fun prepend ( (val (Pair Int)) (l (MyList (Pair Int))) ) (MyList (Pair Int)) (cons val l))

(declare-fun get ( (MyList Int) Int ) Int)
(assert (forall ( (h Int) (t (MyList Int)) (i Int) )
                (ite (<= i 0)
                     (= (get (cons h t) i) h)
                     (= (get (cons h t) i) (get t (- i 1))))))

(declare-fun list_length ( (MyList Int) ) Int)
(assert (= (list_length (as nil (MyList Int))) 0))
(assert (forall ( (val Int) (l (MyList Int)) )
                (= (list_length (cons val l)) (+ 1 (list_length l)))))

(declare-fun tail ( (MyList Int) Int ) (MyList Int))
(assert (forall ( (start Int) (h Int) (t (MyList Int)) )
                (ite (<= start 0)
                     (= (tail (cons h t) start) (cons h t))
                     (= (tail (cons h t) start) (tail t (- start 1))))))
(assert (forall ( (start Int) )
                (= (tail (as nil (MyList Int)) start) (as nil (MyList Int)))))


; same list functions but for lists of int pairs -- 
; would be great if there is a way to avoid redefining all these again :(
(declare-fun list_get_pair ( (MyList (Pair Int)) Int ) (Pair Int))
(assert (forall ( (h (Pair Int)) (t (MyList (Pair Int))) (i Int) )
                (ite (<= i 0)
                     (= (list_get_pair (cons h t) i) h)
                     (= (list_get_pair (cons h t) i) (list_get_pair t (- i 1))))))

(declare-fun list_length_pair ( (MyList (Pair Int)) ) Int)
(assert (= (list_length_pair (as nil (MyList (Pair Int)))) 0))
(assert (forall ( (val (Pair Int)) (l (MyList (Pair Int))) )
                (= (list_length_pair (cons val l)) (+ 1 (list_length_pair l)))))

(declare-fun tail_pair ( (MyList (Pair Int)) Int ) (MyList (Pair Int)))
(assert (forall ( (start Int) (h (Pair Int)) (t (MyList (Pair Int))) )
                (ite (<= start 0)
                     (= (tail_pair (cons h t) start) (cons h t))
                     (= (tail_pair (cons h t) start) (tail_pair t (- start 1))))))
(assert (forall ( (start Int) )
                (= (tail_pair (as nil (MyList (Pair Int))) start) (as nil (MyList (Pair Int))))))

(declare-fun concat ( (MyList (Pair Int)) (MyList (Pair Int)) ) (MyList (Pair Int)))
(assert (forall ((xs (MyList (Pair Int))) (ys (MyList (Pair Int))))
            (ite (= (as nil (MyList (Pair Int))) xs)
                 (= (concat xs ys) ys)
                 (= (concat xs ys) (prepend (list_get_pair xs 0) (concat (tail_pair xs 1) ys))))))                 

(assert (forall ((xs (MyList (Pair Int))) (ys (MyList (Pair Int))))
            (=> (= (as nil (MyList (Pair Int))) ys)
                (= (concat xs ys) xs))))

; two functions defined using recursive construct                
(define-funs-rec
(
(cross_helper ((i Int) (ys (MyList Int))) (MyList (Pair Int)))
(cross ((xs (MyList Int)) (ys (MyList Int))) (MyList (Pair Int)))
)
(
; cross_helper - given e and [a, b, c] return [(e,a), (e,b), (e,c)]
(ite (= ys (as nil (MyList Int))) (as nil (MyList (Pair Int)))
     (prepend (pair i (get ys 0)) (cross_helper i (tail ys 1))))


; cross - given [a, b] and [c, d] return [(a,c), (a,d), (b,c) (b,d)]
(ite (= xs (as nil (MyList Int))) (as nil (MyList (Pair Int)))
     (concat (cross_helper (get xs 0) ys) (cross (tail xs 1) ys)))
))


(declare-const in1 (MyList Int)) (declare-const in2 (MyList Int))
(declare-const i Int) (declare-const j Int)
(declare-const in11 Int) (declare-const in12 Int) 
(declare-const in21 Int) (declare-const in22 Int)


; this works
; cross([in11], [in21, in22]) = ([in11, in21], [in11, in22])
(push)
(assert (= in1 (cons in11 (as nil (MyList Int)))))
(assert (= in2 (cons in21 (cons in22 (as nil (MyList Int))))))

(assert (not (= (cross in1 in2) (cons (pair in11 in21) (cons (pair in11 in22)                               
                                                            (as nil (MyList (Pair Int))))))))
(check-sat) (pop)

; but this doesn't work
; cross([in11, in12], [in21, in22]) = ([in11, in21], [in11, in22], [in12, in21], [in12, in22])
(push)
(assert (= in1 (cons in11 (cons in22 (as nil (MyList Int))))))
(assert (= in2 (cons in21 (cons in22 (as nil (MyList Int))))))

(assert (not (= (cross in1 in2) (cons (pair in11 in21) (cons (pair in11 in22)
                               (cons (pair in12 in21) (cons (pair in12 in22)
                                                            (as nil (MyList (Pair Int))))))))))
(check-sat) (pop)
JRR
  • 6,014
  • 6
  • 39
  • 59
  • What do you mean "cannot reason"? Does it produce an incorrect answer, or does it simply not answer your query in a reasonable amount of time? – alias Feb 09 '19 at 20:55
  • The second `check-sat` never returns AFAIK. So I wonder if this has to do with how many times z3 unfolds a recursive definition. – JRR Feb 09 '19 at 21:34

1 Answers1

0

It is really not correct to talk about a "maximum recursive bound" in the context of an SMT solver. I can see your inclination to call it as such; as you're hoping it would simply unroll the definitions as much as necessary. But that's simply not how an SMT solver works.

In general, when you have a recursive function it induces a set of quantified axioms. (You can find the translation in page 63 of http://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2017-07-18.pdf.) So, what you might view as a "function" actually is a short-hand for writing a bunch of quantified axioms.

Note that you also have your own quantified axioms, and all of those constraints get together and the solver goes to work. Unfortunately, this makes the logic semi-decidable; meaning there is no decision procedure. (A decision procedure is one that always terminates and answers correctly.) In theory, this means it can always find a "proof" if it exists eventually but may loop indefinitely if something isn't true. However, in practice, this usually means it'll loop sufficiently long enough in both cases that either your patience or your computer's memory will run out first.

There are algorithms (such as macro-finding and e-matching) that deal with quantifiers. But these are all necessarily incomplete, and in my experience quite brittle. You can also try helping z3 by providing patterns for quantifier instantiation, see here: https://rise4fun.com/z3/tutorialcontent/guide#h28. But the technique is neither easy to use, nor it scales in practice.

Long story short, SMT solvers are just not good for reasoning with quantifiers. And recursive definitions imply quantification. There are both theoretical limits to what they can do, and also practical considerations on usability, performance, and honestly return-on-investment: If you want to reason about recursive functions and recursive data-types, SMT solvers are arguably just not the right tool. Instead, use a theorem-prover such as HOL, Isabelle, Coq, Agda, Lean; etc., which are designed to work with such structures. (Most of these tools can automatically call the SMT-solver on your behalf for simplified goals; so you get the best of both worlds.)

I hope this explanation is clear. The rule of thumb is that reasoning about recursive functions requires induction, and inductive proofs require the invention of necessary invariants. SMT solvers cannot come up with the required invariants for you, nor they allow you to specify what those invariants are even if you were willing to provide them. But theorem provers can help you state and prove those invariants; and should be preferred for such problems.

alias
  • 28,120
  • 2
  • 23
  • 40