Previous: Type annotations, Up: System features
Scheme48 supports a simple low-level macro system based on explicitly renaming identifiers to preserve hygiene. The macro system is well-integrated with the module system; see Macros in concert with modules.
Explicit renaming macro transformers operate on simple S-expressions extended with identifiers, which are like symbols but contain more information about lexical context. In order to preserve that lexical context, transformers must explicitly call a renamer procedure to produce an identifier with the proper scope. To test whether identifiers have the same denotation, transformers are also given an identifier comparator.
The facility provided by Scheme48 is almost identical to the explicit
renaming macro facility described in [Clinger 91].1 It differs only by the transformer
keyword, which is described in the paper but not used by Scheme48, and
in the annotation of auxiliary names.
Introduces a derived syntax name with the given transformer, which may be an explicit renaming transformer procedure, a pair whose car is such a procedure and whose cdr is a list of auxiliary identifiers, or the value of a
syntax-rules
expression. In the first case, the added operand aux-names may, and usually should except in the case of local (non-exported) syntactic bindings, be a list of all of the auxiliary top-level identifiers used by the macro.
Explicit renaming transformer procedures are procedures of three
arguments: an input form, an identifier renamer procedure, and an
identifier comparator procedure. The input form is the whole form of
the macro's invocation (including, at the car, the identifier whose
denotation was the syntactic binding). The identifier renamer accepts
an identifier as an argument and returns an identifier that is
hygienically renamed to refer absolutely to the identifier's denotation
in the environment of the macro's definition, not in the environment of
the macro's usage. In order to preserve hygiene of syntactic
transformations, macro transformers must call this renamer procedure
for any literal identifiers in the output. The renamer procedure is
referentially transparent; that is, two invocations of it with the same
arguments in terms of eq?
will produce the same results in the
sense of eq?
.
For example, this simple transformer for a swap!
macro is
incorrect:
(define-syntax swap! (lambda (form rename compare) (let ((a (cadr form)) (b (caddr form))) `(LET ((TEMP ,a)) (SET! ,a ,b) (SET! ,b TEMP)))))
The introduction of the literal identifier temp
into the output
may conflict with one of the input variables if it were to also be
named temp
: (swap! temp foo)
or (swap! bar temp)
would produce the wrong result. Also, the macro would fail in another
very strange way if the user were to have a local variable named
let
or set!
, or it would simply produce invalid output if
there were no binding of let
or set!
in the environment
in which the macro was used. These are basic problems of abstraction:
the user of the macro should not need to know how the macro is
internally implemented, notably with a temp
variable and using
the let
and set!
special forms.
Instead, the macro must hygienically rename these identifiers using the renamer procedure it is given, and it should list the top-level identifiers it renames (which cannot otherwise be extracted automatically from the macro's definition):
(define-syntax swap! (lambda (form rename compare) (let ((a (cadr form)) (b (caddr form))) `(,(rename 'LET) ((,(rename 'TEMP) ,a)) (,(rename 'SET!) ,a ,b) (,(rename 'SET!) ,b ,(rename 'TEMP))))) (LET SET!))
However, some macros are unhygienic by design, i.e. they insert
identifiers into the output intended to be used in the environment of
the macro's usage. For example, consider a loop
macro that
loops endlessly, but binds a variable named exit
to an escape
procedure to the continuation of the loop
expression, with
which the user of the macro can escape the loop:
(define-syntax loop (lambda (form rename compare) (let ((body (cdr form))) `(,(rename 'CALL-WITH-CURRENT-CONTINUATION) (,(rename 'LAMBDA) (EXIT) ; Literal, unrenamed EXIT. (,(rename 'LET) ,(rename 'LOOP) () ,@body (,(rename 'LOOP))))))) (CALL-WITH-CURRENT-CONTINUATION LAMBDA LET))
Note that macros that expand to loop
must also be unhygienic;
for instance, this naïve definition of a
loop-while
macro is incorrect, because it hygienically renames
exit
automatically by of the definition of syntax-rules
,
so the identifier it refers to is not the one introduced
unhygienically by loop
:
(define-syntax loop-while (syntax-rules () ((LOOP-WHILE test body ...) (LOOP (IF (NOT test) (EXIT)) ; Hygienically renamed. body ...))))
Instead, a transformer must be written to not hygienically rename
exit
in the output:
(define-syntax loop-while (lambda (form rename compare) (let ((test (cadr form)) (body (cddr form))) `(,(rename 'LOOP) (,(rename 'IF) (,(rename 'NOT) ,test) (EXIT)) ; Not hygienically renamed. ,@body))) (LOOP IF NOT))
To understand the necessity of annotating macros with the list of
auxiliary names they use, consider the following definition of the
delay
form, which transforms (delay
exp)
into
(make-promise (lambda ()
exp))
, where make-promise
is some non-exported procedure defined in the same module as the
delay
macro:
(define-syntax delay (lambda (form rename compare) (let ((exp (cadr form))) `(,(rename 'MAKE-PROMISE) (,(rename 'LAMBDA) () ,exp)))))
This preserves hygiene as necessary, but, while the compiler can know
whether make-promise
is exported or not, it cannot in
general determine whether make-promise
is local, i.e.
not accessible in any way whatsoever, even in macro output, from any
other modules. In this case, make-promise
is not local,
but the compiler cannot in general know this, and it would be an
unnecessarily heavy burden on the compiler, the linker, and related
code-processing systems to assume that all bindings are not local. It
is therefore better2 to annotate such
definitions with the list of auxiliary names used by the transformer:
(define-syntax delay (lambda (form rename compare) (let ((exp (cadr form))) `(,(rename 'MAKE-PROMISE) (,(rename 'LAMBDA) () ,exp)))) (MAKE-PROMISE LAMBDA))