Macros

Template macros, quasiquoting, and compile-time code generation.

Template Macros

A function takes values and returns a value. A macro takes syntax and returns syntax. It runs at compile time, reading the raw shape of your code and rewriting it into new code that gets compiled in its place. You define one with macro.

LOON
[macro unless [cond body]
  `[if [not ~cond] ~body]]

[unless false [println "runs!"]]

When the compiler sees [unless false [println "runs!"]], it does not call a function. It hands the raw syntax to your macro, which rewrites it into [if [not false] [println "runs!"]]. By the time the program runs, the macro has vanished and only the expansion remains.

That is the source of the power: you can grow new control flow, new syntax, and new abstractions that read as if they were built into the language all along.

Quasiquoting

Writing a macro means assembling a syntax tree by hand, and quasiquoting lets you draw that tree instead of constructing it node by node. Three marks do all the work.

Backtick

The backtick ` quotes a template. Everything inside is taken as literal syntax rather than code to run. Read it as: give me this exact structure, untouched.

LOON
`[+ 1 2]  ; produces the syntax [+ 1 2]

Unquote ~

A frozen template is a stencil with no openings. To let computed syntax in, you punch holes with ~ (unquote): it evaluates the expression and drops the result into the template at that spot.

LOON
[macro double [x]
  `[+ ~x ~x]]

Here ~x means "insert whatever syntax was passed as x." Call [double 5] and the macro produces [+ 5 5].

Unquote-splicing ~@

When you hold a list of syntax elements, you usually want them poured into a form one by one, not dropped in as a single nested list. ~@ (unquote-splicing) does exactly that.

LOON
[macro do-all [& forms]
  `[do ~@forms]]

The & forms gathers every argument into a list, and ~@forms spreads that list flat into the do block. So [do-all a b c] becomes [do a b c], not [do [a b c]]. The difference is the whole point.

Procedural Macros

Templates carry you a long way, but some output cannot be drawn ahead of time; it has to be computed. Since a macro body is ordinary Loon code, the entire language is available to build the syntax you return.

LOON
[macro repeat [n body]
  [let forms [map [range 0 n] [fn [_] body]]]
  `[do ~@forms]]

This macro takes a number n and a body, then emits n copies of that body wrapped in a do block. It runs map and range at compile time to build the list, then splices it into the output with ~@. The full language is at your disposal; the only rule is that it all happens before runtime.

Type-Aware Macros

A regular macro runs before type checking and sees only raw syntax, never the meaning behind it. When you need to know what a value actually is, reach for macro+: it runs after the type checker, so it can inspect types and generate code tailored to them.

LOON
[macro+ derive-debug [t]
  `[fn debug [val] [str [type-name ~t] ": " [inspect val]]]]]

This is the foundation for automatic serializers, debug formatters, and derive macros — anything that must read the structure of a type to write code for it. Most macros never need it; the few that do would be impossible without it.

Note

macro+ macros run after type checking, so they can inspect types and generate type-specific code. Use regular macro when you do not need type information.

Debugging with macroexpand

Code that writes code fails in a peculiar way: the bug lives in syntax you never typed. macroexpand makes that hidden syntax visible, showing exactly what a macro produces without running the result.

LOON
[macroexpand [unless false [println "hi"]]]
; [if [not false] [println "hi"]]

If the expansion looks wrong, the bug is in your macro. If it looks right, the bug is elsewhere. That single distinction collapses most of the guesswork, and it is worth its weight when macros nest inside macros.

Common Patterns

A handful of macros earn their place in nearly every Loon project. Each one shows what macros do best: erase a repetitive shape and leave an abstraction that reads like intent.

When / Unless

A conditional for when you care about only one branch. Pairing & body with ~@body lets the caller write several expressions without spelling out a do.

LOON
[macro when [cond & body]
  `[if ~cond [do ~@body]]]

Thread-First

The thread-first macro -> takes a value and threads it through a series of calls, slotting it in as the first argument to each. Nested calls that read inside-out become a pipeline that reads top-to-bottom.

LOON
[macro -> [x & forms]
  [fold forms x [fn [acc form]
    `[~[first form] ~acc ~@[rest form]]]]]

Swap

The classic exchange of two mutable bindings. Without a macro, you would rewrite the temporary-variable shuffle by hand at every call site.

LOON
[macro swap! [a b]
  `[do [let tmp ~a]
     [set! ~a ~b]
     [set! ~b tmp]]]