Inferior runloops is a problem which I'm not even certain I understand in it's entirety. I do know about certain specific facets of it, and I do understand the mechanism that causes the problem. Let's look at one specific control flow:
- We start Parrot. This creates the interpreter and the dynamic context. We call the runloop function to begin executing code.
- We create a new class Foo and an object Bar of type Foo. Foo has several VTABLE overrides defined to implement custom behavior.
- We perform almost any operation on Foo. Since all operations on PMCs are defined through VTABLES, we are likely to be calling a VTABLE to implement that operation.
- The op is a C function, which calls the Object VTABLE (another C function). This searches for an override, finds it, and executes it.
- Executing the VTABLE override cannot be done in the current runloop. This is so for several reasons: We're several functions down in the C stack and would need to jump back to it, we very likely require a return value for additional processing somewhere between the runloop and the call to the override, etc. So, we call a new runloop function which is created further down on the C system stack.
- The override throws an exception, but doesn't itself define any handlers. The scheduler does find an exception that has been defined previously in the outer runloop, and invokes that.
- Exception handlers are just labels, so we handle the exception and continue execution like normal. Except we are now in the inner-runloop further down the C stack, but are executing code that had been being executed by the outer runloop.
- The program executes like normal and reaches the end. The inner runloop returns, the VTABLE returns, the op returns, and we end up in the outer runloop again
- The outer runloop continues execution, but the program has already ended. The context is completely inconsistent. Bad things happen
If exception handlers were separate subroutines instead of simply being labels, they would have a finite ending point, and control flow would not just "continue" past the point when the exception was handled. When the handler exits, control flow can move to anywhere it needs to move which will likely be some kind of resume continuation, or the next exception handler if the exception was rethrown.
Another possible solution, which is also worth thinking about here, is that if we could avoid recursing runloops, we could avoid this issue entirely. The problem really stems from recursion, and no recursion means no problem. This is the kind of thing that we could achieve with Lorito, eventually. But, that's another topic for another post.
This problem has been a source of issues for Rakudo recently. At least, I am mostly certain that this is the cause. A hallmark sign that this is the root cause of a problem is that we start getting weird segfaults after the program appears to have run to completion. This is because the program has run to completion in the inner runloop, but then execution continues in the outer runloop which is now inconsistent and causes segfaults. The program appears to run to completion, throws a segfault at the end, and the back trace seems to show more things happening in the main runloop after the program has ended.
I sent a quick email to Allison the other day where I outlined a handful of potential solutions--ranging from quick and dirty to more comprehensive--and asking for an opinion of which direction we should be moving in to get this resolved once and for all. There was some back-and-forth between her and chromatic, who have apparently put some significant thought into it already. The way forward, I think, is to convert all exception handlers to be subroutines instead of labels (the latter will be deprecated, probably following 2.0), which should resolve this problem once and for all. It will be a significant effort to get all our HLLs to use the new exception handler type, and we have some issues of syntax to sort out, but it's going to be worthwhile in the long run to do it.
NotFound posted a short-term fix that should get everybody working through the release: Add an explicit "exit 0" command to the end of your main functions. This will force Parrot to exit when the inner runloop reaches the end of the program, instead of unwinding the stack and trying to continue execution in the outer runloop.
I'll definitely post more information about all this as work progresses.