9.4.2 The Scheme Compiler

The job of the Scheme compiler is to expand all macros and all of Scheme to its most primitive expressions. The definition of “primitive expression” is given by the inventory of constructs provided by Tree-IL, the target language of the Scheme compiler: procedure calls, conditionals, lexical references, and so on. This is described more fully in the next section.

The tricky and amusing thing about the Scheme-to-Tree-IL compiler is that it is completely implemented by the macro expander. Since the macro expander has to run over all of the source code already in order to expand macros, it might as well do the analysis at the same time, producing Tree-IL expressions directly.

Because this compiler is actually the macro expander, it is extensible. Any macro which the user writes becomes part of the compiler.

The Scheme-to-Tree-IL expander may be invoked using the generic compile procedure:

(compile '(+ 1 2) #:from 'scheme #:to 'tree-il)
#<tree-il (call (toplevel +) (const 1) (const 2))>

(compile foo #:from 'scheme #:to 'tree-il) is entirely equivalent to calling the macro expander as (macroexpand foo 'c '(compile load eval)). See Macro Expansion. compile-tree-il, the procedure dispatched by compile to 'tree-il, is a small wrapper around macroexpand, to make it conform to the general form of compiler procedures in Guile’s language tower.

Compiler procedures take three arguments: an expression, an environment, and a keyword list of options. They return three values: the compiled expression, the corresponding environment for the target language, and a “continuation environment”. The compiled expression and environment will serve as input to the next language’s compiler. The “continuation environment” can be used to compile another expression from the same source language within the same module.

For example, you might compile the expression, (define-module (foo)). This will result in a Tree-IL expression and environment. But if you compiled a second expression, you would want to take into account the compile-time effect of compiling the previous expression, which puts the user in the (foo) module. That is the purpose of the “continuation environment”; you would pass it as the environment when compiling the subsequent expression.

For Scheme, an environment is a module. By default, the compile and compile-file procedures compile in a fresh module, such that bindings and macros introduced by the expression being compiled are isolated:

(eq? (current-module) (compile '(current-module)))
⇒ #f

(compile '(define hello 'world))
(defined? 'hello)
⇒ #f

(define / *)
(eq? (compile '/) /)
⇒ #f

Similarly, changes to the current-reader fluid (see current-reader) are isolated:

(compile '(fluid-set! current-reader (lambda args 'fail)))
(fluid-ref current-reader)
⇒ #f

Nevertheless, having the compiler and compilee share the same name space can be achieved by explicitly passing (current-module) as the compilation environment:

(define hello 'world)
(compile 'hello #:env (current-module))
⇒ world