Problem about `quote` in macro definition

There are many things going on here. I’m not sure I can explain everything, but let’s at least try.

The relevant part of the documentation is the section about macro hygiene. We see there that a symbol appearing in a macro is renamed (gensymed, like var"#5#f1" in your example) if the corresponding variable is considered local.

In macro m1, the name f1 is considered to refer to a global variable. But in macro m2, it is detected as being local and is renamed. My guess is that this change in behavior comes from the implicit begin...end block generated by quote...end. But I’m actually not sure whether this is intended or not.

The reason why macro m3 does not work is a bit tricky to explain. It is not really because of the presence of parentheses: as you can easily verify, parentheses surrounding the function name are allowed.

julia> (f)(x) = 2x
f (generic function with 1 method)

julia> f(4)
8

Rather, the presence of parentheses is a subtle indicator that the generated code is not really what you think it is. Let’s inspect it further:

julia> using MacroTools
julia> dump(MacroTools.striplines(@macroexpand T.@m3 foo))
Expr
  head: Symbol block
  args: Array{Any}((1,))
    1: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Expr
          head: Symbol call
          args: Array{Any}((2,))
            1: f1 (function of type typeof(Main.T.f1)) # The function itself; not its name
            2: Expr
              head: Symbol ::
              args: Array{Any}((1,))
                1: Expr
        2: Expr
          head: Symbol block
          args: Array{Any}((1,))
            1: Int64 1
(For comparison, the same output for `@m1`, which works)
julia> dump(MacroTools.striplines(@macroexpand T.@m1 foo))
Expr
  head: Symbol =
  args: Array{Any}((2,))
    1: Expr
      head: Symbol call
      args: Array{Any}((2,))
        1: GlobalRef            # (qualified) name Main.T.f1
          mod: Module Main.T
          name: Symbol f1
        2: Expr
          head: Symbol ::
          args: Array{Any}((1,))
            1: Expr
              head: Symbol call
              args: Array{Any}((2,))
                1: GlobalRef
                2: Symbol foo
    2: Expr
      head: Symbol block
      args: Array{Any}((1,))
        1: Int64 1

What we see is that, instead of the name of f1, we got the function itself. Let’s try and understand what happens with this whole $(esc(...)) syntax. In the case of $(esc(sym)), which you got right:

  1. sym is evaluated in the context of the macro expansion, yielding the symbol :foo, because that’s what the macro argument was named
  2. this symbol :foo is escaped, meaning that it is marked to not be changed for hygiene purposes → it therefore reaches the macro expansion without modification
  3. when the macro expansion gets evaluated (in the context of the macro call), :foo therefore refers to the function named foo in the current scope

Let’s follow the same steps in the case of $(esc(f1)):

  1. f1 is evaluated in the context of the macro expansion, yielding the function T.f1 (not its name: the function itself)

  2. escaping the function doesn’t mean much; nothing is done about it regarding hygiene (EDIT: to be clear, and as noted by @mcabbott, esc has no effect whatsoever here, and can be removed without changing anything)

  3. when the macro expansion gets evaluated, Julia tries to interpret function T.f1 as a function name, which does not make sense => error

    julia> T.@m3 foo
    ERROR: syntax: invalid function name "typeof(T.f1)()"
    



All in all, if you surround your code in a quote...end block (which I would advise to do), you’ll have to make it so that Julia understands f1 as being a global variable. There are (at least) two ways of doing this:

  • mark it with the global keyword (-> @m2 in the example below)
  • qualify the function name with the module name (-> @m3 in the example below)
module T

function f1 end

macro m1(sym)
    :(f1(::typeof($(esc(sym)))) = 1)
end

macro m2(sym)
    quote
        global f1
        f1(::typeof($(esc(sym)))) = 1
    end
end

macro m3(sym)
    quote
        T.f1(::typeof($(esc(sym)))) = 1
    end
end

end

We can check that all three macros work:

julia> using .T: @m1, @m2, @m3, f1

julia> foo1() = 1;
julia> @m1(foo1)
f1 (generic function with 1 method)

julia> foo2() = 1;
julia> @m2(foo2)
f1 (generic function with 2 methods)

julia> foo3() = 1;
julia> @m3(foo3)

julia> methods(T.f1)
# 3 methods for generic function "f1":
[1] f1(::typeof(foo3)) in Main at REPL[1]:18
[2] f1(::typeof(foo2)) in Main at REPL[1]:12
[3] f1(::typeof(foo1)) in Main at REPL[1]:6
2 Likes