Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the disjunction macro to use new JuMP extension API #103

Closed
odow opened this issue Dec 11, 2023 · 3 comments
Closed

Update the disjunction macro to use new JuMP extension API #103

odow opened this issue Dec 11, 2023 · 3 comments

Comments

@odow
Copy link
Contributor

odow commented Dec 11, 2023

Following jump-dev/JuMP.jl#3513 (comment)

What is the full range of syntax you want the macro to support? Why even have a macro? What is the trade-off of just having a functional form?

@pulsipher
Copy link
Collaborator

For @disjunction, the main motivation for the macro is container creation. It is a common use-case to create disjunctions over sets. It is not currently used for an any specialized expression parsing, though this may be added in the future. One complicating factor for @disjunction is that is it tricky to distinguish the anonymous singleton case from the vectorized case which is what leads to most of the complexity.

From the JuMP extension perspective, it would be nice to not have to duplicate all the helper methods:

################################################################################
# BASIC HELPERS
################################################################################
# Macro error function
# inspired from https://github.com/jump-dev/JuMP.jl/blob/709d41b78e56efb4f2c54414266b30932010bd5a/src/macros.jl#L923-L928
function _macro_error(macroname, args, source, str...)
error("At $(source.file):$(source.line): `@$macroname($(join(args, ", ")))`: ",
str...)
end
# Escape when needed
# taken from https://github.com/jump-dev/JuMP.jl/blob/709d41b78e56efb4f2c54414266b30932010bd5a/src/macros.jl#L895-L897
_esc_non_constant(x::Number) = x
_esc_non_constant(x::Expr) = isexpr(x,:quote) ? x : esc(x)
_esc_non_constant(x) = esc(x)
# Extract the name from a macro expression
# Inspired from https://github.com/jump-dev/JuMP.jl/blob/45ce630b51fb1d72f1ff8fed35a887d84ef3edf7/src/Containers/macro.jl#L8-L17
_get_name(c::Symbol) = c
_get_name(c::Nothing) = ()
_get_name(c::AbstractString) = c
function _get_name(c::Expr)
if isexpr(c, :string)
return c
else
return c.args[1]
end
end
# Given a base_name and idxvars, returns an expression that constructs the name
# of the object.
# Inspired from https://github.com/jump-dev/JuMP.jl/blob/709d41b78e56efb4f2c54414266b30932010bd5a/src/macros.jl#L930-L946
function _name_call(base_name, idxvars)
if isempty(idxvars) || base_name == ""
return base_name
end
ex = Expr(:call, :string, base_name, "[")
for i in eachindex(idxvars)
# Converting the arguments to strings before concatenating is faster:
# https://github.com/JuliaLang/julia/issues/29550.
esc_idxvar = esc(idxvars[i])
push!(ex.args, :(string($esc_idxvar)))
i < length(idxvars) && push!(ex.args, ",")
end
push!(ex.args, "]")
return ex
end
# Process macro arguments
function _extract_kwargs(args)
arg_list = collect(args)
if !isempty(args) && isexpr(args[1], :parameters)
p = popfirst!(arg_list)
append!(arg_list, (Expr(:(=), a.args...) for a in p.args))
end
extra_kwargs = filter(x -> isexpr(x, :(=)) && x.args[1] != :container &&
x.args[1] != :base_name, arg_list)
container_type = :Auto
base_name = nothing
for kw in arg_list
if isexpr(kw, :(=)) && kw.args[1] == :container
container_type = kw.args[2]
elseif isexpr(kw, :(=)) && kw.args[1] == :base_name
base_name = esc(kw.args[2])
end
end
pos_args = filter!(x -> !isexpr(x, :(=)), arg_list)
return pos_args, extra_kwargs, container_type, base_name
end
# Add on keyword arguments to a function call expression and escape the expressions
# Adapted from https://github.com/jump-dev/JuMP.jl/blob/d9cd5fb16c2d0a7e1c06aa9941923492fc9a28b5/src/macros.jl#L11-L36
function _add_kwargs(call, kwargs)
for kw in kwargs
push!(call.args, esc(Expr(:kw, kw.args...)))
end
return
end
# Add on positional args to a function call and escape
# Adapted from https://github.com/jump-dev/JuMP.jl/blob/a325eb638d9470204edb2ef548e93e59af56cc19/src/macros.jl#L57C1-L65C4
function _add_positional_args(call, args)
kw_args = filter(arg -> isexpr(arg, :kw), call.args)
filter!(arg -> !isexpr(arg, :kw), call.args)
for arg in args
push!(call.args, esc(arg))
end
append!(call.args, kw_args)
return
end
# Ensure a model argument is valid
# Inspired from https://github.com/jump-dev/JuMP.jl/blob/d9cd5fb16c2d0a7e1c06aa9941923492fc9a28b5/src/macros.jl#L38-L44
function _valid_model(_error::Function, model, name)
is_gdp_model(model) || _error("$name is not a `GDPModel`.")
end
# Check if a macro julia variable can be registered
# Adapted from https://github.com/jump-dev/JuMP.jl/blob/d9cd5fb16c2d0a7e1c06aa9941923492fc9a28b5/src/macros.jl#L66-L86
function _error_if_cannot_register(
_error::Function,
model,
name::Symbol
)
if haskey(JuMP.object_dictionary(model), name)
_error("An object of name $name is already attached to this model. If ",
"this is intended, consider using the anonymous construction ",
"syntax, e.g., `x = @macro_name(model, ...)` where the name ",
"of the object does not appear inside the macro. Alternatively, ",
"use `unregister(model, :$(name))` to first unregister the ",
"existing name from the model. Note that this will not delete ",
"the object; it will just remove the reference at ",
"`model[:$(name)]`")
end
return
end
# Update the creation code to register and assign the object to the name
# Inspired from https://github.com/jump-dev/JuMP.jl/blob/d9cd5fb16c2d0a7e1c06aa9941923492fc9a28b5/src/macros.jl#L88-L120
function _macro_assign_and_return(_error, code, name, model)
return quote
_error_if_cannot_register($_error, $model, $(quot(name)))
$(esc(name)) = $code
$model[$(quot(name))] = $(esc(name))
end
end
# Wrap the macro generated code for better stacttraces (assumes model is escaped)
# Inspired from https://github.com/jump-dev/JuMP.jl/blob/d9cd5fb16c2d0a7e1c06aa9941923492fc9a28b5/src/macros.jl#L46-L64
function _finalize_macro(_error, model, code, source::LineNumberNode)
return Expr(:block, source,
:(_valid_model($_error, $model, $(quot(model.args[1])))), code)
end

A lot of repetition of JuMP-like macro code happens when making the build calls that potentially create containers and parse naming:
# process the name
name = _get_name(c)
if isnothing(base_name)
base_name = is_anon ? "" : string(name)
end
if !isa(name, Symbol) && !is_anon
_error("Expression $name should not be used as a disjunction name. Use " *
"the \"anonymous\" syntax $name = @disjunction(model, " *
"...) instead.")
end
# make the creation code
if isa(c, Symbol)
# easy case with single parameter
creation_code = :( _disjunction($_error, $esc_model, $x, $base_name) )
_add_positional_args(creation_code, extra)
_add_kwargs(creation_code, extra_kwargs)
else
# we have a container of parameters
idxvars, inds = JuMP.Containers.build_ref_sets(_error, c)
if model in idxvars
_error("Index $(model) is the same symbol as the model. Use a ",
"different name for the index.")
end
name_code = _name_call(base_name, idxvars)
disjunction_call = :( _disjunction($_error, $esc_model, $x, $name_code) )
_add_positional_args(disjunction_call, extra)
_add_kwargs(disjunction_call, extra_kwargs)
creation_code = JuMP.Containers.container_code(idxvars, inds, disjunction_call,
container_type)
end

@pulsipher
Copy link
Collaborator

Now that Oscar has finished his public API for macro extensions in JuMP and released them in the latest release, we should update the macros to use the API. This should simply the macro code quite a bit :)

@pulsipher pulsipher changed the title The disjunction macro Update the disjunction macro to use new JuMP extension API Jan 17, 2024
@pulsipher
Copy link
Collaborator

This was fixed with #110.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants