Skip to content

LISP - Code Generation Using Macro

In Lisp, macros are a powerful tool for metaprogramming—writing code that writes code. Unlike functions, which evaluate their arguments before execution, macros are given the unevaluated Lisp code of their arguments. This allows you to transform that code before it’s compiled and run, effectively extending the Lisp language with new syntax and control structures. In this chapter, we’ll explore how to build our own language features using macros.

The key to understanding macros is knowing that they operate on code, while functions operate on data (the results of evaluated code). Let’s see why this matters.

Consider a square function:

(defun square-fn (n) (* n n))

If you call it with (square-fn (+ 2 3)), the (+ 2 3) is evaluated first, becoming 5, and then (square-fn 5) is called. This is usually what you want.

But what if the argument has a side effect?

(let ((x 1))
(square-fn (incf x))) ; incf increments x and returns the new value

Here, (incf x) runs once, x becomes 2, and (square-fn 2) is called, returning 4. The side effect happens only once. A macro behaves differently, which can lead to unexpected behavior if not written carefully.

Let’s define a macro to generate the code for squaring a number. We use defmacro and the backquote ( `) syntax for templating.

;; A macro to generate code that squares an expression.
(defmacro my-square (expr)
"Expands to code that multiplies the expression by itself."
`(* ,expr ,expr))
;; --- Usage ---
;; Use the built-in macroexpand-1 to see what the macro generates.
(print (macroexpand-1 '(my-square 5)))
(terpri)
;; Normal execution
(print (my-square 5))
(terpri)
;; What happens with a side effect?
(let ((counter 0))
(print (my-square (incf counter))) ; Expands to (* (incf counter) (incf counter))
(print counter)) ; The counter was incremented TWICE!
(* 5 5)
T
25
4
2
  • defmacro my-square (expr): Defines a macro named my-square that takes one argument, expr. Importantly, expr is the unevaluated code passed to the macro, not its value.
  • Backquote ( `): The backquote starts a template. Everything inside is treated as literal data, not code to be executed immediately.
  • Comma (,): The comma unquotes an item, telling Lisp to evaluate it and splice its value into the template. Here, ,expr inserts the code tree for 5 or (+ 2 3). The double-evaluation pitfall with incf is a classic macro problem.
  • macroexpand-1: This is a crucial debugging tool. It shows you the code your macro generates in one step, allowing you to verify its output before running it.

Example: A Hygienic square Macro with Gensym

Section titled “Example: A Hygienic square Macro with Gensym”

To fix the multiple evaluation problem, we need to ensure the expression is evaluated only once. We can do this by creating a temporary variable inside the macro expansion. To avoid accidentally conflicting with variables in the surrounding code, we must use gensym to create a unique, un-interned symbol.

;; A better, safer square macro.
(defmacro my-safe-square (expr)
"Safely squares an expression by evaluating it only once."
(let ((temp-var (gensym)))
`(let ((,temp-var ,expr))
(* ,temp-var ,temp-var))))
;; --- Usage ---
(print (macroexpand-1 '(my-safe-square (incf counter))))
(terpri)
(let ((counter 0))
(print (my-safe-square (incf counter)))
(print counter)); The counter is now incremented only once!
(LET ((#<GENSYM> (INCF COUNTER))) (* #<GENSYM> #<GENSYM>))
T
1
1
  • Hygiene: This macro is considered “hygienic” because it avoids unintended variable capture and repeated evaluation.
  • gensym: Generates a new, unique symbol (like #<GENSYM>) that cannot possibly clash with any existing variable name.
  • let: The let form inside the macro expansion creates a temporary variable. The result of evaluating expr is stored in this variable, which is then used twice.

Macros shine when creating new control structures. Here’s a simple while loop, a structure not built into Common Lisp.

;; A macro for a classic 'while' loop.
(defmacro my-while (condition &body body)
"Executes body expressions as long as condition is true."
`(loop while ,condition
do (progn
,@body)))
;; --- Usage ---
(let ((i 0))
(my-while (< i 5)
(print i)
(incf i)))
0
1
2
3
4
  • &body: This is a special lambda-list keyword that collects all remaining forms into a list, perfect for the body of a loop or function-like structure.
  • Comma-At (,@): The comma-at operator splices a list of forms into the template. If body is ((print i) (incf i)), then ,@body inserts (print i) (incf i) directly into the progn form. progn is used to group multiple expressions into a single block.
  • When to use a macro?: Only when a function won’t work. Use a macro if you need to control evaluation, create new syntax, or perform compile-time computations.
  • Keep them simple: Complex macros are hard to debug. If a macro gets too complicated, consider using helper functions within the macro definition.
  • Always use gensym: For any temporary variables in your macro expansion, use gensym to avoid bugs.
  • Debug with macroexpand-1: Always check the code your macro generates. It’s the most effective way to find bugs.