Skip to content

Guard against infinite recursion when NonlinearProblem(prob) makes no progress - #1327

Closed
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix/steadystate-nothing-recursion-guard
Closed

ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix/steadystate-nothing-recursion-guard

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Please ignore until reviewed by @ChrisRackauckas.

Context

This is a companion to a review finding on SciML/SciMLBase.jl#1614. The original bug report was GROUP=SymbolicIndexingInterface red on SciMLBase.jl (CI run https://github.com/SciML/SciMLBase.jl/actions/runs/35839187919): init(ssprob, DynamicSS(Tsit5())) on an MTK-lowered SCC steady-state problem throws FieldError: type SCCNonlinearProblem has no field u0. A reviewer of the first fix attempt (SciMLBase PR #1614, which added NonlinearProblem(prob::SCCNonlinearProblem) = prob) found that identity alone turns that FieldError into an infinite recursion / StackOverflowError on this repo's src/default.jl:59-62:

function SciMLBase.__init(prob::SciMLBase.AbstractSteadyStateProblem, ::Nothing, args...; kwargs...)
    nlprob = SciMLBase.NonlinearProblem(prob)
    return SciMLBase.__init(nlprob, nothing, args...; kwargs...)
end

AbstractSteadyStateProblem is a type alias for AbstractNonlinearProblem, so this method also matches an SCCNonlinearProblem. First call: NonlinearProblem(ssprob) returns the stored SCCNonlinearProblem — correct, and already tested. Second call (recursing on that result): NonlinearProblem(scc) returns scc itself (once SciMLBase's identity ships) — and the method recurses on the same object forever.

The actual CI failure is fixed at its real cause in SciML/SteadyStateDiffEq.jl#173 (adding DynamicSS/SICNM-specific __init there means this default-algorithm path is never reached for that case — confirmed by testing that SteadyStateDiffEq's fix alone, with today's unmodified SciMLBase and this repo, already resolves the reported failure). This PR is defense-in-depth for the general case: any algorithm without its own __init/__solve for a SteadyStateProblem, or a bare init(ssprob)/solve(ssprob) with no algorithm at all, still falls through to this converter.

What changed

Guard both __init and __solve for AbstractSteadyStateProblem with ::Nothing: if NonlinearProblem(prob) returns prob unchanged (no progress was made toward an actual NonlinearProblem), raise a clear ArgumentError instead of recursing.

Verification

This repo's own test suite passes unchanged (GROUP=Core, tail):

     Testing NonlinearSolve tests passed

The guard's nlprob === prob branch is unreachable against the currently-released SciMLBase: NonlinearProblem(::SCCNonlinearProblem) still throws a FieldError one line earlier there, so this PR is a no-op change today (confirmed: adding a @test_throws ArgumentError regression test to test/Core/default_alg_tests__item1.jl and running GROUP=Core against the released SciMLBase reproduces the pre-existing FieldError, not the new ArgumentError — so that test was not committed; it cannot pass until SciMLBase/SciMLBase.jl#1614 merges and releases). Instead, verified by Pkg.develop-ing this branch together with a local checkout of SciMLBase.jl#1614 (Pkg.develop on both) and running:

using SciMLBase, NonlinearSolve
inner_prob = NonlinearProblem((du, u, p) -> (du .= u .^ 2 .- p), [1.0], 4.0)
sccprob = SciMLBase.SCCNonlinearProblem((inner_prob,), (Returns(nothing),))
ode_f = ODEFunction((du, u, p, t) -> (du .= -u .+ p))
ssprob = SteadyStateProblem(ode_f, [1.0], 4.0; lowered_problem = sccprob)
init(ssprob)   # no algorithm at all

Before this PR (SciMLBase's identity + unmodified NonlinearSolve):

Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
ERROR: LoadError: StackOverflowError:

After this PR (SciMLBase's identity + this guard):

ERROR: LoadError: ArgumentError: No default nonlinear algorithm exists for `SCCNonlinearProblem` here: `SciMLBase.NonlinearProblem` on it returns the problem itself rather than a plain `NonlinearProblem` (e.g. an `SCCNonlinearProblem` lowering), which this default-algorithm conversion cannot solve. Specify an algorithm explicitly.

What I did not verify / anything to push back on

  • No regression test is committed here (see above — it cannot pass until the SciMLBase companion PR merges and releases; a follow-up PR bumping this repo's SciMLBase compat bound and adding the @test_throws ArgumentError test would be the natural next step once that happens).
  • Did not run the full GROUP=Everything/QA suites, only GROUP=Core.
  • This PR has no effect by itself; it only matters paired with Raise a clear ArgumentError converting an SCCNonlinearProblem to NonlinearProblem SciMLBase.jl#1614. Reviewer's call on whether to merge ahead of time (harmless, forward-compatible) or wait.

Runic --check and typos are clean on the changed file (Runic reformatted the throw(ArgumentError(...)) call).

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01NB3oFTNzGW79UtDt8AyjsM

… progress

SciMLBase.__init/__solve for an AbstractSteadyStateProblem with no algorithm
convert via SciMLBase.NonlinearProblem(prob) and recurse on the result. That
conversion returns some inputs verbatim (a stored NonlinearProblem or
LinearProblem lowering already tested elsewhere, or - once
SciMLBase.NonlinearProblem(::SCCNonlinearProblem) is an identity, see
SciML/SciMLBase.jl#1614 - an SCCNonlinearProblem, which has no u0 field to
rebuild from). Recursing when nlprob === prob makes no progress and previously
stack-overflowed instead of raising a clear error.

This guard is a no-op against the currently released SciMLBase, where
NonlinearProblem(::SCCNonlinearProblem) still throws a FieldError one line
earlier; it only matters once the SciMLBase companion PR ships. See that PR's
body for a full fail-before/pass-after trace using a locally `Pkg.develop`'d
SciMLBase.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251 (subagent)
Agent-Model: claude-sonnet
Agent-Session: https://claude.ai/code/session_01NB3oFTNzGW79UtDt8AyjsM
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Closing: SciMLBase.jl#1614 was changed to have NonlinearProblem(::SCCNonlinearProblem) raise an ArgumentError instead of returning the same object unchanged. With SciMLBase.NonlinearProblem(prob) never returning prob itself, the nlprob === prob recursion this PR guarded against cannot occur, so the guard is unnecessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants