Hello,
ftsf on IRC and I were trying to get a macro work that helps assign value to a Nim object field where the field is specified by a string value.
Discussion: https://irclogs.nim-lang.org/14-06-2019.html#17:39:56
We almost have a solution: https://play.nim-lang.org/index.html?ix=1LMK
How can that be fixed?
Thanks!
Macros expand at compile time, while for loops execute at run time.
You need to loop it inside a macro.
This is not possible with a macro. Macros are limited to compile time only, and what you want to do is access the fields at run time.
Here is the code inline for reference:
import macros
macro assignField*(obj, fieldName, value: typed) =
let fieldSym = newIdentNode($`fieldName`)
result = quote do:
`obj`.`fieldSym`=`value`
type
SomeObj* = object
a*: int
b*: int
c*: int
var
foo = SomeObj()
for field in @["a", "c"]:
# this doesn't work
assignField(foo, field, 50)
# /usercode/in.nim(19, 14) template/generic instantiation of `assignField` from here
# /usercode/in.nim(5, 12) Error: attempting to call undeclared routine: 'field='
# this works
assignField(foo, "a", 100)
assignField(foo, "c", 200)
block:
# this works
assignField(foo, "b", 300)
echo foo
In assignField(foo, field, 50) above in the for loop, the compiler can only infer the name you passed directly to the macro, which is field. So what the compiler is doing is trying to call foo.field = 50, but field doesn't exist.
Since you are trying to use a seq to loop through (for field in @["a", "c"]:), the compiler can't use that information since at run time a seq may change. In theory, you could use a constant array, but I was unable to get it to work since it seems that for loops always execute at runtime from the compiler's point of view.
try this:
import macros
proc replaceSymAndIdent(a : NimNode, b : NimNode, c : NimNode, isLit : static[bool] = true) =
for i in 0..len(a)-1:
if a[i].kind == nnkSym:
if ident(strVal(a[i])) == b:
a[i] = c
elif a[i].kind == nnkClosedSymChoice or a[i].kind == nnkOpenSymChoice:
if ident(strVal(a[i][^1])) == b:
a[i] = c
elif a[i].kind == nnkIdent:
if a[i] == b:
a[i] = c
elif a[i].len != 0:
if a[i].kind == nnkDotExpr:
when not isLit:
if a[i][0].kind == nnkSym:
if ident(strVal(a[i][0])) == b:
a[i][0] = c
elif a[i][0].kind == nnkClosedSymChoice or a[i][0].kind == nnkOpenSymChoice:
if ident(strVal(a[i][0][^1])) == b:
a[i][0] = c
elif a[i][0].kind == nnkIdent:
if a[i][0] == b:
a[i][0] = c
else:
replaceSymAndIdent(a[i], b, c, isLit)
macro iterateSeq(a : untyped, b : static[seq[string]], code : untyped) : untyped =
let a =
if a.kind == nnkSym:
ident($a)
elif a.kind == nnkClosedSymChoice or a.kind == nnkOpenSymChoice:
ident($a[^1])
else:
a
#echo type(a)
result = newStmtList()
for item in b:
var newCode = code.copy()
replaceSymAndIdent(newCode, a, newLit(item), isLit = true)
result.add(newCode)
#echo treeRepr result
#echo repr result
#echo ""
#result = newStmtList()
macro assignField*(obj, fieldName, value: typed) : untyped =
let fieldSym = newIdentNode($`fieldName`)
result = quote do:
`obj`.`fieldSym`=`value`
type
SomeObj* = object
a* : int
b* : int
c* : int
var
foo = SomeObj()
iterateSeq(field, @["a", "c"]):
assignField(foo, field, 50)
echo foo
assignField(foo, "a", 100)
assignField(foo, "c", 200)
block:
assignField(foo, "b", 300)
echo foo
Also, I noticed you were missing a return type on your macro so I don't know how your code compiled at all.