# defmacro
Define a compile-time macro that transforms code before evaluation.
Macros operate on AST forms (unevaluated SExprs), not values.
They expand at compile time — zero runtime cost.
## Syntax
defmacro name { |params| body }
## Parameters
Macros support the same parameter syntax as functions:
| Syntax | Description |
|--------|-------------|
| `a` | Simple positional parameter |
| `a^string` | Typed parameter |
| `a="default"` | Parameter with default value |
| `@rest` | Collect remaining args as a list (must be last) |
### Variadic `@rest`
The `@` prefix mirrors the `@{...}` vector literal — it signals "collect
remaining args into a list". The name after `@` is bound verbatim; pick
whatever reads best at the call site (`@rest`, `@items`, `@args`, …):
defmacro greet {|greeting @names| [each $names {[echo [str $greeting " " $it "!"]]}] } [greet "Hello" "Alice" "Bob" "Charlie"]
The rest-collector works in both `defmacro` and `def` (functions), and
combines with defaults:
def f {|a b=10 @rest| ($a + $b + [len $rest])}
## Code Generation
Macros substitute `$param` references with the matching argument's AST.
Most macros don't need anything beyond this direct substitution — reach
for `macro/quote` only when generating brand-new code from scratch.
### `macro/quote` / `macro/unquote`
`[macro/quote ...]` builds code while preserving `$param` references
as literal symbols (they are not substituted). `[macro/unquote $expr]`
is the escape: inside a quote, it substitutes the parameter's value. Both
forms must use `[...]` brackets — deft's `( ... )` parens are infix-only
and are not function calls.
defmacro unless {|cond body| [macro/quote [if [macro/unquote $cond] nil [macro/unquote $body]]] } [unless false { echo "ran" }] # expands to: [if false nil { echo "ran" }]
The quoted body becomes the macro expansion and is compiled in place.
### `macro/splice` / `macro/unquote-splice`
`[macro/splice $list]` (also written `[macro/unquote-splice $list]`)
inlines the elements of a list into the parent form — useful for
building calls with a dynamic number of args. Again, use `[...]`
brackets:
defmacro call-with {|func @args| [macro/quote [macro/unquote $func [macro/splice $args]]] } [call-with str "a" "b" "c"] # expands to [str "a" "b" "c"]
Without `macro/splice` the whole list would land as a single argument:
defmacro call-with-broken {|func @args| [macro/quote [macro/unquote $func $args]] } [call-with-broken str "a" "b" "c"] # expands to [str @{"a" "b" "c"}] — wrong!
## Hygiene
Deft macros are not hygienic by default — a symbol introduced by a
macro will collide with a same-named symbol at the call site. Two tools
fix this:
### `[macro/gensym "prefix"]`
Generate a unique keyword value of the form `:prefix#N`. Each call
returns a distinct value; useful as a unique map key or opaque token.
defmacro with-token {|body| set tok [macro/gensym "tok"] [echo $tok] [macro/unquote $body] }
Note: `macro/gensym` runs at runtime and returns a value, so it can be
used as a key/token but cannot become the literal name of a new
variable (deft `set` takes a literal name, not a value). For
compile-time symbol hygiene inside generated code, use
`macro/syntax-quote`.
### `[macro/syntax-quote ...]`
Like `macro/quote`, but with two differences aimed at hygienic code
generation:
1. `$param` references are substituted (acts like the normal macro
body) — you don't need `[macro/unquote ...]` for parameters.
2. Bare symbols (e.g. `tmp`) are automatically renamed to unique
`tmp#N` names, so they cannot collide with caller variables.
defmacro swap {|a b| [macro/syntax-quote { set tmp $a set $a $b set $b $tmp }] }
Here `tmp` is auto-gensym'd to `tmp#42` (or similar), so it won't
capture a caller variable named `tmp`. `$a` and `$b` are substituted
normally.
Use `[macro/unquote $expr]` inside `macro/syntax-quote` for the rare case
where you need to force substitution of something that isn't a `$param`
reference.
### `begin`
`[begin e1 e2 ... en]` evaluates each expression in order and returns
the value of the last. Multi-statement macro bodies (and `{...}` blocks
with more than one statement) are wrapped in `begin` by the parser, so
explicit use is rare — but it's useful inside
`macro/quote`/`macro/syntax-quote` when you want to emit a multi-step
expansion as a single expression.
defmacro inc-and-return {|x| [macro/quote [begin [set $x ($x + 1)] $x]] }
## String Types in Macros
Backtick strings (`` `...` ``) are ideal for macro template strings:
they support `$var` substitution but treat `[...]` as literal characters
(no embedded expression evaluation).
defmacro debug {|expr| [macro/quote { set result [macro/unquote $expr] echo `DEBUG: $result` }] }
## Compile-time conditionals — `macro/compile-when`
`[macro/compile-when cond body]` evaluates `cond` during compilation
(through the VM). If truthy, `body` is compiled in place; if falsy,
`body` is discarded entirely — the call site emits `push_null`,
producing zero runtime code.
This is how level-filtered logging achieves zero cost: filtered-out
calls never enter the bytecode.
def log-level ([os/env "DEFT_LOG_LEVEL"] or "debug") defmacro debug {|@args| [macro/compile-when [in? @{"debug"} $log-level] [log/debug [macro/splice $args]]] }
The condition must be evaluable at compile time — literals, globals
from `require`d modules, and pure functions of those. It cannot
reference runtime locals or closure captures. See
macro/compile-when for full details.
## Notes
- Macros must be defined before use (evaluated top-to-bottom)
- Macros can shadow builtins (checked before builtins in `compileCall`)
- Self-referential macros (calling themselves) are detected and fall through to the builtin
- Maximum 32 fixed parameters (plus one `@rest`)
- Maximum expansion depth: 64 levels
## See Also
`def`, `macro/quote`, `macro/unquote`, `macro/syntax-quote`,
`macro/gensym`, `macro/splice`, `begin`, `macro/compile-when`