Skip to content

Commit e8d027f

Browse files
committed
Update
1 parent 7e8b273 commit e8d027f

File tree

2 files changed

+103
-116
lines changed
  • docs/src/submodules/FileFormats
  • src/FileFormats/LP

2 files changed

+103
-116
lines changed

docs/src/submodules/FileFormats/LP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ tokens, but they're an outlier.
193193
In general, an identifier may contain the letters a-z, A-Z, the digits 0-9, and
194194
the characters ```!"#\$%&()/,.;?@_'`|~```.
195195

196-
Additional solvers put additional restrictictions:
196+
Additional solvers put additional restrictions:
197197

198198
* In (all?) solvers except Gurobi, the identifier must not start with a digit
199199
or a `.` (in Gurobi, identifiers must be separated by whitespace)

src/FileFormats/LP/read.jl

Lines changed: 102 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -48,39 +48,35 @@ function Base.read!(io::IO, model::Model{T}) where {T}
4848
end
4949
state = _LexerState(io)
5050
cache = _ReadCache(model)
51-
keyword = :UNKNOWN
5251
while (token = peek(state, _Token)) !== nothing
53-
if token.kind == _TOKEN_KEYWORD
54-
_ = read(state, _Token)
55-
keyword = Symbol(token.value)
56-
elseif token.kind == _TOKEN_NEWLINE
52+
if token.kind == _TOKEN_NEWLINE
5753
_ = read(state, _Token, _TOKEN_NEWLINE)
58-
elseif keyword == :MINIMIZE
54+
continue
55+
end
56+
keyword = _parse_keyword(state, cache)
57+
if keyword == :MINIMIZE
5958
MOI.set(cache.model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
6059
_parse_objective(state, cache)
61-
keyword = :UNKNOWN
6260
elseif keyword == :MAXIMIZE
6361
MOI.set(cache.model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
6462
_parse_objective(state, cache)
65-
keyword = :UNKNOWN
6663
elseif keyword == :CONSTRAINTS
67-
_parse_constraint(state, cache)
64+
while _parse_constraint(state, cache)
65+
end
6866
elseif keyword == :BINARY
69-
x = _parse_identifier(state, cache)
70-
MOI.add_constraint(cache.model, x, MOI.ZeroOne())
67+
while _parse_binary(state, cache)
68+
end
7169
elseif keyword == :INTEGER
72-
x = _parse_identifier(state, cache)
73-
MOI.add_constraint(cache.model, x, MOI.Integer())
70+
while _parse_integer(state, cache)
71+
end
7472
elseif keyword == :BOUNDS
75-
_parse_bound_expression(state, cache)
73+
while _parse_bound_expression(state, cache)
74+
end
7675
elseif keyword == :SOS
77-
_parse_constraint(state, cache)
76+
while _parse_constraint(state, cache)
77+
end
7878
elseif keyword == :END
79-
_throw_parse_error(
80-
state,
81-
token,
82-
"No file contents are allowed after `end`.",
83-
)
79+
break
8480
else
8581
_throw_parse_error(
8682
state,
@@ -89,9 +85,11 @@ function Base.read!(io::IO, model::Model{T}) where {T}
8985
)
9086
end
9187
end
92-
# if keyword != :END
93-
# TODO(odow): decide if we should throw an error here.
94-
# end
88+
_skip_newlines(state)
89+
if (p = peek(state, _Token)) !== nothing
90+
msg = "No file contents are allowed after `end`."
91+
_throw_parse_error(state, p, msg)
92+
end
9593
for x in cache.variable_with_default_bound
9694
MOI.add_constraint(model, x, MOI.GreaterThan(0.0))
9795
end
@@ -164,7 +162,6 @@ Hopefully they're all self-explanatory.
164162
"""
165163
@enum(
166164
_TokenKind,
167-
_TOKEN_KEYWORD,
168165
_TOKEN_IDENTIFIER,
169166
_TOKEN_NUMBER,
170167
_TOKEN_ADDITION,
@@ -190,7 +187,6 @@ This dictionary makes `_TokenKind` to a string that is used when printing error
190187
messages. The string must complete the sentence "We expected this token to be ".
191188
"""
192189
const _KIND_TO_MSG = Dict{_TokenKind,String}(
193-
_TOKEN_KEYWORD => "a keyword",
194190
_TOKEN_IDENTIFIER => "a variable name",
195191
_TOKEN_NUMBER => "a number",
196192
_TOKEN_ADDITION => "the symbol `+`",
@@ -372,59 +368,6 @@ function Base.peek(state::_LexerState, ::Type{_Token}, n::Int = 1)
372368
return nothing
373369
end
374370
push!(state.peek_tokens, token)
375-
if token.kind != _TOKEN_IDENTIFIER
376-
continue
377-
end
378-
# Here we have a _TOKEN_IDENTIFIER. But if it is not preceeded by a
379-
# _TOKEN_NEWLINE, it cannot be a _TOKEN_KEYWORD.
380-
if !_nothing_or_newline(_prior_token(state))
381-
continue
382-
end
383-
# It might be a _TOKEN_KEYWORD.
384-
(kw = _case_insenstive_identifier_to_keyword(token.value))
385-
if kw !== nothing
386-
# The token matches a single word keyword. All keywords are followed
387-
# by a new line, or an EOF.
388-
t = _peek_inner(state)
389-
if _nothing_or_newline(t)
390-
state.peek_tokens[end] = _Token(_TOKEN_KEYWORD, kw, token.pos)
391-
end
392-
if t !== nothing
393-
push!(state.peek_tokens, t)
394-
end
395-
continue
396-
end
397-
# There are two keyword that contain whitespace: `subject to` and
398-
# `such that`
399-
for (a, b) in ("subject" => "to", "such" => "that")
400-
if !_compare_case_insenstive(token, a)
401-
continue
402-
end
403-
# This _might_ be `subject to`, or it might just be a variable
404-
# named `subject`, like `obj:\n subject\n`.
405-
token_b = _peek_inner(state)
406-
if token_b === nothing
407-
# The next token is EOF. Nothing to do here.
408-
break
409-
elseif !_compare_case_insenstive(token_b, b)
410-
# The second token doesn't match. Store `token_b` and break
411-
push!(state.peek_tokens, token_b)
412-
break
413-
end
414-
# We have something that matches (a, b), but a TOKEN_KEYWORD needs
415-
# to be followed by a new line.
416-
token_nl = _peek_inner(state)
417-
if _nothing_or_newline(token_nl)
418-
state.peek_tokens[end] =
419-
_Token(_TOKEN_KEYWORD, "CONSTRAINTS", token.pos)
420-
else
421-
push!(state.peek_tokens, token_b)
422-
end
423-
if token_nl !== nothing
424-
push!(state.peek_tokens, token_nl)
425-
end
426-
break
427-
end
428371
end
429372
return state.peek_tokens[n]
430373
end
@@ -523,6 +466,33 @@ function _next_non_newline(state::_LexerState)
523466
end
524467
end
525468

469+
function _parse_keyword(state::_LexerState, cache::_ReadCache)::Symbol
470+
token = read(state, _Token, _TOKEN_IDENTIFIER)
471+
kw = _case_insenstive_identifier_to_keyword(token.value)
472+
if kw !== nothing
473+
return Symbol(kw)
474+
end
475+
# Check `subject to`
476+
if _compare_case_insenstive(token, "subject")
477+
token_b = peek(state, _Token)
478+
if _compare_case_insenstive(token_b, "to")
479+
_ = read(state, _Token, _TOKEN_IDENTIFIER)
480+
return :CONSTRAINTS
481+
end
482+
elseif _compare_case_insenstive(token, "such")
483+
token_b = peek(state, _Token)
484+
if _compare_case_insenstive(token_b, "that")
485+
_ = read(state, _Token, _TOKEN_IDENTIFIER)
486+
return :CONSTRAINTS
487+
end
488+
end
489+
return _throw_parse_error(
490+
state,
491+
token,
492+
"Expected a keyword.",
493+
)
494+
end
495+
526496
# <identifier> :== "string"
527497
#
528498
# There _are_ rules to what an identifier can be. We handle these when lexing.
@@ -637,10 +607,16 @@ function _parse_quadratic_expression(
637607
while (p = peek(state, _Token)) !== nothing
638608
if p.kind == _TOKEN_ADDITION
639609
p = read(state, _Token)
640-
push!(f.quadratic_terms, _parse_quadratic_term(state, cache, prefix))
610+
push!(
611+
f.quadratic_terms,
612+
_parse_quadratic_term(state, cache, prefix),
613+
)
641614
elseif p.kind == _TOKEN_SUBTRACTION
642615
p = read(state, _Token)
643-
push!(f.quadratic_terms, _parse_quadratic_term(state, cache, -prefix))
616+
push!(
617+
f.quadratic_terms,
618+
_parse_quadratic_term(state, cache, -prefix),
619+
)
644620
elseif p.kind == _TOKEN_NEWLINE
645621
_ = read(state, _Token)
646622
elseif p.kind == _TOKEN_CLOSE_BRACKET
@@ -782,10 +758,7 @@ function _parse_expression(state::_LexerState, cache::_ReadCache{T}) where {T}
782758
p = read(state, _Token)
783759
_add_to_expression!(f, _parse_term(state, cache, -one(T)))
784760
elseif p.kind == _TOKEN_NEWLINE
785-
if _next_token_is(state, _TOKEN_KEYWORD, 2)
786-
break
787-
end
788-
_ = read(state, _Token)
761+
_ = read(state, _Token, _TOKEN_NEWLINE)
789762
else
790763
break
791764
end
@@ -855,30 +828,40 @@ function _parse_set_prefix(state, cache)
855828
end
856829
end
857830

858-
# <name> :== [<identifier> :]
831+
# <name> :== <identifier> :
832+
833+
function _is_name(state::_LexerState)
834+
return _next_token_is(state, _TOKEN_IDENTIFIER, 1) &&
835+
_next_token_is(state, _TOKEN_COLON, 2)
836+
end
837+
859838
function _parse_name(state::_LexerState, cache::_ReadCache)
860-
_skip_newlines(state)
861-
if _next_token_is(state, _TOKEN_IDENTIFIER, 1) &&
862-
_next_token_is(state, _TOKEN_COLON, 2)
863-
name = read(state, _Token)
864-
_ = read(state, _Token) # Skip :
865-
return name.value
866-
end
867-
return nothing
839+
name = read(state, _Token, _TOKEN_IDENTIFIER)
840+
_ = read(state, _Token, _TOKEN_COLON)
841+
return name.value
868842
end
869843

870844
# <objective> :== <name> [<expression>]
871845
function _parse_objective(state::_LexerState, cache::_ReadCache)
872846
_ = _parse_name(state, cache)
873847
_skip_newlines(state)
874-
if _next_token_is(state, _TOKEN_KEYWORD)
875-
return # A line like `obj:\nsubject to`
876-
end
877848
f = _parse_expression(state, cache)
878849
MOI.set(cache.model, MOI.ObjectiveFunction{typeof(f)}(), f)
879850
return
880851
end
881852

853+
function _parse_integer(state::_LexerState, cache::_ReadCache)
854+
x = _parse_identifier(state, cache)
855+
MOI.add_constraint(cache.model, x, MOI.Integer())
856+
return true
857+
end
858+
859+
function _parse_binary(state::_LexerState, cache::_ReadCache)
860+
x = _parse_identifier(state, cache)
861+
MOI.add_constraint(cache.model, x, MOI.ZeroOne())
862+
return true
863+
end
864+
882865
function _add_bound(
883866
cache::_ReadCache,
884867
x::MOI.VariableIndex,
@@ -922,21 +905,21 @@ function _parse_bound_expression(state, cache)
922905
x = _parse_identifier(state, cache)
923906
set = _parse_set_suffix(state, cache)
924907
_add_bound(cache, x, set)
925-
return
926-
end
927-
# `a op x` or `a op x op b`
928-
lhs_set = _parse_set_prefix(state, cache)
929-
x = _parse_identifier(state, cache)
930-
_add_bound(cache, x, lhs_set)
931-
if _next_token_is(state, _TOKEN_GREATER_THAN) ||
932-
_next_token_is(state, _TOKEN_LESS_THAN) ||
933-
_next_token_is(state, _TOKEN_EQUAL_TO) # `a op x op b`
934-
# We don't add MOI.Interval constraints to follow JuMP's convention of
935-
# separate bounds.
936-
rhs_set = _parse_set_suffix(state, cache)
937-
_add_bound(cache, x, rhs_set)
908+
else
909+
# `a op x` or `a op x op b`
910+
lhs_set = _parse_set_prefix(state, cache)
911+
x = _parse_identifier(state, cache)
912+
_add_bound(cache, x, lhs_set)
913+
if _next_token_is(state, _TOKEN_GREATER_THAN) ||
914+
_next_token_is(state, _TOKEN_LESS_THAN) ||
915+
_next_token_is(state, _TOKEN_EQUAL_TO) # `a op x op b`
916+
# We don't add MOI.Interval constraints to follow JuMP's convention of
917+
# separate bounds.
918+
rhs_set = _parse_set_suffix(state, cache)
919+
_add_bound(cache, x, rhs_set)
920+
end
938921
end
939-
return
922+
return true
940923
end
941924

942925
function _is_sos_constraint(state)
@@ -1022,11 +1005,15 @@ function _parse_constraint_indicator(
10221005
end
10231006

10241007
# <constraint> :==
1025-
# <name> <expression> <set-suffix>
1026-
# | <name> <constraint-sos>
1027-
# | <name> <constraint-indicator>
1008+
# [<name>] <expression> <set-suffix>
1009+
# | [<name>] <constraint-sos>
1010+
# | [<name>] <constraint-indicator>
10281011
function _parse_constraint(state::_LexerState, cache::_ReadCache)
1029-
name = _parse_name(state, cache)
1012+
name = if _is_name(state)
1013+
_parse_name(state, cache)
1014+
else
1015+
nothing
1016+
end
10301017
# Check if this is an SOS constraint
10311018
c = if _is_sos_constraint(state)
10321019
_parse_constraint_sos(state, cache)
@@ -1040,5 +1027,5 @@ function _parse_constraint(state::_LexerState, cache::_ReadCache)
10401027
if name !== nothing
10411028
MOI.set(cache.model, MOI.ConstraintName(), c, name)
10421029
end
1043-
return
1030+
return true
10441031
end

0 commit comments

Comments
 (0)