Macro for partially quoting (or partially evaluating) an expression

I would like to quote expressions, but evaluate some Symbols (which ones? see below). It should be possible to accomplish this using macros. I’ve tried to give an idea below of what I’m after:

"""
e.g.

```julia
julia> eval(uneval(Expr(:my_call, :arg1, :arg2)))
:($(Expr(:my_call, :arg1, :arg2)))

julia> eval(eval(uneval(:(sqrt(9)))))
3.0
```
"""
function uneval(x::Expr)
    # the `Expr` below is assumed to be available in the scope and to be `Base.Expr`
    :(Expr($(uneval(x.head)), $(map(uneval, x.args)...)))
end

# tangential TODO: determine which one
uneval(x) = Meta.quot(x)
# uneval(x) = Meta.QuoteNode(x)

"""
e.g.
julia> dump(let x = 9
       @xquote sqrt(x)
       end)
Expr
    head: Symbol call
    args: Array{Any}((2,))
        1: sqrt (function of type typeof(sqrt))
        2: Int64 9
"""
macro xquote(ex)
    uneval(ex) # TODO escape `::Symbol` in `args`.
end

The current behavior is

julia> dump(let x = 9
       @xquote sqrt(x)
       end)
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: Symbol sqrt
    2: Symbol x

but I would like the output to be what I wrote in the docstring for @xquote. For this particular expression, the following definition produces the desired output:

macro xquote_sqrt9(ex)
    uneval(Expr(:call, sqrt, 9))
endI

as does explicitly quoting:

julia> dump(let x = 9
           :($(sqrt)($x))
       end)
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: sqrt (function of type typeof(sqrt))
    2: Int64 9

I’ve played around with f(x) = esc(x), and f(x) = Expr(:$, x) in
escape_all_symbols(ex) = MacroTools.postwalk(x -> x isa Symbol ? f(x) : x, ex), but I’m a bit lost about how to move forward.

I think this is tangential to my question, but I won’t be evaluating every single Symbol. Specifically, I would not evaluate Symbols that correspond to function arguments. For example, currently:

julia> dump(let x = 9
       @xquote y -> x + y
       end)
Expr
  head: Symbol ->
  args: Array{Any}((2,))
    1: Symbol y
    2: Expr
      head: Symbol block
      args: Array{Any}((2,))
        1: LineNumberNode
          line: Int64 2
          file: Symbol REPL[125]
        2: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol +
            2: Symbol x
            3: Symbol y

And in my desired output, Symbol + and Symbol x would be evaluated, but Symbol y would not be. I think I’ve got that aspect covered, however, and it would be enough for my purposes to see a definition of @xquote where, for example every Symbol except for :y is evaluated.

This is tightly related to this problem I encountered: https://julialang.zulipchat.com/#narrow/stream/225542-helpdesk/topic/running.20name.20resolution.20within.20a.20macro

You essentially want to do name-resolution within a macro, but you want to go a step further than me. Consider

x = 1
let sqrt = x -> x + 1, x = 2
  @xquote sqrt(x)
end

There is simply no way for the macro @xquote to know at macroexpansion time that :sqrt here is not Base.sqrt and x is not Main.x.

However, you could potentially figure out something at runtime. I think what you need

@xquote sqrt(x)

to give something like

quote
  let sqrt = @isdefined(sqrt) ? sqrt : QuoteNode(:sqrt), x = @isdefined(x) ? x : QuoteNode(:x)
    Expr(:call, sqrt, x)
  end |> simplify
end

and then have the simplify function be something that can recognize that Expr(:call, sqrt, x) can be replaced with sqrt(x).

1 Like

I will check out the related problem, thanks for the pointer and the reply.

If I am understanding correctly, I don’t think this is what I’m asking for.
whether you have

julia> @show sqrt;
sqrt = sqrt

or

julia> let sqrt=3
       @show sqrt
       end;
sqrt = 3

the code emitted from the macro is the same, and I’d like it to be the same for @xquote too.

I see now that I specified poorly what I want in my original post. I should show the output of @macroexpand, instead of showing what the macro evaluates to in each context. Thanks for bringing it to my attention.

julia> @macroexpand @xquote_sqrt9 sqrt(9)
:(Main.Expr(:call, sqrt, 9))

what sqrt resolves to is to be determined at runtime, but the expression that the macro emits just still has the sqrt escaped.

I just noticed this isn’t particularly helpful in clarifying, since two different expressions print the same way:

julia> :(Expr(:call, sqrt, 9))
:(Expr(:call, sqrt, 9))

julia> :(Expr(:call, $sqrt, 9))
:(Expr(:call, sqrt, 9))

julia> dump(:(Expr(:call, sqrt, 9)))
Expr
  head: Symbol call
  args: Array{Any}((4,))
    1: Symbol Expr
    2: QuoteNode
      value: Symbol call
    3: Symbol sqrt
    4: Int64 9

julia> dump(:(Expr(:call, $sqrt, 9)))
Expr
  head: Symbol call
  args: Array{Any}((4,))
    1: Symbol Expr
    2: QuoteNode
      value: Symbol call
    3: sqrt (function of type typeof(sqrt))
    4: Int64 9

I desire the :(Expr(:call, $sqrt, 9)) one.

Does replacing :sqrt with GlobalRef(Base, :sqrt) do what you want? In the general case, what you are asking for would only be possible by calling eval inside the macro, which will miss locally defined functions and the use of eval during macro expansion might have other undesired consequences.

1 Like

I made a mistake in the original post. That special case macro should not have been

macro xquote_sqrt9(ex)
    uneval(Expr(:call, sqrt, 9))
end

but instead

macro xquote_sqrt_x(ex)
    uneval(Expr(:call, esc(:sqrt), 9))
end

Sorry about that, and thanks for the answers so far to the question I asked, instead of the question I meant to ask. That explains why @Mason thought I wanted to do name resolution at macro-expansion time.

I believe I have answered my own question:

using MacroTools

"""
e.g.

```julia
julia> eval(uneval(Expr(:my_call, :arg1, :arg2)))
:($(Expr(:my_call, :arg1, :arg2)))

julia> eval(eval(uneval(:(sqrt(9)))))
3.0
```

Note the special case for `:(esc(x))`.
"""
function uneval(x::Expr)
    x.head === :escape && return x
    # the `Expr` below is assumed to be available in the scope and to be `Base.Expr`
    :(Expr($(uneval(x.head)), $(map(uneval, x.args)...)))
end

# tangential TODO: determine which one
uneval(x) = Meta.quot(x)
# uneval(x) = Meta.QuoteNode(x)

escape_all_symbols(ex) = MacroTools.postwalk(x -> x isa Symbol ? esc(x) : x, ex)

"""
e.g.
julia> dump(let x = 9
       @xquote sqrt(x)
       end)
Expr
    head: Symbol call
    args: Array{Any}((2,))
        1: sqrt (function of type typeof(sqrt))
        2: Int64 9
"""
macro xquote(ex)
    uneval(escape_all_symbols(ex))
end

which gives the following behavior:

julia> dump(let x = 9
           @xquote sqrt(x)
       end)
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: sqrt (function of type typeof(sqrt))
    2: Int64 9

julia> dump(let x = 9, sqrt=sin
           @xquote sqrt(x)
       end)
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: sin (function of type typeof(sin))
    2: Int64 9
1 Like