Macros

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

Template Macros

Macros let you extend the language itself. They run at compile time, taking in raw syntax and producing new syntax that gets compiled in its place. You define them 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 try to call a function. Instead, it hands the raw syntax to your macro, which rewrites it into [if [not false] [println "runs!"]]. By the time the code actually runs, the macro is gone and only the expanded code remains.

This is powerful because it means you can create new control flow, new syntax patterns, and new abstractions that feel native to the language.

Quasiquoting

Writing macros means building syntax trees, and quasiquoting makes that process feel natural. There are three pieces to learn.

Backtick

The backtick ` quotes a template. Everything inside is treated as literal syntax rather than code to execute. It is like saying "give me this exact structure, do not evaluate it."

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

Unquote ~

But a static template is not very useful. You need to punch holes in it where computed values go. That is what ~ (unquote) does: it evaluates the expression and splices the result into the template.

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

Here, ~x means "insert whatever syntax was passed as x." If you call [double 5], the macro produces [+ 5 5].

Unquote-splicing ~@

Sometimes you have a list of syntax elements and you want to inline them into a form, not insert the list itself. ~@ (unquote-splicing) does exactly that.

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

The & forms collects all arguments into a list. Then ~@forms flattens that list into the do block. So [do-all a b c] becomes [do a b c], not [do [a b c]]. The difference matters.

Procedural Macros

Template macros with quasiquoting handle most cases, but sometimes you need to compute the output programmatically. Since macro bodies are regular Loon code, you can use the full language to build your syntax.

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 generates n copies of that body wrapped in a do block. It calls map and range at compile time to build the list, then uses quasiquoting with ~@ to splice the result into the output. You have the full power of the language available; the only constraint is that it all happens before runtime.

Type-Aware Macros

Regular macros run before type checking, so they only see raw syntax. Sometimes that is not enough. macro+ macros run after the type checker, which means they can inspect types and generate type-specific code.

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

This is how you build things like automatic serializers, debug formatters, or derive macros that need to know the structure of a type. Most macros do not need this, but when you do, it is there.

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

Macros can be tricky to get right because you are writing code that writes code. When things go wrong, macroexpand is your best friend. It shows you exactly what a macro produces without actually running the result.

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

If the expanded code looks wrong, you know the bug is in your macro. If it looks right, the bug is somewhere else. This simple tool saves enormous amounts of debugging time, especially with complex nested macros.

Common Patterns

Here are a few macros that show up in almost every Loon project. They are good examples of what macros are best at: eliminating repetitive patterns and creating expressive abstractions.

When / Unless

A cleaner conditional when you only care about one branch. The & body with ~@body lets you write multiple expressions without an explicit do.

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

Thread-First

The thread-first macro -> takes a value and pipes it through a series of function calls, inserting it as the first argument to each one. It turns deeply nested calls into a readable pipeline.

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

Swap

A classic utility that swaps two mutable bindings. Without macros, you would need to write the temporary variable dance every time.

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