Debugging macros in Nim
2 April, 2021
Nim provides three essential macros in the built-in macros
module that are very helpful when writing or debugging macros. They are: dumpTree
, dumpAstGen
and expandMacros
.
One thing to note about all these macros is that they'll output their result (printing) at compile-time, which means if you compile the same source code twice without any changes, you won't get any output the second time.
dumpTree
dumpTree
will print out the parsed AST of a code block. When writing a Macro that you know
the code output of but not exactly sure how to write the AST-generating code, this will
be helpful to figure that out (especially for figuring out how many Empty nodes a
statement list node should contain).
For example, when writing a Macro that generates the AST of a simple function (or method of Line
)
proc default(line: Line): bool =
write(stdout, ["*** Unknown syntax: ", line.text, " ***\n"])
You can create a simple ast.nim
file with:
import macros
# Other stuff
dumpTree:
proc default(line: Line): bool =
write(stdout, ["*** Unknown syntax: ", line.text, " ***\n"])
and then:
nim r ast.nim
This will print out:
StmtList
ProcDef
Ident "default"
Empty
Empty
FormalParams
Ident "bool"
IdentDefs
Ident "line"
Ident "Line"
Empty
Empty
Empty
StmtList
Call
Ident "write"
Ident "stdout"
Bracket
StrLit "*** Unknown syntax: "
DotExpr
Ident "line"
Ident "text"
StrLit " ***\n"
dumpAstGen
dumpAstGen
is somewhat like dumpTree
macro above, but instead of returning the AST, it generates a snippet of code that would return that AST.
Replacing dumpTree
with dumpAstGen
in the example above would print (at compile time) the following:
nnkStmtList.newTree(
nnkProcDef.newTree(
newIdentNode("default"),
newEmptyNode(),
newEmptyNode(),
nnkFormalParams.newTree(
newIdentNode("bool"),
nnkIdentDefs.newTree(
newIdentNode("line"),
newIdentNode("Line"),
newEmptyNode()
)
),
newEmptyNode(),
newEmptyNode(),
nnkStmtList.newTree(
nnkCall.newTree(
newIdentNode("write"),
newIdentNode("stdout"),
nnkBracket.newTree(
newLit("*** Unknown syntax: "),
nnkDotExpr.newTree(
newIdentNode("line"),
newIdentNode("text")
),
newLit(" ***\n")
)
)
)
)
)
expandMacros
The expandMacros
macro is somewhat the reverse. It prints out the expanded version of macros used in its block. If you're curious about how a certain macro does what it does, you can use expandMacros
to see the AST it generates.
Say we have a simpler macro called greeter
that generates
a function that greets us.
macro greeter*(ident: untyped) =
result = nnkStmtList.newTree(
nnkProcDef.newTree(
newIdentNode(repr ident),
newEmptyNode(),
newEmptyNode(),
nnkFormalParams.newTree(
newEmptyNode(),
nnkIdentDefs.newTree(
newIdentNode("name"),
newIdentNode("string"),
newEmptyNode()
)
),
newEmptyNode(),
newEmptyNode(),
nnkStmtList.newTree(
nnkCall.newTree(
newIdentNode("echo"),
newLit("Hi "),
newIdentNode("name")
)
)
)
)
We can use macro above like this:
greeter(hej)
If we want to know what greeter
is doing we could wrap the call to it with a expandMacros
:
expandMacros:
greeter(hej)
This will print out the following (at compile time):
proc hej(name: string) =
echo(["Hi ", name])
Read more about metaprogramming in Nim in Chapter 9 of Nim in Action by Dominik Picheta.