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