Skip to content

Commit 5c28ebe

Browse files
committed
Update
1 parent b0d32f3 commit 5c28ebe

2 files changed

Lines changed: 312 additions & 72 deletions

File tree

src/FileFormats/LP/read.jl

Lines changed: 82 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ function Base.read!(io::IO, model::Model{T}) where {T}
3535
keyword = :UNKNOWN
3636
while (token = peek(state, Token)) !== nothing
3737
if token.kind == _TOKEN_KEYWORD
38-
read(state, Token)
38+
_ = read(state, Token)
3939
keyword = Symbol(token.value)
4040
continue
4141
elseif token.kind == _TOKEN_NEWLINE
42-
read(state, Token)
42+
_ = read(state, Token)
4343
continue
4444
elseif keyword == :MINIMIZE
4545
MOI.set(cache.model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
@@ -173,6 +173,28 @@ struct Token
173173
value::Union{Nothing,String}
174174
end
175175

176+
"""
177+
struct UnexpectedToken <: Exception
178+
token::Token
179+
end
180+
181+
This error is thrown when we encounter an unexpected token when parsing the LP
182+
file. No other information is available.
183+
184+
TODO: we could improve this by storing line information or other context to help
185+
the user diagnose the problem.
186+
"""
187+
struct UnexpectedToken <: Exception
188+
token::Token
189+
end
190+
191+
function _expect(token::Token, kind::_TokenKind)
192+
if token.kind != kind
193+
throw(UnexpectedToken(token))
194+
end
195+
return token
196+
end
197+
176198
"""
177199
mutable struct LexerState
178200
io::IO
@@ -210,10 +232,18 @@ end
210232

211233
function Base.read(state::LexerState, ::Type{Token})
212234
token = peek(state, Token, 1)
235+
if isempty(state.peek_tokens)
236+
throw(UnexpectedToken(Token(_TOKEN_UNKNOWN, "EOF")))
237+
end
213238
popfirst!(state.peek_tokens)
214239
return token
215240
end
216241

242+
function Base.read(state::LexerState, ::Type{Token}, kind::_TokenKind)
243+
token = read(state, Token)
244+
return _expect(token, kind)
245+
end
246+
217247
_is_idenfifier(c::Char) = !(isspace(c) || c in ('+', '-', '*', '^', ':'))
218248
_is_number(c::Char) = isdigit(c) || c in ('.', 'e', 'E', '+', '-')
219249

@@ -257,13 +287,13 @@ function _peek_inner(state::LexerState)
257287
if l_val == "subject"
258288
t = peek(state, Token)
259289
if t.kind == _TOKEN_IDENTIFIER && lowercase(t.value) == "to"
260-
read(state, Token) # Skip "to"
290+
_ = read(state, Token) # Skip "to"
261291
return Token(_TOKEN_KEYWORD, "CONSTRAINTS")
262292
end
263293
elseif l_val == "such"
264294
t = peek(state, Token)
265295
if t.kind == _TOKEN_IDENTIFIER && lowercase(t.value) == "that"
266-
read(state, Token) # Skip "such"
296+
_ = read(state, Token) # Skip "such"
267297
return Token(_TOKEN_KEYWORD, "CONSTRAINTS")
268298
end
269299
end
@@ -276,8 +306,11 @@ function _peek_inner(state::LexerState)
276306
if c == '-' && peek(state, Char) == '>'
277307
read(state, Char)
278308
return Token(_TOKEN_IMPLIES, "->")
309+
elseif c == '=' && peek(state, Char) in ('<', '>')
310+
c = read(state, Char) # Allow =< and => as <= and >=
311+
return Token(_OPERATORS[c], string(c))
279312
elseif c in ('<', '>', '=') && peek(state, Char) == '='
280-
read(state, Char) # Allow <=, >=, and ==
313+
_ = read(state, Char) # Allow <=, >=, and ==
281314
end
282315
return Token(op, string(c))
283316
else
@@ -287,28 +320,6 @@ function _peek_inner(state::LexerState)
287320
return
288321
end
289322

290-
"""
291-
struct UnexpectedToken <: Exception
292-
token::Token
293-
end
294-
295-
This error is thrown when we encounter an unexpected token when parsing the LP
296-
file. No other information is available.
297-
298-
TODO: we could improve this by storing line information or other context to help
299-
the user diagnose the problem.
300-
"""
301-
struct UnexpectedToken <: Exception
302-
token::Token
303-
end
304-
305-
function _expect(token::Token, kind::_TokenKind)
306-
if token.kind != kind
307-
throw(UnexpectedToken(token))
308-
end
309-
return
310-
end
311-
312323
"""
313324
_next_token_is(state::LexerState, kind::_TokenKind, n::Int = 1)
314325
@@ -323,19 +334,18 @@ end
323334

324335
function _skip_newlines(state::LexerState)
325336
while _next_token_is(state, _TOKEN_NEWLINE)
326-
read(state, Token)
337+
_ = read(state, Token)
327338
end
328339
return
329340
end
330341

331-
# IDENTIFIER --> "string"
342+
# IDENTIFIER := "string"
332343
#
333344
# There _are_ rules to what an identifier can be. We handle these when lexing.
334345
# Anything that makes it here is deemed acceptable.
335346
function _parse_variable(state::LexerState, cache::Cache)::MOI.VariableIndex
336347
_skip_newlines(state)
337-
token = read(state, Token)
338-
_expect(token, _TOKEN_IDENTIFIER)
348+
token = read(state, Token, _TOKEN_IDENTIFIER)
339349
x = get(cache.variable_name_to_index, token.value, nothing)
340350
if x !== nothing
341351
return x
@@ -370,47 +380,49 @@ function _parse_number(state::LexerState, cache::Cache{T})::T where {T}
370380
else
371381
throw(UnexpectedToken(token))
372382
end
373-
else
374-
_expect(token, _TOKEN_NUMBER)
375383
end
384+
_expect(token, _TOKEN_NUMBER)
376385
return parse(T, token.value)
377386
end
378387

379388
# QUAD_TERM :=
380389
# "+" QUAD_TERM
381390
# | "-" QUAD_TERM
382-
# | [NUMBER] IDENTIFIER "^" "2"
383-
# | [NUMBER] IDENTIFIER "*" IDENTIFIER
391+
# | [NUMBER] [*] IDENTIFIER "^" "2"
392+
# | [NUMBER] [*] IDENTIFIER "*" IDENTIFIER
384393
function _parse_quad_term(
385394
state::LexerState,
386395
cache::Cache{T},
387396
prefix::T,
388397
) where {T}
389398
_skip_newlines(state)
390399
if _next_token_is(state, _TOKEN_ADDITION)
391-
read(state, Token)
400+
_ = read(state, Token)
392401
return _parse_quad_term(state, cache, prefix)
393402
elseif _next_token_is(state, _TOKEN_SUBTRACTION)
394-
read(state, Token)
403+
_ = read(state, Token)
395404
return _parse_quad_term(state, cache, -prefix)
396405
end
397406
coef = prefix
398407
if _next_token_is(state, _TOKEN_NUMBER)
399408
coef = prefix * _parse_number(state, cache)
400409
end
410+
if _next_token_is(state, _TOKEN_MULTIPLICATION)
411+
_skip_newlines(state)
412+
_ = read(state, Token) # Skip optional multiplication
413+
end
401414
x1 = _parse_variable(state, cache)
402415
_skip_newlines(state)
403416
if _next_token_is(state, _TOKEN_EXPONENT)
404-
read(state, Token) # ^
417+
_ = read(state, Token) # ^
405418
_skip_newlines(state)
406-
n = read(state, Token)
407-
if n.kind != _TOKEN_NUMBER && n.value != "2"
419+
n = read(state, Token, _TOKEN_NUMBER)
420+
if n.value != "2"
408421
throw(UnexpectedToken(n))
409422
end
410423
return MOI.ScalarQuadraticTerm(T(2) * coef, x1, x1)
411424
end
412-
token = read(state, Token)
413-
_expect(token, _TOKEN_MULTIPLICATION)
425+
token = read(state, Token, _TOKEN_MULTIPLICATION)
414426
x2 = _parse_variable(state, cache)
415427
if x1 == x2
416428
coef *= T(2)
@@ -426,8 +438,7 @@ function _parse_quad_expression(
426438
cache::Cache{T},
427439
prefix::T,
428440
) where {T}
429-
token = read(state, Token)
430-
_expect(token, _TOKEN_OPEN_BRACKET)
441+
token = read(state, Token, _TOKEN_OPEN_BRACKET)
431442
f = zero(MOI.ScalarQuadraticFunction{T})
432443
push!(f.quadratic_terms, _parse_quad_term(state, cache, prefix))
433444
while (p = peek(state, Token)) !== nothing
@@ -438,20 +449,20 @@ function _parse_quad_expression(
438449
p = read(state, Token)
439450
push!(f.quadratic_terms, _parse_quad_term(state, cache, -prefix))
440451
elseif p.kind == _TOKEN_NEWLINE
441-
read(state, Token)
452+
_ = read(state, Token)
442453
elseif p.kind == _TOKEN_CLOSE_BRACKET
443-
read(state, Token)
454+
_ = read(state, Token)
444455
break
445456
else
446457
return throw(UnexpectedToken(p))
447458
end
448459
end
449460
_skip_newlines(state)
450461
if _next_token_is(state, _TOKEN_DIVISION)
451-
read(state, Token) # /
462+
_ = read(state, Token) # /
452463
# Must be /2
453-
n = read(state, Token)
454-
if n.kind != _TOKEN_NUMBER && n.value != "2"
464+
n = read(state, Token, _TOKEN_NUMBER)
465+
if n.value != "2"
455466
throw(UnexpectedToken(n))
456467
end
457468
for (i, term) in enumerate(f.quadratic_terms)
@@ -481,11 +492,11 @@ function _parse_term(
481492
_skip_newlines(state)
482493
if _next_token_is(state, _TOKEN_ADDITION)
483494
# "+" TERM
484-
read(state, Token)
495+
_ = read(state, Token, _TOKEN_ADDITION)
485496
return _parse_term(state, cache, prefix)
486497
elseif _next_token_is(state, _TOKEN_SUBTRACTION)
487498
# "-" TERM
488-
read(state, Token)
499+
_ = read(state, Token, _TOKEN_SUBTRACTION)
489500
return _parse_term(state, cache, -prefix)
490501
elseif _next_token_is(state, _TOKEN_IDENTIFIER)
491502
# IDENTIFIER
@@ -499,7 +510,7 @@ function _parse_term(
499510
return MOI.ScalarAffineTerm(coef, x)
500511
elseif _next_token_is(state, _TOKEN_MULTIPLICATION)
501512
# NUMBER * IDENTIFIER
502-
read(state, token) # skip *
513+
_ = read(state, Token, _TOKEN_MULTIPLICATION)
503514
x = _parse_variable(state, cache)
504515
return MOI.ScalarAffineTerm(coef, x)
505516
else
@@ -510,7 +521,7 @@ function _parse_term(
510521
# QUADRATIC_EXPRESSION
511522
return _parse_quad_expression(state, cache, prefix)
512523
end
513-
return nothing
524+
return throw(UnexpectedToken(peek(state, Token)))
514525
end
515526

516527
function _add_to_expression!(f::MOI.ScalarQuadraticFunction{T}, x::T) where {T}
@@ -547,7 +558,7 @@ function _parse_expression(state::LexerState, cache::Cache{T}) where {T}
547558
p = read(state, Token)
548559
_add_to_expression!(f, _parse_term(state, cache, -one(T)))
549560
elseif p.kind == _TOKEN_NEWLINE
550-
read(state, Token)
561+
_ = read(state, Token)
551562
else
552563
break
553564
end
@@ -609,19 +620,19 @@ function _parse_set_prefix(state, cache)
609620
end
610621
end
611622

612-
# NAME --> [IDENTIFIER OP_COLON]
623+
# NAME := [IDENTIFIER :]
613624
function _parse_optional_name(state::LexerState, cache::Cache)
614625
_skip_newlines(state)
615626
if _next_token_is(state, _TOKEN_IDENTIFIER, 1) &&
616627
_next_token_is(state, _TOKEN_COLON, 2)
617628
name = read(state, Token)
618-
read(state, Token) # Skip :
629+
_ = read(state, Token) # Skip :
619630
return name.value
620631
end
621632
return nothing
622633
end
623634

624-
# OBJECTIVE --> [NAME] [EXPRESSION]
635+
# OBJECTIVE := [NAME] [EXPRESSION]
625636
function _parse_objective(state::LexerState, cache::Cache)
626637
_ = _parse_optional_name(state, cache)
627638
_skip_newlines(state)
@@ -663,7 +674,7 @@ function _add_bound(cache::Cache, x::MOI.VariableIndex, ::Nothing)
663674
return
664675
end
665676

666-
# BOUND -->
677+
# BOUND :=
667678
# IDENFITIER SET_SUFFIX
668679
# | SET_PREFIX IDENTIFIER
669680
# | SET_PREFIX IDENTIFIER SET_SUFFIX
@@ -689,22 +700,28 @@ function _parse_bound(state, cache)
689700
return
690701
end
691702

703+
function _is_sos_constraint(state)
704+
return _next_token_is(state, _TOKEN_IDENTIFIER, 1) &&
705+
_next_token_is(state, _TOKEN_COLON, 2) &&
706+
_next_token_is(state, _TOKEN_COLON, 3)
707+
end
708+
692709
# SOS_CONSTRAINT :=
693710
# [NAME] S1:: (IDENTIFIER:NUMBER)+ \n
694711
# | [NAME] S2:: (IDENTIFIER:NUMBER)+ \n
695712
#
696713
# The newline character is required.
697714
function _parse_sos_constraint(state::LexerState, cache::Cache{T}) where {T}
698-
t = read(state, Token) # Si
715+
t = read(state, Token, _TOKEN_IDENTIFIER) # Si
699716
if !(t.value == "S1" || t.value == "S2")
700717
throw(UnexpectedToken(t))
701718
end
702-
_expect(read(state, Token), _TOKEN_COLON)
703-
_expect(read(state, Token), _TOKEN_COLON)
719+
_ = read(state, Token, _TOKEN_COLON)
720+
_ = read(state, Token, _TOKEN_COLON)
704721
f, w = MOI.VectorOfVariables(MOI.VariableIndex[]), T[]
705722
while true
706723
push!(f.variables, _parse_variable(state, cache))
707-
_expect(read(state, Token), _TOKEN_COLON)
724+
_ = read(state, Token, _TOKEN_COLON)
708725
push!(w, _parse_number(state, cache))
709726
if _next_token_is(state, _TOKEN_NEWLINE)
710727
break
@@ -717,12 +734,6 @@ function _parse_sos_constraint(state::LexerState, cache::Cache{T}) where {T}
717734
end
718735
end
719736

720-
function _is_sos_constraint(state)
721-
return _next_token_is(state, _TOKEN_IDENTIFIER, 1) &&
722-
_next_token_is(state, _TOKEN_COLON, 2) &&
723-
_next_token_is(state, _TOKEN_COLON, 3)
724-
end
725-
726737
function _is_indicator_constraint(state)
727738
return _next_token_is(state, _TOKEN_IDENTIFIER, 1) &&
728739
_next_token_is(state, _TOKEN_EQUAL_TO, 2) &&
@@ -738,17 +749,16 @@ function _parse_indicator_constraint(
738749
cache::Cache{T},
739750
) where {T}
740751
z = _parse_variable(state, cache)
741-
_expect(read(state, Token), _TOKEN_EQUAL_TO)
742-
t = read(state, Token)
743-
_expect(t, _TOKEN_NUMBER)
752+
_ = read(state, Token, _TOKEN_EQUAL_TO)
753+
t = read(state, Token, _TOKEN_NUMBER)
744754
indicator = if t.value == "0"
745755
MOI.ACTIVATE_ON_ZERO
746756
elseif t.value == "1"
747757
MOI.ACTIVATE_ON_ONE
748758
else
749759
throw(UnexpectedToken(t))
750760
end
751-
_expect(read(state, Token), _TOKEN_IMPLIES)
761+
_ = read(state, Token, _TOKEN_IMPLIES)
752762
f = _parse_expression(state, cache)
753763
set = _parse_set_suffix(state, cache)
754764
return MOI.add_constraint(

0 commit comments

Comments
 (0)