It’s not just leaks outwards, of symbols defined in the macro existing outside, as in the @my_time
example. But also inwards, of symbols re-defined elsewhere affecting what the macro does:
julia> let println = throw
@my_time begin # version of @my_time which escapes everything
sleep(1)
a = 42
end
end
ERROR: "Elapsed time: 1.0061688423156738s."
julia> macro defifndef1(x, y)
:(if !(@isdefined($x))
$x = sqrt($y)
end) |> esc # escapes everything, including sqrt
end;
julia> @macroexpand1 @defifndef1(a, 2)
:(if !(#= REPL[8]:2 =# @isdefined(a))
#= REPL[8]:3 =#
a = sqrt(2)
end)
julia> @defifndef1 a 2
julia> let sqrt = cbrt
@defifndef1 b 2 # wrong answer!
end
1.2599210498948732
julia> macro defifndef2(x, y)
:(if !(@isdefined($x)) # weirdly, not esc(x) here
$(esc(x)) = sqrt($(esc(y))) # now uses sqrt from definition's scope
end)
end;
julia> @macroexpand1 @defifndef2(a, 2)
:(if !(#= REPL[23]:2 =# @isdefined(a))
#= REPL[23]:3 =#
a = Main.sqrt(2)
end)
julia> let sqrt = cbrt
@defifndef2 c 2 # now this works
end
1.4142135623730951
julia> macro defifndef3(x, y)
ex = Expr(:macrocall, Symbol("@isdefined"), nothing, x) # not esc(x)
:(if !($ex)
$(esc(x)) = sqrt($(esc(y)))
end)
end;
julia> @macroexpand1 @defifndef3(a, 2)
:(if !(@isdefined(a))
#= REPL[12]:4 =#
a = Main.sqrt(2)
end)
julia> let sqrt = cbrt
@defifndef3 c 2
end
1.4142135623730951
These examples are artificial but someone using (say) sum
as the name of a local variable is something that will happen, and will break your macro if it intended to call Base.sum
. You could fix @defifndef1
by writing $sqrt
to use the one from the defining scope (and probably $!
too). That, in addition to gensym
for symbols defined within the macro, is generally safe.
But I don’t think you can write $@isdefined
. Perhaps you don’t have to, e.g. let var"@isdefined" = var"@show"
changes nothing. And, surprisingly, you do not need to escape its argument.