Constructing expressions with local macro variables and Symbols

I want to construct an expression inside a macro, given a particular input parameter.

The behavior should be like that:

julia> @macroexpand @mymacro mysym[15]
:([[:mysym], 15])

So I tried to write something like that:

macro mymacro(ex)
    return :([[ :$(ex.args[1]) ], $(ex.args[2]) ])
end
# but this gave me:
julia> @macroexpand @mymacro mysym[15]
:([[(:$)((Main.ex).args[1])], 15])

I am really having trouble to understand this output.
ex.args[2] evaluated perfectly as intended to 15.
But ex.args[1] is searching to be evaluated in the Main module. I don’t want that. I just want ex to be evaluated locally inside the macro.

I reached close when I escaped ex.args[1], although I didn’t quite understood why, since I was thinking of the esc() as the way to access user namespace:

macro mymacro(ex)
    return :([[ $(esc(ex.args[1])) ], $(ex.args[2]) ])
end
# so now only the `:` is missing
julia> @macroexpand @mymacro mysym[15]
:([[mysym], 15])

But adding the : really destroys everything:

macro mymacro(ex)
    return :([[ :($(esc(ex.args[1]))) ], $(ex.args[2]) ])
end
julia> @macroexpand @mymacro mysym[15]
:([[Main.esc((Main.ex).args[1])], 15])

I finally got it working with

macro mymacro2(ex)
    return Expr(:vect, Expr(:vect, QuoteNode(ex.args[1])), ex.args[2])
end

But that was quite hard and unintuitive to construct.
Can there be a simpler solution using :(....) like in my first attempts?

Do you really want a quoted expression, or just the literal vector?

macro mymacro(ex)
   [[esc(ex.args[1])],ex.args[2]]
end

?

I really want a quoted expression.
The reason is using it to build a macro for evaluating JuMP variables.
e.g.

julia> @macroexpand @val myvar[(14,53)]
:((model[:myvar])[(14,53)] |> OptNetILPs.value)

I just constructed a minimal problem out of it.

As said, I did implemented it using expressions:

"evaluate indexed variable for JuMP model `model`"
macro val(ex::Expr)
    Expr(:call, :|>, Expr(:ref, Expr(:ref, esc(:model), QuoteNode(ex.args[1])), ex.args[2]), :value)
end

"evaluate single variable for JuMP model `model`"
macro val(sy::Symbol)
    Expr(:call, :|>, Expr(:ref, esc(:model), QuoteNode(sy)), :value)
end

But I find it quite complicated and I don’t understand why the :(...) doesn’t work, as mentioned above.

Yeah, I’ve been fiddling around with it… I think you want:

macro mymacro(ex)
   return :([[$(esc(ex.args[1])) ], $(ex.args[2]) ])
end
@macroexpand @mymacro mysym[15]
:([[mysym], 15])

??

as a general principle of operation, macros are not designed to be composable, they are fundamentally different from functions. Thus, if you find something really useful, maybe try file an [Feature] issue on Github and see if they can add this to existing macros.

1 Like

I think the issue is you’re asking for a quote around mysym, but are you sure you need that? mysym is a symbol, quoting a symbol is not necessary normally.

This is exactly what I tried here:

But as you also mentioned, it gives out :([[mysym], 15]) rather the desired :([[:mysym], 15]).

mysym in :([[mysym], 15]) will only be a literal, not a symbol.
When the returning expression will be evaluated, it will not know to evaluate mysym as Symbol("mysym")

You can see the difference like that:

# mysym is a literal
julia> :([[mysym], 15]) |> eval
ERROR: UndefVarError: mysym not defined

# mysym is a symbol
julia> :([[:mysym], 15]) |> eval
2-element Vector{Any}:
   [:mysym]
 15

You mean the Julia repository ??
I am even having trouble expressing the possible feature in question here. :sweat_smile:

Here you’re saying you want the literal vector, so then:

macro mymacro(ex)
   return [[$(esc(ex.args[1]))],$(ex.args[2])]
end

I’m having a hard time understanding how you use the output of this macro and hence what the macro should actually output. I think the reason you’re having trouble is that you’re outputting something that’s really unusual, it’s a quote node containing another quote node.

When the macro returns should it be giving you:

quote
  [[ quote mysym end ],15]
end

or should it be giving you

[[mysym],15]

?
And if it’s the first and not the second, is that because you plan to call eval on it? in which case it just becomes the second one except evaluated at top level?

This is the same as before but without quoting since giving:
ERROR: LoadError: syntax: "$" expression outside quote around /u/home/wima/fchrstou/code/julia/test/macro.jl:18

Sorry for not being super clear.
This is the desired behavior:

Let me first say that I fully support what @jling said about macros not being composable, and that I would advise you to try and avoid having to use macros in this case.

That being said, as you found out, QuoteNode is the key here, as it allows you to build a quoted symbol (and is the only way to do so AFAIK). But you can perfectly use it in an otherwise standard quoted expression.
Your original solution could therefore be reduced to something like:

julia> macro mymacro(ex)
           name, idx = ex.args
           return :([[ $(QuoteNode(name)) ], $(idx) ])
       end
@mymacro (macro with 1 method)

julia> @macroexpand @mymacro mysym[15]
:([[:mysym], 15])

which I don’t find too unintuitive (once you know about QuoteNode)

2 Likes

Thanks! That’s exactly it.

Since I already mentioned the real use case of it:

what would you suggest for this situation if not a macro like the one you provided ?
i.e.

macro val(ex)
    name, idx = ex.args
    :(model[ $(QuoteNode(name)) ][$(idx)] |> value)
end

Well, certainly more intuitive than this: xD

If this is something useful to JuMP users, it could be proposed upstream, so that the @val macro lives alongside other JuMP macros.


I haven’t thought about it much, but in this specific situation, maybe you could (ab)use getproperty in order to have a syntax like val.myvar instead of @val myvar. A sketch of the implementation could look like:

struct Value
    model
end

Base.getproperty(v::Value, sym::Symbol) = v.model[sym]

v = Value(model)
v.myvar[(14,53)]  # equivalent to model[:myvar][(14, 53)]
1 Like