This code will execute the IO action doStuff, and if an exception occurs it will catch it. If that exception resulted from a call to the 'error' function then it does nothing, otherwise it rethrows the exception.

1.1 The exception stack

catch blocks can be nested inside each other and throw returns control to the handler for the inner-most catch block. This structure naturally leads us to having a stack of exception handlers with each new catch block pushing a new handler on the stack at the beginning of the block and removing the top handler at the end of the block.

1.2 Haskell level implementation

1.2.1 throw

throw is implemented very simply

throw :: Exception -> a
throw = primThrow
primThrow :: a -> b

primCatch cannot be written in Haskell and is instead defined directly in bytecode (see src/runtime/BCKernel/primitive.c) as

primThrow e

PUSH_ZAP_ARG e
THROW

The THROW instruction removes the value on the top of the program stack 'e' and removes the exception handler on the top of the exception stack. In then returns control to that exception handler, this strips the program stack back to the place where the exception handler was created. THROW then pushes 'e' on the new top of the program 'stack'.

Diagramatically (see Yhc/RTS/Machine for comparison). Before executing THROW we have:

'CATCH_BEGIN label' creates a new exception handler and pushes it on the top of the exception stack. The new exception handler will return control to the function that executed the CATCH_BEGIN instruction and resume execution at the code given by 'label'.

Having pushed the new exception handler on the stack, primCatch forces evaluation of the action, causing the code inside the catch block to be executed.

If evaluation of the action succeeds without throwing an exception then CATCH_END is executed which removes the handler on the top of the stack (which is necessarily the same handler as was pushed by CATCH_BEGIN).

However, if evaluation of the action results in a call to throw then execution returns to 'handler'. Here we need to remember that THROW pushes the exception thrown on the program stack after stripping back to the exception handler. Thus at 'handler' we know that the exception thrown is on the top of the program stack.

We thus 'PUSH_ZAP_ARG h' to push the handler function on the stack, and 'APPLY 1' to apply the handler function to the exception and finally 'RETURN_EVAL' to call the handler function.

1.3 Interpreter implementation

In the C interpreter exception handlers are stored in the heap as standard Haskell heap nodes. The structure is given in src/runtime/BCKernel/node.h

vapptr, ip and fpOffs is basically the same information as stored in stack frames (see Yhc/RTS/Machine). spOffs is included for completeness although in practice it isn't strictly necessary.

fpOffs and spOffs are offsets from G_spBase rather than direct pointers because program stacks are stored in the heap, and thus might be moved by the garbage collector. The offset from the base of the stack, however, will not be changed by the garbage collector.

The 'next' field allows us to setup a stack of exception handlers. Each process has its own exception stack, so the handler on the top of the stack is stored in the process information structure (see Yhc/RTS/Concurrency).

Ensuring that the ExceptionHandlerNodes are treated correctly by the GC is ensured by simply having CATCH_BEGIN push the created ExceptionHandlerNode on the program stack. There it remains until it's either removed by the corresponding CATCH_END or it is stripped off the stack by THROW. This simple trick means we can avoid having to scan process information structures for pointers to heap nodes.

1.4 Changing the type of IO

Before imprecise exceptions the IO type was defined as:

newtypeIO a =IO(World ->EitherIOError a)

World is a dummy argument to prevent us from accidentally introducing a CAF, and the function either returns (Left err) if there was some error performing the IO action or (Right a) if the IO action succeeded with value 'a'.

However imprecise exceptions allow us to improve this to:

newtypeIO a =IO(World -> a)

since we can simply implement throwing and catching IOErrors using 'throw' and 'catch'. This requires a slight change in the code for the IO monad to: