Skip to content

LISP - Scope and Binding

Lisp - Macro Hygienics: Scope and Bindings

Section titled “Lisp - Macro Hygienics: Scope and Bindings”

Macros are powerful code-transformation tools, but this power comes with a critical responsibility: managing variable scope. Because macros inject code directly into their call site, they can accidentally interfere with existing variables. This chapter explains the problem of variable capture and how to write clean, predictable, hygienic macros in Common Lisp.

The Problem: Unhygienic Macros and Variable Capture

Section titled “The Problem: Unhygienic Macros and Variable Capture”

A macro is unhygienic if it introduces bindings that can accidentally capture or clash with variables in the surrounding code. This happens when a symbol used inside the macro is the same as a symbol at the call site.

Let’s define a macro to swap the values of two places. A naive implementation might use a temporary variable named temp.

;; Flawed macro: uses a fixed symbol `temp` for its internal variable.
(defmacro unhygienic-swap (a b)
`(let ((temp ,a))
(setf ,a ,b)
(setf ,b temp)))

This works fine in most cases. However, it fails spectacularly if one of the variables we want to swap is also named temp.

(let ((temp 10)
(my-var 20))
(format t "Before: temp=~d, my-var=~d~%" temp my-var)
(unhygienic-swap temp my-var)
(format t "After: temp=~d, my-var=~d~%" temp my-var))
Before: temp=10, my-var=20
After: temp=20, my-var=20

The swap failed! To see why, we can use macroexpand-1 to view the generated code:

;; CL-USER> (macroexpand-1 '(unhygienic-swap temp my-var))
(LET ((TEMP TEMP))
(SETF TEMP MY-VAR)
(SETF MY-VAR TEMP))

The (let ((temp temp)) binding shadows the outer temp, causing my-var to be set to its own new value instead of the original value of temp.

The solution is to use symbols that are guaranteed to be unique. The gensym function creates a new, uninterned symbol on each call, which cannot clash with any existing symbols.

;; Correct macro: uses a generated symbol for its internal variable.
(defmacro hygienic-swap (a b)
(let ((temp-sym (gensym)))
`(let ((,temp-sym ,a))
(setf ,a ,b)
(setf ,b ,temp-sym))))

Now, let’s try the same test case again:

(let ((temp 10)
(my-var 20))
(format t "Before: temp=~d, my-var=~d~%" temp my-var)
(hygienic-swap temp my-var)
(format t "After: temp=~d, my-var=~d~%" temp my-var))
Before: temp=10, my-var=20
After: temp=20, my-var=10

It works correctly! The expansion now uses a unique symbol (e.g., #:G1234) that doesn’t conflict with our temp variable.

The macrolet special operator allows you to define local macros that are only available within a specific lexical scope. This is useful for creating syntactic sugar or a small domain-specific language (DSL) inside a function, without polluting the global namespace.

(defun calculate-trajectory (x)
;; Define a local macro 'square' available only inside this function.
(macrolet ((square (n)
`(* ,n ,n)))
;; Use the local macro.
(+ (square x) (square (+ x 1)))))
(print (calculate-trajectory 5))
61
  • Always use gensym for any temporary variables introduced within your macro expansion.
  • Avoid multiple evaluations of macro arguments. If an argument might have side effects (e.g., (incf i)), store its result in a gensym’d variable and use that variable in the expansion.