Previous: Type annotations, Up: System features


4.1.12 Explicit renaming macros

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.

— syntax: define-syntax name transformer [aux-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))

Footnotes

[1] For the sake of avoiding any potential copyright issues, the paper is not duplicated here, and instead the author of this manual has written the entirety of this section.

[2] However, the current compiler in Scheme48 does not require this, though the static linker does.