Example: sliding window ADT
In this example, we implement a "sliding window" as an abstract datatype.
This is similar to the sliding window module described in window.ivy,
but since it is a datatype, we can treat our sliding windows as values, passing
them over the network.
As before, the window is an array that stores a range of values from
an unbounded sequence. The window provides an operation trim that
moves the window offset forward, forgetting the values between the
old and new offsets. This structure might be useful for keeping a
log of events that is occasionally truncated at the beginning, or
for maintaining a window of packets in an ordered transport
protocol.
Also as before, from the point of view of decidability, the main
problem that we need to solve is hiding the arithmetic needed to
express the representation invariant. That is, if s is our
sequence, w is the window and T is the offset of the window,
then we want to say "for all positions I in the sequence, if I >=T<code> then </code>s\[I\] = w\[I-T\]". Unfortunately, this use of arithmetic
would put us outside the decidable fragment, since we are applying
subtraction to universally quantified variables. Thus, we will hide
the function I-T in an isolate, using an uninterpreted relation to
represent it and proving just the properties of this relation needed
to implement our window operations.
However, for an abstract datatype, we have an additional problem,
which is that we have to define an abstraction function (that is, a
map from concrete values to abstract values. In this case, this is
given by a relation value(W,I,V) that tells us wheter window W
has value V at position I. To make things a little harder, we
add a special value none to indiciate positions outside the window.
That is, we require that value(W,I,none) holds when I is outside
the range of window W.
References¤
We will need the order and collections libraries for arrays. We
also use deduction to manually prove a property of the shift relation (see below).
include order
include collections
include deduction
Shift relation¤
In order to "slide" the window, we need a relation that describes a
"shift" in the sequence. The module shift_theory below provides a
relation r(T,X,Y) that holds when X = Y + T. The idea is that T
is the amount of left-shift, X is an element of the original
sequence, and Y is the corresponding element of the shifted
sequence.
The module provides an action f that, given T,X, returns Y. This
action can be used to find an element in the shifted sequence. By hiding
the arithmetic in this action, we can avoid using arithmetic in the
sliding window implementation. The action b provides the inverse of f.
The module takes a parameter idx which must be is an ordered
sequence type.
module shift_theory(idx) = {
r an an action f
that computes the shift (that is, subtracts t from x, to get the
position of x relative to offset t) and an action b that
computes the inverse of f.
relation r(T : idx, X : idx, Y : idx)
action f(t:idx,x:idx) returns (y:idx)
action b(t:idx,x:idx) returns (y:idx)
specification {
r relation:
- It is a partial function from
XtoY - It is a partial function from
YtoX - It is increasing in
X - It is decreasing in
T - It is preserved by successor
- For
T = 0, it is the identity relation - Indices are non-negative, so
Y <= XandT <= X.
Property 2 isn't actually needed, but it might be helpful in other uses of this module. Several of these properties were added in response to counterexamples in verifying the sliding window implementation below.
property r(T,X,Y) & r(T,X,Y1) -> Y = Y1
property r(T,X,Y) & r(T,X1,Y) -> X = X1
property r(T,X1,Y1) & r(T,X2,Y2) & X1 < X2 -> Y1 < Y2
property r(T1,X,Y1) & r(T2,X,Y2) & T1 < T2 -> Y2 < Y1
property r(T,X1,Y1) & idx.succ(X1,X2) & idx.succ(Y1,Y2) -> r(T,X2,Y2)
property r(0,X,X)
property r(X,X,0)
property r(T,X,Y) -> Y <= X & T <= X
none values in the abstract semantics,
we also need the property that every X greater than or equal to the
offset T has a corresponding shifted value Y. Because X and Y
are both of type idx, this AE quantifier alternation creates a function
cycle. For this reason, we state the property as a theorem that must
be manually applied. The proof is similar to the proof of the corresponding
propery in the index reversal theory of list_reverse.ivy. That is,
we first remove the premise of the implication with introImp, then
we manually witness the existential quantifier. In this case, the witness
is the shifted value of X, which is X-T.
theorem [total] {property X >= T -> exists Y. r(T,X,Y)}
proof {
apply introImp;
apply introE with witness = X - T
}
f and b are easy to give in terms of the
shift relation r.
around f {
require t <= x;
...
ensure r(t,x,y)
}
after b {
ensure r(t,y,x)
}
} # end specification section
r and the implemenations of f and b are
also easy to give using arithmetic operations. The implmentation
hides these arithmetic operators, so we don't have to reason with
arithmetic in our window implemenation.
implementation {
definition r(T,X,Y) = (X = Y + T)
implement f {
y := x - t
}
implement b {
y := x + t
}
}
with idx.impl to use the implementation of idx using
the natural number theory, which is needed to prove the above
properties. Note it's very important not to say with idx
here, since the specification of idx (see order.ivy) contains
various universally quantified properties that, when mixed with
arithmetic, would put us outside the decidable fragment.
isolate iso = this with idx.impl
}
domain: the index typerange: the value typenone: the special value indicatingout of range
module window(domain,range,none) = {
type this
value relation is described above. The begin
and end relations tell us the beginning index (i.e., the
offset) and the ending index (i.e., one past the last position
in the window).
relation value(S:this,D:domain,R:range)
relation begin(S:this,D:domain)
relation end(S:this,D:domain)
empty returns an empty window (with offset i)
action empty(i:domain) returns (w:this)
read returns the value in a given position.
action read(w:this, i:domain) returns (v:range)
set modifies a window so that position i has value v.
action set(w:this, i:domain, v:range) returns (w:this)
trim moves the offset position forward, erasing data.
action trim(w:this, i:domain) returns (w:this)
getBegin gets the beginning position
action getBegin(w:this) returns (i:domain)
getEnd gets the ending position
action getEnd(w:this) returns (i:domain)
specification {
around set {
require v ~= none;
require exists I. begin(w, I) & I <= i;
...
ensure I = i -> (value(w, I, V) <-> V = v);
ensure I ~= i & value(old w, I, V) -> value(w, I, V)
}
around read {
require exists I. begin(w, I) & I <= i;
...
ensure value(w, i, v);
}
around trim {
require exists I. begin(w, I) & I <= i;
...
ensure begin(w, I) <-> I = i;
ensure i <= I & value(old w, I, V) -> value(w, I, V)
proof assume shift.total with T = i, X = I;
ensure I < i -> (value(w, I, V) <-> V = none);
}
after empty {
ensure value(w, I, V) <-> V = none
proof assume shift.total with T = i, X = I;
ensure begin(w, I) <-> I = i;
}
after getBegin {
ensure begin(w, i);
}
We also specify a few useful properties of the abstract semantics.
property value(W, I, V1) & value(W, I, V2) -> V1 = V2
property value(W, I, V) -> 0 <= I
property begin(W, I) & begin(W, I2) -> I = I2
}
implementation {
instance shift : shift_theory(slot)
instance arr : array(domain,range)
destructor elems(W:this) : arr
destructor offset(W:this) : domain
property X < offset(W) -> value(W,X,none)
property shift.r(offset(W),X,Y) & Y < elems(W).end -> value(W,X,elems(W).value(Y))
property value(W, I, V1) & value(W, I, V2) -> V1 = V2
property value(W, I, V) -> 0 <= I
value has an
existential quantifier. That is, we have value V at
position X if there exists a shifted position Y with
value V in the array. This would introduce a function
cycle, so we play a trick that is common for ADT's: we use a
macro to define the abstraction function. The lower case
parameters are instantiated by matching against occurrences
of value in whatever property we are trying to prove. This saves
us some effort compared to fully manual instantation.
definition value(w:this,x:domain,v:range) =
(exists Y. shift.r(offset(w),x,Y) &
(v = elems(w).value(Y) if Y < elems(w).end else none))
| x < offset(w) & v = none
definition begin(W,I) = (I = offset(W))
definition end(W,I) = shift.r(offset(W),I,elems(W).end)
set action, we have to
check whether the index to be written is beyond the array end and
and resize the array accordingly. Notice when we resize, we fill in
the gap with none.
implement set {
var index := shift.f(offset(w), i);
if w.elems.end <= index {
w.elems := w.elems.resize(index.next, none);
};
w.elems := w.elems.set(index, v);
}
read, we also check the array index and return none
if it is out of bounds.
implement read {
var index := shift.f(offset(w),i);
v := none if (index >= w.elems.end) else w.elems.value(index)
}
A difficulty in the proof is to deal with positions beyond
the end of the array. That is, suppose X is a position
beyond the end. We can assume that on entry,
value(w,X,none) holds. By the definition of value, we
know there is a corresponding shifted position X that is
beyond the end of the array. We now have to prove that on
exit we still have value(w,X,none). However, the offset of
w has changed, and as a result nothing tells us that a
shifted psoition X still exists. To get this fact, we have
to instantiate theorem total in the shift theory. This is
done manually in the proof of the postcondition of trim
above.
Often, when we really need a function from type t to type
t to be total, the easiest option is to state totality as
a theorem and manually instantiate it for the required
domain value. Often this isn't necessary because the domain
value of interest is a ground term (for example, a program
variable), in which case we can get the value of the
function by calling an appropriate action. Here, though, it
is a universally quantified variable, which requires us to
do some manual instantiation.
implement trim {
var bound := shift.b(offset(w),w.elems.end);
var j := i;
if j < bound {
while j < bound
invariant i <= j & j <= bound
invariant shift.r(offset(old w),bound,old w.elems.end)
invariant j <= X & X < bound & shift.r(offset(old w),X,Y) -> value(old w,X,w.elems.value(Y))
invariant X < j & shift.r(i,X,Y) -> value(old w,X,w.elems.value(Y))
invariant w.offset <= j & w.offset = offset(old w)
invariant w.elems.end = elems(old w).end
{
w.elems := w.elems.set(shift.f(i,j),w.elems.value(shift.f(w.offset,j)));
j := j.next;
};
w.elems := w.elems.resize(shift.f(i,bound),0);
w.offset := i
}
else {
assume false;
w.elems := arr.empty;
w.offset := i;
};
bound := shift.b(offset(w),w.elems.end) # just for a witness
}
implement empty {
w.offset := i;
w.elems := arr.empty
}
implement getBegin {
i := w.offset
}
implement getEnd {
i := shift.b(w.offset,w.elems.end)
}
}
isolate iso = this with domain
}
instance slot : unbounded_sequence
type data
individual none : data
instance foo : window(slot,data,none)
export foo.set
export foo.read
export foo.trim
export foo.empty
export foo.getBegin
export foo.getEnd