Interpolating variable to expr to be used in macro

I want to write a macro that calls 2 macros. The second macro depends on the first macro. The first macro returns an expression that I want to pass to the second macro and I am unsure how to do this. See a simplified example. (Note: first macro is @code_expr)

julia> using Revise, CodeTracking, Handcalcs

julia> macro macro_main(expr)
           return quote
               found_func = @code_expr calc_Ix(5, 15)
               latex = @mac2 found_func
           end
       end
@macro_main (macro with 1 method)

julia> macro mac2(expr)
       quote foo($expr)
       end
       end
@mac2 (macro with 1 method)

julia> function foo(ex) # this is simplified, but the function builds an expression
       len = length(ex.args)
       return Expr(:(=), :x, len)
       end
foo (generic function with 1 method)

julia> @macro_main calc_Ix(5, 15) # calc_Ix defined in Handcalcs
:(x = 2)

You can see the way I wrote @mac2 allowed me to interpolate the found_func variable in @macro_main. However, this returns an expression :(x = 2) rather than the evaluated expression since I have to put it inside a quote block.

How do I get, in this simplified example, the expression evaluated, so when I do:

julia> x
2

I get 2.

I have tried multiple ways so far and I always end up with just the unevaluated expression, or having to use the eval function which only allows the macro to work within the global scope.

It is unclear to me why @mac2 needs to be a macro. The way you have written it right now @mac2 only ever gets the symbol found_func. It appears to me that you want to have the contents of found_func to be passed to the inside of @mac2 and then to the function foo.
So you should just make @mac2 a normal function and remove the quote inside it (and then you could also remove it alltogether and just have @macro_main expand to a call to foo).

I need info from @code_expr though which happens at macro expansion time, so it would not expand again on the call to foo. That is why I believe I need another macro, @mac2.

I currently do not have @mac2 (in my working code) and have been trying within @macro_main and I just get the unevaluated expression. The only way I get it “working” is by using the eval function or macro, but it only allows it to work within a global scope. The @mac2 way was sort of my next idea… but still can’t get it working.

I should have written found_func in the @macro_main a bit different. See below:

julia> macro macro_main(expr)
           return quote
               found_func = @code_expr $expr
               latex = @mac2 found_func
           end
       end

julia> a = 5
5

julia> b = 15
15

julia> @macro_main calc_Ix(a, b) # calc_Ix defined in Handcalcs
:(x = 2)

This does not pass anything from @code_expr to @mac2 though. All macros are expanded before anything is evaluated. Thus @mac2 only ever gets the symbol :found_func as argument and never its runtime value.

I don’t think what you plan can be realised just with macros in a simple way, because some information (like the types of a and b) is available only at runtime. Thus code_expr can only find the correct method at runtime. After that it is too late to go back to macro expansion.

So macros are not the answer but I think you can use a generated function instead since they have access to the types (but not the values) of things. We also take some inspiration from @code_expr.

This does not work because you cannot use reflection from inside of generated function. So I don’t know how your goal can be achieved. Sorry. :confused:

For completeness, here is my non-functional draft:

macro macro_main(ex0...)
    # if you want to do something with the return value of gen_func
    # generate more complicated stuff here
    InteractiveUtils.gen_call_with_extracted_types_and_kwargs(__module__, :gen_func, ex0)
end
@generated function gen_func(f::F, types) where F
    # types is Type{Tuple{argtype1,argtype2,...}} we need to convert it to (argtype1,...)
    argtype_tuple = tuple(types.parameters[1].types...)
    # line below does not work because it uses reflection!
    found_func = code_expr(f, argtype_tuple) # use type information to get the function
    # now generate the body that you want to execute
    return :(foo($found_func))
end

To summarize: I think what you want to do is impossible in general. The problem is you need to do reflection which only works at runtime (since function definitions are only meaningful at runtime) but then still want to do some level of code generation depending on that information from reflection.

Yeah I was hoping to do something like the @eval macro does where it interpolates. See below:

julia> let y = 7
           z = @eval $y
       end
end
7

But I can’t figure out how to do this inside a quote. See below:

julia> quote
       let y = 7
           z = @eval $y
       end
       end
ERROR: UndefVarError: `y` not defined
Stacktrace:
 [1] top-level scope
   @ REPL[46]:1

Yeah I am getting to this point too… There is something in my gut though that is telling me that it should be possible. Thanks for the help!

You can figure this out by inspecting the AST:

julia> dump(:(:($x)))
Expr
  head: Symbol quote
  args: Array{Any}((1,))
    1: Expr
      head: Symbol $
      args: Array{Any}((1,))
        1: Symbol x

julia> quote
              let y = 7
                  z = @eval $(Expr(:$, :y))
              end
       end
quote
    #= REPL[14]:2 =#
    let y = 7
        #= REPL[14]:3 =#
        z = #= REPL[14]:3 =# @eval($(Expr(:$, :y)))
    end
end

julia> macro testm()
       quote
              let y = 7
                  z = @eval $(Expr(:$, :y))
              end
       end
       end
@testm (macro with 1 method)

julia> @testm
7

But I still think this will not work if you try to get runtime information at macro expansion time. Simply because at macro expansion the runtime information does not exist yet. I don’t see how this interpolation will help you in that regard.

I think we are close. See below:

julia> macro macro_main(expr)
           return quote
               found_func = @code_expr $expr
               latex = @eval @mac2 $(Expr(:$, :found_func))
           end
       end
@macro_main (macro with 1 method)

julia> @macro_main calc_Ix(a, b) # calc_Ix defined in Handcalcs
2

However, variables in a local scope do not work:

julia> let u=4, v = 15
       @macro_main calc_Ix(u, v)
       end
ERROR: UndefVarError: `u` not defined
Stacktrace:
 [1] macro expansion
   @ REPL[47]:3 [inlined]
 [2] top-level scope
   @ REPL[50]:2

I think there may be something here though.

I think I may have it. I will try to implement in actual package. See below:

julia> macro macro_main(expr)
           return esc(quote
               found_func = @code_expr $expr
               latex = @eval @mac2 $(Expr(:$, :found_func))
           end)
       end
@macro_main (macro with 1 method)

julia> let u=4, v = 15
       @macro_main calc_Ix(u, v)
       end
2

This leaks the variables found_func and latex to the outer scope. Better escape the relevant parts only:

macro macro_main(expr)
           return quote
               found_func = $(esc(:(@code_expr $expr)))
               latex = @eval @mac2 $(Expr(:$, :found_func))
           end
       end
1 Like