Templates & Macros
Templates
A template is the lighter tool: it expands to code at each call site, with fresh names for anything it declares. Reach for a template when you need the caller's syntax — a statement block, a lazily evaluated expression — rather than its value.
A Template as Compile-Time Substitution
template twice(body: untyped) =
## 'untyped' = accept the raw syntax; the argument is not evaluated first.
body
body
twice:
echo "tick" # expands to two echo statements
# Templates can also produce expressions (inlined at the call site):
template maxOf(a, b: untyped): untyped =
if a > b:
a
else:
b
echo maxOf(3, 7) # 7
Hygiene and Operator Precedence
Template substitution is textual, which is exactly why two rules matter: variables declared inside are hygienic (they cannot collide with the caller's names), but arguments are pasted as tokens (they can be re-parsed against surrounding operators).
template square(x: untyped): untyped =
x * x # the tokens of x are pasted twice
echo square(4) # 16
echo square(3 + 1) # 7 — expansion is 3 + 1 * 3 + 1, not (3 + 1) * (3 + 1)!
template safeSquare(x: untyped): untyped =
(x) * (x) # parentheses pin the argument into one group
echo safeSquare(3 + 1) # 16
template withCounter() =
var count = 0 # hygienic: this 'count' cannot collide with
inc count # any 'count' that exists at the call site
echo count
withCounter() # 1
var count = 100 # the caller's own variable is untouched
withCounter() # 1
echo count # 100
When a Template Beats a Proc
- Control-flow helpers — logging wrappers, retry guards, benchmarks: the body must run in the caller's scope, not in a new one.
- Zero-cost sugar — a short alias for a long fluent call, expanded and optimized like the code you would have written by hand.
- Not for anything that needs its own type checking, recursion, or a debugger breakpoint: that is a procedure's job. When in doubt, write a proc; convert to a template only when you must accept syntax.
Macros
A macro receives the arguments as a syntax tree, inspects it, and returns the tree that should replace the call. Nothing is textual any more: you manipulate nodes, so you can rename symbols, count expressions, read types, or refuse to compile.
Figure 1 — templates and macros act between parsing and code generation, so their cost is paid once, at compile time.
Macros Work on the Syntax Tree
import std/macros
macro debug(args: varargs[untyped]): untyped =
## Print the source text of each argument next to its evaluated value.
result = newStmtList() # we BUILD the replacement code
for arg in args:
result.add newCall(
bindSym"echo", # resolve 'echo' in the current scope
newLit(arg.repr), # the argument exactly as the caller wrote it
newLit(" = "), # a literal string node
arg) # the argument itself, evaluated once
let a = 3
let b = 4
debug(a, a + b) # a = 3
# a + b = 7
Three ideas make the example readable: arg.repr turns a tree back into source text, newLit wraps a value as a literal node, and bindSym"echo" resolves a name at macro-definition time so the expansion cannot be hijacked by a caller symbol with the same name.
quote do and Static Parameters
import std/macros
macro assertPositive(x: untyped): untyped =
## 'quote do' builds a tree from ordinary Nim source; backticks splice nodes in.
result = quote do:
if `x` <= 0:
raise newException(ValueError, "expected a positive value")
`x`
let value = assertPositive(5) # expands to a check plus the value
echo value # 5
# assertPositive(-1) # the generated check raises ValueError
import std/macros
macro greeting(name: static string): untyped =
## 'static' parameters must be known while compiling.
newLit("hello " & name) # the concatenation happens in the compiler
echo greeting("nim") # hello nim
const who = "world"
echo greeting(who) # a const counts as compile-time known
# var runtimeName = "world"
# echo greeting(runtimeName) # compile error: not a static value
quote do is the readable way to produce code; hand-built nodes are for cases the quote cannot express — renaming, counting, generating many declarations from data.
A Statement-List Macro
import std/macros
macro repeat(times: static int, body: untyped): untyped =
## Expand the body 'times' times: no counter, no loop, no runtime cost.
result = newStmtList()
for i in 0 ..< times:
result.add body.copyNimTree # each copy needs its own AST nodes
repeat(3):
echo "tick" # prints tick three times
copyNimTree is not optional: reusing the same node in several places would make the compiler treat the copies as one shared tree. Forgetting it is the most common macro bug.
Metaprogramming in Practice
Compile-time code is invisible at runtime, which is both its strength and its debugging difficulty. Two habits keep it manageable: verify the expansion instead of guessing, and keep generated code small enough to review by hand.
Inspecting the Expansion
import std/macros
macro triple(value: untyped): untyped =
## Suspect a macro? Print the tree it receives, at compile time.
static:
echo "received: ", value.treeRepr # printed while compiling
result = quote do:
3 * `value`
echo triple(2 + 5) # 21
# Print every expansion of a macro or template by name:
nim c --expandMacro:triple main.nim
# Inspect the ARC/ORC lowering of one procedure (memory, not macros):
nim c --expandArc:someProc main.nim
static: echo node.treeReprinside the macro shows the tree it received.{.expandMacros.}on a procedure asks the compiler to report the expanded body.error("message", node)is the correct way to reject bad input: the message points at the caller's code, not at the macro.
Review Checklist
- Solve it with a proc or a generic first; keep the macro for what the type system cannot express.
- Parenthesize every untyped argument you substitute more than once.
- Copy nodes with
copyNimTreewhenever one node is emitted twice. - Prefer
quote doover hand-built node lists — the code stays readable and re-checkable. - Use
staticparameters anderror(...)to fail during compilation, with a message a beginner can act on. - Test what the expansion does, not just what the macro returns;
--expandMacrois your debugger.