From 4ee40273d7f09d4282cc089036cee950b480bcde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:58:04 +0000 Subject: [PATCH 01/12] Initial plan From 5b5c19b7523c31dacebf45002daec0e5aa84ca31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:11:30 +0000 Subject: [PATCH 02/12] Change stop_after from int to std::optional in C++ and reject stop_after <= 0 in Python Closes #1067 Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/gui/dlnash.cc | 13 ++++++++++--- src/gui/nashspec.h | 4 ++-- src/pygambit/gambit.pxd | 6 +++--- src/pygambit/nash.pxi | 28 ++++++++++++++++++++-------- src/pygambit/nash.py | 14 ++++++-------- src/solvers/enumpoly/efgpoly.cc | 14 +++++++++----- src/solvers/enumpoly/enumpoly.h | 5 +++-- src/solvers/enumpoly/nfgpoly.cc | 14 +++++++++----- src/solvers/lcp/lcp.h | 4 +++- src/solvers/lcp/nfglcp.cc | 15 ++++++++------- src/tools/enumpoly/enumpoly.cc | 4 ++-- src/tools/lcp/lcp.cc | 5 +++-- tests/test_nash.py | 16 ++++++++++++++++ 13 files changed, 94 insertions(+), 48 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index d16229b5eb..234269f36f 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -158,7 +158,10 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) } else if constexpr (std::is_same_v) { return prefix + - wxString::Format("enumpoly -d 10 -e %d -m %.17g", method.stopAfter, + wxString::Format("enumpoly -d 10 -e %d -m %.17g", + method.stopAfter.has_value() + ? static_cast(method.stopAfter.value()) + : 0, method.maxRegret) + strategic; } @@ -175,7 +178,11 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) return prefix + wxT("lp") + strategic; } else if constexpr (std::is_same_v) { - return prefix + wxString::Format("lcp -e %d -r %d", method.stopAfter, method.maxDepth) + + return prefix + wxString::Format("lcp -e %d -r %d", + method.stopAfter.has_value() + ? static_cast(method.stopAfter.value()) + : 0, + method.maxDepth) + strategic; } else if constexpr (std::is_same_v) { @@ -244,7 +251,7 @@ wxString ParameterDescription(const NashMethodSpec &p_method) return std::visit( [](const Method &method) { if constexpr (std::is_same_v) { - if (method.stopAfter == 1) { + if (method.stopAfter.has_value() && method.stopAfter.value() == 1) { return wxString::Format(" (stop after one equilibrium; maximum regret %.4g)", method.maxRegret); } diff --git a/src/gui/nashspec.h b/src/gui/nashspec.h index a9319b38c6..50af9a87d3 100644 --- a/src/gui/nashspec.h +++ b/src/gui/nashspec.h @@ -72,7 +72,7 @@ struct EnumMixedNashSpec { }; struct EnumPolyNashSpec { - int stopAfter{0}; + std::optional stopAfter{std::nullopt}; double maxRegret{1.0e-4}; std::optional MakeSolver(NashRepresentation p_representation) const; @@ -99,7 +99,7 @@ struct LPNashSpec { }; struct LCPNashSpec { - int stopAfter{0}; + std::optional stopAfter{std::nullopt}; int maxDepth{0}; std::optional MakeSolver(NashRepresentation p_representation) const; diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 29ece38f0c..320ed09a32 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -554,7 +554,7 @@ cdef extern from "solvers/enummixed/enummixed.h": cdef extern from "solvers/lcp/lcp.h": stdlist[c_MixedStrategyProfile[T]] LcpStrategySolve[T]( - c_Game, int p_stopAfter, int p_maxDepth + c_Game, optional[size_t] p_stopAfter, int p_maxDepth ) except +RuntimeError stdlist[c_MixedBehaviorProfile[T]] LcpBehaviorSolve[T](c_Game) except +RuntimeError @@ -594,10 +594,10 @@ cdef extern from "solvers/nashsupport/nashsupport.h": cdef extern from "solvers/enumpoly/enumpoly.h": stdlist[c_MixedStrategyProfile[double]] EnumPolyStrategySolve( - c_Game, int, float, size_t + c_Game, optional[size_t], float, size_t ) except +RuntimeError stdlist[c_MixedBehaviorProfile[double]] EnumPolyBehaviorSolve( - c_Game, int, float, size_t + c_Game, optional[size_t], float, size_t ) except +RuntimeError cdef extern from "solvers/logit/logit.h": diff --git a/src/pygambit/nash.pxi b/src/pygambit/nash.pxi index 64fffa696d..4a5a14d00f 100644 --- a/src/pygambit/nash.pxi +++ b/src/pygambit/nash.pxi @@ -86,15 +86,21 @@ def _lcp_behavior_solve_rational( def _lcp_strategy_solve_double( - game: Game, stop_after: int, max_depth: int + game: Game, stop_after, max_depth: int ) -> list[MixedStrategyProfileDouble]: - return _convert_mspd(LcpStrategySolve[double](game.game, stop_after, max_depth)) + cdef optional[size_t] c_stop_after + if stop_after is not None: + c_stop_after = stop_after + return _convert_mspd(LcpStrategySolve[double](game.game, c_stop_after, max_depth)) def _lcp_strategy_solve_rational( - game: Game, stop_after: int, max_depth: int + game: Game, stop_after, max_depth: int ) -> list[MixedStrategyProfileRational]: - return _convert_mspr(LcpStrategySolve[c_Rational](game.game, stop_after, max_depth)) + cdef optional[size_t] c_stop_after + if stop_after is not None: + c_stop_after = stop_after + return _convert_mspr(LcpStrategySolve[c_Rational](game.game, c_stop_after, max_depth)) def _lp_behavior_solve_double(game: Game) -> list[MixedBehaviorProfileDouble]: @@ -178,20 +184,26 @@ def _nashsupport_strategy_solve( def _enumpoly_strategy_solve( game: Game, - stop_after: int, + stop_after, maxregret: float, max_rectangles: int, ) -> list[MixedStrategyProfileDouble]: - return _convert_mspd(EnumPolyStrategySolve(game.game, stop_after, maxregret, max_rectangles)) + cdef optional[size_t] c_stop_after + if stop_after is not None: + c_stop_after = stop_after + return _convert_mspd(EnumPolyStrategySolve(game.game, c_stop_after, maxregret, max_rectangles)) def _enumpoly_behavior_solve( game: Game, - stop_after: int, + stop_after, maxregret: float, max_rectangles: int, ) -> list[MixedBehaviorProfileDouble]: - return _convert_mbpd(EnumPolyBehaviorSolve(game.game, stop_after, maxregret, max_rectangles)) + cdef optional[size_t] c_stop_after + if stop_after is not None: + c_stop_after = stop_after + return _convert_mbpd(EnumPolyBehaviorSolve(game.game, c_stop_after, maxregret, max_rectangles)) def _logit_strategy_solve( diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index 11639dad6d..d22b8d980c 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -242,15 +242,15 @@ def lcp_solve( raise ValueError( "lcp_solve(): max_depth can only be used on the strategic representation" ) - if stop_after is not None and stop_after < 0: + if stop_after is not None and stop_after <= 0: raise ValueError( - f"lcp_solve(): stop_after argument must be a non-negative number; got {stop_after}" + f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" ) if not game.is_tree or use_strategic: if rational: - equilibria = libgbt._lcp_strategy_solve_rational(game, stop_after or 0, max_depth or 0) + equilibria = libgbt._lcp_strategy_solve_rational(game, stop_after, max_depth or 0) else: - equilibria = libgbt._lcp_strategy_solve_double(game, stop_after or 0, max_depth or 0) + equilibria = libgbt._lcp_strategy_solve_double(game, stop_after, max_depth or 0) elif rational: equilibria = libgbt._lcp_behavior_solve_rational(game) else: @@ -709,12 +709,10 @@ def enumpoly_solve( ----- PHCpack is available at https://homepages.math.uic.edu/~jan/PHCpack/phcpack.html """ - if stop_after is None: - stop_after = 0 - elif stop_after < 0: + if stop_after is not None and stop_after <= 0: raise ValueError( f"enumpoly_solve(): " - f"stop_after argument must be a non-negative number; got {stop_after}" + f"stop_after argument must be a positive integer; got {stop_after}" ) if maxregret <= 0.0: raise ValueError( diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index b8b51645c4..87310ad262 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -183,7 +183,8 @@ FindNashExtension(const MixedBehaviorProfile &p_baseProfile, double p_ma std::list> SolveSupport(const BehaviorSupportProfile &p_support, bool &p_isSingular, bool &p_budgetExceeded, - int p_stopAfter, double p_maxRegret, + std::optional p_stopAfter, + double p_maxRegret, size_t p_maxRectangles, const CancelToken &p_cancel) { @@ -201,7 +202,7 @@ std::list> SolveSupport(const BehaviorSupportProfil std::list> roots; try { roots = solver.FindRoots({bottoms, tops}, - (p_stopAfter > 0) ? p_stopAfter : std::numeric_limits::max(), + p_stopAfter.value_or(std::numeric_limits::max()), p_maxRectangles, p_budgetExceeded, p_cancel); } catch (const SingularMatrixException &) { @@ -228,7 +229,7 @@ std::list> SolveSupport(const BehaviorSupportProfil namespace Gambit::Nash { std::list> -EnumPolyBehaviorSolve(const Game &p_game, int p_stopAfter, double p_maxregret, +EnumPolyBehaviorSolve(const Game &p_game, std::optional p_stopAfter, double p_maxregret, size_t p_maxRectangles, BehaviorCallbackType p_onEquilibrium, EnumPolyEventCallbackType p_onEvent, const CancelToken &p_cancel) @@ -253,7 +254,10 @@ EnumPolyBehaviorSolve(const Game &p_game, int p_stopAfter, double p_maxregret, bool budgetExceeded = false; for (const auto &solution : SolveSupport(support, isSingular, budgetExceeded, - std::max(p_stopAfter - static_cast(ret.size()), 0), p_maxregret, + p_stopAfter.has_value() + ? std::optional(p_stopAfter.value() - std::min(ret.size(), p_stopAfter.value())) + : std::nullopt, + p_maxregret, p_maxRectangles, p_cancel)) { p_onEquilibrium(solution); ret.push_back(solution); @@ -264,7 +268,7 @@ EnumPolyBehaviorSolve(const Game &p_game, int p_stopAfter, double p_maxregret, if (budgetExceeded) { p_onEvent(EnumPolyBudgetExceededSupportEvent{support}); } - if (p_stopAfter > 0 && static_cast(ret.size()) >= p_stopAfter) { + if (p_stopAfter.has_value() && ret.size() >= p_stopAfter.value()) { break; } } diff --git a/src/solvers/enumpoly/enumpoly.h b/src/solvers/enumpoly/enumpoly.h index f148a1e3e5..03f37781bf 100644 --- a/src/solvers/enumpoly/enumpoly.h +++ b/src/solvers/enumpoly/enumpoly.h @@ -24,6 +24,7 @@ #ifndef GAMBIT_SOLVERS_ENUMPOLY_ENUMPOLY_H #define GAMBIT_SOLVERS_ENUMPOLY_ENUMPOLY_H +#include #include #include "solvers/nash.h" @@ -62,7 +63,7 @@ template void NullEnumPolyEventCallback(const EnumPolyEvent> -EnumPolyStrategySolve(const Game &p_game, int p_stopAfter, double p_maxregret, +EnumPolyStrategySolve(const Game &p_game, std::optional p_stopAfter, double p_maxregret, size_t p_maxRectangles = kDefaultEnumPolyMaxRectangles, StrategyCallbackType p_onEquilibrium = NullStrategyCallback, EnumPolyEventCallbackType p_onEvent = @@ -70,7 +71,7 @@ EnumPolyStrategySolve(const Game &p_game, int p_stopAfter, double p_maxregret, const CancelToken &p_cancel = CancelToken()); std::list> -EnumPolyBehaviorSolve(const Game &, int p_stopAfter, double p_maxregret, +EnumPolyBehaviorSolve(const Game &, std::optional p_stopAfter, double p_maxregret, size_t p_maxRectangles = kDefaultEnumPolyMaxRectangles, BehaviorCallbackType p_onEquilibrium = NullBehaviorCallback, EnumPolyEventCallbackType p_onEvent = diff --git a/src/solvers/enumpoly/nfgpoly.cc b/src/solvers/enumpoly/nfgpoly.cc index d257e23edd..2a681b1605 100644 --- a/src/solvers/enumpoly/nfgpoly.cc +++ b/src/solvers/enumpoly/nfgpoly.cc @@ -103,7 +103,8 @@ namespace Gambit::Nash { std::list> EnumPolyStrategySupportSolve(const StrategySupportProfile &support, bool &is_singular, - bool &p_budgetExceeded, int p_stopAfter, size_t p_maxRectangles, + bool &p_budgetExceeded, std::optional p_stopAfter, + size_t p_maxRectangles, const CancelToken &p_cancel) { auto space = std::make_shared(support.MixedProfileLength() - @@ -120,7 +121,7 @@ EnumPolyStrategySupportSolve(const StrategySupportProfile &support, bool &is_sin std::list> roots; try { roots = solver.FindRoots({bottoms, tops}, - (p_stopAfter > 0) ? p_stopAfter : std::numeric_limits::max(), + p_stopAfter.value_or(std::numeric_limits::max()), p_maxRectangles, p_budgetExceeded, p_cancel); } catch (const SingularMatrixException &) { @@ -142,7 +143,7 @@ EnumPolyStrategySupportSolve(const StrategySupportProfile &support, bool &is_sin } std::list> -EnumPolyStrategySolve(const Game &p_game, int p_stopAfter, double p_maxregret, +EnumPolyStrategySolve(const Game &p_game, std::optional p_stopAfter, double p_maxregret, size_t p_maxRectangles, StrategyCallbackType p_onEquilibrium, EnumPolyEventCallbackType p_onEvent, const CancelToken &p_cancel) @@ -167,7 +168,10 @@ EnumPolyStrategySolve(const Game &p_game, int p_stopAfter, double p_maxregret, bool budget_exceeded; for (auto solution : EnumPolyStrategySupportSolve( support, is_singular, budget_exceeded, - std::max(p_stopAfter - static_cast(ret.size()), 0), p_maxRectangles, p_cancel)) { + p_stopAfter.has_value() + ? std::optional(p_stopAfter.value() - std::min(ret.size(), p_stopAfter.value())) + : std::nullopt, + p_maxRectangles, p_cancel)) { const MixedStrategyProfile fullProfile = solution.ToFullSupport(); if (fullProfile.GetMaxRegret() < p_maxregret) { p_onEquilibrium(fullProfile); @@ -181,7 +185,7 @@ EnumPolyStrategySolve(const Game &p_game, int p_stopAfter, double p_maxregret, if (budget_exceeded) { p_onEvent(EnumPolyBudgetExceededSupportEvent{support}); } - if (p_stopAfter > 0 && static_cast(ret.size()) >= p_stopAfter) { + if (p_stopAfter.has_value() && ret.size() >= p_stopAfter.value()) { break; } } diff --git a/src/solvers/lcp/lcp.h b/src/solvers/lcp/lcp.h index 2e21b1c5a3..328fc4a266 100644 --- a/src/solvers/lcp/lcp.h +++ b/src/solvers/lcp/lcp.h @@ -23,13 +23,15 @@ #ifndef GAMBIT_SOLVERS_LCP_LCP_H #define GAMBIT_SOLVERS_LCP_LCP_H +#include + #include "solvers/nash.h" namespace Gambit::Nash { template std::list> -LcpStrategySolve(const Game &p_game, int p_stopAfter, int p_maxDepth, +LcpStrategySolve(const Game &p_game, std::optional p_stopAfter, int p_maxDepth, StrategyCallbackType p_onEquilibrium = NullStrategyCallback, const CancelToken &p_cancel = CancelToken()); diff --git a/src/solvers/lcp/nfglcp.cc b/src/solvers/lcp/nfglcp.cc index c7887e424e..9e7173197b 100644 --- a/src/solvers/lcp/nfglcp.cc +++ b/src/solvers/lcp/nfglcp.cc @@ -113,7 +113,7 @@ template Vector Make_b2(const Game &p_game) template class NashLcpStrategySolver { public: - NashLcpStrategySolver(int p_stopAfter, int p_maxDepth, + NashLcpStrategySolver(std::optional p_stopAfter, int p_maxDepth, StrategyCallbackType p_onEquilibrium = NullStrategyCallback, const CancelToken &p_cancel = CancelToken()) : m_onEquilibrium(p_onEquilibrium), m_stopAfter(p_stopAfter), m_maxDepth(p_maxDepth), @@ -128,7 +128,8 @@ template class NashLcpStrategySolver { enum class SearchResult { Continue, PruneBranch, LimitReached }; StrategyCallbackType m_onEquilibrium; - int m_stopAfter, m_maxDepth; + std::optional m_stopAfter; + int m_maxDepth; CancelToken m_cancel; class Solution; @@ -216,7 +217,7 @@ NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p_tabl m_onEquilibrium(profile); p_solution.m_equilibria.push_back(profile); - if (m_stopAfter > 0 && p_solution.EquilibriumCount() >= m_stopAfter) { + if (m_stopAfter.has_value() && p_solution.EquilibriumCount() >= static_cast(m_stopAfter.value())) { return SearchResult::LimitReached; } @@ -283,7 +284,7 @@ std::list> NashLcpStrategySolver::Solve(const Game &p const Vector b2 = Make_b2(p_game); linalg::LHTableau B(A1, A2, b1, b2); - if (m_stopAfter != 1) { + if (!m_stopAfter.has_value() || m_stopAfter.value() != 1) { AllLemke(p_game, 0, B, solution, 0); } else { @@ -295,7 +296,7 @@ std::list> NashLcpStrategySolver::Solve(const Game &p template std::list> -LcpStrategySolve(const Game &p_game, int p_stopAfter, int p_maxDepth, +LcpStrategySolve(const Game &p_game, std::optional p_stopAfter, int p_maxDepth, StrategyCallbackType p_onEquilibrium, const CancelToken &p_cancel) { return NashLcpStrategySolver(p_stopAfter, p_maxDepth, p_onEquilibrium, p_cancel) @@ -303,8 +304,8 @@ LcpStrategySolve(const Game &p_game, int p_stopAfter, int p_maxDepth, } template std::list> -LcpStrategySolve(const Game &, int, int, StrategyCallbackType, const CancelToken &); +LcpStrategySolve(const Game &, std::optional, int, StrategyCallbackType, const CancelToken &); template std::list> -LcpStrategySolve(const Game &, int, int, StrategyCallbackType, const CancelToken &); +LcpStrategySolve(const Game &, std::optional, int, StrategyCallbackType, const CancelToken &); } // end namespace Gambit::Nash diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index e43e9d7923..40d3caaaf2 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -147,7 +147,7 @@ int main(int argc, char *argv[]) bool quiet = false; bool useStrategic = false; double maxregret = 1.0e-8; - int stopAfter = 0; + std::optional stopAfter; size_t maxRectangles = Nash::kDefaultEnumPolyMaxRectangles; int long_opt_index = 0; @@ -174,7 +174,7 @@ int main(int argc, char *argv[]) maxregret = atof(optarg); break; case 'e': - stopAfter = atoi(optarg); + stopAfter = static_cast(atoi(optarg)); break; case 'r': maxRectangles = std::strtoull(optarg, nullptr, 10); diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index 5671f603db..0f756010ab 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -70,7 +70,8 @@ int main(int argc, char *argv[]) int c; bool useFloat = false, useStrategic = false, quiet = false; bool printDetail = false; - int numDecimals = 6, stopAfter = 0, maxDepth = 0; + int numDecimals = 6, maxDepth = 0; + std::optional stopAfter; int long_opt_index = 0; option long_options[] = { @@ -88,7 +89,7 @@ int main(int argc, char *argv[]) printDetail = true; break; case 'e': - stopAfter = atoi(optarg); + stopAfter = static_cast(atoi(optarg)); break; case 'h': PrintHelp(argv[0]); diff --git a/tests/test_nash.py b/tests/test_nash.py index 607bdce1e3..d4d476d74d 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -3421,6 +3421,22 @@ def test_qre_solver(test_case: QREquilibriumTestCase, subtests) -> None: ################################################################################################## +def test_enumpoly_solve_error_with_invalid_stop_after(): + game = games.read_from_file("const_sum_game.nfg") + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=0) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=-1) + + +def test_lcp_solve_error_with_invalid_stop_after(): + game = games.read_from_file("const_sum_game.nfg") + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=0) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=-1) + + def test_logit_solve_branch_error_with_invalid_maxregret(): game = games.read_from_file("const_sum_game.nfg") with pytest.raises(ValueError, match="must be positive"): From 195f0c3928776716a4e3c8f29a431cad94c4bc01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:17:56 +0000 Subject: [PATCH 03/12] Address code review: fix size_t cast comparison and validate CLI stopAfter input Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/solvers/lcp/nfglcp.cc | 2 +- src/tools/enumpoly/enumpoly.cc | 8 ++++++-- src/tools/lcp/lcp.cc | 8 ++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/solvers/lcp/nfglcp.cc b/src/solvers/lcp/nfglcp.cc index 9e7173197b..8744f0eb64 100644 --- a/src/solvers/lcp/nfglcp.cc +++ b/src/solvers/lcp/nfglcp.cc @@ -217,7 +217,7 @@ NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p_tabl m_onEquilibrium(profile); p_solution.m_equilibria.push_back(profile); - if (m_stopAfter.has_value() && p_solution.EquilibriumCount() >= static_cast(m_stopAfter.value())) { + if (m_stopAfter.has_value() && static_cast(p_solution.EquilibriumCount()) >= m_stopAfter.value()) { return SearchResult::LimitReached; } diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index 40d3caaaf2..d42154526f 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -173,9 +173,13 @@ int main(int argc, char *argv[]) case 'm': maxregret = atof(optarg); break; - case 'e': - stopAfter = static_cast(atoi(optarg)); + case 'e': { + int val = atoi(optarg); + if (val > 0) { + stopAfter = static_cast(val); + } break; + } case 'r': maxRectangles = std::strtoull(optarg, nullptr, 10); break; diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index 0f756010ab..8eb944bda0 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -88,9 +88,13 @@ int main(int argc, char *argv[]) case 'D': printDetail = true; break; - case 'e': - stopAfter = static_cast(atoi(optarg)); + case 'e': { + int val = atoi(optarg); + if (val > 0) { + stopAfter = static_cast(val); + } break; + } case 'h': PrintHelp(argv[0]); break; From 8edec61760a037964088a2575fa944cd457edc9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:33:09 +0000 Subject: [PATCH 04/12] Reject non-integer stop_after, fix clang-format violations Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/gui/dlnash.cc | 19 +++++++++---------- src/pygambit/nash.py | 9 +++++++++ src/solvers/enumpoly/efgpoly.cc | 15 +++++++-------- src/solvers/enumpoly/nfgpoly.cc | 12 ++++++------ src/solvers/lcp/nfglcp.cc | 15 ++++++++++----- src/tools/enumpoly/enumpoly.cc | 5 +++-- src/tools/lcp/lcp.cc | 6 ++++-- tests/test_nash.py | 8 ++++++++ 8 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 234269f36f..02f184d992 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -158,11 +158,10 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) } else if constexpr (std::is_same_v) { return prefix + - wxString::Format("enumpoly -d 10 -e %d -m %.17g", - method.stopAfter.has_value() - ? static_cast(method.stopAfter.value()) - : 0, - method.maxRegret) + + wxString::Format( + "enumpoly -d 10 -e %d -m %.17g", + method.stopAfter.has_value() ? static_cast(method.stopAfter.value()) : 0, + method.maxRegret) + strategic; } else if constexpr (std::is_same_v) { @@ -178,11 +177,11 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) return prefix + wxT("lp") + strategic; } else if constexpr (std::is_same_v) { - return prefix + wxString::Format("lcp -e %d -r %d", - method.stopAfter.has_value() - ? static_cast(method.stopAfter.value()) - : 0, - method.maxDepth) + + return prefix + + wxString::Format( + "lcp -e %d -r %d", + method.stopAfter.has_value() ? static_cast(method.stopAfter.value()) : 0, + method.maxDepth) + strategic; } else if constexpr (std::is_same_v) { diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index d22b8d980c..54b85f8fec 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -246,6 +246,10 @@ def lcp_solve( raise ValueError( f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" ) + if stop_after is not None and isinstance(stop_after, float) and not stop_after.is_integer(): + raise ValueError( + f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" + ) if not game.is_tree or use_strategic: if rational: equilibria = libgbt._lcp_strategy_solve_rational(game, stop_after, max_depth or 0) @@ -714,6 +718,11 @@ def enumpoly_solve( f"enumpoly_solve(): " f"stop_after argument must be a positive integer; got {stop_after}" ) + if stop_after is not None and isinstance(stop_after, float) and not stop_after.is_integer(): + raise ValueError( + f"enumpoly_solve(): " + f"stop_after argument must be a positive integer; got {stop_after}" + ) if maxregret <= 0.0: raise ValueError( f"enumpoly_solve(): maxregret must be a positive number; got {maxregret}" diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index 87310ad262..d0eb89a117 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -184,8 +184,7 @@ FindNashExtension(const MixedBehaviorProfile &p_baseProfile, double p_ma std::list> SolveSupport(const BehaviorSupportProfile &p_support, bool &p_isSingular, bool &p_budgetExceeded, std::optional p_stopAfter, - double p_maxRegret, - size_t p_maxRectangles, + double p_maxRegret, size_t p_maxRectangles, const CancelToken &p_cancel) { ProblemData data(p_support); @@ -201,9 +200,9 @@ std::list> SolveSupport(const BehaviorSupportProfil PolynomialSystemSolver solver(equations); std::list> roots; try { - roots = solver.FindRoots({bottoms, tops}, - p_stopAfter.value_or(std::numeric_limits::max()), - p_maxRectangles, p_budgetExceeded, p_cancel); + roots = + solver.FindRoots({bottoms, tops}, p_stopAfter.value_or(std::numeric_limits::max()), + p_maxRectangles, p_budgetExceeded, p_cancel); } catch (const SingularMatrixException &) { p_isSingular = true; @@ -255,10 +254,10 @@ EnumPolyBehaviorSolve(const Game &p_game, std::optional p_stopAfter, dou for (const auto &solution : SolveSupport(support, isSingular, budgetExceeded, p_stopAfter.has_value() - ? std::optional(p_stopAfter.value() - std::min(ret.size(), p_stopAfter.value())) + ? std::optional(p_stopAfter.value() - + std::min(ret.size(), p_stopAfter.value())) : std::nullopt, - p_maxregret, - p_maxRectangles, p_cancel)) { + p_maxregret, p_maxRectangles, p_cancel)) { p_onEquilibrium(solution); ret.push_back(solution); } diff --git a/src/solvers/enumpoly/nfgpoly.cc b/src/solvers/enumpoly/nfgpoly.cc index 2a681b1605..93066a6c6e 100644 --- a/src/solvers/enumpoly/nfgpoly.cc +++ b/src/solvers/enumpoly/nfgpoly.cc @@ -104,8 +104,7 @@ namespace Gambit::Nash { std::list> EnumPolyStrategySupportSolve(const StrategySupportProfile &support, bool &is_singular, bool &p_budgetExceeded, std::optional p_stopAfter, - size_t p_maxRectangles, - const CancelToken &p_cancel) + size_t p_maxRectangles, const CancelToken &p_cancel) { auto space = std::make_shared(support.MixedProfileLength() - support.GetGame()->NumPlayers()); @@ -120,9 +119,9 @@ EnumPolyStrategySupportSolve(const StrategySupportProfile &support, bool &is_sin is_singular = false; std::list> roots; try { - roots = solver.FindRoots({bottoms, tops}, - p_stopAfter.value_or(std::numeric_limits::max()), - p_maxRectangles, p_budgetExceeded, p_cancel); + roots = + solver.FindRoots({bottoms, tops}, p_stopAfter.value_or(std::numeric_limits::max()), + p_maxRectangles, p_budgetExceeded, p_cancel); } catch (const SingularMatrixException &) { is_singular = true; @@ -169,7 +168,8 @@ EnumPolyStrategySolve(const Game &p_game, std::optional p_stopAfter, dou for (auto solution : EnumPolyStrategySupportSolve( support, is_singular, budget_exceeded, p_stopAfter.has_value() - ? std::optional(p_stopAfter.value() - std::min(ret.size(), p_stopAfter.value())) + ? std::optional(p_stopAfter.value() - + std::min(ret.size(), p_stopAfter.value())) : std::nullopt, p_maxRectangles, p_cancel)) { const MixedStrategyProfile fullProfile = solution.ToFullSupport(); diff --git a/src/solvers/lcp/nfglcp.cc b/src/solvers/lcp/nfglcp.cc index 8744f0eb64..874bd8150c 100644 --- a/src/solvers/lcp/nfglcp.cc +++ b/src/solvers/lcp/nfglcp.cc @@ -217,7 +217,8 @@ NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p_tabl m_onEquilibrium(profile); p_solution.m_equilibria.push_back(profile); - if (m_stopAfter.has_value() && static_cast(p_solution.EquilibriumCount()) >= m_stopAfter.value()) { + if (m_stopAfter.has_value() && + static_cast(p_solution.EquilibriumCount()) >= m_stopAfter.value()) { return SearchResult::LimitReached; } @@ -303,9 +304,13 @@ LcpStrategySolve(const Game &p_game, std::optional p_stopAfter, int p_ma .Solve(p_game); } -template std::list> -LcpStrategySolve(const Game &, std::optional, int, StrategyCallbackType, const CancelToken &); -template std::list> -LcpStrategySolve(const Game &, std::optional, int, StrategyCallbackType, const CancelToken &); +template std::list> LcpStrategySolve(const Game &, + std::optional, int, + StrategyCallbackType, + const CancelToken &); +template std::list> LcpStrategySolve(const Game &, + std::optional, int, + StrategyCallbackType, + const CancelToken &); } // end namespace Gambit::Nash diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index d42154526f..9748ae3b36 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -174,8 +174,9 @@ int main(int argc, char *argv[]) maxregret = atof(optarg); break; case 'e': { - int val = atoi(optarg); - if (val > 0) { + char *endptr; + double val = std::strtod(optarg, &endptr); + if (*endptr == '\0' && val > 0 && val == static_cast(val)) { stopAfter = static_cast(val); } break; diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index 8eb944bda0..1a45e87a34 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -20,6 +20,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include #include #include #include @@ -89,8 +90,9 @@ int main(int argc, char *argv[]) printDetail = true; break; case 'e': { - int val = atoi(optarg); - if (val > 0) { + char *endptr; + double val = std::strtod(optarg, &endptr); + if (*endptr == '\0' && val > 0 && val == static_cast(val)) { stopAfter = static_cast(val); } break; diff --git a/tests/test_nash.py b/tests/test_nash.py index d4d476d74d..09db787aa0 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -3427,6 +3427,10 @@ def test_enumpoly_solve_error_with_invalid_stop_after(): gbt.nash.enumpoly_solve(game, stop_after=0) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.enumpoly_solve(game, stop_after=-1) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=0.5) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=1.2) def test_lcp_solve_error_with_invalid_stop_after(): @@ -3435,6 +3439,10 @@ def test_lcp_solve_error_with_invalid_stop_after(): gbt.nash.lcp_solve(game, stop_after=0) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.lcp_solve(game, stop_after=-1) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=0.5) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=1.2) def test_logit_solve_branch_error_with_invalid_maxregret(): From 44193355f66ab3481281e6e2943d7070d27b309f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:34:43 +0000 Subject: [PATCH 05/12] Add overflow guard for size_t cast and type-agnostic integer check Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/pygambit/nash.py | 4 ++-- src/tools/enumpoly/enumpoly.cc | 5 ++++- src/tools/lcp/lcp.cc | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index 54b85f8fec..fabe427390 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -246,7 +246,7 @@ def lcp_solve( raise ValueError( f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" ) - if stop_after is not None and isinstance(stop_after, float) and not stop_after.is_integer(): + if stop_after is not None and int(stop_after) != stop_after: raise ValueError( f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" ) @@ -718,7 +718,7 @@ def enumpoly_solve( f"enumpoly_solve(): " f"stop_after argument must be a positive integer; got {stop_after}" ) - if stop_after is not None and isinstance(stop_after, float) and not stop_after.is_integer(): + if stop_after is not None and int(stop_after) != stop_after: raise ValueError( f"enumpoly_solve(): " f"stop_after argument must be a positive integer; got {stop_after}" diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index 9748ae3b36..845b7524f1 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -20,10 +20,12 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include #include #include #include #include +#include #include #include "games.h" #include "solvers/enumpoly/enumpoly.h" @@ -176,7 +178,8 @@ int main(int argc, char *argv[]) case 'e': { char *endptr; double val = std::strtod(optarg, &endptr); - if (*endptr == '\0' && val > 0 && val == static_cast(val)) { + if (*endptr == '\0' && val > 0 && val == std::floor(val) && + val <= static_cast(std::numeric_limits::max())) { stopAfter = static_cast(val); } break; diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index 1a45e87a34..edaa416ad6 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -20,9 +20,11 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include #include #include #include +#include #include #include #include "games.h" @@ -92,7 +94,8 @@ int main(int argc, char *argv[]) case 'e': { char *endptr; double val = std::strtod(optarg, &endptr); - if (*endptr == '\0' && val > 0 && val == static_cast(val)) { + if (*endptr == '\0' && val > 0 && val == std::floor(val) && + val <= static_cast(std::numeric_limits::max())) { stopAfter = static_cast(val); } break; From cd33991d56d5de553905d83cd06f4dac2a1ddb64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:22:25 +0000 Subject: [PATCH 06/12] CLI tools: print error and exit(1) for non-positive/non-integer -e values Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/tools/enumpoly/enumpoly.cc | 14 +++++++++++--- src/tools/lcp/lcp.cc | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index 845b7524f1..6764a76145 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -178,10 +178,18 @@ int main(int argc, char *argv[]) case 'e': { char *endptr; double val = std::strtod(optarg, &endptr); - if (*endptr == '\0' && val > 0 && val == std::floor(val) && - val <= static_cast(std::numeric_limits::max())) { - stopAfter = static_cast(val); + if (*endptr != '\0' || val != std::floor(val) || + val > static_cast(std::numeric_limits::max())) { + std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." + << std::endl; + exit(1); } + if (val <= 0) { + std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." + << std::endl; + exit(1); + } + stopAfter = static_cast(val); break; } case 'r': diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index edaa416ad6..d4a2f9acf2 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -94,10 +94,18 @@ int main(int argc, char *argv[]) case 'e': { char *endptr; double val = std::strtod(optarg, &endptr); - if (*endptr == '\0' && val > 0 && val == std::floor(val) && - val <= static_cast(std::numeric_limits::max())) { - stopAfter = static_cast(val); + if (*endptr != '\0' || val != std::floor(val) || + val > static_cast(std::numeric_limits::max())) { + std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." + << std::endl; + exit(1); } + if (val <= 0) { + std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." + << std::endl; + exit(1); + } + stopAfter = static_cast(val); break; } case 'h': From b8e13bae06e5db040979bbda121c2e2aef39342b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:23:09 +0000 Subject: [PATCH 07/12] CLI tools: merge duplicate if-blocks for -e validation Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/tools/enumpoly/enumpoly.cc | 7 +------ src/tools/lcp/lcp.cc | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index 6764a76145..d5866798c6 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -178,17 +178,12 @@ int main(int argc, char *argv[]) case 'e': { char *endptr; double val = std::strtod(optarg, &endptr); - if (*endptr != '\0' || val != std::floor(val) || + if (*endptr != '\0' || val <= 0 || val != std::floor(val) || val > static_cast(std::numeric_limits::max())) { std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." << std::endl; exit(1); } - if (val <= 0) { - std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." - << std::endl; - exit(1); - } stopAfter = static_cast(val); break; } diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index d4a2f9acf2..7e70e26ec4 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -94,17 +94,12 @@ int main(int argc, char *argv[]) case 'e': { char *endptr; double val = std::strtod(optarg, &endptr); - if (*endptr != '\0' || val != std::floor(val) || + if (*endptr != '\0' || val <= 0 || val != std::floor(val) || val > static_cast(std::numeric_limits::max())) { std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." << std::endl; exit(1); } - if (val <= 0) { - std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." - << std::endl; - exit(1); - } stopAfter = static_cast(val); break; } From 358c02e822659de9ebfa27a617a4fc8db6ac1494 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:29:55 +0000 Subject: [PATCH 08/12] Validate CLI -e as integer and omit unset -e in GUI commands Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/gui/dlnash.cc | 24 ++++++++++++------------ src/tools/enumpoly/enumpoly.cc | 13 +++++++------ src/tools/lcp/lcp.cc | 13 +++++++------ 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 02f184d992..b5c56336f9 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -157,12 +157,12 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) return prefix + wxT("enummixed"); } else if constexpr (std::is_same_v) { - return prefix + - wxString::Format( - "enumpoly -d 10 -e %d -m %.17g", - method.stopAfter.has_value() ? static_cast(method.stopAfter.value()) : 0, - method.maxRegret) + - strategic; + wxString command = wxString::Format("enumpoly -d 10 -m %.17g", method.maxRegret); + if (method.stopAfter.has_value()) { + command += wxString::Format(" -e %llu", + static_cast(method.stopAfter.value())); + } + return prefix + command + strategic; } else if constexpr (std::is_same_v) { return prefix + wxString::Format("gnm -d 10 -n %d -m %.17g -c %d -f %d -i %d", @@ -177,12 +177,12 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) return prefix + wxT("lp") + strategic; } else if constexpr (std::is_same_v) { - return prefix + - wxString::Format( - "lcp -e %d -r %d", - method.stopAfter.has_value() ? static_cast(method.stopAfter.value()) : 0, - method.maxDepth) + - strategic; + wxString command = wxString::Format("lcp -r %d", method.maxDepth); + if (method.stopAfter.has_value()) { + command += wxString::Format(" -e %llu", + static_cast(method.stopAfter.value())); + } + return prefix + command + strategic; } else if constexpr (std::is_same_v) { return prefix + wxString::Format("liap -d 10 -n %d -i %d -m %.17g", diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index d5866798c6..e0b418a9cd 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -20,7 +20,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // -#include +#include #include #include #include @@ -176,15 +176,16 @@ int main(int argc, char *argv[]) maxregret = atof(optarg); break; case 'e': { - char *endptr; - double val = std::strtod(optarg, &endptr); - if (*endptr != '\0' || val <= 0 || val != std::floor(val) || - val > static_cast(std::numeric_limits::max())) { + size_t parsed; + const auto *begin = optarg; + const auto *end = optarg + std::char_traits::length(optarg); + const auto result = std::from_chars(begin, end, parsed); + if (result.ec != std::errc{} || result.ptr != end || parsed == 0) { std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." << std::endl; exit(1); } - stopAfter = static_cast(val); + stopAfter = parsed; break; } case 'r': diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index 7e70e26ec4..2738f8a7c7 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -20,7 +20,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // -#include +#include #include #include #include @@ -92,15 +92,16 @@ int main(int argc, char *argv[]) printDetail = true; break; case 'e': { - char *endptr; - double val = std::strtod(optarg, &endptr); - if (*endptr != '\0' || val <= 0 || val != std::floor(val) || - val > static_cast(std::numeric_limits::max())) { + size_t parsed; + const auto *begin = optarg; + const auto *end = optarg + std::char_traits::length(optarg); + const auto result = std::from_chars(begin, end, parsed); + if (result.ec != std::errc{} || result.ptr != end || parsed == 0) { std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." << std::endl; exit(1); } - stopAfter = static_cast(val); + stopAfter = parsed; break; } case 'h': From b0771874454eee08b94816939c6f75b99164ce7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:30:30 +0000 Subject: [PATCH 09/12] Use from_chars with strlen for CLI stop-after parsing Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/tools/enumpoly/enumpoly.cc | 3 ++- src/tools/lcp/lcp.cc | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc index e0b418a9cd..0539629ff7 100644 --- a/src/tools/enumpoly/enumpoly.cc +++ b/src/tools/enumpoly/enumpoly.cc @@ -21,6 +21,7 @@ // #include +#include #include #include #include @@ -178,7 +179,7 @@ int main(int argc, char *argv[]) case 'e': { size_t parsed; const auto *begin = optarg; - const auto *end = optarg + std::char_traits::length(optarg); + const auto *end = optarg + std::strlen(optarg); const auto result = std::from_chars(begin, end, parsed); if (result.ec != std::errc{} || result.ptr != end || parsed == 0) { std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc index 2738f8a7c7..37de3ed05e 100644 --- a/src/tools/lcp/lcp.cc +++ b/src/tools/lcp/lcp.cc @@ -21,6 +21,7 @@ // #include +#include #include #include #include @@ -94,7 +95,7 @@ int main(int argc, char *argv[]) case 'e': { size_t parsed; const auto *begin = optarg; - const auto *end = optarg + std::char_traits::length(optarg); + const auto *end = optarg + std::strlen(optarg); const auto result = std::from_chars(begin, end, parsed); if (result.ec != std::errc{} || result.ptr != end || parsed == 0) { std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'." From 1dd4dad4e4837a053970bf3824e24eebb4e0d1a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:50:59 +0000 Subject: [PATCH 10/12] Use size_t root limits in enumpoly solver Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/solvers/enumpoly/efgpoly.cc | 2 +- src/solvers/enumpoly/nfgpoly.cc | 2 +- src/solvers/enumpoly/polysolver.cc | 2 +- src/solvers/enumpoly/polysolver.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index d0eb89a117..d78225f1fa 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -201,7 +201,7 @@ std::list> SolveSupport(const BehaviorSupportProfil std::list> roots; try { roots = - solver.FindRoots({bottoms, tops}, p_stopAfter.value_or(std::numeric_limits::max()), + solver.FindRoots({bottoms, tops}, p_stopAfter.value_or(std::numeric_limits::max()), p_maxRectangles, p_budgetExceeded, p_cancel); } catch (const SingularMatrixException &) { diff --git a/src/solvers/enumpoly/nfgpoly.cc b/src/solvers/enumpoly/nfgpoly.cc index 93066a6c6e..6c99574392 100644 --- a/src/solvers/enumpoly/nfgpoly.cc +++ b/src/solvers/enumpoly/nfgpoly.cc @@ -120,7 +120,7 @@ EnumPolyStrategySupportSolve(const StrategySupportProfile &support, bool &is_sin std::list> roots; try { roots = - solver.FindRoots({bottoms, tops}, p_stopAfter.value_or(std::numeric_limits::max()), + solver.FindRoots({bottoms, tops}, p_stopAfter.value_or(std::numeric_limits::max()), p_maxRectangles, p_budgetExceeded, p_cancel); } catch (const SingularMatrixException &) { diff --git a/src/solvers/enumpoly/polysolver.cc b/src/solvers/enumpoly/polysolver.cc index 69983f44a0..8bd709ad58 100644 --- a/src/solvers/enumpoly/polysolver.cc +++ b/src/solvers/enumpoly/polysolver.cc @@ -181,7 +181,7 @@ Vector PolynomialSystemSolver::ImprovingNewtonStep(const Vector } std::list> PolynomialSystemSolver::FindRoots(const Rectangle &r, - const int max_roots, + const size_t max_roots, const size_t max_rectangles, bool &p_budgetExceeded, const CancelToken &p_cancel) diff --git a/src/solvers/enumpoly/polysolver.h b/src/solvers/enumpoly/polysolver.h index 7d1cda68b2..1e427a795c 100644 --- a/src/solvers/enumpoly/polysolver.h +++ b/src/solvers/enumpoly/polysolver.h @@ -105,7 +105,7 @@ class PolynomialSystemSolver { // Find up to `max_roots` roots inside rectangle `r`, examining at most // `max_rectangles` subdivisions. Sets `p_budgetExceeded` if that budget // runs out first, in which case the roots returned may be incomplete. - std::list> FindRoots(const Rectangle &r, int max_roots, + std::list> FindRoots(const Rectangle &r, size_t max_roots, size_t max_rectangles, bool &p_budgetExceeded, const CancelToken &p_cancel = CancelToken()); }; From 4db1513995bd5d86ce59eadd001d0a33eee25325 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:20:38 +0000 Subject: [PATCH 11/12] Reject infinite stop_after in pygambit Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/pygambit/nash.py | 9 +++++++-- tests/test_nash.py | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index fabe427390..ad0bb12f45 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -27,6 +27,7 @@ from __future__ import annotations import dataclasses +import math import pathlib from collections.abc import Iterator @@ -246,7 +247,9 @@ def lcp_solve( raise ValueError( f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" ) - if stop_after is not None and int(stop_after) != stop_after: + if stop_after is not None and ( + not math.isfinite(stop_after) or int(stop_after) != stop_after + ): raise ValueError( f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" ) @@ -718,7 +721,9 @@ def enumpoly_solve( f"enumpoly_solve(): " f"stop_after argument must be a positive integer; got {stop_after}" ) - if stop_after is not None and int(stop_after) != stop_after: + if stop_after is not None and ( + not math.isfinite(stop_after) or int(stop_after) != stop_after + ): raise ValueError( f"enumpoly_solve(): " f"stop_after argument must be a positive integer; got {stop_after}" diff --git a/tests/test_nash.py b/tests/test_nash.py index 09db787aa0..84ac275e03 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -3431,6 +3431,8 @@ def test_enumpoly_solve_error_with_invalid_stop_after(): gbt.nash.enumpoly_solve(game, stop_after=0.5) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.enumpoly_solve(game, stop_after=1.2) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=float("inf")) def test_lcp_solve_error_with_invalid_stop_after(): @@ -3443,6 +3445,8 @@ def test_lcp_solve_error_with_invalid_stop_after(): gbt.nash.lcp_solve(game, stop_after=0.5) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.lcp_solve(game, stop_after=1.2) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=float("inf")) def test_logit_solve_branch_error_with_invalid_maxregret(): From 43117452827fe2d549e87a1d2703d6484d12ec56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:18:26 +0000 Subject: [PATCH 12/12] Reject boolean stop_after in pygambit Co-authored-by: rahulsavani <743139+rahulsavani@users.noreply.github.com> --- src/pygambit/nash.py | 19 +++++++------------ tests/test_nash.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index ad0bb12f45..cf0205839f 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -27,9 +27,9 @@ from __future__ import annotations import dataclasses -import math import pathlib from collections.abc import Iterator +from numbers import Integral import pygambit.gambit as libgbt @@ -243,12 +243,10 @@ def lcp_solve( raise ValueError( "lcp_solve(): max_depth can only be used on the strategic representation" ) - if stop_after is not None and stop_after <= 0: - raise ValueError( - f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" - ) if stop_after is not None and ( - not math.isfinite(stop_after) or int(stop_after) != stop_after + isinstance(stop_after, bool) + or not isinstance(stop_after, Integral) + or stop_after <= 0 ): raise ValueError( f"lcp_solve(): stop_after argument must be a positive integer; got {stop_after}" @@ -716,13 +714,10 @@ def enumpoly_solve( ----- PHCpack is available at https://homepages.math.uic.edu/~jan/PHCpack/phcpack.html """ - if stop_after is not None and stop_after <= 0: - raise ValueError( - f"enumpoly_solve(): " - f"stop_after argument must be a positive integer; got {stop_after}" - ) if stop_after is not None and ( - not math.isfinite(stop_after) or int(stop_after) != stop_after + isinstance(stop_after, bool) + or not isinstance(stop_after, Integral) + or stop_after <= 0 ): raise ValueError( f"enumpoly_solve(): " diff --git a/tests/test_nash.py b/tests/test_nash.py index 84ac275e03..6f236acfa5 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -3427,6 +3427,12 @@ def test_enumpoly_solve_error_with_invalid_stop_after(): gbt.nash.enumpoly_solve(game, stop_after=0) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.enumpoly_solve(game, stop_after=-1) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=False) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=True) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.enumpoly_solve(game, stop_after=1.0) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.enumpoly_solve(game, stop_after=0.5) with pytest.raises(ValueError, match="must be a positive integer"): @@ -3441,6 +3447,12 @@ def test_lcp_solve_error_with_invalid_stop_after(): gbt.nash.lcp_solve(game, stop_after=0) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.lcp_solve(game, stop_after=-1) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=False) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=True) + with pytest.raises(ValueError, match="must be a positive integer"): + gbt.nash.lcp_solve(game, stop_after=1.0) with pytest.raises(ValueError, match="must be a positive integer"): gbt.nash.lcp_solve(game, stop_after=0.5) with pytest.raises(ValueError, match="must be a positive integer"):