Skip to main content.

Wednesday, August 17, 2011

And so, we got try/catch statement working in the new organic virtual machine. Funny enough, it just took 3 days (less, actually) instead of the year it took with the old machine, and I was even able to add a finally keyword that would have been nearly impossible to be added in the old engine, with just a few touches more. Another proof, if it was needed, of the potential of the new engine. Here follows some details about what it took and what this is determining.
Again, the twist was that if finding a name to solve the problem. The new name, and the new concept introduced in the virtual machine is the code barrier.

After fumbling around a bit adding a separate try-frame stack, I tried to stuff the "finally" construct in. Other languages making good usage of exception handling, as Java, delegate finally to the role of cleaning up code sections not just in case of error throwing, but even in case of block break-out. This seemed desirable in Falcon as well. We have (willfully) a limited scope protection which makes variables declared inside the try block available also outside it. So, in Falcon it is possible to provide cleanups strategies that can be implemented in other languages through the finally keyword only. For instance, in falcon it is possible to write something like that:

file = InputStream( ... )
catch IoError in e

// if file is an open file, it will be true here, else it will still be nil, and so, false:
if file: file.close()

while this simple finalization rule require a finally block in other languages. So, finally wasn't terribly necessary to implement cleanup strategies per-se, but the fact of providing a cleanup strategy on unstructured instruction issuing was terribly fascinating. Unstructured instructions are those statements that break the flow of the code and interrupt the flow. C has break, continue, return, goto and switch (partially unstructured), C++ adds throw; in Falcon we have:

  • break

  • continue

  • return

  • raise

Moreover, (this is a thing that I wasn't able to write on the blog yet), I have decided to experiment "break values", that is, funcitonal commands or values that instruct foreign code to perform unstructured operations. For instance, it is now possible to write:

function breakMe()
if : break

while cond

Break and continue propagate through the call stack past the call frame, and reach the innermost loop instruction. Actually, this is not a necessity of the new engine, but somehow it seemed to me that it was an interesting ability; however, it's an experimental feature, we might turn it off if it hurts more than help. Anyhow, the interesting thing in that is that you can now see "break" and "continue" as values, so you can "return break", and thus communicate the will to break out from functional loops using a nicer way than the old return oob(0).

This feature already required the ability to unroll the code stack in search for some "landing point" where it was safe to resume the VM control. Of course, the unroll process should have take the call stack status into consideration; willing to enlarge the scope of break/continue statements (scoping them similarly to "raise", that is, globally), it was necessary to consider eventual call frames to be unrolled while progressing backward in the search for a landing code frame.

So, this mechanism was present, but I didn't formalize it (didn't give it a "name") until I saw that "raise" and error catching was more or less doing the same thing. I then formalized the idea of "code barriers" that is, code frames that required a temporary or definitive stop of the code stack unroll process fired by a non-structured instruction.

  • return: stops at code frames marked as function callers by a call frame (call barrier).

  • continue: stops at a "next loop" code barrier.

  • break: stops at a "loop cleanup" code barrier.

  • raise: stops at a "catch barrier" matching the raise item type.

Notice that the nature of this barriers are different. In case of the of the return barrier, the barrier status is determined by a value stored in the return frame; also, the nature of this barrier allows to unroll the call frames without performing a reverse traversal, if conditions are favorable (but now you can consider that a mere optimization). In the case of the catch barrier, the barrier may be active or not depending of the fact that a catch clause can block the raised value or not. In the cases of continue and break, their barriers are mandatory and active unconditionally, each time they are met.

Yet, the mere fact of considering all this phenomenons under a common name allowed me to simplify the code and use just one function to handle all of them. For optimization reasons, the function is actually an inline template, where the template parameter is a code that implements the different behaviors of each barrier.

But the fact of naming this operation as "code unroll" (on unstructured statement), allowed a niftier thing. The finally block could have been named a code barrier as well: an optional code barrier suspending the code unroll process when met.

This allowed to deal almost transparently with break, continue and raise crossing call frames, and the fact that return stopped at first call frame could have been considered an incidental, non-influent detail now.

But this even allowed to sneak in a feature that is almost dreamed of by other languages, even major ones as Java: the ability to deal cleanly double-raise from finally blocks.

In fact, the engine keeps track of the ongoing process during finally allows to deal correctly with errors happening from within it. Our engine provides "sub-errors" natively, so it came pretty natural to me to deal with error raising from within finally handlers (even nested ones) by adding the error they thrown to the ongoing, already-traveling error.

ATM, I think it's sensible to let finally to override the ongoing operation; for instance, if they were excited on a break or continue, they could neutralize it by issuing an explicit return, or conversely, if excited on a clean termination of a try block or on an exception raisal, they could nullify it by issuing a continue or break operation. Also, as raise could throw not just exceptions, but any kind of item, a plain item raised by something inside the try block could (and IMHO should) be overridden by an explicit item raise in the finally block. However, those override operations would all be explicit, and not a side-effect of the language structure as in the case of error reaping in throwing exceptions from a Java finally block.

Yet, as we have full knowledge of what's going on during the finally operation, we might decide to do something else, and issuing an error if finally tries to override the ongoing process, we we find this to be more sensible.

One final note: actually, the time needed to code all this things in was somewhere about 4 hours. The rest of the 3 days were needed, in part, to refine the concepts of unroll process and code barriers, and in part to complete some elements of the engine that will be useful elsewhere: for instance, the Requirement class, a generalization of the Inheritance class, that allows for link-time resolution of extern symbols on even deep structures, as, for instance, the catch blocks deep inside try statements.

(Actually, the catch blocks are implemented as an instance of the select statement, which switches over the type of a value -- so that statement internal structure is complete as well).


No comments yet

Add Comment

This item is closed, it's not possible to add new comments to it or to vote on it