dissoc

Fun ways to break your Macros

Does your macro provide & body arguments? Continue reading.

1. try splicing

Does your macro splice body into a try like so?

`(try ~@body (finally ...))
  1. Try passing (catch Exception e) as your body arguments. Your macro can catch exceptions.
  2. Try passing (finally) as your body arguments. Your macro cannot invoke variables called finally.

For example:

(defmacro foo [& body]
  `(try ~@body (finally 1)))

(foo (finally 1))
;; Syntax error compiling try at (REPL:1:1).
;; finally clause must be last in try expression

2. fn splicing

Does your macro splice body into a fn like so?

`(fn [] ~@body)
  1. Try passing (recur) as your body arguments. Your macro now runs forever.
  2. Try passing {:pre [false]} (inc 1) as your body arguments. Your macro supports preconditions.

For example:

(defmacro bar [& body]
  `((fn [] ~@body)))

(bar {:pre [false]} 1)
;; Execution error (AssertionError) at user/eval155$fn (REPL:1).
;; Assert failed: false

3. Composition of core macros

Does your macro expand to any of these macros?

  • locking, binding, with-bindings, sync, with-local-vars, with-in-str, dosync, with-precision, with-loading-context, with-redefs, delay, lazy-seq, lazy-cat, future, pvalues

Does the call look like this?

`(<core-macro> ... ~@body)

You have inherited one or more of the above entertaining features.

For example:

(defmacro baz [& body]
  `(locking 1
     ~@body))

(baz (finally 1))
;; Syntax error compiling try at (REPL:1:1).
;; finally clause must be last in try expression

How to fix it

Any time you have ~@body, do this instead:

`(let [res# (do ~@body)] res#)

It forces ~@body to always be in an expression position without a recur target.

If that’s too extreme, just wrap it in a (do ~@body) to force expression position.

08 Sep 2022