Is it expected behavior that typed macro arguments have a side effect on call site? For example I was surprised that this code compiles:
macro typedMacro(body: typed): untyped =
result = newCall(ident "echo", newStrLitNode "Hello World")
macro untypedMacro(body: untyped): untyped =
result = newCall(ident "typedMacro", body)
untypedMacro():
echo "here"
proc test(x: int) =
echo x
test(1)
My assumption was: I'm passing an untyped block into a macro and locally I just get the resulting AST echo "Hello World". I didn't expect that test is defined afterwards, because it is not part of the final AST. Is that by design?
What is the best way to get side-effects-free typed arguments? Should I simply wrap them in a block?
It's actually not a side effect. This seems like a bug in the VM or some strange behavior because you are calling a macro from within a macro. Your call to the macro is basically rewriting your code to:
typedMacro:
echo "here"
proc test(x: int) =
echo x
(which you can see with echo result.toStrLit() in your untypedMacro macro)
And I think the compiler gets a bit confused. However, I may be wrong. Usually when I write macros, if I need to break them up, I just use procs. For example, I would usually rewrite your code to:
macro typedMacro(body: typed): untyped =
result = newCall(ident "echo", newStrLitNode "Hello World")
macro untypedMacro(body: untyped): untyped =
result = newCall(ident "typedMacro", body)
untypedMacro():
echo "here"
proc test(x: int) =
echo x
test(1)
Which results in the correct behavior of an error thrown. Any reason you want to call a macro within a macro? Your actual use case might help :)
Maybe I shouldn't have obfuscated the example by the two macro calls. Of course you're right: The first macro simply gets expanded to the typed macro call.
The actual use case is somewhat involved, but maybe not super relevant. I was mainly puzzled by the behavior, and I'm still wondering if it is a bug or intentional. Note that because typed argument actually affect the callsite you can do things like this:
macro takesTwo(a, b: typed): untyped =
result = newStmtList()
takesTwo() do :
let x = 1
do:
x
# but don't dare to redefine x here ;)
where one typed argument gets its type information from the evaluation of another argument. I'm wondering how much Nim code relies on that.