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:
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.
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.
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.
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:
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.
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:
or, if the exception E occurs: