Search code examples
typesfieldmetaprogrammingnim-lang

How to get all fields of an object type `MyType` in a macro from a NimNode ident("MyType")


In nim 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"] ?


Solution

  • Thanks to hugagranstrom over on the nim forum I found the answer.

    The clue is as he stated to have the NimNode that the macro accepts for the proc-definition be of type typed instead of untyped.

    Making that NimNode typed applies some restrictions to what statements you can write, as the semantics are checked to be consistent. So e.g. a lambda definition must have all necessary parts of a lambda definition.

    However, it does allow the compiler to not just view variables as simple "identifiers" but as "Symbols" (Aka NimNodes of kind nnkSym) whose definition you can fetch using getImpl.

    An Untyped NimNode parameter does not have those restrictions but at the same time can not provide Symbols the way Typed does.

    So this works:

    type A = object
      name: string
    
    type B = object
      name: string
      id: int
    
    macro generateMapper(procDef: typed, specialMappings: untyped): untyped =
      echo procDef[0][0].getImpl().treeRepr ## [0][0] is the Symbol of the *resultType* 
    
        
    let mapAToB = generateMapper(proc(source: A, source2: int): B):
      result.id = source2
    

    For reference, this is the node representation of procDef:

    ProcTy              # This is what we have in `procDef`
      FormalParams
        Sym "B"         # Symbol of Result Type
        IdentDefs       # Box representing the first Parameter
          Sym "source"
          Sym "A"
          Empty
        IdentDefs       # Box representing the second Parameter
          Sym "source2"
          Sym "int"
          Empty
      Empty
    

    So procDef[0] is the node FormalParams and FormalParams[0] is the node Sym "B", which is the result-type of the proc definition.

    With getImpl as stated we can get the type, whose treeRepr in this case looks like this:

    # from `echo procDef[0][0].getImpl().treeRepr`
    
    TypeDef
      Sym "B"
      Empty
      ObjectTy
        Empty
        Empty
        RecList           # Box for list of Fields of type called "B"
          IdentDefs       # Box for first Field 
            Ident "name"
            Sym "string"
            Empty
          IdentDefs       # Box for second Field
            Ident "id"
            Sym "int"
            Empty