Generate in macroexpand time an expression that interpolates variables in runtime

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?

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)

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).

4 Likes

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.

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: