gtod
gtod

Reputation: 239

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?

Upvotes: 3

Views: 86

Answers (1)

Joshua Taylor
Joshua Taylor

Reputation: 85913

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)))

Upvotes: 5

Related Questions