[Ada Information Clearinghouse]
Ada '83 Rationale, Sec 14.5: Technical Issues

"Rationale for the Design of the
Ada® Programming Language"

[Ada '83 Rationale, HTML Version]

Copyright ©1986 owned by the United States Government. All rights reserved.
Direct inquiries to the Ada Information Clearinghouse at adainfo@sw-eng.falls-church.va.us.

CHAPTER 14: Exception Handling

14.5 Technical Issues

The discussion of exception handling in [Go 75] classifies exceptions into three categories:
  1. Escape exceptions which require termination of the operation that raised the exception

  2. Notify exceptions, which forbid termination of the operation that raised the exception and require its resumption after completion of the actions of the handler

  3. Signal exceptions, which leave the choice between termination and resumption to the handler
Exceptions in Ada are of the escape category. They serve only for error situations and as terminating conditions, which simplifies the language: notify and signal exceptions are not provided, since these forms of exception violate program modularity and make optimization difficult, if not impossible.

The technical problems of the interactions between exceptions and parallelism have been mentioned in the previous section. The key idea was to provide a simple rule for cases where simultaneous exceptions occur in a given task. For the TASKING_ERROR exception, multiplicity can only occur in the case of nested accept statements, and then the outer exception prevails.

The remainder of this discussion will concentrate on the following issues:

In this section...

14.5.1 Exceptions Raised During the Elaboration of Declarations
14.5.2 Propagation of an Exception Beyond its Scope
14.5.3 Suppression of Checks
14.5.4 Implementation of Exception Handling
14.5.5 The Case Against Asynchronous Exceptions
14.5.6 Proving Programs with Exceptions


14.5.1 Exceptions Raised During the Elaboration of Declarations

The elaboration of declarations may involve the evaluation of some expressions, and in consequence, exceptions may be raised during this elaboration. Consider for example the procedure

procedure A(N :  INTEGER) is
  C :  constant INTEGER  :=  N * N;
  D :  INTEGER  :=  C;
  T :  array (1 .. C) of INTEGER;
begin
  -- statements of A
exception
  -- handlers of A
end;

If an exception occurs during the elaboration of the constant C, the procedure will be in a state where D is not initialized, and the space for the array T is not yet allocated. Consequently, a handler may not be able to do much; any reference to D or T will be erroneous and may cause a further exception. For these reasons an exception raised by the elaboration of a declaration is never handled locally; it is propagated to the place where the elaboration of the declarations was initiated: for example, this place may be just after the call of a subprogram in whose body the declarations are written, or it may be just after a block in whose declarative part the declarations appear.


14.5.2 Propagation of an Exception Beyond its Scope

Since an exception can be propagated, it can be propagated beyond its scope. It is even possible for an exception to be propagated outside its scope and then back again within its scope. Thus, in the following example, if B calls OUTSIDE and OUTSIDE calls A, the exception ERROR raised within A will be propagated to OUTSIDE and again to B:

package D is
  procedure A;
  procedure B;
end;

procedure OUTSIDE is
begin
  ...
  D.A;
  ...
end;

package body D is
  ERROR :  exception;
  procedure A is
  begin
    ... raise ERROR; ...
  end;

  procedure B is
  begin
    ...
    OUTSIDE;
    ...
  exception
    when ERROR =>
      -- ERROR may be propagated by OUTSIDE calling A
  end;
end D;

An exception propagated beyond its scope can only be handled there by a handler for others. It can be further propagated or raised again by the abbreviated form of the raise statement (raise;).

This rule provides a simple and consistent interpretation of the above example and it avoids the complexity and run-time costs that would be incurred if exceptions propagated beyond their scope were converted into a unique undefined exception. This design also considered, and rejected, the possibility of associating the names of the possibly propagated exceptions with each procedure declaration. The main reason for rejecting this possibility is the fact that this would require extra run-time code for filtering the propagation of exceptions. For example, if a procedure were declared as

    procedure P(X :  INTEGER) PROPAGATES A, B, C;      -- not in Ada

its body would have to be compiled as the equivalent of the following procedure:

procedure P(X :  INTEGER) is
  ...
begin
  ...
exception
  when A | B | C =>  raise;
  when others =>  raise anonymous_exception;
end P;

We considered the resulting code expansion to be prohibitive, especially in the case of small functions and procedures.

With the solution adopted in Ada, the user can always put similar information in comment form. The choice others covers all possible anonymous exceptions, not just one.

The philosophy behind the Ada model is that an exception is not an error situation; it is only a name that is declared for an error situation. Like any other declaration, an exception declaration has a scope. The error situation, on the other hand has no such limits, and can always be referred to as part of the others exception choice.

The internal codes associated with exceptions must all be distinct - as are the codes of the different literals of an enumeration type. In general, this assignment of codes to exceptions must be performed at linkage editing time.


14.5.3 Suppression of Checks

A given program unit, and in particular a procedure, may be robust, in that it will perform some computation, and produce some result, for any value of its input parameters. On the other hand, its validity may only be guaranteed for certain values of these parameters. The exception mechanism is a useful tool to achieve robustness, but this may be gained at some cost in efficiency, since detection of some error situations may be expensive unless aided by special hardware.

In some cases where robustness can be attained by means other than run-time checks, the programmer may not wish to incur the cost of checking for certain error situations. The pragma

    pragma SUPPRESS(check_name);

indicates that the check named in the pragma need not be performed. (Should a violation of the corresponding condition occur, behavior of the program would be unpredictable.)

In the presence of such pragmas, the compiler may suppress the named checks, and will do so if this results in an optimization.

However, in the case of exceptions whose detection is aided by special hardware, inhibiting the corresponding hardware mechanisms may be costlier than actually performing the checks. Hence the pragma is not imperative - it does not mean that the checks are not done.

An alternative view of the SUPPRESS pragma would regard it as a directive indicating imperatively that no check is to be performed to detect the exception. This approach would amount to a decision to continue execution of the program in spite of any error situation. It would give an appearance of robustness which might be exploited in cases where the programmer knows that the error situation will have some effects that can be detected at a later time, but it is contrary to the general philosophy of the language.

In addition, the need to provide a semantics that reconciles software- and hardware-detected exceptions would have a negative effect on the efficiency of programs. If the pragma were imperative, then on a machine with hardware-detected exceptions it would be necessary to inhibit the hardware checks for a scope in which a corresponding pragma SUPPRESS is given. Thereafter, it would be necessary to enable the hardware detection again, prior to each call to a unit outside that scope, and again to inhibit the detection following a subsequent return from the call.


14.5.4 Implementation of Exception Handling

One important design consideration for the exception handling facility is that exceptions should add to execution time only if they are raised.

Several techniques may be used to reach that goal, and they may differ from one implementation to another. The essential idea is that the run-time processing costs should be concentrated on the treatment of the raise statement. Consequently, processing of a raise statement may be relatively slow. In contrast, the costs associated with exception declarations and exception handlers are only in terms of space and in terms of compilation time - they have no influence on the execution time.

As a feasibility proof, we outline a possible implementation technique in which no run-time costs whatsoever are incurred for exceptions unless they are raised. This technique has been used for some debugging systems and it bears some resemblance to the technique used in Mesa [GMS 77]. The basic principles are as follows:

  1. When an exception occurs, the specific run-time system that treats exceptions must be able to locate the addresses of the currently active procedure calls. This condition is satisfied if (as is usually the case) return addresses are stored in procedure activations.

  2. Knowing the code address of a procedure, it must be possible to locate the code address of the first handler. Similarly, given a handler, it must be possible to find the next handler. This condition can be satisfied by chaining the handlers and by storing the address of the first handler just before the code of the procedure.

  3. Each handler must start with the indication of the exception code (or codes) that it services. Some convention must be used for the handler for others, which must appear last.

  4. When effecting the association of an exception with a handler, the run-time system locates the procedure address and from there the chain of handlers. It may then inspect the exception codes to find the appropriate handler, if any.
We reiterate that this solution should only be considered as an existence proof that exceptions may be implemented at no cost unless raised. Other techniques may be more suitable, depending on the machine architecture.


14.5.5 The Case Against Asynchronous Exceptions

The normal means of communicating with a task is via entry calls. Hence most situations in which the termination of a task must be decided by another task should be programmed by calling a special entry, say STOP, of the task to be terminated (or by using a terminate alternative). The clear advantage of such a solution is the possibility thus offered of including accept statements for the STOP entry at those places where the termination can be done in an orderly fashion.

The ability for one task to raise an exception in another task must however be viewed as a possibility that has - potentially - extremely severe consequences. In no way should such externally raised exceptions be considered as being normal terminating conditions. Interfering asynchronously with the execution of a task may catch it in a state where it is not prepared to respond to such intervention. There is then always a risk of leaving the task in a state of confusion, and also of contaminating other tasks that were communicating with it.


14.5.6 Proving Programs with Exceptions

The problems of exception handling facilities such as the PL/I on conditions, which permit resumption after the exception, are well- known. For instance, assuming integer working, consider the consecutive statements:

X :=  P + Q;
Y :=  X - Q;
ASSERT (Y = P);

Unless overflow occurs in the evaluation of P + Q, the final assertion should be satisfied. This however would not be true if a handler for overflow were able to provide a different value for X and return to the same statement list.

This simple example shows the near impossibility of proving programs with unrestricted signal and notify exceptions. For the same reasons, such programs are extremely difficult to optimize.

In contrast, for the proposal chosen in Ada, simple proof rules may be given, as has been shown by Bron [BFH 76] and Fokkinga [Fo 77]. Additional examples may be found in the reference [DH 76]. The main idea is a consequence of the definition of the role of a handler.

As mentioned above, when an exception occurs in a procedure, the execution of the handler completes the execution of the procedure considered. Consequently the effect of a procedure is achieved either:

  1. by its body if no exception occurs

    or, if the exception E occurs:

  2. by the part of its body up to the point where the exception E occurs, and then by the handler for E.
Two simple cases have been shown in the programming examples:
  1. In the SAFE_INSERT example of section 14.3.4 we have shown a case where these two rules reduce to a simpler form. The effect of SAFE_INSERT is achieved either:

    1. by its body if no exception occurs, or

    2. by the handler for TABLE_FULL if TABLE_FULL occurs.

  2. In the file example of section 14.3.3, where the exception END_ERROR is used as a terminating condition, the effect of the procedure TRANSFER is achieved by the succession of the effects of

    1. its body

    2. the body of the handler for END_ERROR.
This shows that, with adequate programming conventions, the effect of a procedure that contains an exception handler can be characterized in a simple way. This simplifies correctness proofs.


NEXTPREVIOUSUPTOCINDEX
Address any questions or comments to adainfo@sw-eng.falls-church.va.us.