Background Rambling
What you have there is very late binding macros. This is a workable approach, but it is inefficient, because repeated executions of the same code will repeatedly expand the macros.
On the positive side, this is friendly for interactive development. If the programmer changes a macro, and then re-invokes some code which uses it, such as a previously defined function, the new macro instantly takes effect. This is an intuitive "do what I mean" behavior.
Under a macro system which expands macros earlier, the programmer has to redefine all of the functions that depend on a macro when that macro changes, otherwise the existing definitions continue to be based on the old macro expansions, oblivious to the new version of the macro.
A reasonable approach is to have this late binding macro system for interpreted code, but a "regular" (for lack of a better word) macro system for compiled code.
Expanding macros does not require a separate environment. It should not, because local macros should be in the same namespace as variables. For instance in Common Lisp if we do this (let (x) (symbol-macrolet ((x 'foo)) ...))
, the inner symbol macro shadows the outer lexical variable. The macro expander has to be aware of the variable binding forms. And vice versa! If there is an inner let
for the variable x
, it shadows an outer symbol-macrolet
. The macro expander cannot just blindly substitute all occurrences of x
that occur in the body. So in other words, Lisp macro expansion has to be aware of the full lexical environment in which macros and other kinds of bindings coexist. Of course, during macro expansion, you don't instantiate the environment in the same way. Of course if there is a (let ((x (function)) ..)
, (function)
is not called and x
is not given a value. But the macro expander is aware that there is an x
in this environment and so occurrences of x
are not macros.
So when we say one environment, what we really mean is that there are two different manifestations or instantiations of a unified environment: the expansion-time manifestation and then the evaluation-time manifestation. Late-binding macros simplify the implementation by merging these two times into one, as you have done, but it does not have to be that way.
Also note that Lisp macros can accept an &environment
parameter. This is needed if the macros need to call macroexpand
on some piece of code supplied by the user. Such a recursion back into the macro expander through a macro has to pass the proper environment so the user's code has access to its lexically surrounding macros and gets expanded properly.
Concrete Example
Suppose we have this code:
(symbol-macrolet ((x (+ 2 2)))
(print x)
(let ((x 42)
(y 19))
(print x)
(symbol-macrolet ((y (+ 3 3)))
(print y))))
The effect of this to prints 4
, 42
and 6
. Let's use the CLISP implementation of Common Lisp, and expand this using CLISP's implementation-specific function called system::expand-form
. We cannot use regular, standard macroexpand
because that will not recurse into the local macros:
(system::expand-form
'(symbol-macrolet ((x (+ 2 2)))
(print x)
(let ((x 42)
(y 19))
(print x)
(symbol-macrolet ((y (+ 3 3)))
(print y)))))
-->
(LOCALLY ;; this code was reformatted by hand to fit your screen
(PRINT (+ 2 2))
(LET ((X 42) (Y 19))
(PRINT X)
(LOCALLY (PRINT (+ 3 3))))) ;
(Now firstly, about these locally
forms. Why are they there? Note that they correspond to places where we had a symbol-macrolet
. This is probably for the sake of declarations. If the body of a symbol-macrolet
form has declarations, they have to be scoped to that body, and locally
will do that. If the expansion of symbol-macrolet
does not leave behind this locally
wrapping, then declarations will have the wrong scope.)
From this macro expansion you can see what the task is. The macro expander has to walk the code and recognize all binding constructs (all special forms, really), not only binding constructs having to do with the macro system.
Notice how one of the instances of (print x)
is left alone: the one which is in the scope of the (let ((x ..)) ...)
. The other became (print (+ 2 2))
, in accordance with the symbol macro for x
.
Another thing we can learn from this is that macro expansion just substitutes the expansion and removes the symbol-macrolet
forms. So the environment that remains is the original one, minus all of the macro material which is scrubbed away in the expansion process. The macro expansion honors all of the lexical bindings, in one big "Grand Unified" environment, but then graciously vaporizes, leaving behind just the code like (print (+ 2 2))
and other vestiges like the (locally ...)
, with just the non-macro binding constructs resulting in a reduced version of the original environment.
Thus now when the expanded code is evaluated, just the reduced environment's run-time personality comes into play. The let
bindings are instantiated and stuffed with initial values, etc. During expansion, none of that was happening; the non-macro bindings just lie there asserting their scope, and hinting at a future existence in the run time.