0

My goal is to define a queue as a z3 datatype (in z3py) so that I can perform operations on the queue as constraints. Is there any way to do this, and what is it if there is?

My first instinct, since the three tutorials I'm aware of mentioned them, was to use Algebraic Data Types (ADTs) for this, by means of recursive function definitions like ones frequent in OCaml or Haskell. I found some posts from a while ago discussing ADTs in z3, such as list concat in z3. Some answers on other posts claimed that z3 did not support recursion, but the accepted answer to that one defined a function very similar in style to what I wanted, so I don't know what the right/up-to-date answer is.

def queuegen(sort):
    Queue = Datatype('Queue_of_%s' % sort.name())
    Queue.declare('enqueue', ('last', sort), ('rest', Queue))
    Queue.declare('empty')
    Queue = Queue.create()
    x = Const('x',sort)
    q = Const('q', Queue)
    enqueue = Queue.enqueue
    dequeue = Function('dequeue', Queue, Queue)
    peek = Function('peek', Queue, sort)
    size = Function('size', Queue, IntSort())

# just showing my attempted recursive definition for size(q), 
# since no sense in worrying about the other functions if I can't do this
    sizedef = ForAll(q,If(q == Queue.empty, size(q) == 0,\
                     size(q) == 1 + size(Queue.rest(q))))

    return Queue, [enqueue,dequeue,peek,size], sizedef

z3 does not finish when I add the sizedef constraint I produce to a solver and attempt to check it.

2 Answers2

1

Things like queues, stacks, etc. are essentially recursive definitions. When the answer you linked was written, SMTLib did not have any support for recursive type and function declarations. Quantified axioms were the only mechanism to get some of this stuff in, without much hope for detailed support from the underlying solver.

The good news is that the standard has evolved, and it now stipulates precise mechanisms for writing recursive data-types and functions. See Section 4.2.3 of http://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2017-07-18.pdf.

The not so good news is that solver support is still quite weak. While z3 will accept such definitions, it's unlikely to actually prove any interesting theorems about such structures. Bottom line remains the same: Proving properties of recursive functions (like your size) require induction, and SMT solvers simply don't have inductive capabilities. Quantifier patterns and e-matching only take you so far, and they only work for uninterpreted functions. Long story short, if you want to reason about such structures, use a proper theorem prover. An SMT solver is just not the right choice here. Many theorem provers these days call SMT solvers as oracles anyhow, so you get the best of both worlds. (Check out Isabelle Z3 integration, for instance.)

The other question here is if you can do this from the Python API. Obviously, they added support for defining the recursive data type as you found out, but it isn't clear to me if they also added proper recursive function declarations. The answer is most likely not, but let us know if you find otherwise.

Long story short: If you want to model recursive data types and recursive functions, do not use an SMT solver. It's just the wrong tool.

alias
  • 28,120
  • 2
  • 23
  • 40
  • When you say that z3 accepts such definitions, but won't prove interesting properties about them, would that cause issues in proving properties of a larger system that uses an axiomatized data structure? I don't care about proving the correctness of a queue, but rather properties of a larger system that utilizes queues. – Pierce Douglis Aug 02 '19 at 14:57
  • Probably. If the property you're using actually refers to those definitions, then surely. Your best bet is to try out and see what happens; hard to predict these things without any context. – alias Aug 02 '19 at 17:05
  • If your goal is to prove properties about queues/stacks etc.; Dafny might be a better choice as well: https://www.microsoft.com/en-us/research/project/dafny-a-language-and-program-verifier-for-functional-correctness/ – alias Aug 02 '19 at 17:06
0

For anyone seeking to solve a similar problem, I did not find a way to do it with ADTs (which is a shame, because they are much cleaner). I defined a queue by maintaining three variables: a head, a tail, and a function from ints to values.

Since you can define a z3 function in terms of another function, you can use a function to map to elements of the queue, a head to keep track of the front index, and a tail to determine whether the queue is empty (head == tail). When you enqueue, you just create a new function with the correct value associated with the tail, then increment the tail. When you dequeue (mutable), you just use the head to access the value of the old function and then increment the head (ignoring the previous index from now on).

It's worse than using ADTs, since you have to ensure your invariant yourself with no help from the language, but it does seem to get the job done.