3.2 Searching for Records
This example deals with the specification of a program that searches in an array of records for a
record with a specific key. The example is based on the following program class:
class Record
{
String key;
int value;
Record(String k, int v)
{
key = k;
value = v;
}
boolean equals(String k)
{
boolean e = key.equals(k);
return e;
}
public static int search(Record[] a, String key)
{
int n = a.length;
for (int i=0; i<n; i++)
{
Record r = new Record(a[i].key, a[i].value);
boolean e = r.equals(key);
if (e) return i;
}
return -1;
}
public static void main()
{
int N = 10;
Record[] a = new Record[N];
for (int i=0; i<N; i++)
a[i] = new Record(~abc~, i);
a[5] = new Record(~xyz~, 5);
int i = search(a, ~xyz~);
System.out.println(i);
}
}
This program introduces an object type Record with a string field key and an integer field value. The
type has an constructor to build a record from an given string and integer and a method equals that
allows to check whether the record has the denoted key. This function calls the method equals on
object type String; while this class is part of the Java standard library, it has to be explicitly
defined in a file accessible to the RISC ProgramExplorer. We therefore introduce a dummy
class
package java.lang;
public class String
{
public boolean equals(String s) return false;
}
solely for declaring the method equals (without caring for the actual representation of strings or the
actual implementation of the method). Unlike real Java, our programming language only allows to
call program methods with return values to initialize/assign to variables, not as parts of program
expressions .
The two statements
boolean e = key.equals(k);
return e;
in the body of equals can therefore not be merged into one.
The core of the program is the method search which takes an array a of records and a key and returns
the index of the first record in a that contains that key (or -1, if there is no such record). The core of
the method body is represented by the two statements
Record r = new Record(a[i].key, a[i].value);
boolean e = r.equals(key);
The first statement builds a record r from the key and the value of record a[i]. The second statement
calls the method equals on r to compare its key with key. This apparently clumsy way of using the
function equals is necessary because the specification formalism considers object variables (variables
of object types) to hold object values rather than object references (which considerably simplifies
reasoning because then the modification of an object via one variable cannot affect an object
referenced by another variable).
However, since the programming language Java (like most programming languages) lets object
variables hold references, the semantics of our programming language would deviate from classical
program semantics. Therefore the type checker ensures that two different program variables cannot
refer to the same object; consequently it does not make any difference whether an object variable
holds an object value or an object reference. Consequently, if equals would modify its record, above
solution would not update array a (independent of whether object variables hold object values or
object references) while the solution
Record r = a[i];
boolean e = r.equals(key);
would update a in a language with reference semantics for objects but not in a language with value
semantics. For similar reasons, the even shorter solution
boolean e = a[i].equals(key);
is also (even syntactically) prohibited. See Appendix B for a more thorough description of the
constraints of our programming language compared to Java.
The method main of the program creates an array, fills it with values, updates it, and calls the method
search in the usual way. It also calls the method System.out.println of the Java Standard API. This
method has to be declared with the help of the dummy classes
package java.lang;
import java.io.*;
public class System
{
public static PrintStream out;
}
package java.io;
public class PrintStream
{
public void println(boolean b)
public void println(int i)
public void println(char c)
public void println(String s)
public void println()
}
Type-checking the class Record creates a new theory Record in the same package as the
class. Right-clicking this theory from the tab Symbols and selecting Print Declaration
displays
theory Record uses java.lang.String, Base
{
Record: TYPE =
[#key: java.lang.String.String, value: Base.int,
null: BOOLEAN, new: INT#];
null: Record;
newObject: INT -> Record =
LAMBDA(x: INT): null WITH .null:=FALSE WITH .new:=x;
Array: TYPE =
[#value: ARRAY Base.int OF Record, length: Base.nat,
null: BOOLEAN#];
nullArray: Array;
newArray: Base.nat -> Array;
newArrayAxiom:
AXIOM FORALL(x: Base.nat):
LET y = newArray(x) IN
y.null = FALSE AND y.length = x AND
(FORALL(i: Base.nat): i < x => y.value[i] = null);
}
which introduces the following entities that mathematically model the program operations that
involve program type Record.
- a logical record type Record which contains (among other fields) one field for each
object variable in class Record, the specification language considers program variables
of object type Record to hold values of the logical type Record (more details later);
- a constant null representing a logical counterpart of the null pointer of type Record;
- a function newObject which returns a non-null object of type Record as created by a call
of a constructor of class Record;
- a type Array representing the logical counterpart of the program type Record[];
- a type nullArray representing the logical counterpart of the null pointer of the type
Record[];
- a function newArray which returns an Array;
- an axiom newArrayAxiom which describes the result newArray as a non-null array of a
certain length whose entries are null.
There are some subtleties about the modeling a program object of type Record as a mathematical
Record that should be known for their proper use:
- If the null field of a valid Record value is not FALSE, the value represents the null
pointer of type Record. The Record pointer null is thus not only represented by the
single mathematical constant null but by every Record value whose null field is TRUE
(the constant null is just an unknown Record value for which the opposite cannot be
proved) .
A precondition that asserts that a Record object is not null must therefore state for
the corresponding Record value r that r.null = FALSE holds (the condition r /=
null alone is too weak).
- The field new is initialized to an unknown value at the creation of a record; two records
created in different constructor calls can thus not be proved equal.
The program is now specified with the help of the following local theory
/*@
theory uses Base, Record, java.lang.String, java.io.PrintStream
{
String: TYPE = java.lang.String.String;
notFound: PREDICATE(Record.Array, Base.int, String) =
PRED(a:Record.Array, n: Base.int, key: String):
FORALL(i:INT):
0 <= i AND i < n => a.value[i].key /= key;
}
@*/
class Record { … }
which introduces a predicate notFound to describe that in an array a of records, all positions less than
n hold records whose keys are different from key.
The program method search can now be specified as
public static int search(Record[] a, String key) /*@
requires
(VAR a).null = FALSE AND (VAR key).null = FALSE AND
(FORALL(i:INT): 0 <= i AND i < VAR a.length =>
(VAR a).value[i].null = FALSE AND
(VAR a).value[i].key.null = FALSE);
ensures
(LET result=VALUE@NEXT, n = (VAR a).length IN
IF result = -1 THEN
notFound(VAR a, n, VAR key)
ELSE
0 <= result AND result < n AND
notFound(VAR a, result, VAR key) AND
(VAR a).value[result].key = VAR key
ENDIF);
@*/
{ … }
The method’s precondition states that search must not be called with the array null as argument
and that its result is either -1 (indicating that the given key has not been found in the
array) or that its result is the smallest index of the array such that the corresponding record
has the denoted key. The specification makes use of local logical variables result and
n representing the return value of the method and the length of the method parameter
a.
The core loop of the method’s body can be annotated as
for (int i=0; i<n; i++)
/*@
invariant
(VAR a).null = FALSE AND (VAR key).null = FALSE AND
(FORALL(i:INT): 0 <= i AND i < VAR a.length =>
(VAR a).value[i].null = FALSE AND
(VAR a).value[i].key.null = FALSE) AND
VAR n = (VAR a).length AND
0 <= VAR i AND VAR i <= VAR n AND
notFound(VAR a, VAR i, VAR key) AND
(RETURNS@NEXT =>
VAR i < VAR n AND
(VAR a).value[VAR i].key = VAR key AND
VALUE@NEXT = VAR i);
decreases VAR n - VAR i;
@*/
{ … }
to give a suitable invariant and termination term.
The state after type-checking the annotated program is shown below:
All type checking conditions can be automatically solved; the verification of the other conditions
proceeds in a similar way as discussed in Chapter 4.