X Macros in Nim

X Macros  are known to some C developers, they are a way to leverage C’s macro system to maintain associated lists of values which can be evaluated at compile time.

Let’s have a look at an example of that in C, taken from the Wikipedia article. The following example generates three integer variable declarations.

The list of variables can be defined as such:

#define LIST_OF_VARIABLES \
    X(value1) \
    X(value2) \
    X(value3)

And then they can be used by a macro labeled ‘X’ at compile time to generate code like so:

#define X(name) int name;
LIST_OF_VARIABLES
#undef X

In this case, it defines every variable in LIST_OF_VARIABLES as a new integer in the current scope. In this example, each of the entries in LIST_OF_VARIABLES is expanded to match the definition of X which is provided. X is then undefined at the end of its use.

This approach has a major caveat however, it’s not type checked which means you can’t trust the compiler to stop you if you’ve done something wrong – which can lead to nasty run-time bugs. In C, as this operates on a text basis it is rather easy to shoot yourself in the foot. It’s also not easy to re-use the X Macro expansions themselves as they must share the same name that was defined in the LIST_OF_VARIABLES noted before.

Let’s look at an equivalent example in Nim taking advantage of the template functionality.

template listOfValues(x: untyped): untyped =
  x("hi", 3)
  x("whee", 4)
  x("test", 5)
And some code to test it:
proc main(): void =

  template ex(x:string, y:int): untyped=
    echo x, y

  listOfValues(ex)
You can see here that the trick is that we can pass one template into another and they will be evaluated in the order you expect. This compiles to the equivalent of:
proc main(): void=
  echo "hi", 3
  echo "whee", 4
  echo "test", 5
The output of the program looks like this:
hi3
whee4
test5
You can see already that using templates we have made a major improvement over the C example: Type safety. If you add an entry which looks like x(3, 3) and attempt to compile it, you will be told immediately that:
Error: type mismatch: got 
but expected one of:
template ex(x: string; y: int): untyped
 first type mismatch at position: 1
 required type: string
 but expression '3' is of type: int literal(3)

Given that you could also define the template ex in any scope, you could also reuse a template definition if you wanted to.

Let’s take a look at another example which is more like the C example we looked at above. In this example we’re going to have a list of variable names and their initialisation values, then declare and initialise them using the X-Macros technique.

template listOfNames(x: untyped): untyped =
  x(a, 5)
  x(b, 6)
  x(c, 7)
proc main(): void =
  template declare(x: untyped, y: int): untyped =
    let x = y
  
  listOfNames(declare)
  echo a, b, c

Output:

567

In this example we’ve taken advantage of the fact that you can use symbols in your templates to generate code.

This might strike you as a bit of an unusual trick, but it seems to fit in with Nim’s emphasis on great metaprogramming and demonstrates just how powerful even the template system can be without even touching the macro keyword. It’s quite simple and readable in my opinion and has no run-time overhead.

 

Leave a comment