Skip to content

Commit e48dcfb

Browse files
mlubinclaude
andcommitted
feat(mps_parser): reject Lazy Constraints LP sections
Previously 'Lazy Constraints' was accepted and parsed as a regular constraints block. This quietly pulled in content that's meant to be solver-side metadata, not model structure. Treat it the same as 'User Cuts': recognize the header as a section boundary so the prior section ends cleanly, then throw with a clear "not supported (scope is LP/MIP/QP only)" message. Removes the LazyConstraints enumerator and its dispatch arm, adds the display-name branch so reject_unsupported_section() prints "Lazy Constraints", and adds two tests (unsupported_lazy_constraints_section_throws and unsupported_user_cuts_section_throws). PARSER_TEST: 95/95 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Miles Lubin <mlubin@nvidia.com>
1 parent 56dabd8 commit e48dcfb

2 files changed

Lines changed: 36 additions & 14 deletions

File tree

cpp/libmps_parser/src/lp_parser.cpp

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ class LpParseEngine {
223223
None,
224224
Objective,
225225
Constraints,
226-
LazyConstraints,
227226
Bounds,
228227
Generals,
229228
Binaries,
@@ -529,16 +528,16 @@ bool LpParseEngine<i_t, f_t>::at_section_boundary() const
529528
if (is_binaries_keyword(lower)) return true;
530529
if (is_end_keyword(lower)) return true;
531530

532-
// Multi-word section headers: "Subject To", "Such That", "Lazy Constraints",
533-
// and the unsupported "User Cuts" / "General Constraints".
531+
// Multi-word section headers: "Subject To" / "Such That" are supported;
532+
// "Lazy Constraints", "User Cuts", and "General Constraints" are
533+
// recognized as boundaries so the prior section ends cleanly, but
534+
// reject_unsupported_section() throws once dispatch reaches them.
534535
const LpToken& t2 = peek(1);
535536
if (lower == "subject" && name_equals_ci(t2, "to")) return true;
536537
if (lower == "such" && name_equals_ci(t2, "that")) return true;
537538
if (lower == "st" || lower == "st." || lower == "s.t.") return true;
538539
if (lower == "lazy" && name_equals_ci(t2, "constraints")) return true;
539540
if (lower == "user" && name_equals_ci(t2, "cuts")) return true;
540-
// "General Constraints" is unsupported but still a section boundary;
541-
// reject_unsupported_section() throws before we'd keep parsing.
542541
if (lower == "general" && name_equals_ci(t2, "constraints")) return true;
543542

544543
// Unsupported single-token sections.
@@ -564,6 +563,8 @@ void LpParseEngine<i_t, f_t>::reject_unsupported_section()
564563
name = "Semi-continuous";
565564
} else if (lower == "user" && name_equals_ci(peek(1), "cuts")) {
566565
name = "User Cuts";
566+
} else if (lower == "lazy" && name_equals_ci(peek(1), "constraints")) {
567+
name = "Lazy Constraints";
567568
} else if (lower == "general" && name_equals_ci(peek(1), "constraints")) {
568569
name = "General Constraints";
569570
}
@@ -609,11 +610,6 @@ typename LpParseEngine<i_t, f_t>::SectionKind LpParseEngine<i_t, f_t>::try_consu
609610
advance();
610611
return SectionKind::Constraints;
611612
}
612-
if (lower == "lazy" && name_equals_ci(peek(1), "constraints")) {
613-
advance();
614-
advance();
615-
return SectionKind::LazyConstraints;
616-
}
617613
if (is_bounds_keyword(lower)) {
618614
advance();
619615
return SectionKind::Bounds;
@@ -909,8 +905,6 @@ void LpParseEngine<i_t, f_t>::parse_objective_section()
909905
template <typename i_t, typename f_t>
910906
void LpParseEngine<i_t, f_t>::parse_constraints_section()
911907
{
912-
// Lazy constraints are treated as regular constraints (matching the MPS
913-
// parser's LAZYCONS handling).
914908
while (!at_section_boundary()) {
915909
// Optional "name:" label — present iff the first two tokens are Name + ':'.
916910
std::string row_name;
@@ -1096,8 +1090,7 @@ void LpParseEngine<i_t, f_t>::parse_all()
10961090
parse_objective_section();
10971091
saw_objective = true;
10981092
break;
1099-
case SectionKind::Constraints:
1100-
case SectionKind::LazyConstraints: parse_constraints_section(); break;
1093+
case SectionKind::Constraints: parse_constraints_section(); break;
11011094
case SectionKind::Bounds: parse_bounds_section(); break;
11021095
case SectionKind::Generals: parse_integer_list_section(false); break;
11031096
case SectionKind::Binaries: parse_integer_list_section(true); break;

cpp/libmps_parser/tests/parser_test.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,35 @@ End
17481748
std::logic_error);
17491749
}
17501750

1751+
TEST(lp_parser, unsupported_lazy_constraints_section_throws)
1752+
{
1753+
// Lazy constraints and user cuts are scope-limited out: LP/MIP/QP only.
1754+
EXPECT_THROW(parse_lp_string(R"LP(
1755+
Minimize
1756+
x
1757+
Subject To
1758+
c1: x >= 1
1759+
Lazy Constraints
1760+
lc: x <= 10
1761+
End
1762+
)LP"),
1763+
std::logic_error);
1764+
}
1765+
1766+
TEST(lp_parser, unsupported_user_cuts_section_throws)
1767+
{
1768+
EXPECT_THROW(parse_lp_string(R"LP(
1769+
Minimize
1770+
x
1771+
Subject To
1772+
c1: x >= 1
1773+
User Cuts
1774+
uc: x <= 10
1775+
End
1776+
)LP"),
1777+
std::logic_error);
1778+
}
1779+
17511780
TEST(lp_parser, unknown_file_throws)
17521781
{
17531782
auto call = [] { return parse_lp<int, double>("/definitely/does/not/exist.lp"); };

0 commit comments

Comments
 (0)