Go to the first, previous, next, last section, table of contents.


6.10 Case Analysis

Sometimes functions are best specified in several different cases. To allow this, a spec-case-seq in a function specification may have multiple spec-cases separated by the keyword also.

Recall that there are two forms of a spec-case (see section 6 Function Specifications). The simplest has the following syntax, and contains all the elements discussed so far in this chapter.

 [ let-clause ] req-frame-ens [ example-seq ]

There is also a form of a spec-case that allows for internal nesting of a spec-case-seq. The syntax is as follows.

 [ let-clause ] [ requires-clause-seq ]
 {
    spec-case-seq
 }
 [ ensures-clause-seq ]
 [ example-seq ] 

This second form is useful if a let-clause, requires-clause, ensures-clause or example-seq should be applied to several spec-cases.

The meaning of a function specification with more than one spec-case in its spec-case-seq is that the function has to satisfy each spec-case in the list. The same idea gives the meaning of a nested spec-case-seq. Both kinds of specification can be regarded as syntactic sugar for a specification with only one spec-case, as will be described below.

As an example of the use of multiple cases, and then of the translation into a specification without multiple cases, consider the following.

// @(#)$Id: widen1.lh,v 1.10 1997/06/03 20:30:26 leavens Exp $
extern void widen(int i) throw();
//@ behavior {
//@   extern int low_bound, high_bound;

//@   // "normal" case
//@   requires assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@            /\ INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@            /\ high_bound^ < INT_MAX - i;
//@   modifies low_bound, high_bound;
//@   ensures (high_bound' - low_bound') = (high_bound^ - low_bound^) + i
//@           /\ low_bound' <= low_bound^ /\ high_bound^ <= high_bound';
   
//@ also // other cases

//@   requires assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@            /\ INT_MIN + i >= low_bound^ /\ low_bound^ < high_bound^
//@            /\ high_bound^ < INT_MAX - i;
//@   modifies high_bound;
//@   ensures high_bound' = high_bound^ + i;

//@ also

//@   requires assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@            /\ INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@            /\ high_bound^ >= INT_MAX - i;
//@   modifies low_bound;
//@   ensures  low_bound' = low_bound^ - i;
//@ }

In the "normal" case the function widen is expected to make the difference between low_bound and high_bound greater by the value of the formal parameter i. It can do this by either changing low_bound or high_bound or both. The other cases say that when low_bound is two small, high_bound must be increased, and vice versa.

An equivalent desugared version of the above specification is the following. This specification shows the general way to translate a spec-case-seq into a single spec-case. The general idea is to: form the single precondition by disjoining the preconditions of the cases, list in the single modifies-clause everything listed in each of the modifies-clauses of the cases, and form the single postcondition from the conjunction of implications, with each implication being of one case's precondition implying the postcondition of that case conjoined with an assertion saying that everything is unchanged which is not listed in the modifies-clause for that case.

// @(#)$Id: widen2.lh,v 1.13 1997/06/03 20:30:27 leavens Exp $
extern void widen(int i) throw();
//@ behavior { // a desugared version of widen1
//@   extern int low_bound, high_bound;

//@   requires (assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@                  /\ INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                  /\ high_bound^ < INT_MAX - i)
//@        \/ (assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@                  /\ INT_MIN + i >= low_bound^ /\ low_bound^ < high_bound^
//@                  /\ high_bound^ < INT_MAX - i)
//@        \/ (assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@                  /\ INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                  /\ high_bound^ >= INT_MAX - i);

//@   modifies low_bound, high_bound;

//@   ensures ((assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@                /\ INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                /\ high_bound^ < INT_MAX - i)
//@            => ((high_bound' - low_bound')
//@                       = (high_bound^ - low_bound^) + i
//@                 /\ low_bound' <= low_bound^
//@                 /\ high_bound^ <= high_bound'))
//@        /\ ((assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@                /\ INT_MIN + i >= low_bound^ /\ low_bound^ < high_bound^
//@                /\ high_bound^ < INT_MAX - i)
//@            => (high_bound' = high_bound^ + i /\ unchanged(low_bound)))
//@        /\ ((assigned(low_bound, pre) /\ assigned(high_bound, pre)
//@                /\ INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                /\ high_bound^ >= INT_MAX - i)
//@            => (low_bound' = low_bound^ - i /\ unchanged(high_bound)));
//@ }

If the spec-cases of a spec-case-seq have let-clauses, then these are first desugared before the above translation is applied.

The first specification of widen given above is less compact than it could be, because the first two conjuncts of each spec-case's requires-clause are the same. Also, since the variables low_bound and high_bound occur so frequently in the specification, it is convenient to abbreviate them using a let-clause. The following specification of widen uses these ideas to give a more compact specification of widen.

// @(#)$Id: widen3.lh,v 1.6 1998/08/27 22:56:52 leavens Exp $
extern void widen(int i) throw();
//@ behavior {
//@   extern int low_bound, high_bound;
//@
//@   let lb: Obj[int] be low_bound, hb: Obj[int] be high_bound;
//@   requires assigned(lb, pre) /\ assigned(hb, pre);
//@   {
//@      requires INT_MIN + i < lb^ /\ lb^ < hb^ /\ hb^ < INT_MAX - i;
//@      modifies lb, hb;
//@      ensures (hb' - lb') = (hb^ - lb^) + i /\ lb' <= lb^ /\ hb^ <= hb';
//@   
//@    also // other cases
//@
//@      requires INT_MIN + i >= lb^ /\ lb^ < hb^ /\ hb^ < INT_MAX - i;
//@      modifies hb;
//@      ensures hb' = hb^ + i;
//@
//@    also
//@
//@      requires INT_MIN + i < lb^ /\ lb^ < hb^ /\ hb^ >= INT_MAX - i;
//@      modifies lb;
//@      ensures  lb' = lb^ - i;
//@   }
//@   ensures redundantly inRange(lb') /\ inRange(hb');
//@   example 8 < INT_MAX /\ i = 2 /\ lb^ = 3 /\ hb^ = 6
//@                                /\ lb' = 1 /\ hb' = 6;
//@   example 8 < INT_MAX /\ i = 2 /\ lb^ = 3 /\ hb^ = 6
//@                                /\ lb' = 3 /\ hb' = 8;
//@   example 8 < INT_MAX /\ i = 2 /\ lb^ = 3 /\ hb^ = 6
//@                                /\ lb' = 2 /\ hb' = 7;
//@ }

As illustrated in the above example, the potential scope of a definition made in a let-clause is its entire spec-case, this includes nested spec-cases.

Also illustrated by the above example is that a requires-clause applies to nested spec-cases by conjunction; that is, such a requires-clause is conjoined to the requires-clause of each nested spec-case.

If there are multiple, non-redundant requires-clauses or ensures-clauses in a requires-clause-seq, or ensures-clause-seq in a spec-case, these are first conjoined to make each spec-case have a single non-redundant such clause.

The redundant ensures clause and the examples given in the last specification of widen above apply to the entire specification case. That is, they apply to the meaning of a spec-case with a nested spec-case-seq.

Such a specification can be translated into a specification with just one spec-case by first desugaring the nested spec-case-seq, then conjoining the requires-clause to the desugared requires-clause. If desired, one could also then desugar the let-clause.

For example, the last specification of widen above is equivalent to the following.

// @(#)$Id: widen4.lh,v 1.8 1998/08/27 22:56:53 leavens Exp $
extern void widen(int i) throw();
//@ behavior {
//@   extern int low_bound, high_bound;

//@   requires (assigned(low_bound, pre) /\ assigned(high_bound, pre))
//@            /\ (   (INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                    /\ high_bound^ < INT_MAX - i)
//@                \/ (INT_MIN + i >= low_bound^ /\ low_bound^ < high_bound^
//@                    /\ high_bound^ < INT_MAX - i)
//@                \/ (INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                    /\ high_bound^ >= INT_MAX - i)    );

//@   modifies low_bound, high_bound;

//@   ensures ((INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                /\ high_bound^ < INT_MAX - i)
//@             => ((high_bound' - low_bound')
//@                      = (high_bound^ - low_bound^) + i
//@                /\ low_bound' <= low_bound^ /\ high_bound^ <= high_bound'))
//@        /\ ((INT_MIN + i >= low_bound^ /\ low_bound^ < high_bound^
//@                /\ high_bound^ < INT_MAX - i)
//@             => (high_bound' = high_bound^ + i /\ unchanged(low_bound)))
//@        /\ ((INT_MIN + i < low_bound^ /\ low_bound^ < high_bound^
//@                /\ high_bound^ >= INT_MAX - i)
//@             => (low_bound' = low_bound^ - i /\ unchanged(high_bound)));

//@   ensures redundantly inRange(low_bound') /\ inRange(high_bound');

//@   example 8 < INT_MAX /\ i = 2 /\ low_bound^ = 3 /\ high_bound^ = 6
//@                                /\ low_bound' = 1 /\ high_bound' = 6;
//@   example 8 < INT_MAX /\ i = 2 /\ low_bound^ = 3 /\ high_bound^ = 6
//@                                /\ low_bound' = 3 /\ high_bound' = 8;
//@   example 8 < INT_MAX /\ i = 2 /\ low_bound^ = 3 /\ high_bound^ = 6
//@                                /\ low_bound' = 2 /\ high_bound' = 7;
//@ }

Redundant clauses need not appear in the translation of a case analysis, as they are not needed to give meaning to the specification. Still, it may be informative (or fun) to include them. Examples are easily taken care of by combining all the examples into one large example-seq in the translation. Redundant ensures-clauses are all listed after the non-redundant ensures-clause, but with their post-conds changed into implications. The hypothesis of such an implication is the precondition of its original spec-case, and the antecedent is the original predicate of the claims-clause.

Redundant requires-clauses are more tricky. They can be translated by conjoining all the pre-conds for each spec-case together, and then disjoining the resulting conjunctions to form a single pre-cond for a single redundant requires-clause.

See section 6.11 Exceptions for more examples of case analysis.

This feature of Larch/C++ is inspired by various versions of refinement for function specifications. The simple case of a spec-case-seq without nesting is nearly identical to the mechanism found in Section 4.1.4 of [Wing83], and is similar to the capsule mechanism of [Wills92b].


Go to the first, previous, next, last section, table of contents.