I have a template that defines a proc, and I'd like to be able to modify the contents of the proc depending on parameters passed.
I'm trying to do something like:
import macros
template op1():untyped =
a + b
template op2():untyped =
a - b
macro run(op:string) =
var opNode =
if op.strVal == "plus":
getAst(op1())
else:
getAst(op2())
result = quote do:
proc myFunc(a:int, b:int) =
`opNode`
echo myFunc(1, 1)
run("plus")
But a and b are undeclared identifiers in op1 and op2.
Is there a way to stop the symbol lookup? Do I have to make this one big macro? Would looking into the compiler module help?
it's unclear to me what you're trying to do here, i appreciate your work in simplifying but maybe you went a bit too simple.
here's my initial interpretation of what you're doing:
template op1() = a + b
template op2() = a - b
proc MyFunc[op: static string](a,b:int):int =
when op == "plus":
op1
elif op == "minus":
op2
else:
72
assert MyFunc["plus"](3,5) == 8
assert MyFunc["minus"](13,2) == 11
assert MyFunc["default"](-5,23) == 72
did i get anywhere close?
In your example, the templates are instanced in a proc with the identifiers, but what if the proc MyFunc is inside a template or macro?
Which is what my example code above is trying to do. To give more context, I'm using godot-nim which has a macro that generates a binding to the godot engine.
Here's a simple example:
import godot, godotapi / [control]
gdobj HealtUi of Control:
var health {.gdExport, set:"set_health"}:float
proc set_heatlh(value:float) =
self.health = value
gdobj is a macro that runs other macros like registerGodotField. In the above sample health is a field. registerGodotField is a template that generates a setter and getter function and makes a call to the godot engine to register it. The setter function converts godot data to nim data, and I want to hook into that generated setter so the user can call their own setter.
Here's what registerGodotField looks like:
template registerGodotField(classNameLit, classNameIdent, propNameLit,
propNameIdent, propTypeLit, propTypeIdent,
setFuncIdent, getFuncIdent, hintStrLit,
hintIdent, usageExpr,
hasDefaultValue, defaultValueNode) =
proc setFuncIdent(obj: ptr GodotObject, methData: pointer,
nimPtr: pointer, val: GodotVariant) {.noconv.} =
let variant = newVariant(val)
variant.markNoDeinit()
let (nimVal, err) = godotToNim[propTypeIdent](variant)
case err:
of ConversionResult.OK:
setter
of ConversionResult.TypeError:
let errStr = typeError(propTypeLit, $val, val.getType(),
classNameLit, astToStr(propNameIdent))
printError(errStr)
of ConversionResult.RangeError:
let errStr = rangeError(propTypeLit, $val,
classNameLit, astToStr(propNameIdent))
printError(errStr)
The full listing is here: https://github.com/geekrelief/gdnim/blob/master/deps/godot/nim/godotmacros.nim#L554-L615
See under of ConversionResult.OK:? I want to replace setter with
cast[classNameIdent](nimPtr).propNameIdent = nimVal
or
cast[classNameIdent](nimPtr).setterNameIdent(nimVal)
depending on whether the user has used the {.set.} pragma on the field.
But I'm getting an Error: undeclared identifier 'nimPtr' when I try to construct the ast.
I thought I could make setter a macro like:
macro setterAssign(classNameIdent, propNameIdent, setterNameIdent) =
if setterNameIdent.repr != "NIM":
quote do:
cast[`classNameIdent`](nimPtr).`propNameIdent` = nimVal
else:
quote do:
cast[`classNameIdent`](nimPtr).`setterNameIdent`(nimVal)
And in registerGodotField do:
of ConversionResult.OK:
setterAssign(classNameIdent, propNameIdent, setterNameIdent)
But I get: Error: undeclared identifier: 'nimPtr'
ok, duh.. I just need to pass in those parameters.
macro setterAssign(classNameIdent, nimPtr, propNameIdent, setterNameIdent, nimVal) =
if setterNameIdent.repr != "NIM":
quote do:
cast[`classNameIdent`](`nimPtr`).`propNameIdent` = `nimVal`
else:
quote do:
cast[`classNameIdent`](`nimPtr`).`setterNameIdent`(`nimVal`)
of ConversionResult.OK:
setterAssign(classNameIdent, nimPtr, propNameIdent, setterNameIdent, nimVal)
That compiles. :p
In the original example, {.inject.} works.
import macros
template op1(): untyped =
a + b
template op2(): untyped =
a - b
macro run(op: string) =
var opNode =
if op.strVal == "plus":
getAst(op1())
else:
getAst(op2())
result = quote do:
proc myFunc(a {.inject.}: int, b {.inject.}: int): int =
`opNode`
echo myFunc(1, 1)
run("plus") # 2
{.inject.} is the answer yes. here's @Hlaaftana's answer with a template instead:
:
template op1: untyped = a + b
template op2: untyped = a - b
template run(op:string): untyped =
proc myFunc(a{.inject.}:int, b{.inject.}: int): int =
when op == "plus":
op1
else:
op2
similarly your setterAssign can be a template: I had to mush around with registerGodotField (make it untyped etc) to get it to work outside of its macro environment, pls ignore. the point is the injection of nimPtr and nimVal.
template setterAssign(classNameId,propNameId,setterNameId) =
when setterNameId != "NIM": # whatever the logic is
cast[var classNameId](nimPtr).propNameId = nimVal
else:
propNameId(cast[var classNameId](nimPtr), nimVal)
template registerGodotField(classNameId,propNameId,setterNameId):untyped =
let res = proc(nimPtr{.inject.}:pointer, val:int){.sideEffect.} =
let nimVal{.inject.} = val #or whatever
setterAssign(classNameId,propNameId,setterNameId)
res
type
Foo = object
bar*: int
baz: int
proc bazSetter*(f: var Foo, x: int) = f.baz = x
let setbar = registerGodotField(Foo, bar, "")
let setbaz = registerGodotField(Foo, bazSetter, "NIM")
var f = Foo(bar: 3, baz: 9)
setbar(cast[pointer](f.addr), 5)
setbaz(cast[pointer](f.addr), 11)
assert f.bar == 5
assert f.baz == 11
word of caution, tho: dirtying up templates like this can lead to hard to trace bugs, i.e. the scope of 'nimPtr' is now spread across two templates which might be far from each other in the code.
defining setterAssign inside registerGodotField keeps everything tidier, and you don't need to pass the parameters around. you do still need to inject any new symbols.
:
template registerGodotField(classNameId, propNameId,setterNameId): untyped =
template setterAssign =
when setterNameId != "NIM":
cast[var classNameId](nimPtr).propNameId = nimVal
else:
propNameId(cast[var classNameId](nimPtr), nimVal)
let res = proc(nimPtr:pointer,val:int){.sideEffect.} =
let nimVal{.inject.} = val
setterAssign
res
also, don't forget that Nim lets us define custom setters transparently, which might obviate all of the above. i.e.
proc `set_health=`(f: var Foo, i: int) = f.baz = i
let setbaz = registerGodotField(Foo,set_health, "") #works just like the field access version
@Hlaaftana @shirleyquirk Thanks for the suggestion to use {.inject.} or when when nesting templates. I'll keep them in mind for future use.
I'm familiar with defining setters "transparently" as you suggested. I guess, I would have to change the name of the property to some implementation name like healthValue. It would simplify things for the end user a tiny bit, so maybe I'll do it down the line.
You guys are awesome! Thanks for your help!