Collapsing Macro Tower
In a previous post, I had written about a consequence of the ClojureScript macro compilation model, and how it requires a tower of macro namespaces when self-hosted.
Well, in the case that a macro expands into a macro call, the staging rules are satisfied and no tower is needed.
Here's more detail on the subject. The macros in the previous post satisfy this constraint and the bar.core
namespace can be more simply written:
(ns bar.core)
(defmacro unless [pred a b]
`(if (not ~pred)
~a
~b))
(defmacro abs [x]
`(unless (neg? ~x)
~x
(- ~x)))
Note that, for this code to work without qualifying the
unless
symbol, you needtools.reader
1.0.0-alpha3
or later.
On the surface, it looks like abs
is calling unless
within the same compilation stage. But in reality, abs
only later expands into a call to unless
. For example:
(abs -3)
expands to
(unless (neg? -3)
-3
(- -3))
which expands to
(if (not (neg? -3))
-3
(- -3))
(You can see the difference for yourself if you mess around with macroexpand-1
and macroexpand
in a REPL.)
The above differs from another pattern often employed in macros, where macro code is invoked during expansion. An example might be a macro that computes the square root of a numeric literal at compile time, checking for a negative value:
(ns baz.core)
(defmacro unless [pred a b]
`(if (not ~pred)
~a
~b))
(defmacro sqrt [x]
(unless (neg? x)
(Math/sqrt x)
(throw
(ex-info "Real plz" {}))))
In this case, sqrt
is violating the staging rules by consuming the unless
macro from within the same compilation stage, and a tower would be needed to fix it. The same thing would occur if you have a function in a macro namespace that calls upon a macro defined in that stage.
Nevertheless, the collapsing variant is very nice because macros often dictate whether a library is directly usable in bootstrapped ClojureScript, or, how much porting is required. This capability should make it easier to consume existing ClojureScript libraries in bootstrapped environments.