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 (REFFIXME 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 REFFIXME.

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 below REFFIXME.

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 + 5
        +------------------+    = SCM_FRAME_UPPER_ADDRESS (fp)
        | Return address   |
        | MV return address|
        | Dynamic link     |
        | Heap 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''.

Heap link
     This field is unused and needs to be removed ASAP.

External link
     This field is a reference to the list of heap-allocated variables associated with this frame. A discussion of heap versus stack allocation can be found in REFFIXME.

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. Programs are discussed in REFFIXME!

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
-------------------

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). REFFIXME, for a full API reference.

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. REFFIXME xref toplevel-ref, 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
---------------

CISC, etc. Link to when to add instructions.

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

 -- Instruction: link-now
     Pop a symbol from the stack, and look it and push the corresponding variable object onto the stack. If the symbol is not bound yet, an error will be signalled.

 -- 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
     Set the value of the variable on top of the stack (at sp[0]) to the object located immediately before (at sp[-1]).

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

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

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

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

 -- Instruction: externals
     something here...

 -- Instruction: toplevel-ref offset
     Foo...

 -- Instruction: toplevel-ref offset
     Bar...

 -- Instruction: make-closure
     Pop the program object from the stack and assign it the current closure variable list as its closure.  Push the result program object.

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

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

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

  * If the condition is false, then program execution continues with the next instruction;

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

  * Finally, when the instruction finished, the condition object is removed from the stack.

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 '().

Subprogram Control Instructions
...............................

Programs (read: ``compiled procedure'') may refer to external bindings, like variables or functions defined outside the program itself, in the environment in which it will evaluate at run-time.  In a sense, a program's environment and its bindings are an implicit parameter of every program.

In order to handle such bindings, each program has an object table associated to it.  This table (actually a Scheme vector) contains all constant objects referenced by the program.  The object table of a program is initialized right before a program is loaded with load-program.

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. ThereafterReferences to the corresponding toplevel variables from within the program are then performed via the object-ref instruction and are almost as fast as local variable references.

Let us consider the following program (procedure) which references external bindings frob and %magic:

     (lambda (x)
       (frob x %magic))

This yields the following assembly code:

     (make-int8 64)   ;; number of args, vars, etc. (see below)
     (link "frob")
     (link "%magic")
     (vector 2)       ;; object table (external bindings)
     ...
     (load-program #u8(20 0 23 21 0 20 1 23 36 2))
     (return)

All the instructions occurring before LOAD-PROGRAM (some were omitted for simplicity) form a prologue which, among other things, pushed an object table (a vector) that contains the variable objects for the variables bound to FROB and %MAGIC.  This vector and other data pushed onto the stack are then popped by the LOAD-PROGRAM instruction.

Besides, the LOAD-PROGRAM instruction takes one explicit argument which is the bytecode of the program itself.  Disassembled, this bytecode looks like:

     (object-ref 0)  ;; push the variable object of `frob'
     (variable-ref)  ;; dereference it
     (local-ref 0)   ;; push the value of `x'
     (object-ref 1)  ;; push the variable object of `%magic'
     (variable-ref)  ;; dereference it
     (tail-call 2)   ;; call `frob' with two parameters

This clearly shows that there is little difference between references to local variables and references to externally bound variables since lookup of externally bound variables if performed only once before the program is run.

 -- Instruction: load-integer length
     embeds 32-bit int in instruction stream

 -- Instruction: load-number length
     embeds arbitrary number in instruction stream (as string)

 -- Instruction: load-string length
     embeds string in instruction stream

 -- Instruction: load-symbol length
     embeds symbol in instruction stream

 -- Instruction: load-keyword length
     embeds keyword in instruction stream

 -- Instruction: link-now
     FIXME: should not be in the loaders Pops a symbol, pushes a variable.

 -- Instruction: define
     Pulls a symbol from the instruction stream, pushes the variable.

FIXME: remove late-bind instruction

 -- Instruction: load-program bytecode
     Load the program whose bytecode is BYTECODE (a u8vector), pop its meta-information from the stack, and push a corresponding program object onto the stack.  The program's meta-information may consist of (in the order in which it should be pushed onto the stack):

       * optionally, a pair representing meta-data (see the PROGRAM-META procedure); [FIXME: explain their meaning]

       * optionally, a vector which is the program's object table (a program that does not reference external bindings does not need an object table);

       * either one immediate integer or four immediate integers representing 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).

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

 -- Instruction: return
     Free the program's frame.

 -- Instruction: call nargs
     Call the procedure, continuation or program located at sp[-nargs] with the NARGS arguments located from sp[0] to sp[-nargs + 1].  The procedure/continuation/program and its arguments are dropped from the stack and the result is pushed.  When calling a program, the call instruction reserves room for its local variables on the stack, and initializes its list of closure variables and its vector of externally bound variables.

 -- Instruction: goto/args nargs
     Same as call except that, for tail-recursive calls to a program, the current stack frame is re-used, as required by RnRS. This instruction is otherwise similar to call.

 -- Instruction: call/nargs

 -- Instruction: goto/nargs

 -- Instruction: apply

 -- Instruction: goto/apply

 -- Instruction: call/cc

 -- Instruction: goto/cc

 -- Instruction: mv-call

 -- Instruction: return/values

 -- Instruction: return/values*

 -- Instruction: return/values*

 -- Instruction: truncate-values

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

 -- 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 cons* is to list, so cons-mark is to 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. Say more about this.

 -- 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
...........................

 -- 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

 -- Instruction: car

 -- Instruction: cdr

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

 -- Instruction: add

 -- Instruction: sub

 -- Instruction: mul

 -- Instruction: div

 -- Instruction: quo

 -- Instruction: rem

 -- Instruction: mod

 -- Instruction: ee?

 -- Instruction: lt?

 -- Instruction: gt?

 -- Instruction: le?

 -- Instruction: ge?