- Changes from the original macroutils:
-
replace the implicit Lit converters with explicit toLit procs
- Thue issue: implicit convertion is not easy to debug
- discussions : PMunch#5
-
rename NimNode attribute getter and setter wiht "mu" prefix
- issue: Nim may introduce new macro/template that share the same attribute name with what's defined in macroutils
- discussions : PMunch#1
-
## Before:
## 1) implicit convertion;
## 2) attribute getter/setter are like `arguments`
testTableConst.arguments.insert(0, ExprColonExpr("hello", 32))
## After:
## 1) explicit convertion;
## 2) attribute getter/setter are like `muArguments`
testTableConst.muArguments.insert(0, ExprColonExpr("hello", toLit(32)))
nimble install https://github.com/YesDrX/macroutils2.git
This module is meant to supplement the macros
module in the standard
library. It adds bits and pieces that I've personally missed while writing
macros over the years.
One of the major things is the ability to create and
access the members of nodes more easily. With this module imported you can
create all of the useful nodes simply by dropping nnk
from their name. So
instead of doing something like this:
newStmtList(
nnkCommand.newTree(
newIdentNode("echo"),
newLit("Hello world")))
You can do something like this:
StmtList(
Command(
Ident "echo",
Lit "Hello world"))
This just removes a lot of noise from creating trees like these. But the
procedures here are also smarter than the regular newTree
, and literals
are automatically converted, so the above can also be written as:
StmtList(Command("echo", "Hello world"))
The Command
procedure here is aware that the first argument is often
supposed to be an nnkIdent
, so it will automatically convert that string
into one. And the literal automatically gets turned into a nnkStrLit
.
Another neat feature are the setters and getters for the properties of nodes. You might have come across code like this (if you haven't, consider yourself lucky):
procImpls[0][6][2][1][1].add(
nnkElse.newTree(
nnkStmtList.newTree(nnkDiscardStmt.newTree(newEmptyNode()))))
This example is taken from my protobuf
module and shows how the flat
structure of NimNodes with just a list of children nodes can be really
confusing. If you look at the generator procedures we used above you can use
the same names of the arguments there to access the nodes in those nodes. So
the above can be written as:
procImpls[0].body[2].body[1].branches.add(
Else(StmtList(DiscardStmt(Empty()))))
As you can see it is now more obvious that we're accessing the branches of
the second statement in the body which is the third child of the body of the
first statement in procImpls
. This is still not very clear, but at least
it gives us some context to what we're doing, which makes it easier to
read and debug. Thanks to the Slice
type defined in this module it is also
possible to add
, insert
, index, and assign to positions in lists of
children that are offset in the node.
This alone is a useful feature when working with macros. But this module also has some more convenience things.
Often times when writing macros you want to manipulate only certain nodes within the AST. Either to parse a DSL, or to modify passed in code. For this purpose I've implemented various tree traversal procedures here. But before they're explained I want to include the normal disclaimer that traversing the tree isn't foolproof as it can contain template or macro calls that won't be expanded. It's often times better to solve replacement a different way. That being said, let's dive in.
First off we have forNode
, it accepts a NimNode
tree; a NimNodeKind
,
or set[NimNodeKind]
; an action procedure; and a max depth. There are also
templates that offer various variants of these arguments. It will traverse
the entire tree, and for each node that matches the kind
it will replace
that node with the result of applying action
to it. Note that it goes down
the tree first, then applies the action
on the way up. An example that
replaces all string literals with the word "goodbye" would look like this:
ourTree.forNode(nnkStrLit, (x) => Lit"goodbye")
A version of forNode
named forNodePos
also exists. It takes an action
with two arguments, the node that matched and a sequence of indices into the
tree to get to that node. This is useful if you need to know the context of
the node to change it.
As a simple helper to forNode
there is also replaceAll
which takes
either a kind, a set of kinds, or a node along with a node to be inserted
and replaces every node in the tree that has the same kind, is in the set of
kinds, or is the same as the node with that node.
When writing DSLs it's also interesting to check if your tree is the same as
the structure you wanted. This can be done by a lot of asserts and if and
for statements. But with this module you can also use the sameTree
procedure that compares trees. It also accepts a list of node kinds to
ignore, if you need a placeholder for any kind, and you can specify a max
depth as well. Combine this with forNode
and you can pretty much check any
passed in tree fairly easily. An example of what a sameTree
check would
look like:
ourTree.sameTree(quote do:
echo "A string"
if something:
echo 100
)
This would return true iff ourTree
was a tree that contained one call that
took a string
, and an if statement on a single ident
, with a similar
call that took an int
. Note that it only verifies node kinds, so it
wouldn't have to be a call to echo
, merely a call to any ident
. If you
wanted to verify that the two echo
statements where actually the same you
could use forNode
or forNodePos
to implement that.
One of the most welcome additions to the macros
module has been the
quote
macro. It is able to take a tree and interpolate symbols from your
surrounding code into it. Much like string interpolation works, just for the
AST. But it has certain limits, the most annoying of which is that it only
works for simple symbols. This module includes a superQuote
macro that
allows you to put anything in the quotes, and rewrites it to a normal
quote
statement that declares these as let statements. With this you can
do things like:
macro testSuperQuote(input: untyped): untyped =
let x = [newLit(100), newLit(200)]
result = superQuote do:
echo `$input[0].name`
if `x[0]` == 300:
echo "test"
elif `x[1]` == 200:
echo "hello world"
testSuperQuote:
proc someproc()
Creating trees is all well and good, and with forNode
and the accessors
it's easy to get things from the tree. But to take things one step further
this module also implements what is essentially a reverse superQuote
statement. Since NimNode
object can have a variable amount of children you
can also postfix your arguments with *
to collect them into a sequence of
nodes. If the identifier exists it will assign or add to it, otherwise it
will simply create them. With this you can do something like:
macro testExtract(input: untyped): untyped =
var arguments = newSeq[NimNode](1) # Create space for body
input.extract do:
import `packages*`
proc `procname`(`arguments*`): `retval` =
`arguments[0]`
let x: seq[`gen`]
assert packages == @[Ident "one", Ident "two", Ident "three"]
testExtract:
import one, two, three
proc someproc(arg: int, test: string): string =
echo "Hello world"
echo "Hello"
let x: seq[int]