Liveness refinement
This is an example of using an abstract model to prove liveness. We first prove liveness of the asbtacrt model using model checking. Then we prove liveness of the system using the liveness property of the model. The second proof is done with l2s_auto, spo we can scale it up. It involves writing some invariants relation the state of the system and the state of the abstract model.
We choose a system for which it is tricky to prove liveness with l2s, to show how mdel checking can simplify the proof.
include order
instance nat : unbounded_sequence
include mc_schemata
module signal0 = {
action raise
specification {
relation now
after init { now := false; }
before raise {
now := true;
now := false;
}
invariant ~now
}
}
module signal(data) = {
action raise(val:data)
specification {
relation now
var value : data
after init { now := false; }
before raise {
value := val;
now := true;
now := false;
}
invariant ~now
}
}
- q(X) indicates that the work is 'qualified'. Qualidied work elements must be received in order
- p(X) indicates the kind of work: false = kind1, true = kind2
We can received qualified elements from the work queue using to methods: - recv1 gets the first qualified element if it is of kind1 - recv2 gets the first qualified element if it is of kind2
We want to show that every qualified element in the queue is eventually received, if both recv1 and recv2 are called infinitely often.
module isolate split_queue2(nat,id,q,p) with nat = {
instantiate equality_schemata
instantiate unbounded_sequence_schemata(nat)
action send(x:id)
action recv1
action recv2
var tail : nat
var done(X:nat) : bool
var queue(X:nat) : id
var lastq : nat
var predq(X:nat) : nat
after init {
done(X) := false;
tail := 0;
lastq := 0;
}
instance trying1 : signal0
instance trying2 : signal0
before send {
queue(tail) := x;
predq(tail) := lastq;
if q(x) {
lastq := tail;
}
tail := tail.next;
}
before recv1 {
trying1.raise;
if some x:nat. ~done(x) & x < tail & q(queue(x)) minimizing x {
if p(queue(x)) {
done(x) := true;
}
}
}
before recv2 {
trying2.raise;
if some x:nat. ~done(x) & x < tail & q(queue(x)) minimizing x {
if ~p(queue(x)) {
done(x) := true;
}
}
}
Also, we state the obvious (that there are no nats less than 0) to help the model checker, without adding 0 to the list of skolems.
invariant X < tail & predq(X) < Y & Y < X -> ~q(queue(Y))
invariant lastq < X & X < tail -> ~q(queue(X))
invariant lastq <= tail
invariant X < tail & done(X) & Y < X & q(queue(Y)) -> done(Y)
invariant done(X) -> X < tail
invariant ~(X:nat < 0)
- X is the cell of the elelment to be completed
- Y is the predecessor of this cell
- V1 is the value of cell X
- V2 is the value of cell Y
The idea of the proof is this: we know by induction on time that cell Y is eventually done. Since there are no qualified cells between Y and X (an invariant stated above) we know the next cell to be processed is X. We have to concretize the cell values so we can evaluate the predicates p and q on these cells.
temporal property [lemma1]
(globally eventually trying1.now) &
(globally eventually trying2.now) ->
forall X. (globally X < tail & q(queue(X)) -> eventually done(X))
proof {
tactic mc with Y = predq(X), V1 = queue(X), V2 = queue(Y)
}
}
type id
relation q(X:id)
relation p(X:id)
instance bar : split_queue2(nat,id,q,p)
action send(x:id)
action recv1
action recv2
var tail : nat
var done(X:nat) : bool
var queue(X:nat) : id
after init {
done(X) := false;
tail := 0;
}
instance trying1 : signal0
instance trying2 : signal0
before send {
bar.send(x); # ghost
queue(tail) := x;
tail := tail.next;
}
before recv1 {
trying1.raise;
bar.recv1; # ghost
if some x:nat. ~done(x) & x < tail & q(queue(x)) minimizing x {
if p(queue(x)) {
done(x) := true;
}
}
}
before recv2 {
trying2.raise;
bar.recv2;
if some x:nat. ~done(x) & x < tail & q(queue(x)) minimizing x {
if ~p(queue(x)) {
done(x) := true;
}
}
}
invariant X < tail & done(X) & Y < X & q(queue(Y)) -> done(Y)
invariant done(X) -> X < tail
invariant ~(X:nat < 0)
invariant tail = bar.tail
invariant done(X) = bar.done(X)
invariant X < tail -> queue(X) = bar.queue(X)
Notice in these l2s_auto proofs, work_start and work_end are the same condition. This is because the starting condition persists until the eventuality holds. We do the same proof for kind1 and kind2.
temporal property [gf1]
(globally eventually trying1.now) ->
(globally eventually bar.trying1.now)
proof {
tactic l2s_auto2 with {
definition work_created = true
definition work_needed = true
definition work_done = false
definition work_end = ~eventually bar.trying1.now
definition work_start = ~eventually bar.trying1.now
definition work_progress = trying1.now
}
}
temporal property [gf2]
(globally eventually trying2.now) ->
(globally eventually bar.trying2.now)
proof {
tactic l2s_auto2 with {
definition work_created = true
definition work_needed = true
definition work_done = false
definition work_end = ~eventually bar.trying2.now
definition work_start = ~eventually bar.trying2.now
definition work_progress = trying2.now
}
}
temporal property [lemma1]
(globally eventually trying1.now) &
(globally eventually trying2.now) ->
forall X. (globally X < tail & q(queue(X)) -> eventually done(X))
proof {
tactic skolemizenp;
instantiate bar.lemma1 with X = _X;
tactic l2s_auto2 with {
definition work_created = true
definition work_needed = true
definition work_done = false
definition work_end = ~(_X < tail & q(queue(_X)) -> eventually done(_X))
definition work_start = ~(_X < tail & q(queue(_X)) -> eventually done(_X))
definition work_progress = bar.done(_X)
}
}
export send
export recv1
export recv2