macroscommon-lispspecial-variables

Behaviour of special variables under macro expansion


FUZZ> (defvar *foo* nil)
*FOO*
FUZZ> (defmacro bar ()
        (format t "foo: ~A" *foo*)
        `(+ 1 1))
BAR
FUZZ> (defmacro bot ()
        (let ((*foo* 17))
          `(bar)))
BOT
FUZZ> (bot)
foo: NIL

My mental model (clearly wrong) of macro expansion says the following happens in order:

Run the macro expansion of bot (which binds *foo* to 17), run the macro expansion of bar, which prints the current value of *foo* (being 17), and returns the form (+ 1 1), which is not a macro, macro expansion time is now over, finally evaluate the form (+ 1 1), and returns 2.

Why am I wrong?

Is there an easy way to do what I intend?


Solution

  • When the REPL is told to evaluate (bot), it first has to perform macroexpansion. It calls the macroexpansion function bot, which means, in effect, evaluating

    (let ((*foo* 17))
      `(bar))
    

    That returns (bar) and then the binding of from let is unwound. Now we've got (bar). bar is a macro, so it's time for another round of macroexpansion, which means evaluating

    (progn 
      (format t "foo: ~a" *foo*)
      `(+ 1 1))
    

    which prints foo: NIL, and returns (+ 1 1).

    If you want the macroexpansion to be performed in the scope of some bindings, you'll need to call the macroexpansion function yourself. E.g., you can use macroexpand:

    CL-USER> (defparameter *foo* nil)
    *FOO*
    CL-USER> (defmacro bar ()
               (format t "foo: ~a" *foo*)
               `(+ 1 1))
    BAR
    CL-USER> (defmacro baz ()
               (let ((*foo* 42))
                 (macroexpand '(bar))))
    BAZ
    CL-USER> (baz)
    foo: 42
    2
    

    But, if you're going to do macroexpansion yourself, be sure to preserve environment arguments. In this case, a better definition of baz would be:

    (defmacro baz (&environment env)
      (let ((*foo* 42))
        (macroexpand '(bar) env)))