The How of Macros 5: Unquoting
As you now know, quote
descends the syntax tree that is its argument and converts it into code that will, when executed, build the original syntax tree. quote
is usually not useful unless the tree contains unquote
or unquote_splicing
nodes. This post explains how those are handled. The related function, expand
, is explained later.
The series: Elixir compilation, syntax trees, literal data, quote
, unquote
, escape
, hygiene and var
, what's up with require
?
What does the compiler's quote
-handling code do when it discovers an unquote
node?
The code to handle unquote
is written in Erlang, but its Elixir equivalent would be:
def do_quote({:unquote, _metadata, expression) do
expression
end
Let's look at an example of how that interacts with quote
handling to produce the correct syntax tree.
Reading the Erlang code for do_quote
, I was reminded that Erlang's and
isn't short-circuiting; that is, it evaluates both its arguments even if the first is false. Erlang's equivalent of Elixir's and
and &&
is andalso
. It would have to be written as a macro in order to control whether the second argument is evaluated. Here's a typical implementation:
defmacro andalso(left_ast, right_ast) do
quote do
if unquote(left_ast) do # left code always evaluated
if unquote(right_ast) do # right maybe evaluated
true
else
false
end
else
false
end
end
end
Remember that, like all macro functions, andalso
will be called with a syntax tree at compile time, to produce another syntax tree that will be compiled. So this:
andalso (IO.inspect false), (IO.inspect true)
... will produce this code:
if IO.inspect false do
if IO.inspect true do
true
else
false
end
else
false
end
For variety, let's also implement andalso
without using quote
and call it andalso_literal
:
defmacro andalso_literal(left_ast, right_ast) do
{:if, [context: M, import: Kernel],
[
left_ast, # what `unquote` does
[
do: {:if, [context: M, import: Kernel],
[
right_ast, # what `unquote` does
[do: true, else: false]
]},
else: false
]
]}
end
That works just fine. Not only that, the two versions are identical in the sense that they will hand the same syntax tree to the bytecode generator. It'd be tedious to walk through the transformations of the whole tree to show that equivalence, so I'll sketch it out in pictures and text.
First, let's look at these two pieces of code:
quote do: (if ...) # andalso
{:if, [metadata], ...} # andalso_literal
Here's a picture of the processing:

In the andalso
case, quote
's argument is parsed into the kind of 3-tuple the compiler uses for the if
special form:
{:if, [metadata], ...}
That expression is (when it comes to this top level) the same as the literal 3-tuple used in andalso_literal
. There's a difference, though: andalso
's 3-tuple was created by the parser, but andalso_literal
's 3-tuple is given to the parser. So they'll go through different processing routes despite experiencing the same transformation.
In the andalso
case, the :if
3-tuple is processed by the quote
context-expansion code (do_quote
), which converts it into tuple-creation code like this:
{:{}, [...], [:if, [metadata], ...]
In the andalso_literal
case, the 3-tuple is parsed as a literal data structure, which... converts the top-level expression into the same :{}
tuple-creation code.
The two {:if, ..., ...}
syntax trees aren't identical all the way down to their leaves. andalso
contains unquote
expressions that wrap variable names, whereas andalso_literal
contains just the variable names:
... unquote(left_ast)... # andalso
... left_ast ... # andalso_literal
And so, again, there's different processing:

In the andalso
case, the unquote
expression is parsed into an :unquote
syntax tree that wraps the typical 3-tuple for a variable dereference: {:left_ast, [...], atom}
. Since the context-expansion of an :unquote
3-tuple is just the unquote
's single argument, what gets used for bytecode generation is that tuple, an instruction to dereference left_ast
.
In the andalso_literal
case, a name is parsed, which just produces the same variable-dereference tuple without the need for the need for an intermediate :unquote
tree.
unquote_splicing
unquote_splicing
also does nothing to its AST argument. However, the do_quote
pattern-matching has to happen up one level in the context-expansion because it has to capture both the list the unquoted tree is to be spliced into and also the value to splice in. As an example, consider the case where the unquote_splicing
is at the head of a list:
quote do
...
[unquote_splicing(left), 3, IO.inspect a]
...
A function to transform that list might look like this:
def do_list_tail([{:unquote_splicing, _meta, [to_splice]} | rest]),
do: [to_splice | rest]
... except that there might be another unquote_splicing
later in the list, so we need some recursion:
def do_list_tail([{:unquote_splicing, _meta, [to_splice]} | rest]),
do: [to_splice | do_list_tail(rest)]
^^^^^^^^^^^^^^^^^^
... which is good, because a day without recursion is like a day without sunshine.
We also need the case where the head of the list isn't an :unquote_splicing
:
def do_list_tail([head | rest]),
do: [head | do_list_tail(rest)]
And, as always with explicit recursion, we need a base (end of recursion) case:
def do_list_tail([]), do: []
That finishes up the basics of compiling macros. Programming language designers have been working with this style of compile-time syntax-tree transformations since 1963, the date they were first proposed for Lisp. The Elixir syntax tree and, consequently, quote
and unquote
handling is tricksier than in Lisps (at least the ones I've looked at). I don't know the reason. I suspect it has to do with compatibility with the Erlang syntax tree format (which was not designed to support macros), and perhaps with pattern matching.
There's more to say about macros, but I'm inclined to ask if people are interested in other topics before I put in the effort. Let me know via email or twitter (publicly with @marick
or via my open DMs).
Previous: quote