Creating a simple macro

Guest post
This is a guest post by Antonis Geralis. If you would like to publish articles as a guest author on nim-lang.org then get in touch with us via Twitter or otherwise. You can also just create a PR like this author did.

Hello, as you might know Nim is a powerful programming language that supports metaprogramming using macros. Though a lot of Nim programmers are unaware of their merits due to lack of learning resources. The first part of this series will discuss the use of macros to simplify the creation of boilerplate code in Nim.

Suppose we have code which builds a directed graph.

  proc buildCityGraph(): Digraph =
    result = initGraph()
    result.addEdge(initEdge(initNode("Boston"), initNode("Providence")))
    result.addEdge(initEdge(initNode("Boston"), initNode("New York")))
    result.addEdge(initEdge(initNode("Providence"), initNode("Boston")))
    result.addEdge(initEdge(initNode("Providence"), initNode("New York")))
    ...

A template could be used to reduce the amount of typing. For example:

  template adder(graph, src, dest): untyped =
    graph.addEdge(initEdge(initNode(src), initNode(dest)))

However I would like to use an operator with a nice syntax, like: "Boston" -> "Providence" A template could do so too here, but I want to show how macros work.

Macros can be used to implement domain specific languages.
—From the Nim manual.

To begin, this is how calling our macro will look like:

  proc buildCityGraph(): Digraph =
    result = initGraph()
    edges(result):
      "Boston" -> "Providence"
      "Boston" -> "New York"
      "Providence" -> "Boston"
      "Providence" -> "New York"
      ...

We can pass the body of this unfinished macro to dumpTree to better understand how it will work.

StmtList
  Infix
    Ident "->"
    StrLit "Boston"
    StrLit "Providence"
  Infix
    Ident "->"
    StrLit "Boston"
    StrLit "New York"
  ...

A working first version looks like this:

macro edges(head, body: untyped): untyped =
  template adder(graph, src, dest): untyped =
    graph.addEdge(initEdge(initNode(src), initNode(dest)))

  # Create a NimNode of kind nnkStmtList for the result
  result = newStmtList()
  for n in body:
    # Check if it is an Infix NimNode with the operator
    # we look to implement.
    if n.kind == nnkInfix and $n[0] == "->":
      # we pass the template to getAst to avoid constructing
      # the AST manually
      result.add getAst(adder(head, n[1], n[2]))

This macro is incomplete however, it doesn’t replace nested usages of ->. We would like to replace -> anywhere in the passed body, so let’s use recursion with the help of a helper called graphDslImpl.

  proc graphDslImpl(head, body: NimNode): NimNode =
    template adder(graph, src, dest): untyped =
      graph.addEdge(initEdge(initNode(src), initNode(dest)))

    if body.kind == nnkInfix and $body[0] == "->":
      result = getAst(adder(head, body[1], body[2]))
    else:
      # copyNimNode instead of newStmtList to makes sure
      # a parent node is created with the correct kind.
      result = copyNimNode(body)
      for n in body:
        result.add graphDslImpl(head, n)

Finally our macro is declared:

  macro edges(head, body: untyped): untyped =
    result = graphDslImpl(head, body)
    echo result.treeRepr # let us inspect the result

That’s it for now. This first article shows how to structure procedures that transform the Nim AST and how to then use them in a macro. Later posts will look at more advanced macro usage.

Exercise
There is an undirected edge in the buildCityGraph proc. Can you add another operator (i.e. "Boston" -- "Providence") that takes care of it?