Macros and AST Manipulation

28.1 What Macros Are

Procedures that run at compile time, receive the AST (NimNode) of their arguments, and return a new AST.

import std/macros.

28.2 Inspecting ASTs

import std/macros
dumpTree:
  let x = 1 + 2
# StmtList
#   LetSection
#     IdentDefs
#       Ident "x"
#       Empty
#       Infix
#         Ident "+"
#         IntLit 1
#         IntLit 2
dumpAstGen: echo "hi"       # prints the code that constructs this AST
macro show(x: untyped): untyped =
  echo x.treeRepr           # print at compile time
  echo x.repr               # back to source
  result = x

28.3 Building ASTs

macro debugPrint(args: varargs[untyped]): untyped =
  result = newStmtList()
  for a in args:
    result.add newCall("echo", newLit(a.repr & " = "), a)

let x = 5; let y = "hi"
debugPrint(x, y, x * 2)
# x = 5
# y = hi
# x * 2 = 10

28.4 quote do and genAst

macro repeat(n: static int, body: untyped): untyped =
  result = quote do:
    for i in 0 ..< `n`:          # backticks interpolate NimNodes / values
      `body`

import std/genasts
macro assertPositive(x: untyped): untyped =
  let s = x.repr
  result = genAst(x, s):
    if x <= 0: raise newException(ValueError, s & " must be positive")

28.5 Common NimNode API

kind, len, [], add, insert, del Node navigation and editing

newIdentNode, ident("x"), newLit(v), newCall, newStmtList, newProc, newTree(nnkX, ...)

expectKind, expectLen, error("msg", node) Validation and errors strVal, intVal, floatVal

getType, getTypeImpl, getTypeInst, getImpl Type information (typed macros)

treeRepr, lispRepr, repr, astGenRepr bindSym, genSym parseStmt, parseExpr Parse a string into an AST copyNimTree, body, params, name

28.6 Macro Examples

# 1. Generate getters for all fields of an object
macro genGetters(T: typedesc): untyped =
  result = newStmtList()
  let impl = T.getImpl
  let recList = impl[2][2]
  for field in recList:
    let fname = field[0]
    let ftype = field[1]
    let getter = ident("get" & capitalizeAscii($fname))
    result.add quote do:
      proc `getter`*(o: `T`): `ftype` = o.`fname`

# 2. Macro pragma: time a proc
macro timed(p: untyped): untyped =
  p.expectKind nnkProcDef
  let name = $p.name
  let body = p.body
  p.body = quote do:
    let t0 = cpuTime()
    defer: echo `name`, " took ", cpuTime() - t0
    `body`
  result = p

proc slowWork() {.timed.} = sleep(100)

# 3. Typed macro using type info
macro fieldCount(T: typedesc): int =
  newLit(T.getTypeImpl[1].getTypeImpl[2].len)

28.7 Macro Best Practices

Prefer procs → generics → templates → macros (least power first).

Validate input and report errors with error("...", node) so users see good messages.

Use quote/genAst for readability. Test macros with expandMacros and dumpTree.