Use when nim's metaprogramming including macros, templates, compile-time evaluation, AST manipulation, code generation, DSL creation, and leveraging compile-time computation for performance and abstraction in systems programming.
Read-only skill
Additional assets for this skill
This skill cannot use any tools. It operates in read-only mode without the ability to modify files or execute commands.
Nim's metaprogramming system provides powerful compile-time code generation and manipulation through templates, macros, and compile-time evaluation. This enables zero-overhead abstractions, domain-specific languages, and optimizations performed at compile time rather than runtime.
The system operates on Nim's abstract syntax tree (AST), allowing inspection and transformation of code structure. Templates provide hygienic macro-like substitution, while macros enable full AST manipulation. Compile-time evaluation executes Nim code during compilation for constant folding and validation.
This skill covers templates for code substitution, macros for AST transformation, compile-time evaluation with static blocks, AST inspection and manipulation, code generation patterns, DSL creation, and metaprogramming best practices.
Templates provide hygienic code substitution with type safety and inline expansion.
# Basic template
template max(a, b: untyped): untyped =
if a > b: a else: b
echo max(5, 10) # Expands inline
# Template with multiple statements
template withFile(filename: string, body: untyped): untyped =
let file = open(filename, fmRead)
try:
body
finally:
file.close()
withFile("data.txt"):
for line in file.lines:
echo line
# Template with inject
template defineProperty(name: untyped, typ: typedesc): untyped =
var `name Value`: typ
proc `get name`(): typ {.inject.} =
`name Value`
proc `set name`(value: typ) {.inject.} =
`name Value` = value
defineProperty(age, int)
setAge(30)
echo getAge() # 30
# Template accepting block
template benchmark(name: string, code: untyped): untyped =
let start = cpuTime()
code
let elapsed = cpuTime() - start
echo name, ": ", elapsed, " seconds"
benchmark "computation":
var sum = 0
for i in 1..1000000:
sum += i
# Template with typed parameters
template swap(a, b: typed): untyped =
let tmp = a
a = b
b = tmp
var x = 5
var y = 10
swap(x, y)
echo x, " ", y # 10 5
# Template for DSL
template html(body: untyped): string =
var result = ""
template tag(name: string, content: untyped): untyped =
result.add "<" & name & ">"
content
result.add "</" & name & ">"
body
result
let page = html:
tag "html":
tag "body":
tag "h1":
result.add "Hello World"
# Template with generic constraints
template findMax[T: Ordinal](items: openArray[T]): T =
var maxVal = items[0]
for item in items:
if item > maxVal:
maxVal = item
maxVal
echo findMax([1, 5, 3, 9, 2])
# Template forwarding
template forward(call: untyped): untyped =
echo "Before call"
call
echo "After call"
proc myProc() =
echo "Inside proc"
forward myProc()
# Template for custom operators
template `~=`(a, b: float): bool =
abs(a - b) < 0.0001
echo 1.0 ~= 1.00001 # true
# Template with immediate parameter
template log(msg: string): untyped =
when defined(debug):
echo "[LOG] ", msg
log "Debug message" # Only in debug builds
Templates provide compile-time code substitution with hygiene and type safety.
Macros transform AST at compile time, enabling powerful code generation and domain-specific languages.
import macros
# Basic macro
macro debug(n: varargs[typed]): untyped =
result = newStmtList()
for arg in n:
result.add quote do:
echo astToStr(`arg`), " = ", `arg`
let x = 5
let y = 10
debug x, y, x + y
# Macro examining AST
macro inspect(node: untyped): untyped =
echo node.treeRepr
result = node
inspect:
let x = 5 + 3
# Building AST manually
macro makeProc(name: untyped, body: untyped): untyped =
result = newProc(
name = name,
params = [newEmptyNode()],
body = body
)
makeProc greet:
echo "Hello!"
greet()
# Macro for property generation
macro property(name: untyped, typ: typedesc): untyped =
let
fieldName = ident($name & "Field")
getName = ident("get" & $name)
setName = ident("set" & $name)
result = quote do:
var `fieldName`: `typ`
proc `getName`(): `typ` =
`fieldName`
proc `setName`(value: `typ`) =
`fieldName` = value
property(count, int)
setCount(42)
echo getCount()
# Case statement macro
macro switch(value: typed, branches: varargs[untyped]): untyped =
result = nnkCaseStmt.newTree(value)
for branch in branches:
expectKind(branch, nnkCall)
let pattern = branch[0]
let body = branch[1]
result.add nnkOfBranch.newTree(pattern, body)
switch(5):
1: echo "one"
2: echo "two"
5: echo "five"
# Builder pattern macro
macro build(typ: typedesc, fields: varargs[untyped]): untyped =
result = nnkObjConstr.newTree(typ)
for field in fields:
expectKind(field, nnkExprEqExpr)
result.add field
type Person = object
name: string
age: int
let p = build(Person, name = "Alice", age = 30)
# Compile-time assertion macro
macro staticAssert(condition: bool, message: string): untyped =
if not condition.boolVal:
error(message.strVal)
result = newEmptyNode()
staticAssert sizeof(int) >= 4, "int must be at least 4 bytes"
# Unrolling loop macro
macro unroll(count: static[int], body: untyped): untyped =
result = newStmtList()
for i in 0..<count:
let iNode = newLit(i)
result.add body.replace(ident("it"), iNode)
unroll 5:
echo "Iteration: ", it
# Pattern matching macro
macro match(value: typed, patterns: varargs[untyped]): untyped =
result = nnkIfStmt.newTree()
for pattern in patterns:
expectMinLen(pattern, 2)
let condition = pattern[0]
let body = pattern[1]
let check = quote do:
`value` == `condition`
result.add nnkElifBranch.newTree(check, body)
Macros enable compile-time code transformation and generation through AST manipulation.
Nim executes code at compile time for constants, optimizations, and validations.
# Compile-time constants
const maxSize = 100
const computed = maxSize * 2 + 10 # Evaluated at compile time
# Compile-time function evaluation
proc factorial(n: int): int =
if n <= 1: 1 else: n * factorial(n - 1)
const fact10 = factorial(10) # Computed at compile time
# Static block
static:
echo "This runs at compile time"
echo "Factorial of 10 is: ", factorial(10)
# Compile-time type information
proc sizeInfo[T](x: T): string =
static:
"Size of " & $T & " is " & $sizeof(T) & " bytes"
echo sizeInfo(5)
echo sizeInfo(5.0)
# Compile-time conditional compilation
when sizeof(int) == 8:
proc printSize() = echo "64-bit platform"
else:
proc printSize() = echo "32-bit platform"
# Compile-time string operations
const
version = "1.0.0"
parts = version.split('.')
major = parts[0].parseInt
when major >= 1:
echo "Version 1.0 or later"
# Compile-time file reading
const configData = staticRead("config.txt")
proc getConfig(): string =
configData
# Compile-time HTTP requests (with stdlib)
const apiResponse = staticExec("curl -s https://api.example.com/data")
# Compile-time code generation
proc generateAccessors(fields: seq[string]): string =
result = ""
for field in fields:
result.add &"""
proc get{field.capitalizeAscii}(): int = {field}
proc set{field.capitalizeAscii}(val: int) = {field} = val
"""
const accessors = generateAccessors(@["x", "y", "z"])
# Static parameter constraints
proc processArray[T; N: static[int]](arr: array[N, T]) =
static:
echo "Array size: ", N
for item in arr:
echo item
processArray([1, 2, 3, 4, 5])
# Compile-time assertions
static:
doAssert sizeof(int) >= 4, "int too small"
doAssert sizeof(ptr) == sizeof(int), "pointer size mismatch"
# Compile-time regex compilation
import re
const emailPattern = re"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
proc validateEmail(email: string): bool =
email.match(emailPattern)
# Compile-time validation
proc validateConfig() =
static:
when not fileExists("config.txt"):
{.fatal: "config.txt not found".}
# Compile-time optimization
proc optimizedPower(base: float, exp: static[int]): float =
when exp == 0:
1.0
elif exp == 1:
base
elif exp mod 2 == 0:
let half = optimizedPower(base, exp div 2)
half * half
else:
base * optimizedPower(base, exp - 1)
echo optimizedPower(2.0, 10)
Compile-time evaluation enables zero-runtime-cost abstractions and build-time validations.
Nim's metaprogramming enables creating domain-specific languages with natural syntax.
# HTML DSL
macro html(body: untyped): string =
proc processNode(node: NimNode): NimNode =
case node.kind
of nnkCall, nnkCommand:
let tag = node[0]
let attrs = newSeq[NimNode]()
var content = newStmtList()
for i in 1..<node.len:
if node[i].kind == nnkExprEqExpr:
attrs.add node[i]
else:
content.add processNode(node[i])
result = quote do:
result.add "<" & `tag`.astToStr & ">"
`content`
result.add "</" & `tag`.astToStr & ">"
of nnkStrLit:
result = quote do:
result.add `node`
else:
result = newStmtList()
result = quote do:
var result = ""
`processNode(body)`
result
let page = html:
html:
head:
title: "My Page"
body:
h1: "Welcome"
p: "Hello World"
# SQL DSL
macro select(fields: varargs[untyped]): string =
var fieldList = ""
for i, field in fields:
if i > 0: fieldList.add ", "
fieldList.add $field
result = newLit("SELECT " & fieldList)
macro fromTable(table: untyped): string =
newLit(" FROM " & $table)
let query = select(name, age) & fromTable(users)
# Test DSL
macro describe(name: string, tests: untyped): untyped =
result = newStmtList()
for test in tests:
if test.kind == nnkCall and $test[0] == "it":
let testName = test[1]
let testBody = test[2]
result.add quote do:
echo "Testing: ", `testName`
try:
`testBody`
echo " ✓ Passed"
except:
echo " ✗ Failed"
describe "Math operations":
it "adds numbers":
doAssert 1 + 1 == 2
it "multiplies numbers":
doAssert 2 * 3 == 6
# Configuration DSL
macro config(body: untyped): untyped =
result = nnkObjConstr.newTree(ident("Config"))
for stmt in body:
if stmt.kind == nnkCall:
let key = stmt[0]
let value = stmt[1]
result.add nnkExprColonExpr.newTree(key, value)
type Config = object
host: string
port: int
debug: bool
let cfg = config:
host("localhost")
port(8080)
debug(true)
# Builder DSL
macro builder(typ: typedesc, body: untyped): untyped =
var assignments = newStmtList()
for stmt in body:
if stmt.kind == nnkCall:
let field = stmt[0]
let value = stmt[1]
assignments.add quote do:
result.`field` = `value`
result = quote do:
var result: `typ`
`assignments`
result
type Request = object
url: string
method: string
headers: seq[string]
let req = builder(Request):
url("https://api.example.com")
method("GET")
DSLs enable domain-specific notation within Nim while maintaining type safety.
Metaprogramming enables automated code generation from specifications or runtime data.
# Generate enum from compile-time list
macro generateEnum(name: untyped, values: static[seq[string]]): untyped =
result = nnkTypeSection.newTree(
nnkTypeDef.newTree(
name,
newEmptyNode(),
nnkEnumTy.newTree(newEmptyNode())
)
)
for value in values:
result[0][2].add ident(value)
generateEnum(Color, @["Red", "Green", "Blue"])
# Generate getters/setters
macro generateAccessors(typ: typedesc): untyped =
let impl = typ.getImpl
result = newStmtList()
for field in impl[2][2]:
let fieldName = field[0]
let fieldType = field[1]
let getter = ident("get" & ($fieldName).capitalizeAscii)
let setter = ident("set" & ($fieldName).capitalizeAscii)
result.add quote do:
proc `getter`(obj: `typ`): `fieldType` =
obj.`fieldName`
proc `setter`(obj: var `typ`, value: `fieldType`) =
obj.`fieldName` = value
type Person = object
name: string
age: int
generateAccessors(Person)
# Generate pattern matching
macro matchGen(value: typed, patterns: varargs[untyped]): untyped =
result = nnkCaseStmt.newTree(value)
for pattern in patterns:
let condition = pattern[0]
let body = pattern[1]
result.add nnkOfBranch.newTree(condition, body)
# Generate state machine
macro stateMachine(states: varargs[untyped]): untyped =
var stateEnum = nnkEnumTy.newTree(newEmptyNode())
for state in states:
stateEnum.add ident($state)
result = nnkTypeSection.newTree(
nnkTypeDef.newTree(
ident("State"),
newEmptyNode(),
stateEnum
)
)
stateMachine Idle, Running, Stopped
# Generate serialization
macro deriveJson(typ: typedesc): untyped =
result = newStmtList()
# Generate toJson proc
result.add quote do:
proc toJson(obj: `typ`): JsonNode =
result = newJObject()
# Add fields...
# Generate validators
macro validate(typ: typedesc, rules: untyped): untyped =
result = quote do:
proc validate(obj: `typ`): bool =
# Generated validation logic
true
Code generation reduces boilerplate and ensures consistency across similar implementations.
Use templates for simple substitutions to avoid macro complexity when AST manipulation isn't needed
Prefer typed macro parameters over untyped when possible for better type checking
Test macros thoroughly as compile-time errors are harder to debug than runtime errors
Document macro usage with examples since expanded code isn't visible to users
Use quote do for AST generation to write natural Nim code instead of manual AST construction
Leverage compile-time evaluation for validations and optimizations without runtime cost
Keep macros focused on single responsibilities for maintainability
Use static blocks for compile-time side effects like logging or validation
Provide error messages in macros using error() for clear compile-time failures
Test macro expansions by examining generated code with dumpTree or expandMacros
Overusing macros for problems solvable with templates adds unnecessary complexity
Not handling AST node kinds properly causes compilation failures on unexpected input
Forgetting hygiene in templates can capture unintended identifiers from calling scope
Creating overly complex macros makes code hard to understand and maintain
Not validating macro inputs leads to confusing error messages at macro expansion
Mixing runtime and compile-time code without static blocks causes errors
Assuming AST structure without checking node kinds breaks on different inputs
Not using result variable in templates returns last statement unexpectedly
Creating DSLs that are too magical reduces code readability and maintainability
Forgetting to return nodes from macros causes empty code generation
Apply templates for zero-overhead abstractions replacing repetitive patterns.
Use macros when transforming or generating code based on compile-time information.
Leverage compile-time evaluation for constants, validations, and build-time optimizations.
Create DSLs for domain-specific problems requiring specialized notation.
Generate code for boilerplate like serialization, getters, or pattern matching.
Use metaprogramming for performance-critical abstractions with zero runtime cost.