Help: macro expander not gensyming local variables in returned function expr if signature is escaped

macros
metaprogramming

#1

Hey, can anyone help with this macro expansion not working like I expect?

It’s not gensyming the local variables in a function body if the function’s signature is escaped. Anyone seen this before? :slightly_smiling_face: Thanks


I want to emit several functions from a macro. They all have the same function signature except for the name of the function, so I was creating that in a variable and interpolating it into the returned quoted expression.

However, I seem to have hit a problem where escaping the function signature stops the macro expander from gensyming local variables in the function body!

Here’s a MWE:
Un-escaped, everything is fine:

julia> macro m()
         sig = :(f(x))
         quote
           $(Expr(:function, sig, quote
             v = x + 1
             v
           end))
         end
       end
@m (macro with 2 methods)

julia> @macroexpand @m()
quote
    #= none:3 =#
    function #178#f(#180#x)
        #= none:4 =#
        #179#v = #180#x + 1
        #= none:5 =#
        #179#v
    end
end

But if I escape the signature, v isn’t escaped:

julia> macro m()
         sig = :(f(x))
         quote
           $(Expr(:function, esc(sig), quote
             v = x + 1
             v
           end))
         end
       end
@m (macro with 2 methods)

julia> @macroexpand @m()
quote
    #= none:3 =#
    function f(x)
        #= none:4 =#
        Main.v = Main.x + 1
        #= none:5 =#
        Main.v
    end
end

Any ideas? Is this a bug in the macro expander?

Also, weirdly, it works if I don’t wrap the returned expression in a quote block, and just return the function expression directly, but I think I need the quote block because i have three functions to return, not just one?


#2

I guess i can go through and escape each piece of the function signature, which does seem to work, but i don’t want to because i want it to work for any kind of signature (including f(x::T) where T, etc) and I don’t want to have to muck about with that structure if i don’t have to.

julia> macro m()
         quote
           $(Expr(:function, :($(esc(:f))($(esc(:x)))), quote
             v = x + 1
             v
           end))
         end
       end

#3

Yes I think this is a bug in the macroexpander. I’ll see if it can be fixed in a non-breaking way.


#4

Yeah looks like it to me too. Thanks Jeff!!

In the meantime, I think I was able to get the behavior I want by wrapping the function body with a let-block.

:slight_smile: Thanks!

EDIT: JK the let-block doesn’t work because then the function arguments are converted into globals by the macro-expander.


#5

Not sure I understood exactly what you wanted to do, but if only the name varies, maybe you should escape only it, and not the whole signature.

For example, this seems to work fine:

macro m()
    name1 = :f
    name2 = :g
    
    quote
        function $(esc(name1))(x)
            v = x+1
            v
        end

        function $(esc(name2))(x)
            v = x+2
            v
        end
    end
end
julia> @macroexpand @m
quote
    #= REPL[9]:3 =#
    function f(#16#x)
        #= REPL[9]:4 =#
        #15#v = #16#x + 1
        #= REPL[9]:5 =#
        #15#v
    end
    #= REPL[9]:8 =#
    function g(#18#x)
        #= REPL[9]:9 =#
        #17#v = #18#x + 2
        #= REPL[9]:10 =#
        #17#v
    end
end

And the functions are correctly defined with the names you provided:

julia> @m
g (generic function with 1 method)

julia> f(1)
2

julia> g(1)
3

#6

:slight_smile: Thanks for the help, @ffevotte! Ah, sorry i wasn’t clear.

Unfortunately I do want to escape the whole signature, because I want to escape the name, the arguments, and also any type parameters in a where T clause. Since the only thing i was changing was the name, this is what I’m doing:

macro m(funcexpr)
    signature = funcexpr.args[1]

    f1_signature = deepcopy(signature)
    call_expr(generator_signature).args[1] = gensym("f1")

    f2_signature = deepcopy(signature)
    call_expr(generator_signature).args[1] = gensym("f2")
  
     ...
    quote
             $(esc(Expr(:function, signature, body)))
             $(Expr(:function, esc(f1_signature), quote ... end))
             $(Expr(:function, esc(f2_signature), quote ... end))
    end

I do think it’s a bug, but I have managed to find a workaround for now, which is to make those function bodies quote let ... end end instead.

EDIT: JK the let-block doesn’t work because then the function arguments are converted into globals by the macro-expander.


#7

Oh, never mind, the let-block solution doesn’t work, because then the function arguments are converted into globals by the macro-expander:


julia> macro m()
         quote
           $(Expr(:function, esc(:(f(x))), quote let
             v = x + 1
             v
           end end))
         end
       end
>> @m (macro with 1 method)

julia> @macroexpand @m()
>> quote
    #= none:3 =#
    function f(x)
        #= none:3 =#
        let
            #= none:4 =#
            #327#v = Main.x + 1
            #= none:5 =#
            #327#v
        end
    end
end

#8

Can you let me know if you open a github issue/PR about this? I’m interested to see what this code looks like and if maybe i can help! :slight_smile:


#9

I think a let-block could work, provided that you define aliases for all your function arguments.

This seems to work, for example:

macro m()
    quote
        $(Expr(:function, esc(:(f(x))), quote
               let x=$(esc(:x))
                   v = x + 1
                   v
               end
           end))
    end
end
julia> @macroexpand @m
quote
    #= REPL[19]:3 =#
    function f(x)
        #= REPL[19]:3 =#
        let #8#x = x
            #= REPL[19]:4 =#
            #7#v = #8#x + 1
            #= REPL[19]:5 =#
            #7#v
        end
    end
end

Now getting the arguments list to generate the let-assignments is an extra burden, but should not be too difficult…

EDIT: I realize this might be precisely what you wanted to avoid, as you said above :confused:


#10

That’s a really good idea. Thanks!

Actually, what i ended up doing was just manually escaping every part of the signature, which is pretty yucky, but i guess not too bad. it looks like this:

    # TODO: There is a bug in the julia macro-expander, preventing escaping the entire signature.
    # Therefore, for now, we have to manually escape its internals
    methods_helpername = gensym("methods_helper")
    methods_signature = deepcopy(main_signature)
    call_expr(methods_signature).args[1] = esc(methods_helpername)
    call_expr(methods_signature).args[2:end] = [esc(p) for p in function_params]
    # Escape the type parameters in where clauses (TODO: remove when bug is fixed)
    sigexpr = methods_signature
    while sigexpr.head != :call
        sigexpr.args[2:end] = [esc(e) for e in sigexpr.args[2:end])]
        sigexpr = sigexpr.args[1]
    end

Thanks again for your help though! :slight_smile: