Search code examples
macroslispelispelisp-macro

Why does a macro inside a loop only expand once in Elisp?


Let's say I define

(defmacro macro-print (str)
  (print "hello"))

and run

(let ((i 0))
  (while (< i 3)
    (macro-print i)
    (setq i (+ 1 i))))

The output is

"hello"
nil

why is this?

I was expecting to see 3 hellos. This is because, according to the manual, the while special form first evaluates its condition, if it evaluate to non-nil, it evaluates the forms in its body. Since the condition holds for three values of i, I expect to have the (macro-print i) form evaluated 3 times. Every time it expands, it should print "hello". Yet, it seems like the interpreter replaces (macro-print i) with its expansion (since print returns the string passed to it, it would replace it with "hello") upon first evaluation. Why is this happening?

I already know that the correct way to write macro-print would be to return (list 'print ,i), but the fact I was not able to predict the actual behavior of the loop points to a misunderstanding of the lisp model I want to correct.


Solution

  • Macros are essentially functions whose domain and range is source code. In CL they are quite literally this: a macro is implemented by a function whose argument is source code (and an environment object) and which returns other source code. I think that in elisp the implementation is a little less explicit.

    As such macros get expanded (which, in CL, means that the function implementing the macro is called) at least once during evaluation of code, in order that they can return their expansion. They may be expanded exactly once (and, in terms of efficiency, this would clearly be ideal), or more than once, but they are expanded at least once. That's the only guarantee you have in general.

    For an interactive interpreter it would obviously be nice if macros get expanded every time either code referring to the macro is changed or when the macro itself is changed. That can be achieved by expanding macros as part of the interpreter, again at least once.

    What you are seeing is that the elisp interpreter expands macros exactly once in some cases.

    In the case of compiled implementations, evaluation is a two-stage process after source code is read: source code is turned into some compiled representation at compile time, and that compiled representation is executed at run time. Macro expansion will happen during the compilation, not the execution.

    CL enforces this in fact: minimal compilation is defined, in part, so that

    All macro and symbol macro calls appearing in the source code being compiled are expanded at compile time in such a way that they will not be expanded again at run time.

    If you want code that executes at run time then use either a function or, if you need a macro to extend the language, make that code be part of the expansion of the macro: part of the value it returns, not a side-effect of expanding it.