A macro is similar to an advanced but low level template. Macros can be used
to implement domain specific languages. A macro is a routine that operates
directly on the abstract syntax tree (AST) of the Nim programming language.
Unfortunately, the AST is implementation defined.
To write macros, one needs to know how the Nim concrete syntax is
converted to an abstract syntax tree. A macro receives ASTs as its input
parameters and returns a new AST. The compiler then analyzes the result
AST for errors - a macro cannot be used to circumvent error checking. Let r
be the result of a macro invocation m(args). r is inserted into the position of
the invocation m(args). r can contain further macro invocations, these are
processed after the expansion of m. The process iterates until no more macro
expansions can be performed or until some implementation defined
iteration limit is reached. Reaching the limit is a static error.
28.1. Macros API
The tree transformations that a macro can perform are enabled by the macros
standard module. The API is based on a single NimNode type that represents a
single node or a complete tree. Every NimNode has a node kind determining if
the node is an if statement, a routine call, etc. Some NimNodes can also
have children.
The NodeKind is a enum with fields like nnkStmtList or nnkIfStmt.
The nnk prefix exists for historical reasons.
The following example implements a debug command that accepts a variable number of arguments and writes them with their name and value:
193
import std/macros macro debug(args: varargs[untyped]): untyped = 1 result = newNimNode(nnkStmtList, args) 2 for a in args: 3
result.add newCall("write", ident"stdout", toStrLit(a)) 4
result.add newCall("write", ident"stdout", newLit(": ")) 5
result.add newCall("writeLine", ident"stdout", a) 6
var 7 a: array[0..10, int] x = "some string" a[0] = 42 a[1] = 45 debug(a[0], a[1], x) 8 1 Inside the debug macro every parameter (that is not a static parameter) is of type NimNode and not of the type that is written down in the signature. That means within the body of debug args is of type NimNode and not of type varargs[untyped]. 2 debug returns a list of statements (nnkStmtList). 3 For every passed argument do: 4 Add a call to the statement list that writes the expression; toStrLit converts an AST to its string representation. 5 Also add a call to the statement list that writes the string literal ": ". 6 Add a call to the statement list that writes the expressions value of the current argument a. 7 Example data that is passed to the debug macro. 8 Call to the debug macro. The macro call expands to: write(stdout, "a[0]") write(stdout, ": ") writeLine(stdout, a[0]) write(stdout, "a[1]") write(stdout, ": ") writeLine(stdout, a[1]) write(stdout, "x") 194 write(stdout, ": ") writeLine(stdout, x) Arguments that are passed to a varargs parameter are wrapped in an array constructor expression. This is why debug iterates over all of args's children. 28.2. BindSym The above debug macro relies on the fact that write, writeLine and stdout are declared in the system module and are thus visible in the instantiating context. Via the bindSym builtin there is a way to use bound identifiers (a.k.a. symbols) instead of using unbound identifiers: import std/macros macro debug(n: varargs[typed]): untyped =
result = newNimNode(nnkStmtList, n)
for x in n:
# we can bind symbols in scope via 'bindSym':
result.add newCall(bindSym"write", bindSym"stdout", toStrLit(x))
result.add newCall(bindSym"write", bindSym"stdout", newStrLitNode": ")
result.add newCall(bindSym"writeLine", bindSym"stdout", x)
var
a: array[0..10, int]
x = "some string"
a[0] = 42 a[1] = 45 debug(a[0], a[1], x) The macro call expands to: write(stdout, "a[0]") write(stdout, ": ") writeLine(stdout, a[0]) write(stdout, "a[1]") write(stdout, ": ") writeLine(stdout, a[1]) write(stdout, "x") write(stdout, ": ")
195
writeLine(stdout, x) However, the symbols write, writeLine and stdout are already bound and are not looked up again. As the example shows, bindSym does work with overloaded symbols implicitly.
The distinction between bindSym"name" and ident"name" is
easiest to understand when one takes scope into consideration.
This is valid code:
import std/macros
macro m(a: string): untyped =
proc helper(a: string) = echo a
result = newCall(bindSym"helper", a)
m "abc"
And this is invalid code:
import std/macros
macro m(a: string): untyped =
proc helper(a: string) = echo a
result = newCall(ident"helper", a)
m "abc"
Note how in both cases helper is local to the macro m and
invisible outside of `m’s body.
28.3. For loop macros A macro that only takes a single expression of the special type system.ForLoopStmt can rewrite the entire for loop: import std/macros macro example(loop: ForLoopStmt) = result = newTree(nnkForStmt) # Create a new For loop. result.add loop[^3] # This is "item". 196 result.add loop[^2][^1] # This is "[1, 2, 3]". result.add newCall(bindSym"echo", loop[0]) for item in example([1, 2, 3]): discard Expands to: for item in items([1, 2, 3]): echo item Another example: import std/macros macro enumerate(x: ForLoopStmt): untyped = expectKind x, nnkForStmt # check if the starting count is specified: var countStart = if x[^2].len == 2: newLit(0) else: x[^2][1] result = newStmtList() # we strip off the first for loop variable # and use it as an integer counter: result.add newVarStmt(x[0], countStart) var body = x[^1] if body.kind != nnkStmtList:
body = newTree(nnkStmtList, body)
body.add newCall(bindSym"inc", x[0]) var newFor = newTree(nnkForStmt) for i in 1..x.len-3:
newFor.add x[i]
# transform enumerate(X) to 'X':
newFor.add x[^2][^1]
newFor.add body
result.add newFor
# wrap the whole macro in a block to create a new scope:
result = newTree(nnkBlockExpr, newEmptyNode(), result)
for a, b in enumerate(items([1, 2, 3])):
echo a, " ", b
# without wrapping the macro in a block, we'd need to choose different
# names for a and b here to avoid redefinition errors
for a, b in enumerate(10, [1, 2, 3, 5]):
echo a, " ", b
For readability and maintainability it is best to use the least
powerful programming construct that still accomplishes the
197
goal. So the "check list" is:
(1) Use an ordinary proc/iterator, if possible. (2) Else: Use a
generic proc/iterator, if possible. (3) Else: Use a template, if
possible. (4) Else: Use a macro.
The Part III: Mastering Macros contains many more examples of how to write and use macros.