I thought maybe someone would find this interesting: the following macro lets 
you to define functions and containers that accept objects of any type, as long 
as they have a desired set of fields. The objects may have additional fields, 
and the order of the fields in the type definition doesn't matter. It's 
different from generics in that it allows for _runtime_ polymorphism. There 's 
a usage example at the bottom. It's quite cool that Nim allows you to do stuff 
like that. 
    
    
    import macros
    import os
    
    {.experimental.}
    
    proc `-`(a: pointer, b: pointer): int =
      cast[int](a) - cast[int](b)
    
    proc `+`(a: pointer, b: int): pointer =
      cast[pointer](cast[int](a) + cast[int](b))
    
    macro structural(con, inter, fieldDefs): untyped =
      let dummySym = genSym(nskVar, "dummy")
      let vtableType = newNimNode(nnkTupleTy)
      let vtableValRef = newNimNode(nnkPar)
      let vtableValPtr = newNimNode(nnkPar)
      let conTests = newNimNode(nnkStmtList)
      
      for fieldDef in fieldDefs.children:
        # Build up vtable tuple type and value, as well as concept tests, from 
the fields:
        let fname = fieldDef[0]
        let ftype = fieldDef[1][0]
        
        var a = newNimNode(nnkIdentDefs)
        a.add(fname)
        a.add(quote do: tuple[offs: int, tmark: `ftype`])
        a.add(newEmptyNode())
        vtableType.add(a)
        
        var b = newNimNode(nnkExprColonExpr)
        b.add(fname)
        b.add(quote do: (addr(`dummySym`.`fname`) - addr(`dummySym`[]), 
`dummySym`.`fname`))
        vtableValRef.add(b)
        
        var c = newNimNode(nnkExprColonExpr)
        c.add(fname)
        c.add(quote do: (addr(`dummySym`.`fname`) - addr(`dummySym`), 
`dummySym`.`fname`))
        vtableValPtr.add(c)
        
        conTests.add(quote do: T.`fname` is `ftype`)
      
      
      let node = quote do:
        type
          `con` = concept g, type T
            `conTests`
          
          `inter`[T: pointer or RootRef] = object
            base: T
            vtable: ptr[`vtableType`]
        
        # Converter for ref object types:
        # (typedesc is explicitly ruled out to prevent infinite loops in the 
concept tests)
        converter conToInter[T: not typedesc and ref object and `con`](x: T): 
`inter`[RootRef] =
          var vt {.global.} = (proc(): `vtableType` =
            var `dummySym` = T()
            return `vtableValRef`
          )()
          
          `inter`[RootRef](
            # Questionable cast, but sizeof and repr suggest refs are glorified 
pointers anyway:
            base: cast[RootRef](x), # Keeps the proxied object alive, provides 
base address
            vtable: addr vt
          )
        
        # Converter for ptr object types:
        converter conToInter[T: not typedesc and object and `con`](x: ptr T): 
`inter`[pointer] =
          var vt {.global.} = (proc(): `vtableType` =
            var `dummySym` = T()
            return `vtableValPtr`
          )()
          
          `inter`[pointer](
            base: x,
            vtable: addr vt
          )
      
      let node2 = quote("@") do:
        # Syntax sugar for referring to target fields through the proxy
        # This must be separated from the first quote due to issues with the 
backtick splice
        # Uses the vtable's dummy tmarks to cast to the correct field type
        
        template `.`(x: @inter, field: untyped): untyped =
          cast[ptr[(@vtableType).field.tmark]](cast[pointer](x.base) + 
x.vtable.field.offs)[]
        
        template `.=`(x: @inter, field: untyped, value: untyped): untyped =
          cast[ptr[(@vtableType).field.tmark]](cast[pointer](x.base) + 
x.vtable.field.offs)[] = value
      
      
      
      let node3 = newStmtList()
      
      node3.add(node)
      node3.add(node2)
      node3
    
    # ---------------------------------------
    # Usage example:
    
    type
      Vec2 = tuple[x, y: float]
      
      Box = object
        pos: Vec2
        vel: Vec2
      
      Circle = object
        vel: Vec2
        pos: Vec2
        rad: float
      
      BoxRef = ref Box
      CircleRef = ref Circle
      
      Turtle = ref object
        speed: float
        pos: Vec2
    
    structural(Kinetic, IKinetic): # Magic happens here
      pos: Vec2
      vel: Vec2
    
    var ptrSeq: seq[IKinetic[pointer]] = @[]
    var refSeq: seq[IKinetic[RootRef]] = @[]
    
    proc test1(x: IKinetic[RootRef]) =
      echo x.pos
      echo x.vel
      refSeq.add(x)
    
    proc test2(x: IKinetic[pointer]) =
      echo x.pos
      echo x.vel
      ptrSeq.add(x)
    
    test1(BoxRef(vel: (1.0, 2.0), pos: (3.0, 4.0)))
    test1(CircleRef(vel: (5.0, 6.0), pos: (7.0, 8.0), rad: 125.0))
    test2(cast[ptr Circle](alloc0(sizeof Circle)))
    
    echo refSeq[1].vel
    echo ptrSeq[0].vel
    
    #test1(Turtle()) # Gives "Kinetic: concept predicate failed" error
    

Reply via email to