For a long time, Guile only had an interpreter. Guile’s interpreter operated directly on the S-expression representation of Scheme source code.
But while the interpreter was highly optimized and hand-tuned, it still performed many needless computations during the course of evaluating an expression. For example, application of a function to arguments needlessly consed up the arguments in a list. Evaluation of an expression always had 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 was 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. Guile contains a
bytecode interpreter (written in C) and a Scheme to bytecode compiler
(written in Scheme). This way the virtual machine provides what Scheme
needs (tail calls, multiple values,
call/cc) and can provide
optimized inline instructions for Guile (
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.
Before moving on, though, we should note that though we spoke of the interpreter in the past tense, Guile still has an interpreter. The difference is that before, it was Guile’s main evaluator, and so was implemented in highly optimized C; now, it is actually implemented in Scheme, and compiled down to VM bytecode, just like any other program. (There is still a C interpreter around, used to bootstrap the compiler, but it is not normally used at runtime.)
The upside of implementing the interpreter in Scheme is that we preserve tail calls and multiple-value handling between interpreted and compiled code. The downside is that the interpreter in Guile 2.2 is still about twice as slow as the interpreter in 1.8. Since Scheme users are mostly running compiled code, the compiler’s speed more than makes up for the loss. In any case, once we have native compilation for Scheme code, we expect the self-hosted interpreter to handily beat the old hand-tuned C implementation.
Also 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.