LISP - Compiler Macros
Lisp - Compiler Macros
Section titled “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.
Defining and Using Macros
Section titled “Defining and Using Macros”Let’s look at how each is defined and where they are used.
Standard Macro (defmacro)
Section titled “Standard Macro (defmacro)”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..."))Compiler Macro (define-compiler-macro)
Section titled “Compiler Macro (define-compiler-macro)”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.Key Differences Summarized
Section titled “Key Differences Summarized”| Feature | defmacro (Standard Macro) | define-compiler-macro (Compiler Macro) |
|---|---|---|
| Purpose | Syntactic Abstraction, DSLs, Code Generation | Performance Optimization |
| When it Runs | Before compilation (during macro-expansion phase) | During compilation (in the optimization phase) |
| Input | The 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 Use | When 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 and Best Practices
Section titled “Debugging and Best Practices”- Debugging Macros: Use
macroexpand-1andmacroexpandin 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.