简体   繁体   中英

defining list concat in smtlib

I defined my own version of list concat based on Haskell as below:

(declare-datatypes ((MyList 1))
                   ((par (T) ((cons (head T) (tail (MyList T))) (nil)))))
(declare-fun my-concat ( (MyList T1) (MyList T1) ) (MyList T1))
(assert (forall ((xs (MyList T1)) (ys (MyList T1)) (x T1))
            (ite (= (as nil (MyList T1)) xs)
                 (= (my-concat xs ys) ys)
                 (= (my-concat (cons x xs) ys) (cons x (my-concat xs ys))))))

I am wondering why z3 is not able to reason about the following?

(assert (not (= (my-concat (cons 4 (as nil (MyList Int))) (as nil (MyList Int))) 
                (cons 4 (as nil (MyList Int))))))
(check-sat) ; runs forever

Loading your file

If I load your program to z3 as you've given it, it says:

(error "line 3 column 33: Parsing function declaration. Expecting sort list '(': unknown sort 'T1'")
(error "line 4 column 29: invalid sorted variables: unknown sort 'T1'")
(error "line 8 column 79: unknown function/constant my-concat")

That's because you cannot define "polymorphic" functions in SMTLib. At the user-level, only fully monomorphic functions are allowed. (Though internally SMTLib does provide polymorphic constants, there's no way for the user to actually create any polymorphic constants.)

So, I'm not sure how you got to load that file.

Monomorphisation

Looks like you only care about integer-lists anyhow, so let's just modify our program to work on lists of integers. This process is called monomorphisation, and is usually automatically done by some front-end tool before you even get to the SMT-solver, depending on what framework you're working in. That's just a fancy way of saying create instances of all your polymorphic constants at the mono-types they are used. (Since you mentioned Haskell, I'll just throw in that while monomorphisation is usually possible, it's not always feasible: There might be way too many variants to generate making it impractical. Also, if you have polymoprhic-recursion then monomorphisation doesn't work. But that's a digression for the time being.)

If I monomorphise your program to Int 's only, I get:

(declare-datatypes ((MyList 1))
                   ((par (T) ((cons (head T) (tail (MyList T))) (nil)))))
(declare-fun my-concat ( (MyList Int) (MyList Int) ) (MyList Int))
(assert (forall ((xs (MyList Int)) (ys (MyList Int)) (x Int))
            (ite (= (as nil (MyList Int)) xs)
                 (= (my-concat xs ys) ys)
                 (= (my-concat (cons x xs) ys) (cons x (my-concat xs ys))))))
(assert (not (= (my-concat (cons 4 (as nil (MyList Int))) (as nil (MyList Int)))
                (cons 4 (as nil (MyList Int))))))
(check-sat)
(get-info :reason-unknown)

When I run z3 on this, I get:

unknown
(:reason-unknown "smt tactic failed to show goal to be sat/unsat (incomplete quantifiers)")

So, it doesn't loop at all like you mentioned; but your file didn't really load in the first place. So maybe you were working with some other contents in the file as well, but that's a digression for the current question.

But it still doesn't prove it!

Of course, you wanted unsat for this trivial formula. But z3 said it's too hard for it to deal with. The reason-unknown is "incomplete quantifiers?" What does that mean?

In short, SMTLib is essentially logic of many-sorted first order formulas. Solvers are "complete" for the quantifier-free fragment of this logic. But adding quantifiers makes the logic semi-decidable. What that means is that if you give enough resources, and if smart heuristics are in play, the solver will eventually say sat for a satisifiable formula, but it may loop forever if given an unsat one. (It might get lucky and say unsat as well, but most likely it'll loop.)

There are very good reasons why above is the case, but keep in mind that this has nothing to do with z3: First-order logic with quantifiers is semi-decidable. (Here's a good place to start reading: https://en.wikipedia.org/wiki/Decidability_(logic)#Semidecidability )

What usually happens in practice, however, is that the solver will not even answer sat , and will simply give up and say unknown as z3 did above. Quantifiers are simply beyond the means of SMT-solvers in general. You can try using patterns (search stack-overflow for quantifiers and pattern triggers), but that usually is futile as patterns and triggers are tricky to work with and they can be quite brittle.

What course of action do you have:

Honestly, this is one of those cases where "give up" is good advice. An SMT solver is just not a good fit for this sort of a problem. Use a theorem-prover like Isabelle, ACL2, Coq, HOL, HOL-Light, Lean, ... where you can express quantifiers and recursive functions and reason with them. They are built for this sort of thing. Don't expect your SMT solver to handle these sorts of queries. It's just not the right match.

Still, is there anything I can do?

You can try SMTLib's recursive-function definition facilities. You'd write:

(declare-datatypes ((MyList 1))
                   ((par (T) ((cons (head T) (tail (MyList T))) (nil)))))

(define-fun-rec my-concat ((xs (MyList Int)) (ys (MyList Int))) (MyList Int)
    (ite (= (as nil (MyList Int)) xs)
         ys
         (cons (head xs) (my-concat (tail xs) ys))))

(assert (not (= (my-concat (cons 4 (as nil (MyList Int))) (as nil (MyList Int)))
                (cons 4 (as nil (MyList Int))))))
(check-sat)

Note the define-fun-rec construct, which allows for recursive definitions. And voila, we get:

unsat

But this does not mean z3 will be able to prove arbitrary theorems regarding this concat function. If you try anything that requires induction, it'll either give up (saying unknown ) or loop-forever. Of course, as the capabilities improve some induction-like proofs might be possible in z3 or other SMT-solvers, but that's really beyond what they're designed for. So, use with caution.

You can read more about recursive definitions in Section 4.2.3 of http://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2017-07-18.pdf

To sum up

Do not use an SMT-solver for reasoning with quantified or recursive definitions. They just don't have the necessary power to deal with such problems, and it's unlikely they'll ever get there. Use a proper theorem prover for these tasks. Note that most theorem-provers use SMT-solvers as underlying tactics, so you get the best of both worlds: You can do some manual work to guide the inductive proof, and have the prover use an SMT-solver to handle most of the goals automatically for you. Here's a good paper to read to get you started on the details: https://people.mpi-inf.mpg.de/~jblanche/jar-smt.pdf

alias makes valid points, but I don't completely agree with the final conclusion to " not use an SMT-solver for reasoning with quantified or recursive definitions" . In the end, it depends on which kind of properties you need to reason about, what kind of answers you need (is unsat/unknown OK, or do you need unsat/sat and a model?), and how much work you're willing to invest:-)

For example, SMT-based program verifiers such as Dafny and Viper quickly verify the following assertions about lists:

assert [4] + [] == [4]; // holds
assert [4,1] + [1,4] == [4,1,1,4]; // holds
assert [4] + [1] == [1,4]; // fails

Both tools can be used online, but the websites are rather slow and not terribly reliable. You can find the Dafny example here and the Viper example here .

Here is the relevant SMT code that Viper generates:

(set-option :auto_config false) ; Usually a good idea
(set-option :smt.mbqi false)

;; The following definitions are an excerpt of Viper's sequence axiomatisation,
;; which is based on Dafny's sequence axiomatisation.
;; See also:
;;   https://github.com/dafny-lang/dafny
;;   http://viper.ethz.ch

(declare-sort Seq<Int>) ;; Monomorphised sort of integer sequences

(declare-const Seq_empty Seq<Int>)
(declare-fun Seq_length (Seq<Int>) Int)
(declare-fun Seq_singleton (Int) Seq<Int>)
(declare-fun Seq_index (Seq<Int> Int) Int)
(declare-fun Seq_append (Seq<Int> Seq<Int>) Seq<Int>)
(declare-fun Seq_equal (Seq<Int> Seq<Int>) Bool)

(assert (forall ((s Seq<Int>)) (!
  (<= 0 (Seq_length s))
  :pattern ((Seq_length s))
  )))

(assert (= (Seq_length (as Seq_empty  Seq<Int>)) 0))

(assert (forall ((s1 Seq<Int>) (s2 Seq<Int>)) (!
  (implies
    (and
      (not (= s1 (as Seq_empty  Seq<Int>)))
      (not (= s2 (as Seq_empty  Seq<Int>))))
    (= (Seq_length (Seq_append s1 s2)) (+ (Seq_length s1) (Seq_length s2))))
  :pattern ((Seq_length (Seq_append s1 s2)))
  )))

(assert (forall ((s Seq<Int>)) (!
  (= (Seq_append (as Seq_empty  Seq<Int>) s) s)
  :pattern ((Seq_append (as Seq_empty  Seq<Int>) s))
  )))

(assert (forall ((s Seq<Int>)) (!
  (= (Seq_append s (as Seq_empty  Seq<Int>)) s)
  :pattern ((Seq_append s (as Seq_empty  Seq<Int>)))
  )))

(assert (forall ((s1 Seq<Int>) (s2 Seq<Int>) (i Int)) (!
  (implies
    (and
      (not (= s1 (as Seq_empty  Seq<Int>)))
      (not (= s2 (as Seq_empty  Seq<Int>))))
    (ite
      (< i (Seq_length s1))
      (= (Seq_index (Seq_append s1 s2) i) (Seq_index s1 i))
      (= (Seq_index (Seq_append s1 s2) i) (Seq_index s2 (- i (Seq_length s1))))))
  :pattern ((Seq_index (Seq_append s1 s2) i))
  :pattern ((Seq_index s1 i) (Seq_append s1 s2))
  )))

(assert (forall ((s1 Seq<Int>) (s2 Seq<Int>)) (!
  (=
    (Seq_equal s1 s2)
    (and
      (= (Seq_length s1) (Seq_length s2))
      (forall ((i Int)) (!
        (implies
          (and (<= 0 i) (< i (Seq_length s1)))
          (= (Seq_index s1 i) (Seq_index s2 i)))
        :pattern ((Seq_index s1 i))
        :pattern ((Seq_index s2 i))
        ))))
  :pattern ((Seq_equal s1 s2))
  )))

(assert (forall ((s1 Seq<Int>) (s2 Seq<Int>)) (!
  (implies (Seq_equal s1 s2) (= s1 s2))
  :pattern ((Seq_equal s1 s2))
  )))

; ------------------------------------------------------------

; assert Seq(4) ++ Seq[Int]() == Seq(4)
(push)
(assert (not 
  (Seq_equal
    (Seq_append (Seq_singleton 4) Seq_empty)
    (Seq_singleton 4))))
(check-sat) ; unsat -- good!
(pop)

; assert Seq(4, 1) ++ Seq(1, 4) == Seq(4, 1, 1, 4)
(push)
(assert (not 
  (Seq_equal
    (Seq_append
      (Seq_append (Seq_singleton 4) (Seq_singleton 1))
      (Seq_append (Seq_singleton 1) (Seq_singleton 4)))
    (Seq_append
      (Seq_append
        (Seq_append (Seq_singleton 4) (Seq_singleton 1))
        (Seq_singleton 1))
      (Seq_singleton 4)))))
(check-sat) ; unsat -- good!
(pop)

; assert Seq(4) ++ Seq(1) == Seq(1, 4)
(push)
(assert (not (Seq_equal
  (Seq_append (Seq_singleton 4) (Seq_singleton 1))
  (Seq_append (Seq_singleton 1) (Seq_singleton 4)))))
(check-sat) ; unknown -- OK, since property doesn't hold
(pop)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM