parrotcode: Untitled | |
Contents | Language Implementations | .Net |
This document discusses the way that .NET and Parrot provide support for exceptions. It then describes how .NET's exception model can be implemented using the Parrot exception support.
Four different types of handler are provided for. The most commonly used is a typed handler. Here the handler is annotated with a type and is invoked if an exception is thrown that is of that type. There is also a filtered handler. Here, code at a certain offset is run to determine whether or not the handler should be selected to handle the exception; if it is to do so, a value of 1 should be left on the stack, otherwise a 0 is left there.
The remaining two handlers are not exception handlers in the sense that they capture and prevent further propogation of the exception. Instead, they are invoked when the search for an appropriate handler passes over them - in some cases. The first of these is the finally handler, which is run whether or not the "try" region was left due to an exception or naturally. The second is the fault handler, which is run only when the "try" region was left due to an exception being thrown.
Leaving a protected region or handler is greatly restricted. For leaving a protected region or typed handler, only the leave or leave.s instructions can be used. At the end of a finally or fault handler, endfinally must be used. At the end of a filter, endfilter must be used. Similarly, entering a protected region is restricted to falling into it from the top or entering it from a catch block.
At the entry to a try block or the destination of a leave instruction, the evaluation stack must be empty. At entry to a typed or filter handler, the stack will only contain the exception object; for other handlers it must be empty.
The table of exception handlers is sorted inner-most to outer-most where there is nesting.
Exceptions themselves are PMCs; more specifically, an exception must be an instance of the built-in Exception PMC. This PMC provides a keyed interface so data relating to an exception can be stored inside the exception object. A throw instruction is used to throw an exception object.
When searching for an exception handler, the exception stack is checked and the top exception handler is popped off and run. If it wishes to handle the exception, it will do so. If not, it can use the rethrow instruction to continue the unwinding of the exception stack.
As well as handlers, two additional items may reside on the exception stack. The first of these is a mark. A mark is simply an integer value pushed onto the stack. When a mark is popped, any marks and exception handlers above the mark are popped off the stack too. This provides a way of handling scope exits more elegantly. The second item is an action. This is simply a sub PMC that gets invoked if, while unwinding the stack looking for an exception handler, the entry is walked over.
For each handler that starts at the current location, two PIR instructions are emitted. The first is a push_eh instruction. The second is a push_mark instruction which will place a mark on the stack that matches the row number in the exception table that the handler is defined at. The handler starts at the location specified by the handler offset in the exception table apart from in the case of a filter type exception in which case the filter offset will be used instead.
The case where the finally is walked over is relatively trivial to handle. The handler will be invoked just as any other Parrot exception handler would. The exception object will be retrieved and stored. Upon the endfinally instruction it will be re-thrown. This is not completely trivial, since if finally handlers are nested the outermost one must still know which exception to rethrow. Therefore an array of exceptions waiting to be thrown from finally handlers must be maintained.
The array of exceptions waiting to be thrown has a second purpose: an empty (null) entry can be used to signify that the finally block was entered from a leave statement and should instead use the Parrot ret instruction, which returns from a subroutine branch made within the current method. These subroutine branches are emitted in leave instructions and simply invoke the required finally blocks (that is, those not walked over while unwinding the stack). Note that detection of which finally handlers to invoke involves looking at the exception handlers table and locating ones that would not have been walked over and are on the "path" from the current location to the destination of the leave instruction.
The only remaining piece of the puzzle is that after the code emitted at the start of a finally handler, a label must be inserted that can be used to run the finally handler from a leave instruction.
XXX The .NET spec suggests finally is not run if the exception thrown is never caught. That is probably not something that can be handled too easily if true.
Details of what to emit with regards to finally blocks has already been discussed and will not be repeated here. This comes before the process that follows.
When a leave instruction is translated, before the goto a popmark instruction will be inserted. The mark will be computed by scanning through the exception handlers table and locating the first protected region that occupies the location being branched to. Immediately following the popmark, a pushmark will be generated for the same mark. The reason for this is that the intention of the popmark is to clear all handlers on the stack that belong to nested protected regions. However, the mark that also gets removed is that of the region that will be branched into. If there are a sequence of protected regions within another one, failure to restore the mark would cause failure beyond the first in the sequence.
Note that if there is no containing region, the mark 0 should be used. Note that this requires a pushmark 0 to be emitted at the top of every translated method, and a popmark 0 at every return.
|