@testset expands all macros *before* executing the generated code

Hello community. I’ve just spent hours confusingly revolving around the following quirk.

Given this code in my package:

macro a()
    println("expand a")
    :(println("exec a"))
end

macro b()
    println("expand b")
    :(println("exec b"))
end

And this user code:

println("invoke a")
@a
println("invoke b")
@b

I expect the following behaviour (based on the current REPL behaviour):

invoke a
expand a
exec a
invoke b
expand b
exec b

However, this is what happens when I automate the test:

using Test
@testset "" begin
    println("invoke a")
    @a
    println("invoke b")
    @b
end
expand a  # !
expand b  # !
invoke a
exec a
invoke b
exec b
  1. Is that expected behaviour?
  2. Is that somehow configurable?
  3. Can I rely on the REPL behaviour (i.e. macros being expanded on invocation) and just work around @testset for my tests, or should I not assume that Julia upholds any guarantee regarding macros expansion time?

Yes, macros are expanded before the expanded code is executed. They are expanded outside-in (you can check with Base.@macroexpand1, if I’m not mistaken).

No. Macros are an AST transform.

I’m not sure I follow - when a macro is expanded, the code inside the macro building the expression (even if not contributing to the returned expression) still has to run.

2 Likes

It’s not particular to @testset, it’s because a begin block is a compound expression. The REPL reads one complete expression, not line, at a time; the begin block is only considered complete at the end.

julia> begin
           println("invoke a")
           @a
           println("invoke b")
           @b
       end
expand a
expand b
invoke a
exec a
invoke b
exec b
4 Likes

Thank you for feedback @Sukera @Benny :slight_smile: IIUC what I’m observing is rather related to how julia parser / expansion works, and also happens without @testset with plain begin/end blocks.

I guess I just need to be careful not to rely on the exact nesting order between my macro expansions and actual generated code execution, because I cannot enforce that they won’t be invoked within macros or begin/end blocks downstream ^ ^"

1 Like