Skip to content

LISP - Compiler Macros

In Common Lisp, macros are a foundational feature for metaprogramming, allowing you to transform and generate code before it’s compiled. A compiler macro is a specialized tool within this system, designed specifically for optimization. This chapter explores the relationship and distinctions between standard (defmacro) and compiler (define-compiler-macro) macros.

The Core Distinction: Syntax vs. Optimization

Section titled “The Core Distinction: Syntax vs. Optimization”

Understanding when and why each type of macro is applied is key:

  • Standard Macros (defmacro): Their primary purpose is syntactic extension. They allow you to create new control structures, Domain-Specific Languages (DSLs), or simply reduce boilerplate code. A standard macro transforms one S-expression into another before the compiler’s main work begins. The compiler only ever sees the expanded code.
  • Compiler Macros (define-compiler-macro): Their purpose is optimization. They provide an alternative, more efficient implementation for a function call. The Lisp compiler invokes a compiler macro during its optimization phase, giving it a “suggestion” for a faster code sequence. The compiler is free to ignore this suggestion if it chooses.

Let’s look at how each is defined and where they are used.

A standard macro is defined with defmacro. Its arguments are the raw S-expressions from the source code, not their evaluated results. It returns a new S-expression to be compiled and evaluated in its place.

A classic example is creating an unless control structure, which is the opposite of when.

;; Defines a new syntactic construct `unless`.
(defmacro unless (condition &body body)
"Executes body if condition is false."
`(when (not ,condition)
,@body))
;; Usage:
(let ((user-logged-in nil))
(unless user-logged-in
(print "Redirecting to login page...")))
;; The compiler sees:
;; (when (not user-logged-in)
;; (print "Redirecting to login page..."))

A compiler macro is defined with define-compiler-macro and is associated with a specific function. It receives the entire function call form as an argument. Its goal is to return a more optimized form if possible. If it can’t optimize the call, it should return the original form to let the compiler handle it normally.

Consider a function that gets the length of a sequence. We can provide a compiler macro to use a more efficient, type-specific function if the compiler can prove the argument is a list or vector.

;; First, define the regular function.
(defun generic-length (seq)
(length seq))
;; Now, define an optimization hint for the compiler.
(define-compiler-macro generic-length (&whole form arg)
"If the compiler knows the type of arg, use a faster function."
(if (and (listp arg) (= (length arg) 2) (eq (first arg) 'the))
;; The form is `(generic-length (the list ...))` or `(generic-length (the vector ...))`
(let ((type (second arg)))
(cond ((subtypep type 'list) `(list-length ,(third arg)))
((subtypep type 'vector) `(vector-length ,(third arg)))
(t form) ; Can't optimize, return original form
))
form ; Can't determine type, return original form
))
;; In a function where type information is available:
(defun count-my-list-items (my-list)
(declare (type list my-list))
(generic-length my-list)) ; The compiler macro may expand this to (list-length my-list)
;; Note: `list-length` and `vector-length` are hypothetical fast functions.
;; The principle is to substitute a generic call with a specific, faster one.
Featuredefmacro (Standard Macro)define-compiler-macro (Compiler Macro)
PurposeSyntactic Abstraction, DSLs, Code GenerationPerformance Optimization
When it RunsBefore compilation (during macro-expansion phase)During compilation (in the optimization phase)
InputThe arguments (unevaluated s-expressions)The entire function call form (e.g., (my-func a b))
Is it a Function?No. It defines a code transformation, not a callable function.No. It’s a transformation hint for an existing callable function.
Can Compiler Ignore?No. The expansion is mandatory.Yes. The compiler can ignore the hint and use the original function definition.
When to UseWhen you need new syntax that doesn’t exist in Lisp.When a function has a more efficient implementation for specific argument types or values, and this can be determined at compile time.
  • Debugging Macros: Use macroexpand-1 and macroexpand in your REPL to see what your standard macros expand into. This is essential for finding bugs in your generated code.
  • Judicious Use: Compiler macros are an advanced feature. Don’t use them unless you have identified a performance bottleneck through profiling. Premature optimization can lead to complex, hard-to-debug code.
  • Preserve Semantics: A compiler macro must not change the behavior of the original function. It should be a pure optimization.
  • Safe Fallback: Always ensure your compiler macro returns the original form when it cannot apply an optimization. This ensures your code remains correct.