Programming Languages · Metaprogramming

Quasiquotation, Unquotation, and Nesting Depth

How quasiquote builds template structures with holes, why unquote must match the right quasiquote level, and how the level counter decides which parts of a nested template are evaluated.

A macro must return a structure in which some positions are literal and others are filled from the invocation. Writing that structure as an ordinary constructor call is possible and unreadable: building a three-level nested list out of cons calls obscures the shape being built. Quasiquotation is the notation that makes the shape visible again, and its only real subtlety is that a template may contain another template, at which point the notation needs to say which holes belong to which level.

The four markers and what each one does

The notation is a small family of reader prefixes. quote suppresses evaluation entirely and yields the datum as written. quasiquote suppresses evaluation except where a hole is marked. unquote marks a hole at the current level. unquote-splicing marks a hole whose value is a list that should be spliced into the enclosing list rather than inserted as a single element.

`(a b ,x)            ; a three-element list: a, b, and the value of x
`(a b ,@xs)          ; a list starting a, b, then the elements of xs
`(a . ,x)            ; a dotted pair: a in the car, value of x in the cdr

The distinction between ,x and ,@xs is structural, not cosmetic. The first inserts one element; the second inserts however many elements the list has, so the resulting list’s length depends on a value that is not known when the template is written. Splicing is only defined in a list position — splicing into the tail of a dotted pair is an error in most implementations, because there is no enclosing list for the elements to be part of.

The reader’s role is worth separating from the expander’s. ` is read as the two-element form (quasiquote ...), , as (unquote ...), and ,@ as (unquote-splicing ...). A macro expander that receives a template is therefore receiving ordinary nested lists whose heads are these symbols, and the expansion of a quasiquote is a recursive walk over that structure. Nothing about the notation is primitive; it is a surface syntax for a particular shape of tree, and implementations are free to expand it by interpretation or by compiling it into constructor calls.

Depth: the counter that decides

The nesting rule is a single integer. Entering a quasiquote increments the level; leaving it decrements. An unquote at level n is meaningful only if it appears inside exactly n nested quasiquotes, and it cancels the innermost one. An unquote seen at a level other than one is either an error or, in some implementations, a datum to be preserved for a later expansion.

`(a `(b ,(c ,x)))

Reading outward: the first quasiquote sets level 1, the second sets level 2. The comma before (c ,x) sits at level 2, so it cancels the inner quasiquote and makes its argument a level-1 expression — part of the outer template. That argument is itself (c ,x), whose comma is at level 1, so x is evaluated immediately, while the outer template is being built. What remains is the form (c <value>), still wrapped in an unquote inside the inner quasiquote. With x bound to 1 the whole expression yields (a (quasiquote (b (unquote (c 1))))): substituting x happened at the outer level, and applying c is deferred until the inner template is evaluated.

This is the mechanism that makes macros that write macros possible. The outer macro needs to produce a definition whose body is itself a template, and the holes belonging to the outer macro must be filled at the outer level while the holes belonging to the inner macro must survive as holes.

Some Lisp dialects make the level explicit at the hole rather than inferring it from the surrounding nesting, writing a doubled comma for a hole that belongs to the outer template. The suffix removes the counting exercise from the reader but introduces a second way to be wrong: a suffix that does not match the surrounding depth. Common Lisp is a partial case at the specification level. The standard reserves the notation for interpretation without fully specifying how nested backquote is read, so an implementation that accepts ,, may treat it differently from another, and code that relies on the nested behaviour is not portable. Scheme’s syntax-rules takes a third position: its templates have no explicit quasiquote at all, and pattern variables are substituted wherever they appear, with ... marking repetition. The depth problem does not arise there because there is only ever one level.

Building the expander

A quasiquote expander is a recursive function over the template with a level parameter. At a list node, it distinguishes three cases: the head is unquote and the level is 1, in which case the argument is emitted as an expression; the head is unquote and the level is greater than 1, in which case the node is rebuilt with the level decremented; and the head is quasiquote, in which case the level is incremented. Everything else is rebuilt element by element.

(define (qq-expand tmpl level)
  (cond
    ((not (pair? tmpl))
     (list 'quote tmpl))                           ; an atom is embedded as a literal
    ((eq? (car tmpl) 'unquote)
     (cond
       ((= level 1) (cadr tmpl))                   ; a hole: emit it as an expression
       (else (list 'cons (list 'quote 'unquote)     ; rebuild it one level shallower
                   (qq-expand (cdr tmpl) (- level 1))))))
    ((eq? (car tmpl) 'quasiquote)
     (list 'cons (list 'quote 'quasiquote)
           (qq-expand (cdr tmpl) (+ level 1))))
    (else (list 'cons                              ; cons the two halves
                (qq-expand (car tmpl) level)
                (qq-expand (cdr tmpl) level)))))

cond has no => clauses here, so an if is not a clause: the unquote branch must itself be a cond whose two clauses are the hole and the rebuild. Written as a single three-part if inside the outer cond, the expression is a syntax error rather than a wrong answer.

The output of this walk is a program that constructs the template: a nest of cons calls with the hole expressions left in place to be evaluated by the surrounding let or lambda. Compiling the template rather than interpreting it is the standard implementation, and it matters because the constructed program is then subject to ordinary constant folding — a template with no holes at all collapses to a constant, and the compiler can allocate it once instead of rebuilding it at every invocation.

The splicing case needs one more piece. A spliced hole cannot be compiled as a plain cons, because its length is unknown; the expander emits an append between the segments before and after the hole, and a variadic constructor such as list for the segments that are known to be flat. A template with k splices compiles to k+1 segment constructions joined by k append calls, and the append is where the cost lives: it copies the spine of each following segment. A template that splices a long list in front of a long literal tail therefore copies that tail at every expansion. Implementations that care emit a list* chain instead, but the asymptotic shape is the same.

Where the notation leaks

Three failure modes recur.

An unquote at the wrong level is the first. Because the level is implicit in Scheme, a template that is one quasiquote shallower than the author believed produces an unquoted hole where a literal was intended, and the resulting form is evaluated rather than quoted. The error surfaces as an unbound variable or as a call to a function that does not exist, at a location far from the template.

The second is a splice in a non-list position. `(a . ,@xs) has no sensible meaning, and implementations disagree on whether it is a read-time error, an expansion-time error, or a construction of something malformed.

The third is the interaction with hygiene. A template is the primary source of identifiers that the macro introduces, so the expander must record, for each literal identifier in a template, the context of the macro definition. When a template is itself constructed by another macro, the constructed template’s identifiers carry the context of whichever expansion produced them, which is not always the context the eventual user expects. The rules for this are the hardest part of a hygienic expander, and they are why the syntax-object representation described in macro hygiene and identifier capture stores a scope set on every identifier rather than a single origin.

Templates in systems without the notation

Not every macro facility exposes quasiquotation, and the substitutes show what the notation is actually for. C’s replacement list is a template with # and ## as its hole markers, and it has no nesting at all — a macro cannot generate a macro definition, because the preprocessor has no way to express “this directive is produced rather than obeyed”. Rust’s macro_rules! is closer: $name substitutes a fragment, $( ... )* repeats a group over a matched sequence, and the whole thing is one level, so nested templates require a second macro to carry the inner one.

macro_rules! count {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1usize + count!($($tail)*) };
}

The repetition operator is quasiquotation’s splicing counterpart: $( ... )* iterates over the variables bound by the pattern’s repetition, and the separator between repetitions is written after the *. There is no level counter because the replacement is not evaluated — it is a token sequence handed to the parser — so the depth problem that motivates ,, in Common Lisp simply does not exist. What replaces it is the problem of a fragment that must be re-parsed after substitution, covered in procedural macros over token streams.

The general lesson is that quasiquotation is a notation for a specific operation: constructing a tree with designated holes, where the holes may be single values or sequences. Its level counter exists because the operation is closed under itself — a template can contain a template — and any system that supports generating code which generates code needs an equivalent way to say which holes belong to which generation.

References

  1. Revised^7 Report on the Algorithmic Language Scheme — Sperber, Findler, Flatt et al.; Journal of Functional Programming 19(S1), 2009
  2. Common Lisp the Language, 2nd edition — Guy L. Steele Jr., Digital Press, 1990
  3. Common Lisp HyperSpec, section 2.4.6.1 — Notes about Backquote — LispWorks
  4. The Rust Reference — Macros by Example — The Rust project
  5. Lisp 1.5 Programmer's Manual — McCarthy et al., MIT Press, 1962