Why exceptions?
Syntax and informal semantics
Semantic analysis
Operational semantics
Code generation
Runtime system support
“Classroom” programs are written with optimistic assumptions.
Real-world programs must consider “exceptional” situations:
Two ways of dealing with errors:
Handle them where you detect them
Let the caller handle the errors:
The caller has more contextual information
But, we must tell the caller about the error
The callee can signal the error by returning a special return value or error code:
The caller promises to check the error return and either:
It is sometimes hard to select return codes
divide(num: Double, denom: Double) : Double {...}
How many of you always check errors for:
malloc(int)
?open(char *)
?close(int)
?Easy to forget to check error return codes
Exceptions are a language mechanism designed to allow:
We extend the language of expressions:
\[\begin{array}{rcl} expr &::=& \texttt{throw} \; expr\\ &|& \texttt{try} \; expr \; \texttt{catch} \; \texttt{ID} : \texttt{TYPE} \; \texttt{=>} \; e_2 \end{array} \]
(Informal) semantics of throw
\(expr\)
Signals an exception
Interrupts the current evaluation and searches for an exception handler up the activation tree
The value of \(expr\) is an exception parameter and can be used to communicate details about the exception
(Informal) semantics of \(\texttt{try} \; expr \; \texttt{catch} \; \texttt{ID} : \texttt{TYPE} \; \texttt{=>} \; e_2\)
\(expr\) is evaluated first
If \(expr\)’s evaluation terminates normally with \(v\), then \(v\) is the result of the entire expression
Otherwise, (\(expr\)’s evaluation terminates exceptionally)
If the exception parameter is of type \(\leq \texttt{TYPE}\), then
Evaluate \(e_2\) with \(\texttt{ID}\) bound to the exception parameter
The result of evaulating \(e_2\) becomes the result of the entire expression
Else the entire expression terminates exceptionally
We must extend the Cool typing judgment \[O, M, C \vdash e : T\]
We will start with the rule for try:
\[\frac{ \begin{array}{l} O,M,C \vdash e_0 : T_0\\ O[T/x],M,C \vdash e_1 : T_1\\ \end{array}} {O,M,C \vdash \texttt{try} \; e_0 \; \texttt{catch} \; x : T \; \texttt{=>} e_1 : lub(T_0, T_1)} \]
The type of an expression:
Is a description of the possible return values, and
Is used to decide what contexts we can use the expression
throw
does not return to its immediate context but directly to the exception handler
The same throw e
is valid in any context:
if throw e then (throw e) + 1 else (throw e).foo()
As if throw e
has any type
Rule:
\[\frac{O,M,C \vdash e : T_1}{O, M, C \vdash \texttt{throw} \; e : T_2}\]
As long as \(e\) is well typed, throw
\(e\) is well typed with any type needed in the context, that is, \(T_2\) is unbound.
This is convenient because we want to be able to signal errors from any context
Several ways to model the behavior of exceptions
A generalized value is
Either a normal termination value, or
An exception with a parameter value
\[g ::= Norm(v) \; | \; Exc(v)\]
Given a generalized value, we can:
Determine if it is normal or exceptional return, and
Extract the return value or the exception parameter
The existing rules change to use \(Norm(v)\)
Example:
\[\frac{ \begin{array}{l} so, E, S \vdash e_1 : Norm(Int(n_1)), S_1\\ so, E, S \vdash e_2 : Norm(Int(n_2)), S_2 \end{array}} {so, E, S \vdash e_1 + e_2 : Norm(Int(n_1 + n_2)), S_2} \]
throw
returns exceptionally
\[\frac{ so, E, S \vdash e : Norm(v), S_1} {so, E, S \vdash \texttt{throw} \; e : Exc(v), S_1} \]
What if the evaluation of \(e\) itself throws an exception?
\[\frac{ so, E, S \vdash e : Exc(v), S_1} {so, E, S \vdash \texttt{throw} \; e : Exc(v), S_1} \]
\[\frac{ \begin{array}{l} so, E, S \vdash e_1 : Exc(v), S_1\\ \end{array}} {so, E, S \vdash e_1 + e_2 : Exc(v), S_1} \]
\[\frac{ \begin{array}{l} so, E, S \vdash e_1 : Norm(Int(n_1)), S_1\\ so, E, S \vdash e_2 : Exc(v), S_2 \end{array}} {so, E, S \vdash e_1 + e_2 : Exc(v), S_2} \]
The rules for try
expressions (multiple rules similar to conditionals):
\[\frac{so, E, S \vdash e_0 : Norm(v), S_1} {so, E, S \vdash \texttt{try} \; e_0 \; \texttt{catch} \; x : T \; \texttt{=>} \; e_1 : Norm(v), S_1} \]
If \(e\) terminates exceptionally, then we must check whether it terminates with an exception parameter of type \(T\) or not.
If \(e\) does not throw the expected exception:
\[\frac{ \begin{array}{l} so, E, S \vdash e_0 : Exc(v), S_1\\ v = X(...)\\ X \nleq T \end{array}} {so, E, S \vdash \texttt{try} \; e_0 \; \texttt{catch} \; x : T \; \texttt{=>} \; e_1 : Exc(v), S_1} \]
If \(e\) does throw the expected exception:
\[\frac{ \begin{array}{l} so, E, S \vdash e_0 : Exc(v), S_1\\ v = X(...)\\ X \leq T\\ l_{new} = newloc(S_1)\\ so, E[l_{new}/x], S_1[v/l_{new}] \vdash e_1 : g, S_2 \end{array}} {so, E, S \vdash \texttt{try} \; e_0 \; \texttt{catch} \; x : T \; \texttt{=>} \; e_1 : g, S_2} \]
Our semantics is precise
But, is not very clean; it has two or more versions of each original rule
It is not a good recipe for implementation
It models exceptions as “compiler-inserted propagation of error return codes”
There are much better ways of implementing exceptions
There are other semantics that are cleaner and model better implementations (not within the scope of this course)
One method is suggested by the operational semantics
Simple to implement
But not very good
We pay a cost at each call/return (often)
Even though exceptions are rare (exceptional)
A good engineering principle: “don’t pay often for something that you use rarely”, that is, optimize the common case.
A long jump is a non-local goto:
In one shot you can jump back to a function in the caller chain (bypassing many intermediate frames)
A long jump can “return” from many frames at once
Long jumps are a commonly used implementation scheme for exceptions
Disadvantage: (minor) performance penalty at each try
We do not want to pay for exceptions when executing a try
, only when implementing a throw
\(cgen(try \; e_1 \; catch \; e_2) =\) | |
\(\qquad cgen(e_1)\) | ; code for the try block |
\(\qquad\)jump end_try |
|
L_catch: |
|
\(\qquad cgen(e_2)\) | ; code for the catch block |
end_try: |
|
\(\qquad\) \(\cdots\) | |
\(cgen(throw)\) | |
\(\qquad\)jump runtime_throw |
; the trick |
The normal execution proceeds at full speed
When a throw is executed we use a runtime function that finds the correct catch block
For this to be possible, the compiler produces a table where each catch block maps to the corresponding instructions
The runtime_throw
does a table lookup to determine which catch handler to invoke
Advantage: no cost except if an exception is thrown
Disadvantage: Tables take up space
The Java Virtual Machine uses this scheme
Real-world programs must have error-handling code. Errors can be handled where they are detected or the error can be propagated to a caller.
Passing special error return codes is itself error-prone.
Exceptions are a formal and automated way of reporting and handling errors. Exceptions can be implemented efficiently and described formally.