Jass - Documentation

Assertions

1 Assertions

In this chapter the kinds of assertions that are possible in Jass will be described. The description of the syntax will be illustrated by brief examples of the class Buffer and done in the form of grammatical rules.

The chapter is subdivided into the following parts:

2 The assertion language

Jass supports different kind of assertions which are used to specify different properties of a java class. All assertions must appear in formal comments and have a introducing keyword which specifies the kind of the assertion. In the most cases than follows a list of boolean expressions which describe the allowed states. The boolean expressions can contain variables and method calls.

To allow arbitrary boolean expressions and method calls is dangerous. This will "... open the gates of the declarative city to the applicative hordes." Assertions describe states but should not modify them.

Jass does restrict the usage of expressions and methods: The expressions must be pure declarative, so no side effects like assignment and instance creation can be used. Called methods must be free of side effects too.

Example for a simple precondition:

/** require !isFull(); o != null; **/

Attention: After the last boolean expression must appear a semicolon and the comment must be closed through '**/'.

The semicolon connects the assertions like a logical 'and' so that the whole precondition ultimately is nothing but one boolean expression. The semicolon can therefore be used in order to divide the expression into smaller and clearer parts and, if desired, to give each part a label of its own:

/** require [buffer_not_full] !isFull(); 
            [object_is_valid] o != null; **/

The label stands in brackets in front of the assertion part and identifies the assertion in error messages.

The boolean expressions of an assertion are produced through the following EBNF:

BooleanAssertion ::= ( AssertionLabel )? AssertionExpression
AssertionLabel ::= "[" <IDENTIFIER> "]"

The following table gives an overview about the semantic side conditions of the assertions. See the descriptions of the assertions for a detailed explanation.

kind of assertion type of expressions usage of
fields and methods
usage of
local variables
usage of
formal parameters
usage of
Old and Result
[#prepost]precondition boolean special not allowed allowed not allowed
[#prepost]postcondition boolean allowed not allowed allowed allowed
[#invariant]class invariant boolean allowed not allowed not allowed not allowed
[#loop]loop invariant boolean allowed allowed allowed not allowed
[#loop]loop variant int allowed allowed allowed not allowed
[#check]check boolean allowed allowed allowed not allowed

Recursive definitions of assertions are not allowed. This means: A call of the method that declares the assertions in the assertion is forbidden. This is true for indirect recursion too.

3 Forall and exists expressions

With the usage of forall and exists the programmer can express properties that must be valid for all elements of an finite set of assertions or for one element of this set.

Example for a forall expression:

(forall i : {out%buf.length .. in%buf.length-1} # buf[i] != null);

Forall and exists expressions are produced through the following EBNF:

AssertionExpression ::= ( AssertionForAllExistsExpression | AssertionConditionalExpression )
AssertionForAllExistsExpression ::= ( <FORALL> | <EXISTS> ) <IDENTIFIER> ":" ( AssertionFiniteEnumerationCreation | AssertionRangeExpression ) <FORALL_SEP> AssertionExpression
AssertionFiniteEnumerationCreation ::= "new" Name AssertionArguments
AssertionRangeExpression ::= "{" AssertionShiftExpression <RANGE> AssertionShiftExpression "}"

4 Preconditions and postconditions: 'require' and 'ensure'

As in Eiffel, for each method of a class in Java a precondition and a postcondition can be stated by the Jass extensions. The precondition is at the beginning of the method body (directly after the opening brace), the postcondition is at the end (before the close brace).

With the precondition the developer can specify the states in which a method may be invoked. To satisfy the precondition is the duty of the caller. The precondition is introduced through the keyword require.

Example for a precondition:

public void addElement (Object o) {
/** require !isFull(); o != null; **/
...
}

The method addElement can only be invoked if the buffer is not full and the formal parameter contains a reference to an valid object that should be inserted. Inserting a null reference is forbidden.

The called methods and used variables in the precondition must be as visible as the method they appear in. This availability rule ensures that the caller will understand the conditions under which the method can be invoked. This implies that local variables can not be used.

In the above examples the method isFull must be declared public to satisfy this condition.

A postcondition specifies the states in which the method should return. To satisfy the postcondition is the duty of the developer of the method. The postcondition is introduced through the keyword ensure.

Example for a postcondition:

public void addElement (Object o) {
...
/** ensure !isEmpty() && contains(o); **/
}

On return of the method addElement the buffer can not be empty and the object to insert should be contained.

The postcondition can contain three special constructs: Old, changeonly and Result. This constructs are explained in the following.

The object state at the begin of the method is stored in the special variable Old. With this variable the developer can specify relations between entry and exit states. For example the monotony of a counter.

Example for the use of Old:

public void addElement (Object o) {
...
/** ensure !isEmpty() && contains(o); Old.count == count-1; **/
}

To store a copy of the object at the begin of the method the developer must implement the method clone without throwing any exception (The interface java.lang.Cloneable must implemented!). Thus the developer decides how a copy is generated. This is a feature not a bug! Objects may reference complex data structures. Only the developer can decide which parts must be copied and which can be shared.

Another special construct is the changeonly keyword followed by a list of attributes. If such a list is specified in a postcondition only the declared attributes are allowed to change their values. Not listed attributes must remain constant. The empty list stands for: change nothing. To check this a copy of the object is generated with the clone method analogous to the Old construct. The attributes are compared with the method equals which can be overwritten by the developer. This is a feature, too! Attributes of array type are handled differently. If the array type is unidimensional the contents are compared with the equals method through a loop. If the array has more than one dimension the whole array is compared for reference equality. This approach follows the equals method for array objects.

Example for the use of changeonly:

public void addElement (Object o) {
...
/** ensure !isEmpty() &&  contains(o); 
           Old.count == count-1; 
           changeonly{count,buffer}; **/
}

Pre- and Postcondition are produced through the following EBNF:

MethodDeclaration ::= ( "public" | "protected" | "private" | "static" | "abstract" | "final" | "native" | "synchronized" | "strictfp" )* ResultType MethodDeclarator ( "throws" NameList )? ( "{" MethodBodyBlock "}" | ";" )
MethodBodyBlock ::= ( RequireClause )? ( BlockStatement )* ( EnsureClause )? ( RescueClause )?
RequireClause ::= <REQUIRE> ( BooleanAssertion ";" )+ <ASS_END>
EnsureClause ::= <ENSURE> ( BooleanChangeAssertion ";" )+ <ASS_END>
BooleanAssertion ::= ( AssertionLabel )? AssertionExpression
BooleanChangeAssertion ::= ( <CHANGEONLY> "{" ( ChangeList )? "}" | BooleanAssertion )
ChangeList ::= FieldReference ( "," FieldReference )*
FieldReference ::= ( "this" "." )? <IDENTIFIER>

5 The class invariant: 'invariant'

In each class a class invariant may be stated. This invariant contains assertions, that, apart from the parameter names, may be defined as in the preconditions of methods. The class invariant begins with the keyword invariant and is stated at the end of the class like a method declaration.

The class invariant is supposed to include all assertions that contain conditions that hold for the whole class and not just for single methods. For instance, with their help the possible state space defined by the arrays can be restricted.

The class invariant could also explicitly be added to each precondition and postcondition. The semantics would be the same, but its intention as an invariant condition could hardly be recognized. Changes of the invariant would imply changes in all pre- and postconditions.

Like a method, the class invariant is declared at the end of the class and it is called by the other methods at the positions in which it is supposed to be checked.

Example for a class invariant:

public class Buffer {
...
/** invariant 0 <= in - out && in - out <= buffer.length; **/
}

No local variables and formal parameters can be used in the class invariant.

The class invariant is checked whenever a method of the class is called or ended. Analogous to the postcondition the end of a method are the return statements and the end of the body. If the assertion expressions of the invariant contain variables that can be changed through other instances Jass warns the developer with a message. In this case the invariant can be invalidated without any error message produced.

The invariant is produced through the following EBNF:

ClassDeclaration ::= ( "abstract" | "final" | "public" | "strictfp" )* UnmodifiedClassDeclaration
UnmodifiedClassDeclaration ::= "class" <IDENTIFIER> ( "extends" Name )? ( "implements" NameList )? ClassBody
ClassBody ::= "{" ( ClassBodyDeclaration )* ( ClassInvariantClause )? "}"
ClassInvariantClause ::= <INVARIANT> ( InvariantAssertion ";" )+ <ASS_END>

6 The loop invariant and the loop variant: 'invariant' and 'variant'

Some kinds of errors are typical for loops:

To take the edge of the loops the loop invariant and the loop variant are introduced. Both are declared after the head and before the body of a loop. If, in the following, confusion with the class invariant cannot be excluded by the context, we use the expression "loop invariant". This invariant states an assertion that must be influenced by the execution of the loop, while the variant denotes an integer expression that explicitly has to do this. Two conditions are linked to the variant: first, its value has to be positive all the time, second, this value must decrease every time the loop is repeated. The Jass precompiler checks these conditions; as both conditions cannot be maintained infinitely long, the loop has to terminate.

As loops can only be used within methods their assertions are not directly part of the documentation but they describe the implementation. Therefore, they do not contribute to the contract between developer and user. They do have an important function, however. They can reveal properties of the implementation that cannot be detected at first sight; in particular, the time limitation for loops can be shown. They thereby promote the understanding and help the developer formulate a precise precondition and postcondition or class invariant.

In the case of the forms discussed so far (the integration of assertions in the program code), it was implicitly assumed that the method call terminates and therefore returns to the caller at some point. This point of view is correct if loops are disregarded. With loops the time limitation has to be indicated explicitly, as otherwise the postcondition would never be checked and thus no guarantees as in a contract or in the form of a correctness formula could be given (see [#intro2]Design by contract).

Example for a loop invariant and variant:

   public boolean contains(Object o) {
   /** require o != null; **/
      for (int i = 0; i < buffer.length; i++) 
      /** invariant 0 <= i && i <= buffer.length; **/
      /** variant buffer.length - i **/
         if (buffer[i].equals(o)) return true;
      return false;
  /** ensure changeonly{}; **/    
  }

If both constructs are used the loop invariant must declared before the loop variant. The expression of the variant must be of type int and can not be decorated with a label. There is no semicolon at the end of the variant!

The invariant and variant are produced through the following EBNF:

WhileStatement ::= "while" "(" Expression ")" AssertionClause
DoStatement ::= "do" AssertionClause "while" "(" Expression ")" ";"
ForStatement ::= "for" "(" ( ForInit )? ";" ( Expression )? ";" ( ForUpdate )? ")" AssertionClause
AssertionClause ::= ( InvariantClause )? ( VariantClause )? Statement
InvariantClause ::= <INVARIANT> ( BooleanAssertion ";" )+ <ASS_END>
VariantClause ::= <VARIANT> AssertionExpression <ASS_END>

7 The check-statement: 'check'

The check statement is used to state an assertion at any part of the program code of the method body. It shows that at this position the validity of the condition is expected, although the reason for this is not immediately obvious from the program code.

For instance, at some point in the program code let there be the assignment x=y/z. The programmer is sure that at this position z never equals 0. For this reason, he does not want to check this, such a check being superfluous. The reason for a value unequal to 0 may be found at another, more distant, position in the program code, possibly even in another method.

Someone else who looks over the whole code and who cannot recognize the reason immediately, is likely to assume that a mistake has been made or at least that the programming has been done rather sloppily. And even the programmer himself cannot always be aware of every single detail of the whole system and may want to be reminded of this important property. The check-statement formulates it explicitly: check z!=0;. Of course, the check-statement can also be checked during runtime.

Example for a check statement:

/** check x >= 0; **/

The check statement is produced through the following EBNF:

CheckClause ::= <CHECK> ( BooleanAssertion ";" )+ <ASS_END>

8 The rescue-block: 'rescue' and the retry statement: 'retry'

By stating a rescue-block the integrity of the respective instance can be secured in the case of a violated exception (e.g. by closing files or freeing memory). A detailed description of the aims of its use is given in Violations of the contract: success or failure.

It consists of a sequence of catch-statements which each contain a block with statements that can contain a retry statement.

Example for a rescue block:

   public void add(Object o) {
   /** require [valid_object]    o != null; 
               [buffer_not_full] !full();   **/  ...
   /** ensure ... **/
   /** rescue catch (PreconditionException e) {
                 if (e.label.equals("valid_object")) {
		     o = new DefaultObject(); retry;}
                 else throw e;
              }
  }

This example catches a precondition exception and reinitiates the method call with a reassigned formal parameter. A default object is created and inserted in the buffer if the formal parameter was null. The assertion label is used to distinct between the two possible exceptions.

The rescue construct can only be applied to exception of (sub)type of AssertionException. If no retry statement appears in the block the exception is throw again at the end of the code block.

The check statement is produced through the following EBNF:

RescueClause ::= <RESCUE> ( RescueCatch )+ ( ";" )? <ASS_END>
RescueCatch ::= "catch" "(" FormalParameter ")" Block
RetryStatement ::= <RETRY> ";"

9 How assertions are checked

Each assertion, except for the loop invariant, is a boolean expression the validity of which can be checked in a conditional statement with a preceding negation operator. This negation triggers an exception if the assertion condition is invalid.

The precondition and the postcondition are changed at the beginning and at the end of a method call, respectively. The end of a method call is a return statement or the end of the method body. If an Old construct is used a local variable will be declared at the beginning of the methods body. This variable will be assigned the value produced by the clone method. The changeonly construct uses this mechanism too.

Bertrand Meyer has stated a important principal for the evaluation of assertions:

While the evaluation of an assertion no other assertion should be evaluated.

This means: If the evaluation of an assertion invokes a method that declares an assertion too this second assertion should not be evaluated. Jass does implement a compromise of this principal. If an assertion contains a call to a method of the same class this method is copied in an internal version without any assertion checks. While the evaluation of this assertion the internal version is called. If the internal version calls another method with assertions these will be checked again. Thus the principal is implemented on the first level for internal calls.

The class invariant is translated into a new method which is called from the preconditions and postconditions.

With loops the invariant is checked at the beginning of each run and directly after a conditional statement. A local variable is generated for the variant. It temporarily stores the value at the beginning of a run such that it is possible to check at the end whether its value has decreased and whether it is still positive. However for loops need some extra work to be done.

The check-statement is locally expanded like a macro. A conditional statement for the check of its assertion is directly generated from it.

Jass triggers the following exceptions which are located in the package jass.runtime:

Exception Is thrown when ...
PreconditionException ... the precondition of a method does not hold.
PostconditionException ... the postcondition of a method does not hold.
InvariantException ... the invariant of a class does not hold.
LoopInvariantException ... the loop invariant for a certain loop iteration does not hold.
LoopVariantException ... the loop variant is not decreased or out of its bounds.
CheckException ... the assertion of a check statement does not hold.
RefinementException ... the class signals a refinement but does not match the refinement requirements.

10 Comments in assertions

Comments in assertions make it easier to understand what semantic constraints are expressed. Java does not support nested comments, so a Jass developer can not write normal Java comments within an assertion. Therefore Jass supports the special comment signs /# and #/ to be used in assertions.

Example of comments in an assertion:

/** require
 /# The buffer must contain space to store the object: #/ !isFull();
 /# Only 'valid' objects can be used: #/ o != null;
**/


The Jass Page - printer friendly - last update 11/09/05
online version at http://csd.informatik.uni-oldenburg.de/~jass