To summarize macros briefly, a macro is like a function that generates Lisp code, which is then substituted in place of the macro. The first phase of macro processing is macroexpansion, in which the macro generates an expression. The second phase is evaluation, in which the generated expression is evaluated.
arc> (mac foo () (prn "macro executed") '(prn "generated code executed")) #3(tagged mac #<procedure>) arc> (foo) macro executed generated code executed "generated code executed"The macro foo does two things: it prints "macro executed" when it is executed during macroexpansion, and it returns the list:
(prn "generated code executed")
. When that list is executed in evaluation, it will display "generated code executed". (Since prn
returns that string, the string is also displayed a second time as the result of the command.)
Howerver, the two phases of macroexpansion and evaluation can be separated in time. If we use the macro inside a function definition, things get interesting:
arc> (def bar () (prn "bar executed") (foo)) macro executed #<procedure: bar>Macroexpansion takes place above, but evaluation of the generated code does not take place; the generated code becomes part of
bar
. This is demonstrated by the prn
statements, which show that only the macro itself is executed. Next, the macro itself can be destroyed, to illustrate that it is not necessary for the evaluation phase:
arc> (= foo nil) nilFinally, evaluation can take place. If procedure
bar
is executed three times, note that the body of bar
is executed, as well as the code generated by foo
during macroexpansion:
arc> (repeat 3 (bar)) bar executed generated code executed bar executed generated code executed bar executed generated code executed nilThis illustrates the two phases: first, the macro is executed and generates code during macroexpansion. Second, the generated code is executed, potentially much later, during evaluation. Note that the macro itself does not take part in the second step. In the example above, the macro has been destroyed. A more subtle issue is if the macro definition is changed, these changes will have no effect on previous callers of the macro; this can lead to confusion, when the old version of a macro appears to be active.
Another important difference between macros and functions is the arguments to a function are evaluated before passing them to the function, while the arguments to a macro are passed unprocessed. This is vital for macros that execute their arguments multiple times (e.g. repeat
), macros that execute their arguments conditionally (e.g. and
), and macros that process their arguments (e.g. =). To see the difference, note that the function receives the result of evaluating (+ 1 1)
, while the macro receives the list (+ 1 1)
:
arc> (def f args (prn args) nil) #<procedure: f> arc> (mac m args (prn args) nil) #3(tagged mac #<procedure>) arc> (f (+ 1 1)) (2) nil arc> (m (+ 1 1)) ((+ 1 1)) nil
Like functions, macros support destructuring bind on the arguments. Despite its intimidating name, destructuring bind is simply means that the list of parameters can be a complex nested list. The arguments passed in will be "destructured" and mapped onto the parameter list. For example:
arc> (mac db (a (b c d) e) (prs a b c d e) nil) arc> (db 1 (2 3 4) 5) 1 2 3 4 5Note that the list
(2 3 4)
has been destructured and mapped onto individual arguments. In a more complex example, note that the expressions are unevaluated, expressions can be destructured, and any extra arguments are discarded.
arc> (db (+ m n o) (+ p q r) (+ s t u)) (+ m n o) + p q (+ s t u)
Now that the phases of macro processing are clear, these phases can be illustrated in Arc.
ac.scm
foundation, but is implemented in arc.arc
out of annotated functions.
A macro can be generated "manually", illustrating the steps. Create baz
, which is like the macro foo
above, except it is a function and not a macro. Note that when executed, it returns the generated code:
arc> (def baz () ((prn "macro executed") '(prn "generated code executed")) arc> (baz) macro executed (prn "generated code executed")To turn this function into a macro, it is simply annotated as type
'mac
.
The resulting mbaz
functions exactly like the macro foo
:
arc> (= mbaz (annotate 'mac baz)) #3(tagged mac #<procedure: baz>) arc> (mbaz) macro executed generated code executed "generated code executed"To summarize, any function that generates Arc code can be annotated as type
'mac
, and the result is a macro. The annotation process is the key to forming a macro. There is nothing special about using mac
to create a macro; it is just a "convenience" macro that performs this annotation. The fact that mac
is a macro may seem circular, but since it is implemented using annotate
directly, there is no circularity.
The annotate
function provides a general typing system. It is implemented in Scheme by ar-tag
, which creates a Scheme vector of length 3: 'tagged
, the type, and the contents. For macros, the type is 'mac
, but annotate
supports arbitrary types. (For detailed background on annotate
, see Some Work on Arc.)
The internal representation of a macro as a vector can be seen by entering a macro name:
arc> do #3(tagged mac #<procedure>)
Because macros are implemented by functions, the body of the macro is initially processed by ac-fn
. A simple function is turned into a Scheme lambda function, while a function with complex (destructuring) arguments is processed by ac-complex-fn
. The destructuring is implemented by inserting the appropriate combinations of car
and cdr
to pull each argument out of the list. In other words, destructuring is just syntactic sugar; the same effect can be obtained by manually using car
and cdr to extract each argument from a rest argument.
The net result is that a Arc macro becomes an Arc procedure, tagged as 'mac
. Internally, the macro is a Scheme vector, with the third argument a Scheme procedure that will take the macro arguments, perform any necessary destructuring, and execute the macro code.
ac
. (See Arc Internals, Part 1 for background.) Code of the form (foo ...)
is passed by ac
to ac-call
. If the first argument is a macro (i.e. a vector tagged with 'mac
), it is evaluated by ac-mac-call
, which evaluates the macro function on the arguments, and then passes the macroexpanded result to ac
to be converted to Scheme. The net result is the code generated by the macro gets treated as if it were part of the original expression being processed.
Comparing ac-mac-call
to ac-call
shows why macros receive their arguments unevaluated. ac-mac-call
applies the macro function to the arguments, while ac-call
maps ac
on the arguments before applying the function, causing the arguments to be evaluated.
arc-eval
calls eval
on the Scheme code generated by ac
. It may use ar-funcalln
to execute functions, and execution will typically bottom-out with foundation operations implemented in native Scheme. (See Arc Foundation Documentation for details.)
macex
and macex1
functions perform macro expansion. If the outermost form is a macro, macex1
expands it once, while macex
expands it repeatedly until it is no longer a macro. These functions are typically used for debugging, to see what a macros is doing. Note that neither macex
nor macex1
expand inner macros.
arc> (macex1 '(or 1 2 3)) (let gs2418 1 (if gs2418 gs2418 (or 2 3))) arc> (macex '(or 1 2 3)) ((fn (gs2419) (if gs2419 gs2419 (or 2 3))) 1)
The macex
and macex1
functions are
implemented by ac-macex
, which, if passed a macro expands it. Expansion is done by applying the macro function to the arguments. For macex
, ac-macex
calls itself recursively, terminating when the outer call is no longer a macro. However, macex1
passes a 'once
flag to ac-macex
, so only one step of macro expansion is performed.
The set
special form takes a symbol and a value, and sets the variable indicated by the symbol to the value. It is implemented by ac-set
, which uses ac-macex
. This ensures the first argument to set
is a symbol, or a macro that macroexpands to a symbol. Note that something (other than a macro) that evaluates to a symbol is not permitted as the first argument to set. Thus, macro processing for set
uses a separate path from the rest of Arc's macro processing.
In conclusion, Arc's macro processing is implemented relatively straightforwardly. The biggest surprise is the representation of macros as tagged procedures, which are implemented as vectors. Macros may be somewhat nonintuitive, but understanding the internal implementation may help with writing and understanding code that makes use of macros.
Copyright 2008 Ken Shirriff