macrosgetter-setternim-lang

How do I define a setter in a macro in Nim


I am trying to write a macro which will define a getter and setter for a field. The getter/setter should be called Name. However, when I define the setter the procedure name is 'Name=' and the macro seems to be trying to look for the value of Name=. How do I place 'Name=' in the macro so the whole thing is interpreted as a procedure name?

More specifically with the following code

import macros 

type
    Test_Type = object of RootObj
        Name_HIDDEN: string

macro Name_Macro*(vType: untyped): untyped = 
    quote do:
        proc Name*(self: var `vType`): string =
            return self.Name_HIDDEN

        proc `Name=`*(self: var `vType`,  vIndex: string) =
            self.Name_HIDDEN = vIndex

Name_Macro(Test_Type)

var tTest: Test_Type
tTest.Name = "Hello"
echo tTest.Name

I get the following error:

test.nim(14, 15) Error: undeclared identifier: 'Name'
candidates (edit distance, scope distance); see '--spellSuggest': 
 (1, 4): 'name'

Solution

  • You got most of it right, what you're running into is that quote do is using backticks to identify where to insert nodes, but you also need backticks to define operators (which Name= is).

    There's around 3 solutions for what you're facing, here they are in order of recommendation:

    1. Use std/genasts

    Elegantbeef from nim's discord server wants to point out there's also the method of using std/genast which is superior to quote do and has better semantics for situations like this:

    import std/[macros, genasts]
    
    type
      Test_Type = object of RootObj
        Name_HIDDEN: string
    
    macro Name_Macro*(vType: untyped): untyped =
      genast(vType):
        proc Name*(self: var vType): string =
          return self.Name_HIDDEN
    
        proc `Name=`*(self: var vType,  vIndex: string) =
            self.Name_HIDDEN = vIndex
    
    Name_Macro(Test_Type)
    
    var tTest: Test_Type
    tTest.Name = "Hello"
    echo tTest.Name
    

    2. Predefine the Identifier

    Basically just make a NimNode that contains the operator name ahead of time and insert that.

    import macros 
    
    type
        Test_Type = object of RootObj
            Name_HIDDEN: string
    
    macro Name_Macro*(vType: untyped): untyped =
        let setterName = nnkAccQuoted.newTree("Name".ident, "=".ident)
        quote do:
            proc Name*(self: var `vType`): string =
                return self.Name_HIDDEN
    
            proc `setterName`*(self: var `vType`,  vIndex: string) =
                self.Name_HIDDEN = vIndex
    
    Name_Macro(Test_Type)
    
    var tTest: Test_Type
    tTest.Name = "Hello"
    echo tTest.Name
    

    3. Use the quote do parameter

    PMunch from nim's discord server got this to work.

    Quote do has an extra parameter that is rarely used. With it you can change how places to insert nodes are identified. That way you can just use backticks:

    import macros 
    
    type
        Test_Type = object of RootObj
            Name_HIDDEN: string
    
    macro Name_Macro*(vType: untyped): untyped =
        quote("&") do:
            proc Name*(self: var `&vType`): string =
                return self.Name_HIDDEN
    
            proc `Name=`*(self: var `&vType`,  vIndex: string) =
                self.Name_HIDDEN = vIndex
    
    Name_Macro(Test_Type)
    
    var tTest: Test_Type
    tTest.Name = "Hello"
    echo tTest.Name
    

    Final words

    I would recommend if you're on Nim 2.0 to ditch untyped when you can.

    Your macro is outputting something that is valid code, which means you can just omit the output parameter. Not defining an output type is identical to that output type being typed or untyped. For the output type both mean the same since the compiler needs to check if its valid code anyway, so the specification is unnecessary.

    You also accept input that is valid code, which means you can just change that type to be typed.

    It's useful to have typed as a general preference, because it enables nnkSym nodes, which allow you to jump to the implementation of a type and more, it's generally more powerful.

    So your macro could be called macro Name_Macro*(vType: typed) =