Templates & Macros

Nim has no preprocessor and no runtime reflection: metaprogramming happens in the compiler, on syntax trees. Templates substitute source fragments hygienically; macros receive the parsed tree, inspect it and return a new one. Both run while the program is being compiled, so they cost nothing at runtime.

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.

Compile-time pipeline: parse, expand templates and macros, then generate code

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.treeRepr inside 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

  1. Solve it with a proc or a generic first; keep the macro for what the type system cannot express.
  2. Parenthesize every untyped argument you substitute more than once.
  3. Copy nodes with copyNimTree whenever one node is emitted twice.
  4. Prefer quote do over hand-built node lists — the code stays readable and re-checkable.
  5. Use static parameters and error(...) to fail during compilation, with a message a beginner can act on.
  6. Test what the expansion does, not just what the macro returns; --expandMacro is your debugger.