Generate in macroexpand time an expression that interpolates variables in runtime


#1

I have a struct to represent symbolic variable:

struct Var
    name::Symbol
    value::Any 
end

Now I want to generate a function expression that takes objects of type Var and returns an expression made from their .name fields, e.g. like this:

function +(x::Var, y::Var)
    return Expr(:call, :+, x.name, y.name)
end 

x = Var(:a, 1)     # note: julia variable `x`, but internal name is :a 
y = Var(:b, 2)

x + y  # ==> :(a + b) 

Code generation will be wrapped into a macro, so I’ll refer to this stage - stage where we generate function like above - as a macroexpand time. On the contrary, the returned expression, e.g. Expr(:call, :+, x.name, y.name) can only be created in runtime since x.name and y.name aren’t known beforehand.

I have many function like this with different signatures, e.g.:

+(x::Var, y::Var)
exp(x::Var)
^(x::Var, n::Int)
...

Obviously, I don’t want to copy-paste function body for of these cases but instead to have something like:

@symbolic +(x::Var, y::Var)
@symbolic exp(x::Var)
@symbolic ^(x::Var, n::Int)
...

So the list of argument names is always different (we can ignore types for a moment). Now the basic setup of a problem looks like this:

f_vars = [:x, :y]   # the list of `Var` names from function signature, known during macroexpand time
quote
    ex_vars = ...    # something that returns [:a, :b] in runtime
    Expr(:call, :op, ex_vars...)
end

The missing piece is the code to get ex_vars, i.e. values of x.name and y.name during runtime. Note, that during macroexpand time we known the names of variables (i.e. x and y), but not their values.

First I tried the following:

:(ex_vars = [v.name for v in $f_vars])

But f_vars is a list of symbols, not Vars, so v.name fails with type Symbol has no field name.

So I tried to interpolate each v first, meaning that interpolation should happen in runtime:

:(ex_vars = [$(v).name for v in $f_vars])

Surely, it’s interpolated in macroexpand time instead. I also tried multiple options with Expr(:$, ...) and QuoteNode(...), but couldn’t find anything working. One more attempt with escin hope it passes $ as is:

:(ex_vars = [esc($(v).name) for v in $f_vars])

But it also tries to interpolate v right in macroexpand time.

Any other options?


#4

I believe this does what you’re asking for:

macro symbolic(ex::Expr)
    @assert ex.head == :call
    op = ex.args[1]
    args = ex.args[2:end]
    names = []
    types = []
    for arg in args
        @assert arg.head == :(::)
        push!(names, arg.args[1])
        push!(types, arg.args[2])
    end
    Expr(:(=),
        esc(ex),
        Expr(:call, :Expr, QuoteNode(:call), QuoteNode(op), [Expr(:., name, QuoteNode(:name)) for name in names]...)
    )
end

Usage:

julia> struct Var
           name::Symbol
       end

julia> x = Var(:a)
Var(:a)

julia> y = Var(:b)
Var(:b)

julia> import Base: +

julia> @symbolic +(x::Var, y::Var)
+ (generic function with 181 methods)

julia> x + y
:(a + b)

However, there’s a lot of clutter in that macro definition. MacroTools.jl helps clean this up a lot:

using MacroTools

macro symbolic2(ex::Expr)
    @assert @capture(ex, op_(args__))
    argdata = splitarg.(args)
    names = [a[1] for a in argdata]
    Expr(:(=),
        esc(ex),
        Expr(:call, :Expr, QuoteNode(:call), QuoteNode(op), [Expr(:., name, QuoteNode(:name)) for name in names]...)
        )
end

Usage:

julia> @symbolic2 -(x::Var, y::Var)
- (generic function with 1 method)

julia> x - y
:(a - b)

#5

By the way, my general method for approaching this (and other metaprogramming problems) is to manually generate the output I want, then dump that expression and then write the code to generate that output. For example, in this case I did the following:

julia> dump(:(+(x::Var, y::Var) = Expr(:call, :+, x.name, y.name)))
Expr
  head: Symbol =
  args: Array{Any}((2,))
    1: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol +
        2: Expr
          head: Symbol ::
          args: Array{Any}((2,))
            1: Symbol x
            2: Symbol Var
          typ: Any
        3: Expr
          head: Symbol ::
          args: Array{Any}((2,))
            1: Symbol y
            2: Symbol Var
          typ: Any
      typ: Any
    2: Expr
      head: Symbol block
      args: Array{Any}((2,))
        1: Expr
          head: Symbol line
          args: Array{Any}((2,))
            1: Int64 1
            2: Symbol REPL[1]
          typ: Any
        2: Expr
          head: Symbol call
          args: Array{Any}((5,))
            1: Symbol Expr
            2: QuoteNode
              value: Symbol call
            3: QuoteNode
              value: Symbol +
            4: Expr
              head: Symbol .
              args: Array{Any}((2,))
                1: Symbol x
                2: QuoteNode
              typ: Any
            5: Expr
              head: Symbol .
              args: Array{Any}((2,))
                1: Symbol y
                2: QuoteNode
              typ: Any
          typ: Any
      typ: Any
  typ: Any

which showed me exactly where I needed to put the appropriate QuoteNodes and where to replace an Expr with an Expr(:call, :Expr, ...)

Edit: originally I was mistakenly generating Expr(:+, x.name, y.name) instead of Expr(:call, :+, x.name, y.name).


#6

There’s one remaining issue with my macro, which is that it will try to do x.name for all args, regardless of their type. Presumably that’s not what you want for non-Var arguments, but I think instead of doing x.name you can just do _getname(x) and have _getname(x::Var) = x.name with _getname(x) = x as a fallback for all other inputs.


#7

Works like a charm, thanks!

By the way, my general method for approaching this (and other metaprogramming problems) is to manually generate the output I want, then dump that expression and then write the code to generate that output.

Yeah, I walked around this approach multiple times, but this code generation during code generation just mixed up my mind. Now I know that it works in such complicated circumstances as well :slight_smile: