Search code examples
referencemacrossymbolsjulia-jump

How JuMP package (in Julia) creates a variable with its name designated, inside @variable macro?


In the context of an optimal control interface, I am trying to create a variable inside a Julia macro, then return the variable to the REPL, the variable should also have the name I specified from the macro input argument.

For example, I want to have a variable called y, so I use it as the input argument, it should return a variable named y

julia> @add_my_variable(my_model,y>=0)
y

and then I should be able to use the variable in other functions in the REPL without "UndefVarError" saying that y is not defined

julia> y
y
julia> set_initial_guess(y,1)
...

I tried to mimic what JuMP package does to achieve it, so I read the definition of @variable in JuMP, the function below (I excluded the irrelevant part of the function) is the key to achieving the functionality, but I find it hard to understand.

function _macro_assign_and_return(code, variable, name; model_for_registering = nothing,)

return quote

    $variable = $code

    # This assignment should be in the scope calling the macro
    $(esc(name)) = $variable
end
end

I traced all the way to the beginning of the macro for the meaning of the input arguments: "code" is an instance of GenericVariableRef type (detailed at the bottom), "variable" is a gensym(), "name" is the symbol from the macro input expression (like :y in :(y>=0)).

First, how can we assign a structure instance to an anonymous symbol "variable"?

Second, after I implemented on my side, it always reports error at the line of the return (shown below), I assume it cannot recognize y when evaluating $(esc(name)), so I think JuMP must have somehow constructed a variable y in the macro already, using the same name as the symbol :y from the input? I do not know if and how it can be achieved.

julia> @my_variable(model,y>=0)
UndefVarError: y not defined
Stacktrace:
[1] macro expansion

The background information of the structure GenericVariableRef is shown below

struct GenericVariableRef{T} <: AbstractVariableRef
     model::GenericModel{T}
     index::MOI.VariableIndex
end

For more relevant info, you can visit the link below, line 2479 for the macro, line 2681 for the function call in the macro, and line 121 for the complete function definition.

https://github.com/jump-dev/JuMP.jl/blob/89e56230306b76cfb81ab4121dbe253061362776/src/macros.jl#L121

Thank you for reading this far.


Solution

  • This is not really a JuMP question, but a broader Julia question of metaprogramming. It might help to read https://docs.julialang.org/en/v1/manual/metaprogramming/.

    Getting the "hygiene" of macros correct can be quite difficult, because there are some very subtle behaviors.

    For your y example, this might help:

    julia> macro add_variable(model, x)
               return esc(quote
                   $x = 1
               end)
           end
    @add_variable (macro with 1 method)
    
    julia> @add_variable(nothing, y)
    1
    
    julia> @macroexpand @add_variable(nothing, y)
    quote
        #= REPL[20]:3 =#
        y = 1
    end
    
    julia> y
    1
    

    You need to interpolate the Symbol given by the user (:y), and then you need to esc the expression to tell Julia to evaluate it in the user's current scope.

    I don't know if the JuMP code is a good place to look at when learning. It's overly complicated, and we want to rewrite it to simplify it at some point...