While debugging a fun crash
(#1187) in
Factor, I came upon the
callcc0
primitive which is how it implements continuations. It is a concept which it inherits from Scheme, where it is known as Call with current continuation.
In Factor, just like in all other programming languages, control flow is mostly linear. Execution moves forward one function (or word as it's called in Factor) at a time. The exception to that is when an exception is thrown. Then an error object is put on the stack and execution jumps out to the nearest exception handling block.
The surface-level syntax looks like this:
[ "hello!" throw ] [ print ] recover
The first quotation (the try) throws the string "hello!" as an error which is then printed out in the second one (the catch). Now try this in the listener:
IN: scratchpad \ recover see
USING: continuations.private kernel sequences ;
IN: continuations
: recover
( ..a try: ( ..a -- ..b ) recovery: ( ..a error -- ..b ) -- ..b )
[ [ [ catchstack* push ] dip call catchstack* pop* ] curry ]
dip ifcc ; inline
It shows you the definition for the recover
word. With this
knowledge we can directly substitute the call to recover
with its
definition:
[ "hello" throw ]
[ print ]
[
[
[ catchstack* push ] dip
call
catchstack* pop*
] curry
] dip ifcc
And since we know the definition of the try and catch quotations we can further rewrite that as:
[
CONTEXT-OBJ-CATCHSTACK context-object push
"hello!" throw
CONTEXT-OBJ-CATCHSTACK context-object pop
] [ print ] ifcc
In the examples below I won't bother with the intermediate step and always simplify as much as possible. Hopefully that won't make the text to hard to follow.
As an aside, this is how Factor differs from most conventional languages such as Java or Python. In those languages you have the try-catch statement, for loops etc and those are in a way "magic". For example, if Python didn't provide you with a try-catch, you wouldn't be able to write one yourself, using only Python. In Factor you can, as this demonstration of how the try-catch is built shows.
The
ifcc
word is very powerful. Likely the most powerful one in the whole
Factor system. When it is called, it takes a snapshot of the whole
virtual machine, makes an object of it and passes it to the first
quotation. That quotation then either runs to completion or throws an
error, in which case the second quotation is run.
Now, let's focus on the
throw
word and see what we can peel of from it:
[
CONTEXT-OBJ-CATCHSTACK context-object push
"hello!"
ERROR-HANDLER-QUOT special-object call
CONTEXT-OBJ-CATCHSTACK context-object pop
] [ print ] ifcc
As you can see, throwing an exception is nothing more than calling a
special quotation in the vm with the error object on the stack. But
why stop here? ERROR-HANDLER-QUOT
can be inspected and we can
rewrite the call to that quotation with its definition:
[
CONTEXT-OBJ-CATCHSTACK context-object push
! This part is throw
OBJ-CURRENT-THREAD special-object error-thread set-global
current-continuation error-continuation set-global
"hello!" original-error set-global
"hello!" rethrow
CONTEXT-OBJ-CATCHSTACK context-object pop
] [ print ] ifcc
So all throw did was set some dynamic variables in the global
namespace and call
rethrow
. Again
let's do the same substitution of the rethrow
word:
[
CONTEXT-OBJ-CATCHSTACK context-object push
! This part is throw
OBJ-CURRENT-THREAD special-object error-thread set-global
current-continuation error-continuation set-global
"hello!" original-error set-global
"hello!" save-error
"hello!" CONTEXT-OBJ-CATCHSTACK context-object pop continue-with
CONTEXT-OBJ-CATCHSTACK context-object pop
] [ print ] ifcc
(We know that the catch stack is not empty so this rewrite is
significantly simpler than the real definition of the rethrow
word).
This blog post ends kind of abruptly here. :) I know it's awful of me, but unless I learn more about Factor's continuations I can't say much more.