Skip to content

Commit

Permalink
Merge dcdc0f0 into 052e290
Browse files Browse the repository at this point in the history
  • Loading branch information
blegat committed Dec 29, 2018
2 parents 052e290 + dcdc0f0 commit fe6d806
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 44 deletions.
110 changes: 66 additions & 44 deletions src/macros.jl
Expand Up @@ -213,26 +213,46 @@ function _localvar(x::Expr)
end

"""
extract_kwargs(args)
extract_kw_args(args)
Process the arguments to a macro, separating out the keyword arguments.
Return a tuple of (flat_arguments, keyword arguments, and requestedcontainer),
where `requestedcontainer` is a symbol to be passed to `getloopedcode`.
"""
function extract_kwargs(args)
kwargs = filter(x -> isexpr(x, :(=)) && x.args[1] != :container , collect(args))
function extract_kw_args(args)
kw_args = filter(x -> isexpr(x, :(=)) && x.args[1] != :container , collect(args))
flat_args = filter(x->!isexpr(x, :(=)), collect(args))
requestedcontainer = :Auto
for kw in args
if isexpr(kw, :(=)) && kw.args[1] == :container
requestedcontainer = kw.args[2]
end
end
return flat_args, kwargs, requestedcontainer
return flat_args, kw_args, requestedcontainer
end

function addkwargs!(call, kwargs)
for kw in kwargs
"""
add_kw_args(call, kw_args)
Add the keyword arguments `kw_args` to the function call expression `call`,
escaping the expressions. The elements of `kw_args` should be expressions of the
form `:(key = value)`. The `kw_args` vector can be extracted from the arguments
of a macro with [`extract_kw_args`](@ref).
## Examples
```jldoctest; setup = :(using JuMP)
julia> call = :(f(1, a=2))
:(f(1, a=2))
julia> JuMP.add_kw_args(call, [:(b=3), :(c=4)])
julia> call
:(f(1, a=2, $(Expr(:escape, :(b=3))), $(Expr(:escape, :(c=4)))))
```
"""
function add_kw_args(call, kw_args)
for kw in kw_args
@assert isexpr(kw, :(=))
push!(call.args, esc(Expr(:kw, kw.args...)))
end
Expand Down Expand Up @@ -474,18 +494,20 @@ Returns the code for the macro `@constraint_like args...` of syntax
@constraint_like ref con # group of constraints
```
where `@constraint_like` is either `@constraint` or `@SDconstraint`.
The expression `con` is parsed by `parsefun` which returns a code that, when
executed, returns an `AbstractConstraint`. This `AbstractConstraint` is passed
to `add_constraint` with the macro keyword arguments (except the `container`
keyword argument which is used to determine the container type).
The expression `con` is parsed by `parsefun` which returns a `build_constraint`
call code that, when executed, returns an `AbstractConstraint`. The macro
keyword arguments (except the `container` keyword argument which is used to
determine the container type) are added to the `build_constraint` call. The
returned value of this call is passed to `add_constraint` which returns a
constraint reference.
"""
function constraint_macro(args, macro_name::Symbol, parsefun::Function)
_error(str...) = macro_error(macro_name, args, str...)

args, kwargs, requestedcontainer = extract_kwargs(args)
args, kw_args, requestedcontainer = extract_kw_args(args)

if length(args) < 2
if length(kwargs) > 0
if length(kw_args) > 0
_error("Not enough positional arguments")
else
_error("Not enough arguments")
Expand Down Expand Up @@ -522,13 +544,13 @@ function constraint_macro(args, macro_name::Symbol, parsefun::Function)
refcall, idxvars, idxsets, condition = buildrefsets(c, variable)

vectorized, parsecode, buildcall = parsefun(_error, x.args...)
add_kw_args(buildcall, kw_args)
if vectorized
# TODO: Pass through names here.
constraintcall = :(add_constraint.($m, $buildcall))
else
constraintcall = :(add_constraint($m, $buildcall, $(namecall(base_name, idxvars))))
end
addkwargs!(constraintcall, kwargs)
code = quote
$parsecode
$(refcall) = $constraintcall
Expand Down Expand Up @@ -932,7 +954,7 @@ expr = @expression(m, [i=1:3], i*sum(x[j] for j=1:3))
"""
macro expression(args...)

args, kwargs, requestedcontainer = extract_kwargs(args)
args, kw_args, requestedcontainer = extract_kw_args(args)
if length(args) == 3
m = esc(args[1])
c = args[2]
Expand All @@ -944,7 +966,7 @@ macro expression(args...)
else
error("@expression: needs at least two arguments.")
end
length(kwargs) == 0 || error("@expression: unrecognized keyword argument")
length(kw_args) == 0 || error("@expression: unrecognized keyword argument")

anonvar = isexpr(c, :vect) || isexpr(c, :vcat) || length(args) == 2
variable = gensym()
Expand Down Expand Up @@ -987,8 +1009,8 @@ variable_type(m::Model) = VariableRef
# Returns a new variable. Additional positional arguments can be used to dispatch the call to a different method.
# The return type should only depends on the positional arguments for `variable_type` to make sense. See the @variable macro doc for more details.
# Example: `@variable m x` foo will call `build_variable(_error, info, foo)`
function build_variable(_error::Function, info::VariableInfo; extra_kwargs...)
for (kwarg, _) in extra_kwargs
function build_variable(_error::Function, info::VariableInfo; extra_kw_args...)
for (kwarg, _) in extra_kw_args
_error("Unrecognized keyword argument $kwarg")
end
return ScalarVariable(info)
Expand Down Expand Up @@ -1076,15 +1098,15 @@ function parse_variable(_error::Function, infoexpr::VariableInfoExpr, lvalue, ls
end

"""
@variable(model, kwargs...)
@variable(model, kw_args...)
Add an *anonymous* (see [Names](@ref)) variable to the model `model` described
by the keyword arguments `kwargs` and returns the variable.
by the keyword arguments `kw_args` and returns the variable.
@variable(model, expr, args..., kwargs...)
@variable(model, expr, args..., kw_args...)
Add a variable to the model `model` described by the expression `expr`, the
positional arguments `args` and the keyword arguments `kwargs`. The expression
positional arguments `args` and the keyword arguments `kw_args`. The expression
`expr` can either be (note that in the following the symbol `<=` can be used
instead of `≤` and the symbol `>=`can be used instead of `≥`)
Expand Down Expand Up @@ -1115,7 +1137,7 @@ The recognized positional arguments in `args` are the following:
* `PSD`: The square matrix of variable is both `Symmetric` and constrained to be
positive semidefinite.
The recognized keyword arguments in `kwargs` are the following:
The recognized keyword arguments in `kw_args` are the following:
* `base_name`: Sets the name prefix used to generate variable names. It
corresponds to the variable name for scalar variable, otherwise, the
Expand All @@ -1138,7 +1160,7 @@ lower bound 0:
@variable(model, x >= 0)
# Specify the lower bound using a keyword argument
@variable(model, x, lower_bound=0)
# Specify everything in `kwargs`
# Specify everything in `kw_args`
x = @variable(model, base_name="x", lower_bound=0)
```
Expand All @@ -1156,7 +1178,7 @@ ub = Dict(:a => 2, :b => 3)
The single scalar variable or each scalar variable of the container are created
using `add_variable(model, build_variable(_error, info, extra_args...;
extra_kwargs...))` where
extra_kw_args...))` where
* `model` is the model passed to the `@variable` macro;
* `_error` is an error function with a single `String` argument showing the
Expand All @@ -1171,8 +1193,8 @@ extra_kwargs...))` where
`build_variable` without the need to give a positional argument to
`@variable`. In particular, this allows the user to give a positional
argument to the `build_variable` call when using the anonymous single variable
syntax `@variable(model, kwargs...)`; and
* `extra_kwargs` are the unrecognized keyword argument of `kwargs`.
syntax `@variable(model, kw_args...)`; and
* `extra_kw_args` are the unrecognized keyword argument of `kw_args`.
## Examples
Expand Down Expand Up @@ -1215,7 +1237,7 @@ macro variable(args...)

model = esc(args[1])

extra, kwargs, requestedcontainer = extract_kwargs(args[2:end])
extra, kw_args, requestedcontainer = extract_kw_args(args[2:end])

# if there is only a single non-keyword argument, this is an anonymous
# variable spec and the one non-kwarg is the model
Expand All @@ -1230,11 +1252,11 @@ macro variable(args...)
anon_singleton = false
end

info_kwargs = filter(is_info_keyword, kwargs)
extra_kwargs = filter(kw -> kw.args[1] != :base_name && kw.args[1] != :variable_type && !is_info_keyword(kw), kwargs)
base_name_kwargs = filter(kw -> kw.args[1] == :base_name, kwargs)
variable_type_kwargs = filter(kw -> kw.args[1] == :variable_type, kwargs)
infoexpr = VariableInfoExpr(; keywordify.(info_kwargs)...)
info_kw_args = filter(is_info_keyword, kw_args)
extra_kw_args = filter(kw -> kw.args[1] != :base_name && kw.args[1] != :variable_type && !is_info_keyword(kw), kw_args)
base_name_kw_args = filter(kw -> kw.args[1] == :base_name, kw_args)
variable_type_kw_args = filter(kw -> kw.args[1] == :variable_type, kw_args)
infoexpr = VariableInfoExpr(; keywordify.(info_kw_args)...)

# There are four cases to consider:
# x | type of x | x.head
Expand All @@ -1256,10 +1278,10 @@ macro variable(args...)
variable = gensym()
# TODO: Should we generate non-empty default names for variables?
name = getname(var)
if isempty(base_name_kwargs)
if isempty(base_name_kw_args)
base_name = anonvar ? "" : string(name)
else
base_name = esc(base_name_kwargs[1].args[2])
base_name = esc(base_name_kw_args[1].args[2])
end

if !isa(name, Symbol) && !anonvar
Expand All @@ -1280,16 +1302,16 @@ macro variable(args...)
end
end
extra = esc.(filter(ex -> !(ex in [:Int,:Bin]), extra))
if !isempty(variable_type_kwargs)
push!(extra, esc(variable_type_kwargs[1].args[2]))
if !isempty(variable_type_kw_args)
push!(extra, esc(variable_type_kw_args[1].args[2]))
end

info = constructor_expr(infoexpr)
if isa(var,Symbol)
# Easy case - a single variable
sdp && _error("Cannot add a semidefinite scalar variable")
buildcall = :( build_variable($_error, $info, $(extra...)) )
addkwargs!(buildcall, extra_kwargs)
add_kw_args(buildcall, extra_kw_args)
variablecall = :( add_variable($model, $buildcall, $base_name) )
# The looped code is trivial here since there is a single variable
creationcode = :($variable = $variablecall)
Expand All @@ -1304,7 +1326,7 @@ macro variable(args...)

# Code to be used to create each variable of the container.
buildcall = :( build_variable($_error, $info, $(extra...)) )
addkwargs!(buildcall, extra_kwargs)
add_kw_args(buildcall, extra_kw_args)
variablecall = :( add_variable($model, $buildcall, $(namecall(base_name, idxvars))) )
code = :( $(refcall) = $variablecall )
# Determine the return type of add_variable. This is needed to create the container holding them.
Expand Down Expand Up @@ -1382,8 +1404,8 @@ macro NLconstraint(m, x, extra...)
# Two formats:
# - @NLconstraint(m, a*x <= 5)
# - @NLconstraint(m, myref[a=1:5], sin(x^a) <= 5)
extra, kwargs, requestedcontainer = extract_kwargs(extra)
(length(extra) > 1 || length(kwargs) > 0) && error("in @NLconstraint: too many arguments.")
extra, kw_args, requestedcontainer = extract_kw_args(extra)
(length(extra) > 1 || length(kw_args) > 0) && error("in @NLconstraint: too many arguments.")
# Canonicalize the arguments
c = length(extra) == 1 ? x : gensym()
x = length(extra) == 1 ? extra[1] : x
Expand Down Expand Up @@ -1456,7 +1478,7 @@ end

# TODO: Add a docstring.
macro NLexpression(args...)
args, kwargs, requestedcontainer = extract_kwargs(args)
args, kw_args, requestedcontainer = extract_kw_args(args)
if length(args) <= 1
error("in @NLexpression: To few arguments ($(length(args))); must pass the model and nonlinear expression as arguments.")
elseif length(args) == 2
Expand All @@ -1465,7 +1487,7 @@ macro NLexpression(args...)
elseif length(args) == 3
m, c, x = args
end
if length(args) > 3 || length(kwargs) > 0
if length(args) > 3 || length(kw_args) > 0
error("in @NLexpression: To many arguments ($(length(args))).")
end

Expand Down Expand Up @@ -1523,8 +1545,8 @@ JuMP.value(y[9])
"""
macro NLparameter(m, ex, extra...)

extra, kwargs, requestedcontainer = extract_kwargs(extra)
(length(extra) == 0 && length(kwargs) == 0) || error("in @NLperameter: too many arguments.")
extra, kw_args, requestedcontainer = extract_kw_args(extra)
(length(extra) == 0 && length(kw_args) == 0) || error("in @NLperameter: too many arguments.")
if !isexpr(ex, :call) || length(ex.args) != 3 || ex.args[1] != :(==)
error("In @NLparameter($m, $ex): syntax error.")
end
Expand Down
20 changes: 20 additions & 0 deletions test/macros.jl
Expand Up @@ -75,6 +75,24 @@ end
@test test_kw == 5
end

struct PowerCone{T}
exponent::T
end
function JuMP.build_constraint(_error::Function, f, set::PowerCone; dual=false)
moi_set = dual ? MOI.DualPowerCone(set.exponent) : MOI.PowerCone(set.exponent)
return JuMP.build_constraint(_error, f, moi_set)
end
function build_constraint_keyword_test(ModelType::Type{<:JuMP.AbstractModel})
@testset "build_constraint with keyword arguments" begin
model = ModelType()
@variable(model, x)
cref1 = @constraint(model, [1, x, x] in PowerCone(0.5))
@test JuMP.constraint_object(cref1).set isa MathOptInterface.PowerCone{Float64}
cref2 = @constraint(model, [1, x, x] in PowerCone(0.5), dual = true)
@test JuMP.constraint_object(cref2).set isa MathOptInterface.DualPowerCone{Float64}
end
end

function macros_test(ModelType::Type{<:JuMP.AbstractModel}, VariableRefType::Type{<:JuMP.AbstractVariableRef})
@testset "build_constraint on variable" begin
m = ModelType()
Expand Down Expand Up @@ -177,6 +195,8 @@ function macros_test(ModelType::Type{<:JuMP.AbstractModel}, VariableRefType::Typ
@test con[1].set == MOI.LessThan(1.0)
@test con[2].set == MOI.LessThan(2.0)
end

build_constraint_keyword_test(ModelType)
end

@testset "Macros for JuMP.Model" begin
Expand Down

0 comments on commit fe6d806

Please sign in to comment.