Exception raising and handling must require no heap-allocated memory for core functionality. Exception handling should minimize the amount of information on the stack. Exception handling must be able to propagate across external code. Exception handling should gracefully extend into multithreading. Exception handling should be statically checkable. Exception handling should be efficient at runtime.
Exception handling will be split into three parts. First is the code that raises an exception. Second is the code that handles an exception. Third is the code that recovers from an exception.
Exceptions are statically typed, and what exceptions a piece of code can raise are statically determined, either by type inference from exception statements or by explicit declaration.
Exceptions have slots for information to be added and for restarts to be bound. Exceptions also have a code to allow discriminating the exception type.
First an exception type must be created using an appropriate function.
An exception can be raised using the raise function. It cannot have any constructor logic besides setting fields to arguments.
A block of code or an expression can be wrapped in a catch construct. A catch construct can handle one or more exception types with an appropriate catch handler. Control flow passing off the end of a catch handler or off the end of the body proceeds after the catch construct.
A block of code or an expression can be wrapped in a restart construct. A restart construct binds restarts to an exception. Control flow passing off the end of a restart or off the end of the body proceeds after the restart construct.
A catch that does not invoke a restart is essentially equivalent to Java's try-catch-throw mechanism.
A restart allows using high level context to advise a low level operation. This permits graceful error recovery without needing a bunch of extra code paths and data through the entire call hierarchy.
fooException = terralib.types.newexception("fooException", {{"val", int}}, {{"useValue", {int}}})
exception barException {
val: int
restart useValue(int)
}
terra high(): int
var a: int
try
a = mid()
a = a + a
catch e: fooException
e:useValue(2)
end
return a
end
terra mid(): int
var b: int
retry -- previous keyword was restartblock. Could still be improved probably.
b = low()
b = b + b
restart useValue(x: int)
b = x
end
return b
end
terra low(): int
raise(fooException {val = 1})
return 3
end
high() -- returns 4
struct HandlerLink {
prev: &HandlerLink
frame: &opaque -- frame pointer onto the stack for the local variable
code: &opaque -- A strange type of function pointer.
}
struct Exception {
id: int
data...: any
restarts...: pair(&opaque, &opaque) -- A frame pointer and a strange function pointer
}
There is a single thread local or global memory location holding a handler-link. When a handler is installed, it allocates one handler link on the stack, copies the global handler-link to the stack cell and installs itself into the global handler link, linking the global handler-link to the cell on the stack. When a handler is uninstalled, the link can be copied from the stack cell to the global cell.
Restarts get placed into the restart slots of an exception type by a special type of handler.
Only the deepest restart in the stack is acceptable, so these handlers check if the restart has already been set before continuing.
As the exception propagates up the stack, restarts get bound until it reaches a catch handler that catches the exception.
A catch handler is provided the exception info on the top of the stack. The stack does not get unwound prior to the invocation of the catch handler. This means that if the exception is unhandled, a debugger can inspect the exception, inspect the stack, and invoke a restart.
A catch handler is invoked with the frame pointer of its installation context so that it has access to the local variables from outside its installation scope. It is invoked on the top of the stack when the exception is raised, leaving the entire stack beneath the Exception intact.
A catch handler can do any of invoking a restart, eating the exception and transfering flow of control to directly after the protected block, continuing to propogate the same exception up the stack, or producing a new exception to raise.
When a restart is invoked, the stack is unwound to the position of the installation of the restart. If the restart terminates normally, flow of control passes to the line immediately following the restart-protected block. The function that bound the restart is now on the top of the stack.