The job of the Scheme compiler is to expand all macros and all of Scheme to its most primitive expressions. The definition of “primitive” is given by the inventory of constructs provided by Tree-IL, the target language of the Scheme compiler: procedure applications, conditionals, lexical references, etc. 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 '(+ 1 2) #:from 'scheme #:to 'tree-il) ⇒ #<<application> src: #f proc: #<<toplevel-ref> src: #f name: +> args: (#<<const> src: #f exp: 1> #<<const> src: #f exp: 2>)>
Or, since Tree-IL is so close to Scheme, it is often useful to expand
Scheme to Tree-IL, then translate back to Scheme. For that reason the
expander provides two interfaces. The former is equivalent to calling
(macroexpand '(+ 1 2) 'c), where the
'c is for
'e (the default), the result is translated
back to Scheme:
(macroexpand '(+ 1 2)) ⇒ (+ 1 2) (macroexpand '(let ((x 10)) (* x x))) ⇒ (let ((x84 10)) (* x84 x84))
The second example shows that as part of its job, the macro expander renames lexically-bound variables. The original names are preserved when compiling to Tree-IL, but can’t be represented in Scheme: a lexical binding only has one name. It is for this reason that the native output of the expander is not Scheme. There’s too much information we would lose if we translated to Scheme directly: lexical variable names, source locations, and module hygiene.
Note however that
macroexpand does not have the same signature
compile-tree-il is a small wrapper
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,
(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 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-file procedures compile in a fresh module, such
that bindings and macros introduced by the expression being compiled
(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
the compilation environment:
(define hello 'world) (compile 'hello #:env (current-module)) ⇒ world