Extending JuMP.@constraint macro: misunderstanding scope?

I’ve been toying with the idea of extending the JuMP.@constraint macro to make my code somewhat more concise. Here’s an example of what I currently do:

con1 = @constraint(gep.model, [i=1:10], x[i] >= 0) # Make an anonymous constraint
gep.model.ext[:constraints][:con1] = con1 # Save it to dict

gep is a custom type I have which has a field model::JuMP.Model.

What I would like to do is extend the macro so that the above two lines are done in just one:

@constraint(gep, con1[i=1:10], x[i] >= 0) # Make and save constraint

I came up with the following (the function is defined within the module which also exports the gep type):

macro constraint(args...)
    if typeof(eval(args[1])) <: GenerationExpansionPlanningModel
		gep = eval(args[1])
		args[1] = :(gep.model)
        con = _constraint_macro(
			gep.model, args[2:end], :constraint, JuMP.parse_constraint_expr
		)
		# save the constraint to GEP here
    else
    	_constraint_macro(args, :constraint, JuMP.parse_constraint_expr)
    end
end

On the line where I check typeof(eval(args[1])) I get the following error:

ERROR: LoadError: UndefVarError: gep not defined

This error is thrown by Revise, so I’m supposing it’s a syntactical error.

I understood that eval(args[1]) would evaluate gep in the global scope where it is indeed defined, so I don’t understand why this doesn’t work.

That being said I’m starting to realise that this is probably not worth it, but I’m still intrigued as to why this doesn’t work.

Macros are nontrivial to write. You should thoroughly read all of the documentation. https://docs.julialang.org/en/v1/manual/metaprogramming/#

In particular, using eval in a macro is a clear sign that the approach is wrong. Macros should basically take an expression and return a new expression based on the syntax, not the type. They should never evaluate the actual values of the input arguments.

So that means you can’t do things like if typeof(x) :< Foo.

That being said I’m starting to realise that this is probably not worth it

It’s probably easier to overload Base.setindex!on gep so you could go gep[:con1] = @constraint(...).

4 Likes

Makes sense, I think I’ll hold off from messing around with macros for a while. Thanks!