Help with macro calling another macro

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.

3 Likes