import macros
let abc = "abcABC"
let xyz = "xyzXYZ"
## 1. First, consider the intended code to be generated:
#
# echo abc & xyz
## 2. Get the dumpTree of the intended syntax:
#
# dumpTree:
# echo abc & xyz
#
## 3. When compiled, the above generates this AST:
#
# StmtList
# Command
# Ident "echo"
# Infix
# Ident "&"
# Ident "abc"
# Ident "xyz"
## 4. Recreate the above AST with macros:
macro meow(a, b: untyped): untyped =
result = newStmtList()
result.add(
newCall(newIdentNode("echo"), infix(newIdentNode($a), "&", newIdentNode($b)))
)
# result.add quote do: # Alternative
# echo `a` & `b`
echo result.repr
echo result.treeRepr
## 5. Check for 2 things:
#
# (a) That the generated code (from the macro), when run/used, outputs same as (1) above and with when doing `echo result.repr()`
# (b) That the compiled code emits the same AST as (2) above, and when doing `echo result.treeRepr()`
meow(abc, xyz)
Nim, as a language, has powerful metaprogramming features.
But the official documentation - while useful - is a bit all over the place; you're not guided with where/how to start.
So here's my quick guide, which I wish existed when I started. Use this as a starting point for your understanding. You can leapfrog from this onto more advanced Nim macros later. (Or try some exercises, to get a taste of macros.)
The major caveat, always: don't use macros unless you have to.
When metaprogramming, you're working with (and thinking in) tree representations of code - i.e. the AST (Abstract Syntax Tree).
Thus for beginners new to metaprogramming/building Nim macros, you must shift to a new mental model, i.e. where:
You're not writing code to run something.
You're instead writing code to rewrite or generate new code.
As such you must understand the tree structure the compiler sees; this is an additional layer above what your code actually does. You can no longer read macro code procedurally; you must instead know how/where the generated code is being injected (not just whether it runs).
Conceptually, this new mental model is 'easy'.
But this mental shift will initially trip you up, because your normal instinct as a beginner is always to write code that runs, instead of code that compiles code (that are meant to be run).
When you write macros, you're manipulating the AST - not runtime values. Observe the change in mental model:
"What kind of node (not value) am I receiving?"
"What does the tree structure look like?"
"How do I modify or generate a new tree?"
Again, since macros receive and return tree representations:
Nim macros receive code as NimNode
(an AST node).
They must return a transformed NimNode
(an AST node).
Make treeRepr()
your new best friend when learning macros.
treeRepr(x)
lets you see the structure of the code that Nim received - so you don't have to guess what's happening:
import macros
macro inspectMacro(x: untyped): untyped =
echo treeRepr(x)
echo type(x) # `x` is passed as a NimNode
result = x
discard inspectMacro(2+3)
# Outputs:
#
# Infix
# Ident "+"
# IntLit 2
# IntLit 3
dumpTree()
also helps when you're trying to visualize flow:
dumpTree(foo(1, a + 2))
# The above outputs:
#
# Call
# Ident "foo"
# IntLit 1
# Infix "+"
# Ident "a"
# IntLit 2
So now you can understand:
How your function calls, operators, and expressions are structured.
What to pattern-match on.
How to reconstruct or modify the AST.
Additionally, Nim's quote do:
feature is really useful! I will touch on it in a separate article.
Inspect the structure of code with treeRepr
.
Pattern match on node types like nnkCall
, nnkIdent
, nnkInfix
, etc.
Build new code by constructing NimNode
s directly, or using quote do:
.
Return the transformed AST (NimNode
) as result
.
Macros run at compile-time. So once the program is compiled the echo message will no longer appear.
Distinguish dumpTree
in global/standard code sections, vs treeRepr
in macro code sections.