← Home

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.