I've been playing around with some metaprogramming in nim and found myself wanting to have a list of fields of a type I receive a node for from a proc definition. Basically I have a macro that takes in a proc definition and generates a proc based on that and some statements. The syntax looks like this:
type A = object
name: string
type B = object
name: string
id: int
macro generateMapper(body: untyped): untyped =
let procDefNode: NimNode = body[0]
... macro code ...
generateMapper():
proc myMapProc(source: A, target: B): string =
target.name = source.name
I can fetch the proc definition out of the body into procDefNode as shown. The procDefNode.treeRepr is:
ProcDef
Ident "myMapProc"
Empty
Empty
FormalParams
Ident "string"
IdentDefs
Ident "source"
Ident "A"
Empty
IdentDefs
Ident "target"
Ident "B"
Empty
Empty
Empty
StmtList
... some body statements that don't matter right now...
This shows that I can access the nodes of my parameter types, which are A and B.
What I want now is to generate a list from the node for B of all the fields that type has. In this example that would be @["name", "id"]. But how do I do that?
I've skimmed through std/macros and getImpl seems like the thing I want, but I can't figure out how to make it work. This:
macro generateMapper(body: untyped): untyped =
let procDefNode: NimNode = body[0]
let typeNode = procDefNode[3][2][1] # 3 is the node for FormalParams, 2 is the Node of the second parameter and 1 there is the type-Node of the second parameter
echo getImpl(typeNode)
Just errors out with Error: node is not a symbol.
So how do I get from the node ident "B" aka typeNode to all of its fields in the shape of @["name", "id"] ?
Note that for easier googling purposes I also put the question into Stack-Overflow and would forward whatever solution we find here to that question (or vice versa if I can find an answer on SO).
To get the type of a node, you need to use a typed macro:
macro generateMapper(body: typed): untyped =
Simplified explanation: An untyped macro doesn't know anything about what each identifier (ident) actually refers to. A typed macro instead used symbols (as needed per your error message) which do know the type of its parameters.
Thank you so much! Yes that worked and I got my project to work as I wanted. Then a small question for suggestions: The current syntax that I got this to work with is this one:
... lots of procs and macros...
macro generateMapper(procDef: typed, explicitMappings: untyped): untyped =
let definition: LambdaDefinition = parseLambdaAssignment(procDef)
let procBody: NimNode = generateMappingAssignments(
definition.resultFields,
definition.primarySourceName,
definition.primarySourceFields,
explicitMappings
)
return generateLambda(definition, procBody)
## Demonstrating the above
type A = object
name: string
type B = object
name: string
id: int
let mapAToB = generateMapper(proc(source: A, source2: int): B):
result.id = source2
let id = 3
let x = A(name: "Potato")
let expectedB = B(name: "Potato", id: id)
echo mapAToB(x, id) == expectedB
As you can see the syntax currently is a bit clunky in that I demand for a proc definition (proc(source: A, source2: int): B and separately from that for syntactically correct statement that I basically just copy-paste into the proc body that I generate (result.id = source2).
Do you know of any mechanisms or the like that I could abuse to make this look better? E.g. are there any mysterious macro-ways that could be useful here that I just don't know of yet?
For context: I'm so far under the impression that I need to keep the procdef and the explicit assignment statements below separate, so that the statements can be untyped which I think is necessary in order for them to only have to be valid in the fully assembled lambda in the end.
I'm not entirely sure I understand you completely, but I read it as you wanting a nicer syntax for your generateMapper macro?
For context on why I have 2 parameters in my macro: I'm so far under the impression that I need to keep the procdef and the explicit assignment statements below separate, so that the statements can be untyped which I think is necessary in order for them to only have to be valid in the fully assembled lambda in the end.
Did you try this out? As long as the proc body is valid (which it is in this case), it should to the best of my knowledge just work for you to write this as you showed in your original post:
generateMapper:
proc mapAToB(source: A, source2: int): B =
result.id = source2
The proc body is valid in this code, so you should not get any problems with typed'ing the proc body as well. In the macro you could just replace the body of the incoming proc definition with your new one (procBody). And if you return the procdefinition as is, it will create the proc (no need to assign it to a variable).
As an additional enhancement, you could use the macro as a pragma instead. This is equivalent (minus a nnkStmtList) to my above code:
proc mapAToB(source: A, source2: int): B {.generateMapper.} =
result.id = source2
Was this what you were asking for? Let me know if you want a code example where I explain it further :)
Did you try this out? As long as the proc body is valid (which it is in this case), it should to the best of my knowledge just work
I'll add a bit more on this and how to solve this kind of problem when it doesn't work.
Let's say that generateMrapper injected a new variable in your proc, let's call it x. Then this wouldn't be valid code anymore:
generateMapper:
proc mapAToB(source: A, source2: int): B =
# this code is later injected by `generateWrapper`:
# var x = "Hello world"
echo x
Because x isn't defined yet when the macro sees the typed proc body, it will complain that x is undefined and error. So how would we solve it in this case? We could separate the procdef and procbody as you did, but we could also create a wrapper macro for it. The wrapper macro takes the entire procdef + procbody as untyped and then generates a call to the typed macro where they are passed separately:
macro generateMapperInternal(procDef: typed, explicitMappings: untyped): untyped =
# same as you defined in your post
macro generateMapper(all: untyped) =
let (procdef, body) = splitProcDefAndBody(all)
quote do:
generateMapperInternal(procdef, body)
I know this isn't what you asked for, but I hope that it can clarify the difference between typed and untyped a bit at least.
Exactly things like this! I tried your approach and it worked beautifully. In fact, I switched approaches a bit and instead of generating a new proc, I just take the procDefinition, copy the node and add a bunch of assignment statements to the body of the copied proc definition and return that, done.
No new proc generated or nothing, just a copy and a couple additions.
Vastly simpler. Now next I'll have to look into how to turn that into a pragma because that of course would be just chef's kiss
Sweet! :D Macros are pretty frickin' cool when you realize how simple they can be!
As for pragma, you should be set to go already: https://nim-lang.org/docs/manual.html#userminusdefined-pragmas-macro-pragmas