At first glance Nim’s metaprogramming certainly may drive you to scream and run away from your computer to the nearest ditch. Luckily though macros are not overly complicated, they are just a way for programmers to write code with code so we can generate logic and behaviour programatically. This write-up will document creating macros.
Go has an operator which defines a variable and sets the value without having a keyword. This can be done rather simply in Nim, but first let’s look at what we need using
import macros dumpTree: var a = "Test"
The compiler will output anything below
dumpTree, so now we can see the AST required for that code.
StmtList VarSection IdentDefs Ident "a" Empty StrLit "Test"
So if we want to automate this we see that we need the Varsection, an ident, and a value. So let's look at two ways of doing this.
import macros macro `:=`(name, value: untyped): untyped= newVarStmt(name, value)
That's it, we did it, to be certain though we can use the
.repr procedure to check.
import macros macro `:=`(name, value: untyped): untyped= result = newVarStmt(name, value) echo result.repr a := "Test"
Running the above code we will see the compiler echo out the desired
var a = "Test"
This method is equally as short, but uses a nice tool.
quote do lets you write code and it will generate the AST for you, so it's as simple as below. The same checks can be done like above, but I will skip them for the sake of reducing redundancy, as being redundant and repeating yourself is redundant.
import macros macro `:=`(name, value: untyped): untyped = quote do: var `name` = `value` a := "Test"
Sometimes you will want to change the behaviour or function of the language, although Nim does not let you change the syntax you can make your own block logic. One case of this is making a custom
if statement. Below is a desired implementation
let a = "Yellow" expandIf: a == "Hello": echo "Good bye" a == "Yellow": echo "Would be a lot cooler if you liked blue." echo "Yellow sucks" _: echo "You did not speak."
Each condition aside from the underscore will be in
elif bodies. So first we replicate the desired outcome and
dumpTree it like our previous implementation.
import macros dumpTree: if a == "Hello": echo "Good bye." elif a == "Yellow": echo "Would be a lot cooler if you liked blue." echo "Yellow sucks." else: echo "You did not speak."
The above AST looks like such.
StmtList IfStmt ElifBranch Infix Ident "==" Ident "a" StrLit "Hello" StmtList Command Ident "echo" StrLit "Good bye." ElifBranch Infix Ident "==" Ident "a" StrLit "Yellow" StmtList Command Ident "echo" StrLit "Would be a lot cooler if you liked blue." Command Ident "echo" StrLit "Yellow sucks." Else StmtList Command Ident "echo" StrLit "You did not speak."
We can also dumptree the body of our original
expandIf idea, and see the resulting AST nodes we can sample from.
StmtList Infix Ident "==" Ident "a" StrLit "Hello" StmtList Command Ident "echo" StrLit "Good bye" Infix Ident "==" Ident "a" StrLit "Yellow" StmtList Command Ident "echo" StrLit "Would be a lot cooler if you liked blue." Command Ident "echo" StrLit "Yellow sucks" Call Ident "_" StmtList Command Ident "echo" StrLit "You did not speak."
Looking at both of them you can see in the
expandIf body we can get the required infixes for the ElseIfBranch node. If we can seperate the StmtList from each infix, we then can use the
newIfStmt macro to generate the elif branch using
cond which is the infix and
body which is the StmtList.
import macros macro expandIf(statement: untyped): untyped= var branches: seq[(NimNode, NimNode)] #Condition, Body elseBody: NimNode #Else code for cond in statement: #Based off the dumpTree, we know this is the else body. let ifBody = cond.findChild(it.kind == nnkStmtList) if cond.kind == nnkInfix: cond.del 3 #Removes Stmtlist branches.add((cond, ifBody)) elif cond.kind == nnkCall and $cond == "_": #Based off the dumpTree, we know this is the else body. elseBody = ifBody result = newIfStmt(branches) #Generates if stmt result.add newNimNode(nnkElse).add(elseBody) #Appends else body echo result.repr expandIf: 11 == 13 : echo "Test" 12 == 14: echo "Huh" _ : echo "duh"
In the above code you can see the extraction using
findChild. Removal of the StmtList with the
cond.del 3, and finally the creation of the
if. When we compile the compiler sends us a nice message thanks to the
echo result.repr, which is exactly what we wanted a fully constructed if/else statement.
if 11 == 13: echo "Test" elif 12 == 14: echo "Huh" else: echo "duh"
Our next macro will be one that is designed to make constructors so we can quickly and easily pass parameters to the a constructor of an object and have logic called after it's creation. Below is the planned implementation
type CoolThing = object a, b: int c: string CoolThing.construct(true): a: required b: 10 c: "Magic" _: echo result
From the AST we know that each parameter will pass a Call node, that has a Stmtlist. So firstly we extract the useful information.
macro construct(T : typedesc[object | distinct | ref], expNode : static bool, body: untyped): untyped= var postConstructLogic: NimNode requiredParams: seq[NimNode] optionalParams: seq[NimNode] for call in body: if $call == "_": postConstructLogic = call #This is the post constructed node position elif call.kind == nnkIdent and $call == "required" : requiredParams.add(call) #We know it's required else: optionalParams.add(call) #Optional detected
Then we need to extract all the information from the type declaration and make strides to ensure we get the actual type data.
var node = T.getImpl #Get the Type Implementation let nameSym = $T #If distinct get original type if(node.kind == nnkDistinctTy): node = node.getImpl #Check if it's a ref object var isRef = false if(node.kind == nnkRefTy): isRef = true node = node #Get Reclist else: node = node #Get Reclist var identType = initTable[string, NimNode]() #Go for all vars and adding them to an identTable for varDecl in node: #In typedef nnkSym is the type let varType = varDecl for vari in varDecl: if vari.kind == nnkIdent: identType[$vari] = varType #Proc name let constrName = (if isRef: "new" else: "init") & nameSym
Finally with all the extracted information from the type system, and also from the macro we can actually let the code be the coder.
#For each parameter generate a new identdef for req in requiredParams: parameters.add(newIdentDefs(req, identType[$req])) for opt in optionalParams: parameters.add(newIdentDefs(opt, identType[$opt], opt)) #Generate the constructor #Ident tells the constructor the type var objConstr = newNimNode(nnkObjConstr).add(ident($T)) #Generates a: a, for all arguements for param in requiredParams: objConstr.add(newColonExpr(param, param)) for param in optionalParams: objConstr.add(newColonExpr(param, param)) #Set result so we can use the object later in the `_` code let assignment = newAssignment(ident("result"), objConstr) #If ref the convention is new, else init let nameNode = if expNode : postfix(ident(constrName), "*") else: ident(constrName) #Dont have nil if there is no postConstructLogic let procBody = if postConstructLogic.isNil: newStmtList(assignment) else: newStmtList(assignment,postConstructLogic) #Where all our work ends result = newProc(nameNode, parameters, procBody)
Very much a draw the rest of the owl, but nonetheless the above explains my workflow for designing and writting macros for Nim to enable more sensible less redundant code. Now constructor can be called with
initCoolThing(10) and the echo will fire. The entire macro can be seen in one place on Github.