Pigeon Core - Execution Frames

An execution frame is an object that stores the current state of the virtual machine. Every time you call a function, a new execution frame is created to run it in. This holds the parameters passed to the function, the values of any local variables, a stack for temporary data, the program counter, a pointer to the previous (caller) frame, and of course a pointer to the function itself, along with any closure variables.

In Pigeon, execution frames are directly visible and can be manipulated by your code. This is deep magic: powerful, risky, and potentially extremely confusing! This stuff should not be used on a daily basis. Rather, you should use it as a building block for constructing more abstracted higher level facilities.

FRAME

The psuedo variable FRAME always refers to the currently active frame object.

If a lambda expression declares one or more labels, these become psuedo variables referring to the frame of that function. These are lexically scoped, so by labelling a lambda, nested functions gain direct access to the frame of their parent.

The psuedo messages 1, 2, 3, etc, refer to the lexically scoped parents of a frame psuedo variable. For instance:

(1: FRAME)

returns the execution frame of the function in which the currently active function was created.

The message caller returns the dynamically scoped caller of a frame. This is different from the lexical parent:

(def (a
  (lambda ((. A) func)
    (func A))))

(def (b
  (lambda ((. B) a)
    (a (lambda ((. C))
      (print (equals: FRAME C))
      (print (equals: (1: FRAME) B))
      (print (equals: (parent: FRAME) a)))))))

(b)

All three tests will print true. Note that the third test has to use a frame variable passed in as a parameter. It is not possible to test against A directly, because this is not in lexical scope at the point where the test is written.

exec, redo, return

The most basic message you can send to a frame is exec. This immediately transfers control to that frame, abandoning whatever frame is currently running. If you pass a parameter to exec, that becomes the result of whatever function had previously called out of the frame, otherwise this defaults to nil.

These two statements are identical:

(set x (exec: FRAME y))
(set x y)

The exec message can be used to implement non-local returns, coroutines, and exceptions. Be aware that if you store a frame object, then continue running that frame, and then send exec to the stored object, this continues from the current program counter, not the location from when the frame object was stored. It is normally an error to exec a frame after the function has exited, but if it finished with a tail recursive call, the same frame may have been reused by a different function, in which case exec will resume that new function instead.

The redo message is the same as exec, but also resets the program counter to the top of the function. This does not take any parameters.

(return: frame) is a shortcut for (exec: (parent: frame)).

FRAME meets loop

Control constructs can be implemented either as functions that are passed other functions, or directly using the loop special form. The former is more flexible, because you can use frame labels to break out of any level of control nesting, but the latter can be significantly more efficient. In the core language these two approaches are fundamentally distinct, but the higher level syntax uses some tricks to hide the difference.

When compiling high level loop constructs like while and for, the parser will optimise them into a loop form if the loop body lambda has no label, but leave them as true function calls if a label is present. This is because a label signals the intention to use non-local flow control, in which case separate execution frames are required.

If you use break, next, or redo outside of a loop construct (for instance within an unoptimised while or for), the parser replaces these with (break: FRAME), (next: FRAME), and (redo: FRAME) respectively.

Three transformation macros are defined:

(break: frame) -> (exec: (1: frame))
(next: frame)  -> (return: frame)
(return)       -> (return: FUNCTION)

Like the core break function (and unlike the exec message), the break macro defaults to true if no result is specified.

In addition, if a lambda expression is passed directly to a def statement, a macro automatically gives it a FUNCTION label. This makes an unqualified return statement jump right out of the enclosing function, rather than merely the innermost lambda.

In conjunction, these features make the break, next, redo, and return statements work consistently regardless of whether they are applied to an inline loop expression or to a regular function call that merely happens to implement loop behaviour via a lambda block parameter.

When used directly, break, next, and redo apply to the innermost looplike structure, while return applies to the enclosing function, but they can all be sent as messages to arbitrary labels for Perl style multilevel breakage or non-local returns.

Generator Example

Here is a complete example of using execution frames to do something complicated. Once you grok this, you're pretty much sorted.

I'm going to write this using the high level syntax rather than in core s-expression form, because it is easier to read that way. In fact I'm going to start giving all the code examples in regular form from now on. If you aren't used to s-expression syntax by now, you never will be, and my close brace key is in danger of wearing out!

// Smalltalk or Ruby style iteration function.
// This routine is in control, calling out to
// a worker function each time around the loop.

repeat :=
{
    :count
    :code

    i := 0

    while i < count {
        code i
        i++
    }
}


// Turn any iteration function into a
// generator object. Now that's magic!

make_iterator :=
{
    :function
    :args*

    def worker

    {
        .it

        if worker {
            worker.exec
        }
        else {
            function (*args) {
                worker = FRAME.caller
                it.return :1
            }

            undef
        }
    }
}


// Use a Ruby style iteration function as if it was
// a Python style iterator object. This loop is in
// control, calling the iterator each time it wants
// a new value. But the underlying iterator thinks
// it is in control, too. In fact we got us some
// coroutines going on here...

repeat_it := make_iterator repeat 23

while (n := (repeat_it)) != undef {
    print n '\n'
}