Macros
A macro is an ordinary mu function that runs at compile time. It takes code and returns code:
fn(CODE…) -> CODE
Everything else follows from that. There is no separate macro language, no AST map format to learn, and no host API to memorise.
Code is a value
quote { … } evaluates to a code value — mu’s CODE type, as first-class
as a list or a closure. Inside a quote, unquote is a hole: the expression is
evaluated in the enclosing scope and its code is spliced in.
c := quote { x + 1 }
print(type(c)) // CODE
A quote is a literal with holes, not a string. Everything inside it is real mu syntax, parsed and checked as such, and source positions survive into the expansion — so a diagnostic can point at where the code was actually written.
Define a macro
import "macro"
macro.define("unless", fn(condition, body) {
return quote {
if !unquote condition {
unquote body
}
}
})
unless(ready, start())
The handler receives one code value per argument the caller wrote, and returns a code value. Argument counts are checked like any other mu call.
A macro may be variadic, in which case the trailing parameter holds a list of
code values that ... splices back out:
macro.define("listOf", fn(items...) {
return quote { [unquote items...] }
})
listOf(1, 2, 3) // [1, 2, 3]
listOf() // []
macro.define must appear at top level, but it does not have to precede the
first call that uses it. The compiler scans the whole file for macro
registrations before expansion; keeping definitions near their uses is a style
choice, not a language restriction.
Two things worth knowing
unquote binds like a prefix operator, at the same level as ! and -. It
binds tighter than every infix operator but still absorbs call, index and member
chains:
quote { (unquote list)[0] } // index the caller's list
quote { unquote list[0] } // index the code value — almost never right
quote always yields block code, never an expression. That means a quote’s
kind never depends on runtime data. The cost is paid at the splice site: block
code used where an expression is expected unwraps when it holds exactly one
expression, and is an error otherwise.
A worked example: writing iterators
The clearest sign a macro is the right tool is a return that belongs to the
caller. A function cannot return on its caller’s behalf, so anything that has
to leave the calling function has to expand into it.
lib/iter.mu exports two such macros. foreach pulls an iterator until it is
dry, and emit publishes one value — returning from the producer if the
consumer has cancelled it:
import "iter"
fn double(source) {
return iter.iterator(fn(yield) {
iter.foreach(item, source, fn() {
iter.emit(yield, item * 2)
})
return nil
}, source)
}
print(iter.collect(double(iter.from_array([1, 2, 3])))) // [2, 4, 6]
Written out by hand that same combinator is a while true, a pull, an
end-of-stream test and a cancellation guard — four lines of protocol around one
line of work, repeated in every combinator, and wrong the first time anybody
forgets the guard.
foreach splices its body rather than calling it, which is what makes
return, break and continue mean what they look like:
fn find_over(source, limit) {
iter.foreach(value, source, fn() {
if value > limit {
iter.stop(source)
return value // returns from find_over
}
})
return nil
}
A higher-order function taking a callback can offer none of those three. That is
the difference between a macro and a function here, and it is the whole reason
iter.each cannot replace iter.foreach.
One rule comes with writing macros like this: use each argument once, or bind
it first. foreach binds its source to a temporary before the loop, so
iter.foreach(x, from_array(xs), …) builds one iterator rather than a fresh one
on every pass. Hygiene protects names; nothing protects evaluation count.
Where macros run
Macros expand on the build machine, before bytecode is emitted, so nothing about them survives into a compiled binary — there is no runtime cost and no runtime dependency.
For the same reason the macro phase withholds capabilities that would make a
build depend on where it ran: the filesystem, the environment, and raw syscalls.
A module that genuinely needs one opts in with macro.requires(["env"]), which
also records that its expansions are not reproducible across machines.
Read more in docs/Macros.md, docs/MacroTutorial.md, and — for the reasoning and the rejected alternatives — docs/MacroOverhaulRFC.md.
Next steps
- Review the language tour for syntax basics.
- Learn how macros fit into the pipeline in the runtime guide.
- Explore the FFI if you need native bindings.