Contracts Dealing with Data Structures
Note
JH: May want to start within simpler example here (doesn’t involve loop or cases) based on update or swap.
Contracts that capture properties of data structures require
understanding of additional subtleties for the In(...)
and
Modifies
constructs and for the use of quantification. Consider
the following method that takes a sequence of integers and squares
each element of the sequence::
def square(a: ZS): Unit = {
Contract(
Modifies(a),
Case(
"empty",
Requires(a.size == 0), // this case applies when input array is empty
Ensures(
a == In(a) // In(a) gives the input value of a in the pre-state of square
)
),
Case(
"non-empty",
Requires(a.size > 0), // this case applies when input array not is empty
Ensures(
a.size == In(a).size, // nothing is added or removed from array (elements are only transformed)
∀(a.indices)(i => a(i) == In(a(i)) * In(a(i)))
// a.indices returns a Slang "range" representing indices of a
// can use All instead of ∀
// In(a(i)) desugars to (In(a))(i)
)
)
)
for (i <- 0 until a.size) { // iterate with i, 0 <= i <= a.size
Invariant(
Modifies(a), // a is modified by the loop
a.size == In(a).size, // a's size always equals its size at input
∀(0 until i)(j => a(j) == In(a(j)) * In(a(j))), // applies to part that has been processed
∀(i until a.size)(j => a(j) == In(a(j))) // applies to part to be processed
)
a(i) = a(i) * a(i)
}
}
The above contract provides an additional example of Slang’s contract cases notation: one case addresses the situation where the sequence is empty, the second case addresses the non-empty case.
Before the cases, the Modifies
clause indicates that parameter
a
may be modified by the method. Note that this is clearly a
“may” as opposed to a “must” modality since, while the non-empty
case modifies the sequence elements, the empty case does not. In
other terms, the Modifies
clause can be understood as being
“conservative” and an “over-approximation” of what is actually
modified.
In the “empty” case, In
construct is used in the Ensure
clause
to indicate that the value of a
in the post-state is equal to the
value of a
in the pre-state (indicated by In(a)
). Recall
that Slang’s equality ==
is value-based equality.
In the “non-empty” case, In
is used within an object access
expression ... . size
to indicate that the field access applies
to the value of the object in the pre-state. This clause of the
contract captures a common idiom where one wants to indicate that the
size of the data structure has not changed, even though the contents
may be changed. This type of constraint is often necessary to ensure
that reasoning about bounds violations proceeds as expected (if it is
omitted, then Logika will consider that the size of the structure may have
changed and so it is possible that sequence accesses that should be within
bounds cannot be proven to be in bounds).
The other Ensures
clause specifies how the sequence contents have
been transformed: for each index i
of the sequence, the sequence
value at that position in the post-state is equal the square of the
value at that position in the pre-state. Note that the use of
In(a(i))
around the sequence indexing expression a(i)
raises
the question: can arbitrary forms of expressions be surrounded by
In
to indicate the given expression (not just a single variable)
should be evaluated in the pre-state. The answer is “no”:
technically, Slang Contracts In
clause can only be applied to
variables – in the example above, the syntactic sugar In(a(i))
is
introduced as a convenience for (In(a))(i)
. In the desugared
expression, Slang’s rule of “only apply In
to variables” is
followed. See Section insert reference to sequences/quantification
section for an explanation of Slang’s quantification notation.
Note
JH: Ask Jason/Robby what we did in this type of situation in SPARK: an input variable has its internals modified. Here we have no explicit in/out notations.
JH: Need to adapt the previous section explanations of modifies to indicate that the clause also applies to parameters – not just global variables.
In the method body, the Invariant
contract indicates properties
that should hold true at the beginning and end of the loop body
Note
(JH:double-check position of invariant checking).
Modifies(a)
indicates that the sequence may be modified during the execution of
the loop body. The next invariant constraint indicates that the size
of the sequence is not changed: the sequence size is always equal to
its size in the pre-state. The next constraint indicates what has
been accomplished in the loop up to the present iteration: the value
of each element of the sequence up to (but not including) the present
index has been squared. The final constraint indicates that nothing
has “messed with” the other elements in the sequence – they still
match their values in the pre-state.