A Virtual Machine for Guile
===========================

Guile has both an interpreter and a compiler. To a user, the difference is largely transparent---interpreted and compiled procedures can call each other as they please.

The difference is that the compiler creates and interprets bytecode for a custom virtual machine, instead of interpreting the S-expressions directly. Running compiled code is faster than running interpreted code.

The virtual machine that does the bytecode interpretation is a part of Guile itself. This section describes the nature of Guile's virtual machine.

Why a VM?
---------

For a long time, Guile only had an interpreter, called the evaluator. Guile's evaluator operates directly on the S-expression representation of Scheme source code.

But while the evaluator is highly optimized and hand-tuned, and contains some extensive speed trickery (see Memoization), it still performs many needless computations during the course of evaluating an expression. For example, application of a function to arguments needlessly conses up the arguments in a list. Evaluation of an expression always has to figure out what the car of the expression is -- a procedure, a memoized form, or something else. All values have to be allocated on the heap. Et cetera.

The solution to this problem is to compile the higher-level language, Scheme, into a lower-level language for which all of the checks and dispatching have already been done---the code is instead stripped to the bare minimum needed to ``do the job''.

The question becomes then, what low-level language to choose? There are many options. We could compile to native code directly, but that poses portability problems for Guile, as it is a highly cross-platform project.

So we want the performance gains that compilation provides, but we also want to maintain the portability benefits of a single code path. The obvious solution is to compile to a virtual machine that is present on all Guile installations.

The easiest (and most fun) way to depend on a virtual machine is to implement the virtual machine within Guile itself. This way the virtual machine provides what Scheme needs (tail calls, multiple values, call/cc) and can provide optimized inline instructions for Guile (cons, struct-ref, etc.).

So this is what Guile does. The rest of this section describes that VM that Guile implements, and the compiled procedures that run on it.

Note that this decision to implement a bytecode compiler does not preclude native compilation. We can compile from bytecode to native code at runtime, or even do ahead of time compilation. More possibilities are discussed in Extending the Compiler.

VM Concepts
-----------

A virtual machine (VM) is a Scheme object. Users may create virtual machines using the standard procedures described later in this manual, but that is usually unnecessary, as Guile ensures that there is one virtual machine per thread. When a VM-compiled procedure is run, Guile looks up the virtual machine for the current thread and executes the procedure using that VM.

Guile's virtual machine is a stack machine---that is, it has few registers, and the instructions defined in the VM operate by pushing and popping values from a stack.

Stack memory is exclusive to the virtual machine that owns it. In addition to their stacks, virtual machines also have access to the global memory (modules, global bindings, etc) that is shared among other parts of Guile, including other VMs.

A VM has generic instructions, such as those to reference local variables, and instructions designed to support Guile's languages -- mathematical instructions that support the entire numerical tower, an inlined implementation of cons, etc.

The registers that a VM has are as follows:

  * ip - Instruction pointer

  * sp - Stack pointer

  * fp - Frame pointer

In other architectures, the instruction pointer is sometimes called the ``program counter'' (pc). This set of registers is pretty typical for stack machines; their exact meanings in the context of Guile's VM is described in the next section.

A virtual machine executes by loading a compiled procedure, and executing the object code associated with that procedure. Of course, that procedure may call other procedures, tail-call others, ad infinitum---indeed, within a guile whose modules have all been compiled to object code, one might never leave the virtual machine.

Stack Layout
------------

While not strictly necessary to understand how to work with the VM, it is instructive and sometimes entertaining to consider the struture of the VM stack.

Logically speaking, a VM stack is composed of ``frames''. Each frame corresponds to the application of one compiled procedure, and contains storage space for arguments, local variables, intermediate values, and some bookkeeping information (such as what to do after the frame computes its value).

While the compiler is free to do whatever it wants to, as long as the semantics of a computation are preserved, in practice every time you call a function, a new frame is created. (The notable exception of course is the tail call case, see Tail Calls.)

Within a frame, you have the data associated with the function application itself, which is of a fixed size, and the stack space for intermediate values. Sometimes only the former is referred to as the ``frame'', and the latter is the ``stack'', although all pending application frames can have some intermediate computations interleaved on the stack.

The structure of the fixed part of an application frame is as follows:

                  Stack
        |                  | <- fp + bp->nargs + bp->nlocs + 4
        +------------------+    = SCM_FRAME_UPPER_ADDRESS (fp)
        | Return address   |
        | MV return address|
        | Dynamic link     |
        | External link    | <- fp + bp->nargs + bp->nlocs
        | Local variable 1 |    = SCM_FRAME_DATA_ADDRESS (fp)
        | Local variable 0 | <- fp + bp->nargs
        | Argument 1       |
        | Argument 0       | <- fp
        | Program          | <- fp - 1
        +------------------+    = SCM_FRAME_LOWER_ADDRESS (fp)
        |                  |

In the above drawing, the stack grows upward. The intermediate values stored in the application of this frame are stored above SCM_FRAME_UPPER_ADDRESS (fp). bp refers to the struct scm_program* data associated with the program at fp - 1. nargs and nlocs are properties of the compiled procedure, which will be discussed later.

The individual fields of the frame are as follows:

Return address
     The ip that was in effect before this program was applied. When we return from this activation frame, we will jump back to this ip.

MV return address
     The ip to return to if this application returns multiple values. For continuations that only accept one value, this value will be NULL; for others, it will be an ip that points to a multiple-value return address in the calling code. That code will expect the top value on the stack to be an integer---the number of values being returned---and that below that integer there are the values being returned.

Dynamic link
     This is the fp in effect before this program was applied. In effect, this and the return address are the registers that are always ``saved''.

External link
     This field is a reference to the list of heap-allocated variables associated with this frame. For a discussion of heap versus stack allocation, see Variables and the VM.

Local variable N
     Lambda-local variables that are allocated on the stack are all allocated as part of the frame. This makes access to non-captured, non-mutated variables very cheap.

Argument N
     The calling convention of the VM requires arguments of a function application to be pushed on the stack, and here they are. Normally references to arguments dispatch to these locations on the stack. However if an argument has to be stored on the heap, it will be copied from its initial value here onto a location in the heap, and thereafter only referenced on the heap.

Program
     This is the program being applied. For more information on how programs are implemented, see VM Programs.

Variables and the VM
--------------------

Let's think about the following Scheme code as an example:

       (define (foo a)
         (lambda (b) (list foo a b)))

Within the lambda expression, "foo" is a top-level variable, "a" is a lexically captured variable, and "b" is a local variable.

That is to say: b may safely be allocated on the stack, as there is no enclosed procedure that references it, nor is it ever mutated.

a, on the other hand, is referenced by an enclosed procedure, that of the lambda. Thus it must be allocated on the heap, as it may (and will) outlive the dynamic extent of the invocation of foo.

foo is a toplevel variable, as mandated by Scheme's semantics:

       (define proc (foo 'bar)) ; assuming prev. definition of foo
       (define foo 42)          ; redefinition
       (proc 'baz)
       => (42 bar baz)

Note that variables that are mutated (via set!) must be allocated on the heap, even if they are local variables. This is because any called subprocedure might capture the continuation, which would need to capture locations instead of values. Thus perhaps counterintuitively, what would seem ``closer to the metal'', viz set!, actually forces heap allocation instead of stack allocation.

Compiled Procedures are VM Programs
-----------------------------------

By default, when you enter in expressions at Guile's REPL, they are first compiled to VM object code, then that VM object code is executed to produce a value. If the expression evaluates to a procedure, the result of this process is a compiled procedure.

A compiled procedure is a compound object, consisting of its bytecode, a reference to any captured lexical variables, an object array, and some metadata such as the procedure's arity, name, and documentation. You can pick apart these pieces with the accessors in (system vm program). See Compiled Procedures, for a full API reference.

The object array of a compiled procedure, also known as the object table, holds all Scheme objects whose values are known not to change across invocations of the procedure: constant strings, symbols, etc. The object table of a program is initialized right before a program is loaded with load-program. See Loading Instructions, for more information.

Variable objects are one such type of constant object: when a global binding is defined, a variable object is associated to it and that object will remain constant over time, even if the value bound to it changes. Therefore, toplevel bindings only need to be looked up once. Thereafter, references to the corresponding toplevel variables from within the program are then performed via the toplevel-ref instruction, which uses the object vector, and are almost as fast as local variable references.

We can see how these concepts tie together by disassembling the foo function to see what is going on:

     scheme@(guile-user)> (define (foo a) (lambda (b) (list foo a b)))
     scheme@(guile-user)> ,x foo
     Disassembly of #<program foo (a)>:

     Bytecode:

        0    (local-ref 0)                   ;; `a' (arg)
        2    (external-set 0)                ;; `a' (arg)
        4    (object-ref 0)                  ;; #<program #(0 28 #f) (b)>
        6    (make-closure)                                        at (unknown file):0:16
        7    (return)

     ----------------------------------------
     Disassembly of #<program #(0 28 #f) (b)>:

     Bytecode:

        0    (toplevel-ref 0)                ;; `list'
        2    (toplevel-ref 1)                ;; `foo'
        4    (external-ref 0)                ;; (closure variable)
        6    (local-ref 0)                   ;; `b' (arg)
        8    (goto/args 3)                                         at (unknown file):0:28

At ip 0 and 2, we do the copy from argument to heap for a. Ip 4 loads up the compiled lambda, and then at ip 6 we make a closure---binding code (from the compiled lambda) with data (the heap-allocated variables). Finally we return the closure.

The second stanza disassembles the compiled lambda. Toplevel variables are resolved relative to the module that was current when the procedure was created. This lookup occurs lazily, at the first time the variable is actually referenced, and the location of the lookup is cached so that future references are very cheap. See Environment Control Instructions, for more details.

Then we see a reference to an external variable, corresponding to a. The disassembler doesn't have enough information to give a name to that variable, so it just marks it as being a ``closure variable''. Finally we see the reference to b, then a tail call (goto/args) with three arguments.

Instruction Set
---------------

There are about 100 instructions in Guile's virtual machine. These instructions represent atomic units of a program's execution. Ideally, they perform one task without conditional branches, then dispatch to the next instruction in the stream.

Instructions themselves are one byte long. Some instructions take parameters, which follow the instruction byte in the instruction stream.

Sometimes the compiler can figure out that it is compiling a special case that can be run more efficiently. So, for example, while Guile offers a generic test-and-branch instruction, it also offers specific instructions for special cases, so that the following cases all have their own test-and-branch instructions:

     (if pred then else)
     (if (not pred) then else)
     (if (null? l) then else)
     (if (not (null? l)) then else)

In addition, some Scheme primitives have their own inline implementations, e.g. cons.

So Guile's instruction set is a complete instruction set, in that it provides the instructions that are suited to the problem, and is not concerned with making a minimal, orthogonal set of instructions. More instructions may be added over time.

Environment Control Instructions
................................

These instructions access and mutate the environment of a compiled procedure---the local bindings, the ``external'' bindings, and the toplevel bindings.

 -- Instruction: local-ref index
     Push onto the stack the value of the local variable located at INDEX within the current stack frame.

     Note that arguments and local variables are all in one block. Thus the first argument, if any, is at index 0, and local bindings follow the arguments.

 -- Instruction: local-set index
     Pop the Scheme object located on top of the stack and make it the new value of the local variable located at INDEX within the current stack frame.

 -- Instruction: external-ref index
     Push the value of the closure variable located at position INDEX within the program's list of external variables.

 -- Instruction: external-set index
     Pop the Scheme object located on top of the stack and make it the new value of the closure variable located at INDEX within the program's list of external variables.

The external variable lookup algorithm should probably be made more efficient in the future via addressing by frame and index. Currently, external variables are all consed onto a list, which results in O(N) lookup time.

 -- Instruction: externals
     Pushes the current list of external variables onto the stack. This instruction is used in the implementation of compile-time-environment. See The Scheme Compiler.

 -- Instruction: toplevel-ref index
     Push the value of the toplevel binding whose location is stored in at position INDEX in the object table.

     Initially, a cell in the object table that is used by toplevel-ref is initialized to one of two forms. The normal case is that the cell holds a symbol, whose binding will be looked up relative to the module that was current when the current program was created.

     Alternately, the lookup may be performed relative to a particular module, determined at compile-time (e.g. via @ or @@). In that case, the cell in the object table holds a list: (MODNAME SYM INTERFACE?). The symbol SYM will be looked up in the module named MODNAME (a list of symbols). The lookup will be performed against the module's public interface, unless INTERFACE? is #f, which it is for example when compiling @@.

     In any case, if the symbol is unbound, an error is signalled. Otherwise the initial form is replaced with the looked-up variable, an in-place mutation of the object table. This mechanism provides for lazy variable resolution, and an important cached fast-path once the variable has been successfully resolved.

     This instruction pushes the value of the variable onto the stack.

 -- Instruction: toplevel-ref index
     Pop a value off the stack, and set it as the value of the toplevel variable stored at INDEX in the object table. If the variable has not yet been looked up, we do the lookup as in toplevel-ref.

 -- Instruction: link-now
     Pop a value, X, from the stack. Look up the binding for X, according to the rules for toplevel-ref, and push that variable on the stack. If the lookup fails, an error will be signalled.

     This instruction is mostly used when loading programs, because it can do toplevel variable lookups without an object vector.

 -- Instruction: variable-ref
     Dereference the variable object which is on top of the stack and replace it by the value of the variable it represents.

 -- Instruction: variable-set
     Pop off two objects from the stack, a variable and a value, and set the variable to the value.

 -- Instruction: object-ref n
     Push Nth value from the current program's object vector.

Branch Instructions
...................

All the conditional branch instructions described below work in the same way:

  * They pop off the Scheme object located on the stack and use it as the branch condition;

  * If the condition is true, then the instruction pointer is increased by the offset passed as an argument to the branch instruction;

  * Program execution proceeds with the next instruction (that is, the one to which the instruction pointer points).

Note that the offset passed to the instruction is encoded on two 8-bit integers which are then combined by the VM as one 16-bit integer.

 -- Instruction: br offset
     Jump to OFFSET.

 -- Instruction: br-if offset
     Jump to OFFSET if the condition on the stack is not false.

 -- Instruction: br-if-not offset
     Jump to OFFSET if the condition on the stack is false.

 -- Instruction: br-if-eq offset
     Jump to OFFSET if the two objects located on the stack are equal in the sense of EQ?.  Note that, for this instruction, the stack pointer is decremented by two Scheme objects instead of only one.

 -- Instruction: br-if-not-eq offset
     Same as BR-IF-EQ for non-eq? objects.

 -- Instruction: br-if-null offset
     Jump to OFFSET if the object on the stack is '().

 -- Instruction: br-if-not-null offset
     Jump to OFFSET if the object on the stack is not '().

Loading Instructions
....................

In addition to VM instructions, an instruction stream may contain variable-length data embedded within it. This data is always preceded by special loading instructions, which interpret the data and advance the instruction pointer to the next VM instruction.

All of these loading instructions have a length parameter, indicating the size of the embedded data, in bytes. The length itself may be encoded in 1, 2, or 4 bytes.

 -- Instruction: load-integer length
 -- Instruction: load-unsigned-integer length
     Load a 32-bit integer (respectively unsigned integer) from the instruction stream.

 -- Instruction: load-number length
     Load an arbitrary number from the instruction stream. The number is embedded in the stream as a string.

 -- Instruction: load-string length
     Load a string from the instruction stream.

 -- Instruction: load-symbol length
     Load a symbol from the instruction stream.

 -- Instruction: load-keyword length
     Load a keyword from the instruction stream.

 -- Instruction: define length
     Load a symbol from the instruction stream, and look up its binding in the current toplevel environment, creating the binding if necessary. Push the variable corresponding to the binding.

 -- Instruction: load-program length
     Load bytecode from the instruction stream, and push a compiled procedure. This instruction pops the following values from the stack:

       * Optionally, a thunk, which when called should return metadata associated with this program---for example its name, the names of its arguments, its documentation string, debugging information, etc.

          Normally, this thunk its itself a compiled procedure (with no metadata). Metadata is represented this way so that the initial load of a procedure is fast: the VM just mmap's the thunk and goes. The symbols and pairs associated with the metadata are only created if the user asks for them.

          For information on the format of the thunk's return value, see Compiled Procedures.

       * Optionally, the program's object table, as a vector.

          A program that does not reference toplevel bindings and does not use object-ref does not need an object table.

       * Finally, either one immediate integer or four immediate integers representing the arity of the program.

          In the four-fixnum case, the values are respectively the number of arguments taken by the function (NARGS), the number of rest arguments (NREST, 0 or 1), the number of local variables (NLOCS) and the number of external variables (NEXTS) (see Environment Control Instructions).

          The common single-fixnum case represents all of these values within a 16-bit bitmask.

     The resulting compiled procedure will not have any ``external'' variables captured, so it will be loaded only once but may be used many times to create closures.

Finally, while this instruction is not strictly a ``loading'' instruction, it's useful to wind up the load-program discussion here:

 -- Instruction: make-closure
     Pop the program object from the stack, capture the current set of ``external'' variables, and assign those external variables to a copy of the program. Push the new program object, which shares state with the original program. Also captures the current module.

Procedural Instructions
.......................

 -- Instruction: return
     Free the program's frame, returning the top value from the stack to the current continuation. (The stack should have exactly one value on it.)

     Specifically, the sp is decremented to one below the current fp, the ip is reset to the current return address, the fp is reset to the value of the current dynamic link, and then the top item on the stack (formerly the procedure being applied) is set to the returned value.

 -- Instruction: call nargs
     Call the procedure located at sp[-nargs] with the NARGS arguments located from sp[0] to sp[-nargs + 1].

     For non-compiled procedures (continuations, primitives, and interpreted procedures), call will pop the procedure and arguments off the stack, and push the result of calling scm_apply.

     For compiled procedures, this instruction sets up a new stack frame, as described in Stack Layout, and then dispatches to the first instruction in the called procedure, relying on the called procedure to return one value to the newly-created continuation.

 -- Instruction: goto/args nargs
     Like call, but reusing the current continuation. This instruction implements tail calling as required by RnRS.

     For compiled procedures, that means that goto/args reuses the current frame instead of building a new one. The goto/* instruction family is named as it is because tail calls are equivalent to goto, along with relabeled variables.

     For non-VM procedures, the result is the same, but the current VM invocation remains on the C stack. True tail calls are not currently possible between compiled and non-compiled procedures.

 -- Instruction: apply nargs
 -- Instruction: goto/apply nargs
     Like call and goto/args, except that the top item on the stack must be a list. The elements of that list are then pushed on the stack and treated as additional arguments, replacing the list itself, then the procedure is invoked as usual.

 -- Instruction: call/nargs
 -- Instruction: goto/nargs
     These are like call and goto/args, except they take the number of arguments from the stack instead of the instruction stream. These instructions are used in the implementation of multiple value returns, where the actual number of values is pushed on the stack.

 -- Instruction: call/cc
 -- Instruction: goto/cc
     Capture the current continuation, and then call (or tail-call) the procedure on the top of the stack, with the continuation as the argument.

     Both the VM continuation and the C continuation are captured.

 -- Instruction: mv-call nargs offset
     Like call, except that a multiple-value continuation is created in addition to a single-value continuation.

     The offset (a two-byte value) is an offset within the instruction stream; the multiple-value return address in the new frame (see Stack Layout) will be set to the normal return address plus this offset. Instructions at that offset will expect the top value of the stack to be the number of values, and below that values themselves, pushed separately.

 -- Instruction: return/values nvalues
     Return the top NVALUES to the current continuation.

     If the current continuation is a multiple-value continuation, return/values pushes the number of values on the stack, then returns as in return, but to the multiple-value return address.

     Otherwise if the current continuation accepts only one value, i.e. the multiple-value return address is NULL, then we assume the user only wants one value, and we give them the first one. If there are no values, an error is signaled.

 -- Instruction: return/values* nvalues
     Like a combination of apply and return/values, in which the top value on the stack is interpreted as a list of additional values. This is an optimization for the common (apply values ...) case.

 -- Instruction: truncate-values nbinds nrest
     Used in multiple-value continuations, this instruction takes the values that are on the stack (including the number-of-value marker) and truncates them for a binding construct.

     For example, a call to (receive (x y . z) (foo) ...) would, logically speaking, pop off the values returned from (foo) and push them as three values, corresponding to x, y, and z. In that case, NBINDS would be 3, and NREST would be 1 (to indicate that one of the bindings was a rest arguments).

     Signals an error if there is an insufficient number of values.

Data Control Instructions
.........................

These instructions push simple immediate values onto the stack, or manipulate lists and vectors on the stack.

 -- Instruction: make-int8 value
     Push VALUE, an 8-bit integer, onto the stack.

 -- Instruction: make-int8:0
     Push the immediate value 0 onto the stack.

 -- Instruction: make-int8:1
     Push the immediate value 1 onto the stack.

 -- Instruction: make-int16 value
     Push VALUE, a 16-bit integer, onto the stack.

 -- Instruction: make-false
     Push #f onto the stack.

 -- Instruction: make-true
     Push #t onto the stack.

 -- Instruction: make-eol
     Push '() onto the stack.

 -- Instruction: make-char8 value
     Push VALUE, an 8-bit character, onto the stack.

 -- Instruction: list n
     Pops off the top N values off of the stack, consing them up into a list, then pushes that list on the stack. What was the topmost value will be the last element in the list.

 -- Instruction: vector n
     Create and fill a vector with the top N values from the stack, popping off those values and pushing on the resulting vector.

 -- Instruction: mark
     Pushes a special value onto the stack that other stack instructions like list-mark can use.

 -- Instruction: list-mark
     Create a list from values from the stack, as in list, but instead of knowing beforehand how many there will be, keep going until we see a mark value.

 -- Instruction: cons-mark
     As the scheme procedure cons* is to the scheme procedure list, so the instruction cons-mark is to the instruction list-mark.

 -- Instruction: vector-mark
     Like list-mark, but makes a vector instead of a list.

 -- Instruction: list-break
     The opposite of list: pops a value, which should be a list, and pushes its elements on the stack.

Miscellaneous Instructions
..........................

 -- Instruction: nop
     Does nothing!

 -- Instruction: halt
     Exits the VM, returning a SCM value. Normally, this instruction is only part of the ``bootstrap program'', a program run when a virtual machine is first entered; compiled Scheme procedures will not contain this instruction.

     If multiple values have been returned, the SCM value will be a multiple-values object (see Multiple Values).

 -- Instruction: break
     Does nothing, but invokes the break hook.

 -- Instruction: drop
     Pops off the top value from the stack, throwing it away.

 -- Instruction: dup
     Re-pushes the top value onto the stack.

 -- Instruction: void
     Pushes ``the unspecified value'' onto the stack.

Inlined Scheme Instructions
...........................

The Scheme compiler can recognize the application of standard Scheme procedures, or unbound variables that look like they are bound to standard Scheme procedures. It tries to inline these small operations to avoid the overhead of creating new stack frames.

Since most of these operations are historically implemented as C primitives, not inlining them would entail constantly calling out from the VM to the interpreter, which has some costs---registers must be saved, the interpreter has to dispatch, called procedures have to do much typechecking, etc. It's much more efficient to inline these operations in the virtual machine itself.

All of these instructions pop their arguments from the stack and push their results, and take no parameters from the instruction stream. Thus, unlike in the previous sections, these instruction definitions show stack parameters instead of parameters from the instruction stream.

 -- Instruction: not x
 -- Instruction: not-not x
 -- Instruction: eq? x y
 -- Instruction: not-eq? x y
 -- Instruction: null?
 -- Instruction: not-null?
 -- Instruction: eqv? x y
 -- Instruction: equal? x y
 -- Instruction: pair? x y
 -- Instruction: list? x y
 -- Instruction: set-car! pair x
 -- Instruction: set-cdr! pair x
 -- Instruction: slot-ref struct n
 -- Instruction: slot-set struct n x
 -- Instruction: cons x
 -- Instruction: car x
 -- Instruction: cdr x
     Inlined implementations of their Scheme equivalents.

Note that caddr and friends compile to a series of car and cdr instructions.

Inlined Mathematical Instructions
.................................

Inlining mathematical operations has the obvious advantage of handling fixnums without function calls or allocations. The trick, of course, is knowing when the result of an operation will be a fixnum, and there might be a couple bugs here.

More instructions could be added here over time.

As in the previous section, the definitions below show stack parameters instead of instruction stream parameters.

 -- Instruction: add x y
 -- Instruction: sub x y
 -- Instruction: mul x y
 -- Instruction: div x y
 -- Instruction: quo x y
 -- Instruction: rem x y
 -- Instruction: mod x y
 -- Instruction: ee? x y
 -- Instruction: lt? x y
 -- Instruction: gt? x y
 -- Instruction: le? x y
 -- Instruction: ge? x y
     Inlined implementations of the corresponding mathematical operations.