laws for the new constructs, and develop a formal semantics in terms of an ... demonstrate the practical utility of the exit construct in refining programs with excep- .... An exception handler can be implemented by following the handler code by an ... require that all points at which exceptions may be raised are indicated ...
SOFTWARE VERIFICATION RESEARCH CENTRE THE UNIVERSITY OF QUEENSLAND
Queensland 4072 Australia
TECHNICAL REPORT No. 02-06 Refining Exceptions Using King and Morgan’s exit Construct Geoffrey Watson September 2002
Phone: +61 7 3365 1003 Fax: +61 7 3365 1533 http://svrc.it.uq.edu.au
Note: Most SVRC technical reports are available via anonymous ftp, from svrc.it.uq.edu.au in the directory /pub/techreports. Abstracts and compressed postscript files are available via http://svrc.it.uq.edu.au
Refining Exceptions Using King and Morgan’s exit Construct ∗ Geoffrey Watson
Abstract In this paper we discuss the refinement of exceptions. We extend the Guarded Command Language normally used in the refinement calculus, with a simple exception handling statement, which we model using King and Morgan’s exit statement. We derive some variants of King and Morgan’s refinement laws for their exit statement, and illustrate the approach with an example of a refinement of a simple program.
1
Introduction
In earlier work Fidge and Lermer [LF97] have described a theoretical framework for extending the refinement calculus [Mor90] down to the assembler level. One of the goals of that work is to progress towards situating refinement in the context of ‘real world’ programming, rather than simply as an academic ‘methodology’. Another way in which this goal can be approached is to extend the minimal Guarded Command Language (GCL), as typically used in program refinement, with some of the more utilitarian constructs that occur in everyday programming languages. One such construct is an exception mechanism. Elsewhere, King and Morgan have described an extension to the refinement calculus that adds an exit statement and exception blocks [KM95] . They give refinement laws for the new constructs, and develop a formal semantics in terms of an extended version of the weakest precondition. One of the motivations for their work is to model exception mechanisms. We adopted King and Morgan’s mechanism to extend the command language used in our work, in order to create a formalism capable of modelling the type of exception handling code typically inserted by compilers, and used this to refine programs containing exceptions to assembler code. This paper briefly describes our approach and sketches a case study that was used to validate the method. Its aim is two-fold, to demonstrate the practical utility of the exit construct in refining programs with exceptions, and to give the proofs of two derived laws that were found useful in developing such refinements. ∗ This research has been funded by Australian Research Council Large Grant A00104650, Verified Compilation Strategies for Critical Computer Programs.
1
2
The Compilation by Refinement Framework
Program refinement is performed in a wide-spectrum language that includes both specification level and implementation level constructs. The refinement relation P v Q means that command Q is a refinement of, or an implementation of, command P . Typically this is done from a specification to a high-level language, such as the Guarded Command Language, but this has been extended [Fid97, LF97] to include refinement to assembler code. The process of compilation by refinement is an incremental one – an initial specification is first refined to an implementable program in a high-level language, and then this is refined to an assembler program. In this framework the high-level language is just an intermediate stage between specification and assembler code. We can therefore use an ‘idealised language’, introducing only the features necessary to support the refinement process. In this paper we use a version of Dijkstra’s Guarded Command Language (GCL) [Dij76] as our high-level programming language model. This language is simple, well known and expressive. However, basic GCL lacks some practical constructs useful for modelling non-trivial high-integrity programs, and in this example we extend standard GCL with an explicit form of exception mechanism. The syntax of the language that we use for modelling compilation is shown in Figure 1. It includes a specification statement as well as GCL (although it currently lacks procedures). The specification statement w : [P, Q] models a command that, starting in a state described by predicate P , achieves a state described by predicate Q, changing only the variables mentioned in the list w. (When P is true initially the specification is guaranteed to terminate, but if P is initially false then the specification may fail to terminate.) The Guarded Command Language includes multiple assignment, sequential composition, the scoped declaration of variables and constants, and the skip command which does nothing. It also includes multi-branch iteration and alternation, which are constructed from guarded command (G → C) and arbitrary choice (8) primitives. The guard G in a guarded command is a predicate which selects those states in which the command C is activated. Finally GCL contains the assertion {P } command, which records that the predicate P is true at that point in the program (if it is false the program aborts),and the coercion [P ] command, which makes the predicate P true at that point in the program. The exception statements are described in Section 3.2. We have chosen exceptions since their omission is an impediment to the practical application of the refinement framework for fault tolerant programming. In expositions of refinement exceptional cases are typically carefully excluded. For example Morgan [Mor90, p. 44] defines x/0 as an unknown rational number, and requires that z := x/y be guarded by y 6= 0 before it be admitted as code. Often discussions of semantics introduce mechanisms for representing the well-definedness of expressions explicitly, and then omit them from the subsequent discussion for reasons such as ‘because assignments should always be written in contexts in which the expressions can be properly evaluated’ [Gri81, p. 118]. However in practice the violation of such conditions will raise exceptions, which must be anticipated and handled by the programmer, particularly in ‘non-stop’ high-integrity applications. We have, therefore, extended our version of the Guarded Command Language with
2
Specification Statements: w : [ P, Q ]
Specification Statement
Standard GCL: X2 , X2 · · · := E1 , E2 . . . C1 ; C2 skip G→C do G1 → C1 .. . 8 od
Multiple assignment Sequential composition Skip Guarded Command Iteration
Gm → Cm
if G1 → C1 .. . 8 Gn → Cn fi [[ var v : T • C]] [[ con c : T • C]] {P } [P ]
Alternation
Variable Declaration Constant Declaration Assertion Coercion
Exception Statements: [[C]] on E raise H exit
Exception Block Exception Handler Explicit Exit
Figure 1: The Guarded Command Language
3
a high-level exception construct that has the syntax : on condition raise action which specifies a triggering condition and a handling action. The exception is triggered if the condition is true at this point in the program. The action is then performed and control is transferred to the end of the enclosing block. This transfer of control distinguishes exceptions from other ‘error correcting’ features such as assertions or coercions.
3
Exception Handling
Exception mechanisms are part of many current languages. They enable abnormal or infrequent cases to be handled in a way that does not distort the structure of a program designed for normal operation. Clint and Hoare [CH72] describe them as ‘an ugly device . . . thereby well suited to the occasional ugly situation’. Exception handling can also be used to implement fault tolerance and program robustness in the face of errors and unexpected events. Since typically 50% or more of software in embedded systems is devoted to error handling and fault tolerance, it is important that any realistic refinement system includes the ability to model exceptions. Of course, just because languages support exceptions does not mean that their use is to be recommended. For instance, in real-time Ada exceptions are typically only used to handle failures [BW95, page 16], since the analysis of programs that use them for more routine tasks is much more complicated than alternatives which do not use exceptions. Alternatives, that may be applicable in differing circumstances, include: • Analyse the failures at the system level (e.g. by treating them as hazards in a safety analysis) and demonstrate that the risks of ignoring them altogether are acceptable. • Where the pertinent failures are software faults, show rigorously that they cannot occur in the implementation. • Use defensive coding techniques to replace the functionality of the exception mechanism. However, in general programming exceptions are used as a common and efficient way of handling failures and abnormal events, and their use is sometimes necessary, even in high-integrity systems.
3.1
Approaches to the Semantics of Exceptions
Historically, exceptions can be viewed as one of the mechanisms proposed to tame the goto command in the 1960s. Dijkstra [Dij68] suggested that we should abolish gotos, while others were adamant that they had an important place in programming [Knu74]. One compromise was to develop new commands that were equivalent to the use of the
4
goto in a particular limited situation, and which had reasonable properties [AA79]. An exception mechanism is one such command. In the context of refinement a productive way of looking at exceptions is to view them as alternative behaviours. From this viewpoint we can adopt a general model of exceptions with the form: if then otherwise
circumstances are normal execute normal behaviour execute exceptional behaviour
Various models for the semantics of exceptions have been proposed to capture this semantics. Traditional program refinement is modelled mathematically using predicate transformers [Dij76], and particularly weakest precondition semantics [Mor90]. So the models for exceptions that are most relevant in the present context are those based on some modification of the weakest precondition semantics. Some of the best known of these are: • Cristian [Cri84] did pioneering work on the weakest precondition semantics of exceptions. He based his semantics on sets of predicate transformers, with one predicate transformer for each required behaviour. Cristian used a three-place weakest precondition predicate, where, for a command C and postcondition Q, wp(C, ; , Q) is the standard wp, but we may also have wp(C, e, Q) for alternative exit points e. • Leino and van de Snepscheut [LvdS94] developed a semantics based on a predicate transformer wep.C.(P, Q) whose range is pairs of predicates: P = exceptional outcome Q = normal outcome. and where wp.C.Q = wep.C.(f alse, Q) • Manohar and Leino [ML95] investigated an alternative approach using conditional composition. They retained the wp with a single post-state, but use partitioned predicates to indicate normal and exceptional states. • King and Morgan [KM95] also use wp with a pair of behaviours, written wp(C, ν, ). Traditional commands have a single behaviour, and for one of these commands C, the traditional weakest precondition is related to the extended version by wp(C, ν, ) =def wp(C, ν). That is, the behaviour is determined just by the normal behaviour. However, they introduced new commands, for which the exceptional behaviour was significant. King and Morgan constructed their semantics specifically in the context of an extension to the refinement calculus. They extended the calculus with: • An exit statement, with semantics: wp(exit , ν, ) = .
5
• Exception blocks, with semantics: wp(|[S]|, ν, ) = wp(S, ν, ν). That is, all exceptional behaviours are handled within the enclosing exception block. Exits are defined within exception blocks, and executing an exit statement jumps to the end of the innermost enclosing exception block.
3.2
Our Approach
Semantically we handle exceptions using King and Morgan’s exit construct [KM95], as described in Section 3.1. This construct is sufficient to model the behaviour of exception handlers that do not return control to the point at which the exception is raised. We do not consider handlers that do return control, as these can be modelled by procedure calls. An exception handler can be implemented by following the handler code by an exit command which jumps out of the current context. King and Morgan indicate how exceptions could be implemented using these constructs by declaring a handler H as a procedure with body: handler; exit , and then raising the exception by a call to H. Instead, we implement a similar mechanism by adding the construct: on E raise H to our command language, where E is an exception condition which is handled by H. This is given the semantics: if E → H; exit 8 ¬ E → skip fi We thus include the handler code ‘in-line’ which allows a specific handler to be provided for each point where an exception may be raised. More importantly, we distinguish the exception handling and subroutine mechanisms and avoid the implication that exception handling necessarily involves the overheads associated with procedure calls. Modelling exception handlers as procedures also places the exit statement following the exception handling code in the procedure body. This separates the exit from the point at which the exception was detected, and potentially creates a scoping ambiguity when identifying the enclosing exception block to which the exit refers, because it may not be clear whether the relevant scope is that at the point where the exception was detected, or where the procedure body was declared. We avoid this ambiguity by including the exception handling code in-line. Unlike many high-level languages, we do not name exceptions. In our model we require that all points at which exceptions may be raised are indicated explicitly in the code. Additional syntax could be defined to disguise the exception checking code if desired. However, keep in mind that the high-level language compiler must introduce tests for exception conditions explicitly in the assembly code. Although verbose, our model makes the points at which these checks occur, and their potential consequences, clear to the high-integrity programmer. The high-level language constructs used to model exceptions are shown in Figure 1.
6
4
Refinement Laws for Exceptions
King and Morgan define the semantics of their exceptions in terms of a weakest precondition with multiple postconditions [KM95]. One of these postconditions corresponds to the postcondition of the standard weakest precondition, representing normal execution, while the other corresponds to exceptional execution. They also introduce the notation ‘ > ’ as an abbreviation for the choice between normal and exceptional executions defined by: a > b =def a 8 b;exit. The > operator can be introduced by the following equivalence [KM95, Law 7]: P = [[P > P ]]
(1)
(provided that P does not contain exit commands) and eliminated by the refinements [KM95, Law 4]: P >Q v
P
(2)
P >Q v
Q; exit
(3)
or [KM95, Law 5]:
Note that the introduction of > also introduces its enclosing exception block. King and Morgan give a number of refinement laws for exits and exceptions. One is the distribution of sequential composition through > [KM95, Law 8]. a > b v (c > b); (d > e)
(4)
provided a v c; d that b v c; e By law 2 we can always refine away an exception branch, i.e., c > b v c, and applying this law gives the simpler form of law (4), with the same conditions: a > b v c; (d > e)
(5)
One simple case is where there is a single exception condition to handle in a block. In this case we can use a form of law (4) to deal with the exception up-front. Applying the law d > e v d to (4) and putting c = skip , gives the law: a > b v (skip > b); a
(6)
We define more general law for handling a single exception as follows: a > b v c; (skip > e); f provided a v c; f that b v c; e 7
(7)
which is derivable as a special case of applications of laws (4) and (5). Proof From law (5) we have: a > b v c; (x > e) provided that a v c; x and b v c; e Applying law (4) to (x > e) gives: x > e v (y > e); (f > z) provided that x v y; f and e v y; z Combining these two gives: a > b v c; (y > e); (f > z) provided that a v c; y; f and b v c; e and e v y; z Refining (f > z) to f and substituting skip for y gives: a > b v c; (skip > e); f provided that a v c; f and b v c; e
4.1
The Introduction of Exceptions
We need a law that will introduce our particular on E raise H construct, which is an abbreviation for: if E → H; exit 8 ¬E → skip fi (8) First we prove the law:
w : [I, A] > w : [I, B] v if E 8 ¬E fi
→ w : [I ∧ E, B]; exit → w : [I, A] (9)
Proof w : [I, A] > w : [I, B] v Introduction of if if E → {E}; w : [I, A] > w : [I, B] 8 ¬E → {¬E}; w : [I, A] > w : [I, B] fi v Drop opposite parts of the two > ’s using laws (2) and (3) 8
if E → {E}; w : [I, B]; exit 8 ¬E → {¬E}; w : [I, A] fi v Absorb the assumption on the first branch, and refine the assumption on the second branch to skip, which can be dropped if E → w : [I ∧ E, B]; exit 8 ¬E → w : [I, A] fi Then, setting A = I in law (9), we obtain: skip > w : [I, B] v
if E → w : [I ∧ E, B]; exit 8 ¬E → skip fi
Which, by the definition of on-raise is just: skip > w : [I, B] v on E raise w : [I ∧ E, B]
(10)
We can strengthen this slightly by retaining the assumption in the normal case. We replace the last step of the proof of law (9) above with: if E → {E}; w : [I, B]; exit 8 ¬E → {¬E}; w : [I, A] fi v Absorb the assumption on the first branch if E → w : [I ∧ E, B]; exit 8 ¬E → {¬E}; w : [I, A] fi Then putting A = I gives: if E → w : [I ∧ E, B]; exit 8 ¬E → {¬E}; skip fi v Move the assumption past the skip and also add an assumption of {¬E} after the exit (we can add any code after an exit [KM95, Law 1]) if E → w : [I ∧ E, B]; exit ; {¬E} 8 ¬E → skip ; {¬E} fi v Migrate the final assumption out of the if if E → w : [I ∧ E, B]; exit 8 ¬E → skip fi; {¬E}
9
Which is just: skip > w : [I, B] v (on E raise w : [I ∧ E, B]); {¬E}
4.2
(11)
Exception Handling Strategy
King and Morgan derive a number of refinement laws for the exit statement [KM95]. We have added to these with some derived laws to handle our on-raise command. These laws are used in refinement in the following way. An exception and its block are introduced by Law 1: a v [[a > a]]. This is typically done at a high level in the refinement, where a is a specification statement, so the proviso on Law 1 that a be exit-free is satisfied. The block is then further refined to [[b > c]] where b and c embody the normal and exceptional behaviours of a, thanks to the monotonicity of the refinement relation with respect to the > operator. We can then use laws such as those given here to isolate the exception handling to the appropriate part of the block: start, end or middle. For instance, the intention of refinement law (7) is that the initial c will have no effect on the exception condition (so effectively b = e), and similarly that the specification f following the exception handler is protected from the exception condition, and can be refined on the assumption that the exception case cannot occur. Finally laws (10) and (11) can be used to refine specifications of the form: skip > w : [I, B] to exception commands of the form: on E raise H.
5
An Example
We illustrate our use of exceptions by an example which uses law (6). The example is a simple procedure to return the maximum value in the first n elements of an array a of positive non-zero integers. This procedure, which we call M axN , has two input parameters a and n, and returns the result in a third parameter r. In normal cases the maximum value is returned in r, however, if the value of n is invalid (by being greater than the actual size of the array a), then we return −1 in r instead. We make a number of assumptions about the context, and the contract between M axN and the calling program: • We are indexing arrays from 1 and are assuming that arrays are not null, that is that size(a) > 0. • We assume that all the parameters are well defined, to the extent that they are type correct. That is, we assume that the type of n is the set of non-zero positive integers, and a is an array of elements of the same type. This contract with the calling program can be cast in the form of an invariant that holds throughout the routine (and must be respected by its code), however, to simplify the representation of the specification, we do not show this here.
10
We specify M axN with the specification statement: r : [true, (n ≤ size(a) ∧ r = max{a(j) | 1 ≤ j ≤ n})∨ (n > size(a) ∧ r = −1)]
(12)
Rather than adding a precondition requiring n to be valid, we have explicitly stated the desired behaviour in the exceptional circumstance where n is out of range. This specification can be implemented by a simple loop. The exceptional case, where n is invalid, can, of course, be implemented using defensive programming by performing an explicit test on n prior to commencing the loop. However, the aim of this example is to illustrate our exception mechanism, so we assume that the programmer has failed to do this, and instead we perform a check for the exceptional case inside the loop at the current index of the array (rather than using a simple test on n). Thus the exception is detected at the point where the current array index becomes invalid, and prevents the program attempting an access outside the bounds of the array. This models the exception handling code that a compiler would automatically insert for array bounds checking. Apart from the exception handling, this example is a standard case of refinement to a loop, and we omit some of the details here. Details of this type of refinement can be found elsewhere [Mor90, pp. 88–89]. The refinement strategy is to use the iteration introduction rule for a do loop with a single branch: w : [I, I ∧ ¬G] v
do G → w : [I ∧ G, I ∧ (0 ≤ V < V0 ] od
for some invariant I, guard G, and variant V . We introduce a local variable k to be used as the loop variant. This is initialised to 1 and increased to a maximum of n. We also initialise r to zero, which is less than any possible value of the maximum of a, since we are assuming that all values in a are greater than zero. (We can use this, and other assumptions about the contract between M axN and its caller throughout the refinement.) We need to determine an invariant. This must be true before the loop, at the start of each iteration and after the loop is completed. In this case the invariant expresses the condition that the previous iteration has successfully set the value of r, and establishes the normal part of the postcondition J on completion. We also need to add a special disjunct to handle the first time through, before r has been set. Our invariant I is: ( (k − 1 ≤ size(a) ∧ r = max{a(j) | 1 ≤ j ≤ k − 1})∨ ∨ (k = 1 ∧ r = 0) ) ∧ (1 ≤ k ≤ n + 1) Initially we introduce the local variable k and initialise k and r. So, abbreviating the postcondition from the original specification by J, i.e.: J=
((n ≤ size(a) ∧ r = max{a(j) | 1 ≤ j ≤ n})∨ (n > size(a) ∧ r = −1)) 11
the specification of M axN (12) is refined to: [[ var
k• k := 1 r := 0; r, k : [I, J]
]] The final specification in this block will become the loop. The introduction of these assignments is justified by noting that when k = 1 and r = 0 , then: I= (
(0 ≤ size(a) ∧ 0 = max{}) ∨ (0 > size(a) ∧ 0 = −1) ∨ (1 = 1 ∧ 0 = 0) ) ∧ (1 ≤ 1 ≤ n + 1)
which reduces to true, which is our initial precondition. We now introduce the exception block. Recall that we can introduce an exception and its exception block at the same time using the law (1) : a v [[a > a]]. Any exit commands introduced subsequently will jump to the end of this block. It is therefore important to position the handling block at the correct level. There are three main choices for the block in this example: 1. Around the refinement of the whole routine. 2. Around the do loop. 3. At the lowest level within the do loop. We choose the second of these, since it is more efficient than the third, which does not exit the loop if an exception occurs, and it is simpler than the first, not requiring manipulation of the exception during the initialisation code. We use law (1) to introduce the exception block, at this point in the refinement. r, k : [I, J] v [[r, k : [I, J] > r, k : [I, J]]] Within this block the normal part will refine to the main code of the loop, while the exception part will refine to the trapping of the exception. We therefore strengthen the postcondition of each part of the specification by removing one of the disjuncts to leave only the disjunct that expresses the part it is to play in achieving the final result. That is, we remove the disjunct for the exception case from the first postcondition (so this part just has to achieve the normal state) and conversely we remove the disjunct for the normal case from the exceptional part of the specification. The postcondition for the normal part is thus: (n ≤ size(a) ∧ r = max{a(j) | 1 ≤ j ≤ n}) which equals I ∧ k = n + 1. These manipulations give, as the body of the exception
12
block1 : r, k : [I, I ∧ (k = n + 1)]
> r, k : [I, (n > size(a) ∧ r = −1)] We now apply an iteration introduction rule, using as guard k 6= n + 1. King and Morgan introduced a variant of the standard law that handles iteration in the presence of exceptions (this is [KM95, Law 10]), viz.: w : [A, A ∧ ¬G] > w : [A, B] v do G → w : [A ∧ G, A ∧ (0 ≤ V < V0 )] > w : [A ∧ G, B] od The result of applying this law (adapted for an increasing variant bounded above) is: r, k : [I, I ∧ (k = n + 1)] r, k : [I, (n > size(a) ∧ r = −1)] v iteration introduction do k 6= n + 1 → r, k : [I ∧ k 6= n + 1, I ∧ (k0 < k ≤ n + 1)] > r, k : [I ∧ k 6= n + 1, (n > size(a) ∧ r = −1)] od
>
Applying law (6) to the guarded command gives: r, k : [I ∧ k 6= n + 1, I ∧ (k0 < k ≤ n + 1)] > r, k : [I ∧ k 6= n + 1, (n > size(a) ∧ r = −1)] v law 6 skip > r, k : [I ∧ k 6= n + 1, (n > size(a) ∧ r = −1)]; r, k : [I ∧ k 6= n + 1, I ∧ (k0 < k ≤ n + 1)] This is in the form of an exception handler, which we will refine to an on-raise command, followed by a specification for the normal processing of the loop. We first refine the exception case. We apply law (11): skip > r, k : [I ∧ k 6= n + 1, (n > size(a) ∧ r = −1)] v law 11 on k > size(a) raise r, k : [ I ∧ k 6= n + 1 ∧ k > size(a), {k ≤ size(a)} n > size(a) ∧ r = −1]; We then refine the body of the raise to r := −1. The condition for doing this is that 1 King and Morgan use the abbreviated notation w : [a, b > c] for specifications in this form, but we keep to a single notation throughout the example.
13
the precondition implies (n > size(a) ∧ r = −1)[−1/r] which is just n > size(a). The precondition of the specification within the raise is: ((k − 1 ≤ size(a) ∧ r = max{a(j) | 1 ≤ j ≤ k − 1}) ∨ (k − 1 > size(a) ∧ r = −1) ∨ (k = 1 ∧ r = 0)) ∧(1 ≤ k ≤ n + 1) ∧ k 6= n + 1 ∧ k > size(a) and we note that: 1 ≤ k ≤ n + 1 ∧ k 6= n + 1 ∧ k > size(a) ⇒ n > size(a) as required. So we can proceed: v assignment on k > size(a) raise r = −1; {k ≤ size(a)} This leaves the second part of the sequential composition, which can be further refined using standard techniques, which we only sketch here. It is split into a sequential composition of two specifications, the first of which changes only r and the second only k. The first of these can be refined to an alternation, which maintains the invariant on r by either assigning r the value a[k] if this is greater than its current value or else doing nothing. The second specification changes k to maintain the variant during the progress of the loop, and is refined to the assignment: k := k + 1. There is one detail in this process which needs explanation. In order to establish the postcondition we have to show that k ≤ size(a) after each iteration. This is done using the assumption {k ≤ size(a)}, generated by the processing of the exceptional case. We use the law (Law 8.3 of [Mor90]) {K}; w : [A, B] v w : [A ∧ K, B] which brings k ≤ size(a) into the precondition. The final program is given in Figure 2. As mentioned in the introduction, this work is part of a project that is primarily investigating the extension of the refinement calculus down to the assembler level. The approach is to define compilation rules as refinement laws. The compilation rule for the exit statement is simply to insert a jump to the end of the current exception block. This can be incorporated into a rule to handle the on-raise command. Using this approach, the refinement of M axN can be continued down to the assembler level. This gives an assembler program that can be assembled and run.
6
Related Work
The general compilation by refinement approach, continues earlier work by Sampaio [Sam97] and Fidge and Lermer [Fid97, LF97]. Although our approach was motivated from the refinement framework, in practice it adopts the same philosophy as that of M¨uller-Olm [MO97] and the ProCos project, in that it uses a layered approach to the semantic issues of compilation. However, whereas that work is focused on the compiler
14
[[var k : N • k := 1; r := 0; [[ do k 6= n + 1 → on k > size(a) raise r := −1; % Here k ≤ size(a) if a[k] > r → r := a[k] 8 a[k] ≤ r → skip fi ; k := k + 1 od ]] ]]
Figure 2: The subroutine M axN
itself, our approach is to regard compilation as a phase in the total refinement process (from specification to machine code). We have used the approach to exceptions based on an exit statement and exception blocks that is described by King and Morgan [KM95]. They also describe a raise command and prove most of the refinement laws needed to use these constructs. King and Morgan have also provided a semantic foundation for their constructs using an extension of the weakest precondition (wp). Other approaches to a weakest precondition semantics of exceptions [Cri84, LvdS94, ML95] have been discussed in Section 3.3.1. The example that we use is similar to most elementary examples of loop refinement in the literature. We have implemented the error checking by an exception handler, but similar examples are also used by both King and Morgan [KM95, Section 5.3] and Leino [Lei95, Chapter 5], to illustrate their particular methods of refinement of exceptions.
7
Conclusions
We have shown an example of the application of King and Morgan’s extension of the refinement calculus to handle exceptions. This was done in the context of a wider objective of extending the refinement calculus down to the assembler level. Using the King and Morgan framework we were able to add a simple on-raise construct and handle it in a practical way. To do this we extended some of the King and Morgan laws, and adapted others to handle our particular construct. These laws provided a basic toolkit for handling a single exception condition by introducing a handler block, isolating the exception handling code, and refining this
15
to a on-raise command. This provides us with a formalism capable of modelling the exception checking and handling code introduced by typical high-level language compilers. Since King and Morgan worked within the refinement calculus, their method worked well in our framework. We were able to take their approach and use it directly to achieve our goal of adding an exception mechanism to the repertoire of commands in our extended GCL. King and Morgan’s refinement laws also provided an effective basic machinery for performing refinements in this context. Although the approach is limited in its basic form (for instance, exceptions must be handled by the immediately surrounding block) it does provide a practical way to implement a simple exception mechanism in the refinement calculus. Acknowledgments This research has been funded by Australian Research Council Large Grant A00104650, Verified Compilation Strategies for Critical Computer Programs. The author wishes to thank the referees for pointing out errors in the original development of this example, and Colin Fidge for reviewing the final draft.
References [AA79]
M A Arbib and S Alagi`c. Proof rules for gotos. Acta Informatica, 11, 1979.
[BW95]
A Burns and A J Wellings. Concurrency in Ada. Cambridge University Press, 1995.
[CH72]
M Clint and C A R Hoare. Program proving: Jumps and exceptions. Acta Informatica, 1:214–224, 1972.
[Cri84]
Flaviu Cristian. Correct and robust programs. IEEE Transactions on Software Engineering, SE-10(2), March 1984.
[Dij68]
E W Dijkstra. Goto statement considered harmful. Communications of the ACM, pages 147–148, March 1968.
[Dij76]
E W Dijkstra. A Discipline of Programming. Prentice Hall, 1976.
[Fid97]
C J Fidge. Modelling program compilation in the refinement calculus. In D J Duke and A S Evans, editors, 2nd BCS-FACS Northern Formal Methods Workshop, Electronic Workshops in Computing. Springer-Verlag, 1997. http://www.ewic.org.uk/ewic/.
[Gri81]
D Gries. The Science of Programming. Springer-Verlag, 1981.
[KM95]
S King and C Morgan. Exits in the refinement calculus. Formal Aspects of Computing, 7(1):54–76, 1995.
[Knu74] D E Knuth. Structured programming with goto statements. ACM Computing Surveys, 6, 1974.
16
[Lei95]
K Rustan M Leino. Towards Reliable Modular Programs. PhD thesis, Computer Science, 1995.
[LF97]
K Lermer and C J Fidge. Compilation as refinement. In L Groves and S Reeves, editors, Formal Methods Pacific ’97, pages 142–164. Springer, 1997.
[LvdS94] K Rustan M Leino and Jan L A van de Snepscheut. Semantics of exceptions. In E-R Olderog, editor, IFIP Transactions A [Computer Science and Technology], volume 56. Elsevier, 1994. [ML95]
R Manohar and K R Leino. Conditional composition. Formal Aspects of Computer Science, 7:683–703, 1995.
[MO97]
M M¨uller-Olm. Modular Compiler Verification: A Refinement-Algebraic Approach Advocating Stepwise Abstraction, volume 1283 of Lecture Notes in Computer Science. Springer-Verlag, 1997.
[Mor90] C Morgan. Programming from Specifications. Prentice-Hall, first edition, 1990. [Sam97] A Sampaio. An Algebraic Approach to Compiler Design, volume 4 of AMAST Series in Computing. World Scientific, 1997.
17