Follow the path from a macro call to generated Clojure code, compiler analysis, and JVM execution so macro behavior is reviewable rather than magical.
Macro expansion is the source-to-source step that happens before the expanded form is evaluated. A macro receives unevaluated forms, returns a new form, and the compiler analyzes that returned form as Clojure code.
The important discipline is simple: review the expansion, not only the macro definition.
flowchart LR
A["Caller form"] --> B["Macro receives forms"]
B --> C["Macro returns a form"]
C --> D["Compiler analyzes expanded code"]
D --> E["JVM bytecode runs later"]
For a Java engineer, this is closer to source generation than reflection. Reflection inspects runtime classes. A Clojure macro rewrites code before the runtime call exists.
1(defmacro unless
2 [condition & body]
3 `(if (not ~condition)
4 (do ~@body)))
The macro receives the condition form and body forms as data. It returns an if form.
1(macroexpand-1
2 '(unless false
3 (println "disabled")
4 :skipped))
5
6;; roughly:
7(if (clojure.core/not false)
8 (do
9 (println "disabled")
10 :skipped))
The expansion shows three design choices:
| Choice | Why it matters |
|---|---|
condition is unquoted with ~ |
The caller’s condition appears in generated code. |
body is spliced with ~@ |
Multiple body forms become multiple forms inside do. |
The generated form is ordinary if and do |
The reviewer can reason about normal Clojure, not hidden magic. |
| Stage | What to check |
|---|---|
| Read caller form | Are arguments meant to be values or unevaluated code? |
| Invoke macro function | Does the macro make decisions using only compile-time information? |
| Return expanded form | Is the returned code valid, readable Clojure? |
| Expand nested macro calls | Are any generated macro calls intentional and reviewable? |
| Compile and run | Does runtime behavior match the generated code, not wishful intent? |
Macros do not make runtime code faster by default. They move some decisions earlier and can create syntax that functions cannot express.
| Mistake | Better review habit |
|---|---|
| Assuming macro arguments are evaluated first | Remember the macro receives forms, not values. |
| Reading only the macro definition | Expand representative calls with realistic arguments. |
| Ignoring generated locals | Check for auto-gensyms or explicit gensym usage. |
| Trusting nested macros blindly | Use macroexpand-1 first, then full macroexpand when needed. |
| Java mechanism | When it runs | Closest macro lesson |
|---|---|---|
| Reflection | Runtime | Useful for inspecting objects, not for changing source shape. |
| Annotation processing | Compile time | Similar review concern: generated code must be readable. |
| Bytecode generation | Build or runtime | More remote than macros; harder to inspect from normal source. |
| Clojure macro | Expansion time | Generates Clojure forms that can be inspected at the REPL. |