You're definitely on the right track here. The effects solution is definitely cool, and I'd love to see what we can do with effects once they are no longer experimental. In the meantime however we can try to track things in a more manual way.
What I typically do when writing Nim macros like this is that I work backwards. I start with a question of "how do I want to write my code" which you've already done with the `withTransaction` block. Then I think to myself, what extra code do I need to generate to make this code work as-is. In your case with limdb you would need some way of making a transaction track whether it wrote anything or not. Now that we have these pieces we can see that `[]` and `[]=` needs to be overloaded in our `withTransaction` block somehow. The easiest way to achieve such an overload is of course with a distinct type. So if we write a small distinct type around a transaction, along with templates for `[]` and `[]=` on this distinct type that writes to a `writeCheck` compile-time variable, decasts the distinct, and calls the original procedure we should now have some sort of write tracking. Of course the user can still do the decasting themselves and run unsafe code, but at that point they can only blame themselves if things don't work properly. If you want to give the user this flexibility however you can offer procedures to convert the underlying transaction object but which requires them to specify if they need it for reading or writing purposes and set `writeCheck` accordingly. As for your code it is simply a matter of type checking. If you change the argument to your template to `untyped` it actually works just fine. This is because the `static` parts of the code is actually run as the type checking is done, so by the time your template is invoked `called` is already set to true. If you either change it to `untyped` or if you simply move your `called = false` to after your check (to reset it for the next caller) it should work fine. Of course you might not want a global static variable so you might end up with something like this: template wasItCalled(body: untyped): untyped = block: var called {.compileTime.} = false template foo() {.used.} = static: called = true foo() body static: when called: echo "called" else: echo "not called" proc foo() = echo "Foo" wasItCalled: discard # This should echo 'not called' wasItCalled: foo() # This sould echo 'called' Run Or you can do a similar thing with a macro: import macros macro wasItCalled(body: untyped): untyped = let calledSym = genSym(nskVar) quote do: block: var `calledSym` {.compileTime.} = false template foo() = static: `calledSym` = true foo() `body` when `calledSym`: echo "called" else: echo "not called" proc foo() = echo "Foo" wasItCalled: discard # This should echo 'not called' wasItCalled: foo() # This sould echo 'called' Run