Simple example:
proc foo[T](self: T) {.bar.} = discard
foo(3)
bar only gets idents instead of symbols since it is evaluated instantly instead of every time foo is instantiated for a new concrete type. This is really frustrating when requiring the type of T in the macro.
I probably should quickly go over my actual use case: Nim is by and large fast enough that you can just focus on writing readable code and as long as algorithm and data structure choice aren't god awful it will be fast enough. For the occasional very tight loop actual optimization might be required which in large parts means minimizing copying and allocations.
So I figured that a macro that you can put on the actually important functions and which emits warnings on implicit copying might make this a lot less annoying. To check whether a copy will actually allocate memory the macro has to look up whether the type contains any strings or seqs, though, so this runs into a brick wall with generics. Any way to work around this?
Small addendum, each call to warning actually emits two warnings - one for the macro instantiation location and one for the actual warning. Is there a reasonable way to silence the first one? Currently the output looks like this:
test.nim(73, 17) template/generic instantiation from here
test.nim(4, 10) Warning: implicit copy: test.nim(75,7) [User]
test.nim(73, 17) template/generic instantiation from here
test.nim(4, 10) Warning: implicit copy: test.nim(80,9) [User]
test.nim(94, 1) template/generic instantiation from here
test.nim(4, 10) Warning: implicit copy: test.nim(97,8) [User]
If anyone is interested, the macro currently looks like this. Sorry that the code blocks bloat the post so much, really wish they could be hidden behind spoiler tags or limited in size with scrollbar.
import macros
proc warnImplicit(n: NimNode) {.compiletime.} =
warning("implicit copy: " & n.lineinfo & " " & repr n)
proc errorUnkown(n: NimNode) {.compiletime.} =
error("unkown: " & n.lineinfo & "\n" & n.repr & "\n" & treeRepr n)
proc isHeapValue(n: NimNode): bool{.compiletime.} =
eqIdent(n, "seq") or eqIdent(n, "string")
proc containsHeapValue(t: NimNode): bool {.compiletime.} =
# true if type includes seq or string
case t.kind
of nnkObjectTy:
for field in t[^1]:
if field.getType.containsHeapValue(): return true
of nnkTupleTy:
for field in t:
if field.getType.containsHeapValue(): return true
of nnkBracketExpr:
let baseType = t[0]
if baseType.isHeapValue: return true
if eqIdent(baseType, "tuple"):
for i in 1..<t.len:
if t[i].containsHeapValue(): return true
of nnkSym:
if t.isHeapValue: return true
else: return false
proc checkHeapValue(n: NimNode, mutable: bool) {.compiletime.} =
if n.isHeapValue:
if mutable: n.warnImplicit
case n.kind
of nnkPar, nnkBracket, nnkCurly:
for child in n:
child.checkHeapValue(true)
of nnkObjConstr, nnkAsgn:
for i in 1..<n.len:
n[i].checkHeapValue(true)
of nnkPrefix, nnkCall, nnkPostFix, nnkInfix, nnkDotCall, nnkCommand, nnkHiddenCallConv, nnkHiddenStdConv, nnkHiddenSubConv:
# arguments aren't copied so only check for constructor usage
for i in 1..<n.len:
n[i].checkHeapValue(false)
of nnkHiddenDeref, nnkHiddenAddr:
n[0].checkHeapValue(mutable)
of nnkExprColonExpr, nnkConv:
for i in 1..<n.len:
n[i].checkHeapValue(mutable)
of nnkBracketExpr..nnkElseExpr:
if mutable and n.getType().containsHeapValue():
n.warnImplicit()
of nnkSym:
if mutable and n.getType().containsHeapValue():
n.warnImplicit()
of nnkLambda:
for i in 1..<n[3].len:
n[3][i].checkHeapValue(true)
of nnkEmpty, nnkIdent, nnkCharLit..nnkNilLit, nnkStmtListExpr: discard
else:
n.errorUnkown
proc findDeepCopies(n: NimNode, global: bool) {.compiletime.} =
# find all assignments within n
case n.kind
of nnkAsgn, nnkIdentDefs:
# deep copies
n[^1].checkHeapValue(mutable = true)
of nnkLetSection:
# shallow copies by default
# deep copies in global scope and rhs might deep copy while constructing
for child in n:
child[^1].checkHeapValue(mutable = global)
of nnkProcDef, nnkIteratorDef, nnkTemplateDef, nnkConverterDef:
if n[2].kind == nnkEmpty:
for child in n.body:
child.findDeepCopies(global = false)
else:
n[^1] = newStmtList().add newCall("checkexplicit", n[^1].copy(), false.newLit)
of nnkPar, nnkBracket, nnkObjConstr:
for child in n: # deep copies regardless whether assignment is mutable
child.checkHeapValue(true)
of nnkPrefix, nnkCall, nnkPostFix, nnkDotCall, nnkCommand:
# rvalues
for i in 1..<n.len:
n[i].checkHeapValue(false)
else:
for child in n:
child.findDeepCopies(global = global)
macro checkexplicit*(d: typed, global: bool = true): typed =
d.findDeepCopies(global.boolVal)
if d.kind != nnkProcDef: result = d
proc copy*[T](a: T): T = a
And the test code that emitted these warnings looks like this:
type
A = object
a: B
B = object
b: C
C = (string, int)
var
glob = A(a: B(b: ("hi", 1)))
proc test(a: A) {.checkexplicit.} =
var
g = a # copy
f = a.copy # copy without warning
let
b = a # no copy
c = a.a # no copy
d = (a.a, 1) # copy
e = (a.a.copy, 1) # copy without warning
echo repr glob.a
echo repr a.a
echo repr b.a
echo repr c
echo repr d[0]
echo repr e[0]
echo repr f.a
echo repr g.a
glob.test()
checkExplicit:
let
a = @[1, 2]
b = a # copy
I almost certainly forgot a bunch of cases so if you notice anything obvious please tell!
I updated the code above to be somewhat more reliable. There are some very weird edge cases and I am not sure how to handle them yet. For instance:
let
a = ("foo", "bar")
b = a
c = a[0]
(d, e) = a
copies in every case.
let
x = "foo"
y = "foo"
a = (x, y)
b = a
c = a0]
(d, e) = a
Copies during the construction of the tuple but only shallow copies after that. Is there some blog post that explains how nim decides whether or not to copy and is that information accessible to macros? Or some compiler module to check out?