SMT and Transition Systems
Time to use Z3 again: let's try to assert
our transition relation so that we can use Z3 to answer
questions about the system. To do this, we need to write our transition relation in SMT-LIB 2.
There's two new aspects of SMT-LIB 2 that will make things easier for us. First, SMT-LIB
identifiers can be quoted which means they can have form |...|
where ...
is a sequence of
pretty much any character but |
. This is convenient as it allows us to write, for example,
identifiers such as |s'.is_counting|
. This will make things more readable, hopefully.
Second, SMT-LIB 2 allows us to define functions using the define-fun
command. Its
shape is
(define-fun <ident> ( <args> ) <type>
<expression>
)
where <args>
is zero or more arguments (<ident> <type>)
. For example:
(define-fun add_1 ( (n Int) ) Int
(+ n 1)
)
(define-fun is_even ( (n Int) ) Bool
(= (mod n 2) 0)
)
(declare-const n Int)
(assert
(is_even (add_1 n))
)
; Is there an `n` such that `n + 1` is even? (yes)
(check-sat)
(get-model)
Running Z3, we obtain
> z3 test.smt2
sat
(
(define-fun n () Int
(- 1))
)
So, let's define a function for our transition relation. The definition is a bit daunting because, again, SMT-LIB 2 is really designed to be parsed by solvers. Human readability is not the main concern, but relatively small examples are quite readable if indented properly.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
Full code in the Version 1 section below.
Now, if we want to use this transition relation function, we need to give ourselves actual state
variables to apply it to. Let's switch notation to prepare for the more complex manipulations we
will soon investigate. Let's distinguish state variables using indices: instead of referring to s
and s'
to distinguish between previous and next state, let's just say we have a state of
index 0
and another one of index 1
. So, instead of writing s.cnt
and s'.cnt
we can just
write cnt_0
and cnt_1
respectively.
; "Previous" state.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; "Next" state.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
We can now assert
our trans
ition relation over these two states, i.e. force state 1
to be a
successor of state 0
.
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
Nice 😸. Let's see if we can force state 0
and the inputs of state 1
to have Z3 produce a new
state that's a successor of state 0
. For instance,
- state
0
:cnt
is strictly greater than7
andis_counting
isfalse
; - state
1
:start_stop
is pressed andreset
is not.
(assert (and
; constraints over state 0
(> cnt_0 7)
(not is_counting_0)
; constraints over state 1's inputs
start_stop_1
(not reset_1)
))
(check-sat)
(get-model)
The check-sat
command asks Z3 whether there exists a valuation of all state variables verifying
all our constraints. Said constraints include that state 1
must be a legal successor of state 0
.
Full code in the Version 1 section below.
> z3 test.smt2
sat
(
(define-fun cnt_0 () Int
8)
(define-fun reset_1 () Bool
false)
(define-fun start_stop_1 () Bool
true)
(define-fun is_counting_0 () Bool
false)
(define-fun cnt_1 () Int
9)
(define-fun is_counting_1 () Bool
true)
(define-fun reset_0 () Bool
false)
(define-fun start_stop_0 () Bool
false)
)
It works. In this model (yours might differ), Z3 decided to have cnt_0
be 8
which, given all
the constraints, means that cnt_1
is 9
. This is because is_counting_1
is true
, which is a
consequence of the last assertion we wrote (is_counting_0
is false
and start_stop_1
is
true
).
Maybe we can ask for something more interesting, i.e. maybe we can actually prove something.
Let's modify our last assertion: we keep the constraints over state 0
and replace state 1
's by
cnt_1
must be 0
.
(assert (and
; constraints over state 0
(> cnt_0 7)
(not is_counting_0)
; constraints over state 1's inputs
(= cnt_1 0)
))
(check-sat)
(get-model)
Note that this assertion replaces the one above where we constrained state
1
's inputs.
The question we are asking Z3 is now "say cnt > 7
and we're not counting; is it possible then to
have cnt = 0
in one transition?".
Full code in the Version 2 section below.
sat
(
(define-fun start_stop_1 () Bool
true)
(define-fun reset_1 () Bool
true)
(define-fun is_counting_1 () Bool
true)
(define-fun cnt_0 () Int
8)
(define-fun cnt_1 () Int
0)
(define-fun is_counting_0 () Bool
false)
(define-fun reset_0 () Bool
false)
(define-fun start_stop_0 () Bool
false)
)
Z3 answers "yes" (sat
), and shows us that by pressing reset
in state 1
, then we have cnt = 0
. That's fair, but what if we don't allow reset
to be pressed in state 1
?
(assert (and
; constraints over state 0
(> cnt_0 7)
(not is_counting_0)
; constraints over state 1's inputs
(= cnt_1 0)
(not reset_1)
))
(check-sat)
Readers comfortable with our toy stopwatch system know this should not be possible. If reset
is
not pressed, cnt
can only increase or stay the same depending on whether the system is counting.
Humans (us) "knowing" this is not possible is not very valuable, as humans are notoriously great at being wrong. Let's just ask Z3 to prove (or disprove) what we know.
Full code in the Version 1 section below.
> z3 test.smt2
unsat
So, Z3's answer is that "there exists no valuation of the state variables verifying these
constraints". That is, given the constraints on state 0
, it is not possible to reach a state
where cnt = 0
and reset
is not pressed in one transition.
We can see this result as a consequence of a more abstract property of the system. That is, there
are only three possible cnt
values in the successor of any given state. Given cnt_0
, cnt_1
can only be 0
, cnt_0
, or cnt_0 + 1
.
Exercise: have Z3 prove this property by rewriting the last assertion of Version 3. Check out Version 4 for the solution. Hint below.
Hint.
Another way to look at what we want to prove is to say "it is not possible for cnt_1
to be
anything else than 0
, cnt_0
, or cnt_0 + 1
".
So, if we ask Z3 for a model where cnt_1
is none of these and the answer is unsat
, then we
would prove that cnt_1
cannot be anything but one of these three (not necessarily distinct)
values.
Full Code For All Examples
Version 1
Expand this for the full code.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
; "Previous" state.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; "Next" state.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
(assert (and
; constraints over state 0
(> cnt_0 7)
(not is_counting_0)
; constraints over state 1's inputs
start_stop_1
(not reset_1)
))
(check-sat)
(get-model)
Output:
> z3 test.smt2
sat
(
(define-fun cnt_0 () Int
8)
(define-fun reset_1 () Bool
false)
(define-fun start_stop_1 () Bool
true)
(define-fun is_counting_0 () Bool
false)
(define-fun cnt_1 () Int
9)
(define-fun is_counting_1 () Bool
true)
(define-fun reset_0 () Bool
false)
(define-fun start_stop_0 () Bool
false)
)
Version 2
Expand this for the full code.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
; "Previous" state.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; "Next" state.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
(assert (and
; constraints over state 0
(> cnt_0 7)
(not is_counting_0)
; constraints over state 1's inputs
(= cnt_1 0)
))
(check-sat)
(get-model)
Output:
> z3 test.smt2
sat
(
(define-fun start_stop_1 () Bool
true)
(define-fun reset_1 () Bool
true)
(define-fun is_counting_1 () Bool
true)
(define-fun cnt_0 () Int
8)
(define-fun cnt_1 () Int
0)
(define-fun is_counting_0 () Bool
false)
(define-fun reset_0 () Bool
false)
(define-fun start_stop_0 () Bool
false)
)
Version 3
Expand this for the full code.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
; "Previous" state.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; "Next" state.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
(assert (and
; constraints over state 0
(> cnt_0 7)
(not is_counting_0)
; constraints over state 1's inputs
(= cnt_1 0)
(not reset_1)
))
(check-sat)
Output:
> z3 test.smt2
unsat
Version 4
Expand this for the full code.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
; "Previous" state.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; "Next" state.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
(assert
; negate what we want to prove
(not
; three possible values for `cnt_1` (not necessarily different)
(or
(= cnt_1 0)
(= cnt_1 cnt_0)
(= cnt_1 (+ cnt_0 1))
)
)
)
(check-sat)
Output:
> z3 test.smt2
unsat