Hi all, I am trying to teach myself macro. This is what i want to do.
anObject.aProperty1 = anInt
anObject.aProperty2 = anStr
anObject.aProperty3 = aBool
anObject.aProperty4 = anInt2
But i want to write code like this- "With" is the name of my macro.
With anObject :
.aProperty1 = anInt
.aProperty1 = aStr
.aProperty1 = aBool
.aProperty1 = anInt2
So i want to tell compiler, if see this peice of code, please convert and expand it to the above style. I know that some packages are there to achieve this but i would like to learn by doing. So far, i wrote a macro and loop through the body variable. But I can only get the i.kind. See this -
macro with(obj : typed, body : untyped) : untyped =
var bn = body
bn.expectKind(nnkStmtList)
for i in bn :
echo i. kind
Yes, you need result for a macro - it is what the macro's invocation is substituted with. That is, the macro not changes in-place what it gets, but returns its result.
You can just assign body argument to result, and then change it item by item. Much of what you want.
You may instead create a new statement list for the result - result = newStmtList(). And then add to it statements, instead of modifying them (result.add ...).
If you want to create statements from strings (kind with eval in interpreted languages) - parseStmt is what you want. Otherwise there's a bunch of new... procs in macros module.
To get the opposite a source-code representation from a NimNode, use repr.
I.e. someCode.parseStmt.repr == someCode should generally hold (except for whitespace).
Yet result = quote do: some code here is an easy way to start.
Code passed to a macro should be a valid Nim code. Probably you cannot use those dots at line starts. Just remove them. Add them in your macro.
If you want your passed to the macro code be in arbitrary syntax - then you need to use strings ("""your multiline code here"""), like is used for asm statements. Then your body argument needs to be a static[string]. You'll get as just a string, may do any manipulations with it, and use parseStmt for parsing into NimNodes.
Next, you can use dumpAstGen on the wanted result (your first snippet) to see what your final results look like. This does 90% of the job for you — you can copy–paste it in your macro and just change some fields.
The other option (which will produce cleaner/simpler macro), is to use quote.
@miran, This is the result of dumpTree
StmtList
Asgn
DotExpr
Ident "abc"
Ident "x"
IntLit 56
Asgn
DotExpr
Ident "abc"
Ident "y"
IntLit 142
So i think i need to rip the ident "x" and "y" from this stmtList and combine it with "obj" variable. But then i think i should check the type of the last part. BTW, i can see some light of hope now. I can do this. i is a line of code. You can see it with echo i.repr. Why do you think it isn't?
You can easily combine it with "obj" and "dot".
If you want to do it via string manipulation (easier to start), you can use this same i.repr, change it as you need, then use parseStmt to create again AST from it.
If you want to manipulate i directly, as NimNode (which it is), use treeRepr to see what it is. Yet you can use treeRepr on a sample of desired code (what you try to achieve), to see how you need to change i.
If you need, I can give you a working code for your macro, as an example, it should make it much clearer for you; I don't for now, in case you want to write it yourself.
Hi all, How about using this method to get the Ident and xxxLit from a body ?
var aStr = body.treeRepr # here i can get the complete string representation of the code
# Now, i can search for the string "Ident" and then get the Ident.
This is the result of dumpTree
... for something else, not for your example ;)
I have made your macro, but I won't (yet) post a complete solution. I'll try to guide you so you can make it on your own.
This is what I have before I even start with macro:
import macros
type MyObject = object
aProperty1: int
aProperty2: string
aProperty3: bool
aProperty4: int
var anObject: MyObject
and this is DSL I want to make, using dumpTree to see if it is valid:
dumpTree:
with anObject:
aProperty1 = 7
aProperty2 = "abc"
aProperty3 = true
aProperty4 = 42
Try to run it to see what it prints.
Next, we can start writing our macro. Let us just echo some parts to see if everything is as we expect:
macro with(obj: MyObject, body: untyped): untyped =
echo obj
for row in body:
echo row.repr
with anObject:
aProperty1 = 7
aProperty2 = "abc"
aProperty3 = true
aProperty4 = 42
Ok, row is what we expected, but let us split it into pieces to see how that looks like. Change your loop in the macro to:
for row in body:
for r in row:
echo r.repr
We're starting to getting somewhere.
Our next stop is writing the macro to do what we want, and that is to produce:
anObject.aProperty1 = 7
anObject.aProperty2 = "abc"
anObject.aProperty3 = true
anObject.aProperty4 = 42
so when we do (after calling our macro) echo anObject we get: (aProperty1: 7, aProperty2: "abc", aProperty3: true, aProperty4: 42).
I'll leave that one for you.
If you get stuck, post exactly what you have tried.
@miran, Thanks a lot. Great help. By using nested for loops, i can get the identifier and value individually. So this is my assumption now. Please correct me if i am wrong.
for row in body :
result.add(nnkDotExpr(newIdentNode(obj.repr), newIdentNode(i.repr)))
But i got error message. - Undeclared routine nnkDotEcpr. Ok, then there might be a proc to create new dot expression. In newDotExpr you need the 2nd argument to be an identifier. That is, all other dots should be in the 1st argument. That is, you need a nested newDotExpr. Like:
result.add(newAssignment(
newDotExpr(
newDotExpr(obj, newIdentNode"aProperty4"),
newIdentNode"astr"),
i[1]))
Of coarse, you need to get those identifiers programmatically from input, and you probably want not fixed nesting level, so you have more work. It may be easier with a helper recursive compile-time procedure, called from your macro; or you'll need more cycles.
Ok, the problem is that you directly ignore the case when i[0] isn't nnkIdent. When this happens you have to recursively find and replace the leftmost i[0] node with obj.<what you found> e.g. in a.b.c you replace a with obj.a
so your code becomes
proc replaceBase(node: NimNode, obj: NimNode): NimNode =
# later
if i.kind == nnkAsgn:
let l = replaceBase(i[0], obj)
result.add(newAssignment(l, i[1]))
Implementing replaceBase will resolve everything.
Again, this might look harder than the string replace, but for most macros you need to manipulate nodes in a similar way, e.g. when you add X in call(args) -> call(X, args) you can't just add repr-s until you get the result
@alehander42, Thanks, I am trying to write the replaceBase function. But how do i find how many dot expression elements are there. All i can see is the children iterator.
anItem.firstItem.secondItem.thirdItem.forthItem = anInt
Here, 5 items are here. how to rip the anItem from this node ? Um, let me play with it a little more. BTW, could you please post an example code about "quote" ?
Well, I'll post an example of nested with, with a sample quote use, and the corresponding manual construction.
The example uses a loop for dots handling - you can still write a recursive version yourself.
Yet you may want to add support for indexed properties (a.b[c].d), yet other statements kinds besides assignment: handle other prop.kind and i.kind cases; enough scope for experiments.
I'd say this all example is rather educational and practically an overkill - for this particular case that first strings concatenation approach seems to be more appropriate.
type Obj = object
num: int
obj: ref Obj
str: string
var
anObject: Obj
import macros
# this is not for dots: for not having to write ``new anObject.obj``, ...
proc initRefObjectIfNeeded(ob: NimNode): NimNode =
quote do:
when compiles(isNil(`ob`)):
if isNil(`ob`): new `ob`
macro with(obj : typed, body : untyped) : untyped =
result = newStmtList()
for i in body :
if i.kind == nnkAsgn :
#echo "i:\n", i.treeRepr
var prop = i[0] # everything to the left of ``=``
var dots = newSeq[NimNode]() # here we'll gather dot-delimited parts
while true:
case prop.kind
of nnkIdent: # the final part, end of "recursion", breaking here
dots.add prop
break
of nnkDotExpr:
#echo "prop:\n", prop.treeRepr
dots.add prop[1] # only one part is here
prop = prop[0] # all other parts are here, we'll continue with them
else:
error($prop.kind & "not supported")
var lhs = newDotExpr(obj, nil) # we'll replace ``nil`` later
#for j in dots: echo j.treeRepr # what we've gathered
#echo "-"
for j in countdown(dots.high, 1): # we have them in reverse order
lhs[0] = newDotExpr(lhs[0], dots[j])
result.add initRefObjectIfNeeded(lhs[0]) # not to write ``new ...``
lhs[1] = dots[0] # final step: replacing that ``nil`` with the right-most part
#echo "lhs:\n", lhs.treeRepr
let rhs = i[1] # the value being assigned
result.add quote do:
`lhs` = `rhs`
# the same without quote:
#result.add newAssignment(lhs, rhs)
#echo "result:\n", result.treeRepr
#echo result.repr
# we would have need this w/o ``initRefObjectIfNeeded``
#new anObject.obj
#new anObject.obj.obj
#new anObject.obj.obj.obj
with anObject:
num = 42
obj.str = "Pear"
obj.obj.str = "Banana"
obj.obj.obj.str = "YetSomeFruit"
echo anObject.repr
Hi @LeuGim, Great help. I studied your code. At first, it feels like a magic. Then gradually, everything seems to be clear.
The things amazed me are--- 1.
var prop = i[0]
if i print prop, i can see this on screen.
num
obj.str
obj.obj.str
obj.obj.obj.str
As perfect as i assumed. Then i see this line ---
case prop.kind
of nnkIdent:
dots.add prop
I printed dots on screen and got this.
@[num]
@[str, obj]
@[str, obj, obj]
@[str, obj, obj, obj]
I think the order is re arranged, isn't it ? I expect @[obj, str] but it appeard like @[str, obj] Why ? Please explain the reason.
2. This is the birilliant part. I can't imagine this before i see this code. Well, Thanks for this.
of nnkDotExpr:
dots.add prop[1] # only one part is here
prop = prop[0] # all other parts are here
Due to this brilliancy, no need to use a recursive function.
At last, i can see these lines.
result.add quote do:
`lhs` = `rhs`
# the same without quote:
#result.add newAssignment(lhs, rhs)
Ok, i understood. But i think i need to know more about quote. From this comment, all i can understand is quote can be used instead of "newAssignment". But one question is still rasing. What else we can do with quote ?I think the order is re arranged, isn't it ?
No, it isn't. Parts are extracted in this order. They are mostly from dots.add prop[1], and prop[1] is always the last remaining part in the tree (as noticed in comments), taking it into dots, we then deal with others, assigning the rest (prop[0]) to prop, then it's repeated, so we're unwinding the tree by taking each time the last element. Just the tree for nested dot-expression is always nested at left branches: prop[1] is an atom, so cab just taken, and prop[0] - the rest of the tree. Just study treeRepr's outputs, you'll see what's going on.
What else we can do with quote ?
Mm... everything. Just like templates inside macros. Yet another way of constructing AST - something between string manipulation (parseStmt) and nodes building (this example). There is another example for it in the code. And just write any Nim code in it and use backticks to insert code from a variable of the macro.