14.5.5 How Many Times is the Macro Expanded?

Occasionally problems result from the fact that a macro call is expanded each time it is evaluated in an interpreted function, but is expanded only once (during compilation) for a compiled function. If the macro definition has side effects, they will work differently depending on how many times the macro is expanded.

Therefore, you should avoid side effects in computation of the macro expansion, unless you really know what you are doing.

One special kind of side effect can’t be avoided: constructing Lisp objects. Almost all macro expansions include constructed lists; that is the whole point of most macros. This is usually safe; there is just one case where you must be careful: when the object you construct is part of a quoted constant in the macro expansion.

If the macro is expanded just once, in compilation, then the object is constructed just once, during compilation. But in interpreted execution, the macro is expanded each time the macro call runs, and this means a new object is constructed each time.

In most clean Lisp code, this difference won’t matter. It can matter only if you perform side-effects on the objects constructed by the macro definition. Thus, to avoid trouble, avoid side effects on objects constructed by macro definitions. Here is an example of how such side effects can get you into trouble:

(defmacro empty-object ()
  (list 'quote (cons nil nil)))

(defun initialize (condition)
  (let ((object (empty-object)))
    (if condition
        (setcar object condition))

If initialize is interpreted, a new list (nil) is constructed each time initialize is called. Thus, no side effect survives between calls. If initialize is compiled, then the macro empty-object is expanded during compilation, producing a single constant (nil) that is reused and altered each time initialize is called.

One way to avoid pathological cases like this is to think of empty-object as a funny kind of constant, not as a memory allocation construct. You wouldn’t use setcar on a constant such as '(nil), so naturally you won’t use it on (empty-object) either.