Skip to content

Prevent recursive solver factor cycles - #1966

Merged
ChrisRackauckas merged 1 commit into
JuliaSymbolics:masterfrom
ChrisRackauckas-Claude:fix/solver-factor-unit-recursion
Aug 26, 2026
Merged

Prevent recursive solver factor cycles#1966
ChrisRackauckas merged 1 commit into
JuliaSymbolics:masterfrom
ChrisRackauckas-Claude:fix/solver-factor-unit-recursion

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Please ignore this PR until it has been reviewed by @ChrisRackauckas.

What changed and why

solve_univar treated a single Nemo factor as new work whenever the factor was not structurally equal to the input polynomial. Nemo returns a separate factorization unit, so an irreducible polynomial can be represented as u = -1 plus the negated polynomial; recursively factoring that result can alternate between p and -p until a stack overflow.

This PR reconstructs the single-factor result with Nemo's unit before deciding that factorization made progress. It adds a public-API regression for an irreducible degree-5 symbolic polynomial and verifies that symbolic_solve returns RootsOf rather than recursing.

Failing before

I ran the new test against unfixed master with a temporary recursion-depth diagnostic guard (not included in this PR), which turns the otherwise unbounded recursion into a prompt error:

$ julia +1.12 --startup-file=no --project=../solver-instrument-env -e 'using Test, Symbolics; using Groebner, Nemo; @variables x a b c d; @testset "Higher degree univar" begin; expr=-b+a*x+c*x-d*x^2+b*x^3-a*x^4-x^5; root=only(symbolic_solve(expr,x)); @test Symbolics.operation(root)===Symbolics.RootsOf; @test isequal(Symbolics.arguments(root),[Symbolics.unwrap(expr),Symbolics.unwrap(x)]); end'
Higher degree univar: Error During Test
  solve_univar recursion at depth 51 for b - a*x - c*x + d*(x^2) - b*(x^3) + a*(x^4) + x^5
  solve_univar(...) (repeats 51 times)
Test Summary:        | Error  Total     Time
Higher degree univar |     1      1  1m10.5s
ERROR: Some tests did not pass: 0 passed, 0 failed, 1 errored, 0 broken.

The factor trace on unfixed code alternates deterministically:

1 u=-1//1 next=b - a*x - c*x + d*(x^2) - b*(x^3) + a*(x^4) + x^5 equal=false reconstructed=true
2 u=-1//1 next=-b + a*x + c*x - d*(x^2) + b*(x^3) - a*(x^4) - (x^5) equal=false reconstructed=true
3 u=-1//1 next=b - a*x - c*x + d*(x^2) - b*(x^3) + a*(x^4) + x^5 equal=false reconstructed=true

Passing after

The identical test against this branch passes:

$ julia +1.12 --startup-file=no --project=../solver-stack-env -e 'using Test, Symbolics; using Groebner, Nemo; @variables x a b c d; @testset "Higher degree univar" begin; expr=-b+a*x+c*x-d*x^2+b*x^3-a*x^4-x^5; root=only(symbolic_solve(expr,x)); @test Symbolics.operation(root)===Symbolics.RootsOf; @test isequal(Symbolics.arguments(root),[Symbolics.unwrap(expr),Symbolics.unwrap(x)]); end'
Test Summary:        | Pass  Total     Time
Higher degree univar |    2      2  1m08.2s

The complete solver file passes, including the regression:

$ julia +1.12 --startup-file=no --project=../solver-stack-env test/solver.jl
Test Summary:        | Pass  Total  Time
Higher degree univar |    2      2  1.2s
Test Summary:         | Pass  Broken  Total   Time
Isolate/Attract solve |   32       1     33  29.0s

The full Core group reached 17,087 passes. The sole error is the independently reproduced PreallocationTools.DiffCache.any_du failure already fixed in the separate draft PR linked below; the RootFinding solver group is clean.

$ GROUP=Core julia +1.12 --startup-file=no --project=../solver-stack-env -e 'using Pkg; Pkg.test("Symbolics")'
Test Summary:                            |  Pass  Error  Broken  Total      Time
test set                                 | 17087      1     357  17445  16m54.5s
  Nested ForwardDiff Sparsity Test       |            1              1      2.4s
  RootFinding solver                     |   169              5    174   4m52.0s

Additional local checks:

$ GROUP=QA julia +1.12 --startup-file=no --project=../solver-stack-env -e 'using Pkg; Pkg.test("Symbolics")'
Testing Symbolics tests passed

$ julia +1.12 --startup-file=no --project=../formatter-env -m Runic --check src/solver/main.jl test/solver.jl
$ typos src/solver/main.jl test/solver.jl
$ git diff --check

The last three commands exited successfully without output.

Regression boundary

The recursive single-factor condition was introduced by the Symbolics commit linked below. Its parent returned the reduced polynomial as RootsOf in 13.912763 seconds. The dependency behavior that exposes the latent bug begins at the tested Nemo version boundary: Nemo 0.47.5 terminates, while 0.48.0, 0.48.1, 0.49.5, 0.52.4, 0.56.0, and 0.56.1 return alternating unit/factor representations for this polynomial. This is valid factorization behavior; consuming Nemo's unit in Symbolics is the scoped fix.

Not verified

Docs were not built because this changes no docstring, rendered documentation, or public API. GPU, downstream, and allowed-to-fail jobs were not run locally. No dependency or license changes are included.

Reviewer focus: is_unfactored reconstructs only a single factor with Nemo's unit for the progress check. Multi-factor root handling is unchanged.

Links

🤖 Generated with Claude Code
https://chatgpt.com/codex/tasks/01a03a04-70ae-77a0-ba1b-ec7a1ed9ef47

Use Nemo's factorization unit when deciding whether a polynomial is already unfactored, so unit-only sign changes do not recurse indefinitely.

Add a higher-degree symbolic polynomial regression that returns RootsOf.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://chatgpt.com/codex/tasks/01a03a04-70ae-77a0-ba1b-ec7a1ed9ef47
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Investigation and verification checklist:

  • Reduce the recursive factor cycle to a public symbolic_solve reproducer.
  • Capture failing-before evidence with a temporary diagnostic depth guard.
  • Consume Nemo's factorization unit in the factorization progress check.
  • Run the identical regression after the fix (2/2 pass).
  • Run test/solver.jl, the full Core group, QA, Runic, typos, and git diff --check.
  • Identify the Symbolics source boundary and tested Nemo behavior boundary.
  • Await review by @ChrisRackauckas and CI results.

@codecov-commenter

codecov-commenter commented Aug 25, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 0% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 19.29%. Comparing base (acacfd2) to head (2297339).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
src/solver/main.jl 0.00% 3 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1966      +/-   ##
==========================================
- Coverage   23.42%   19.29%   -4.14%     
==========================================
  Files          61       61              
  Lines        5997     5987      -10     
==========================================
- Hits         1405     1155     -250     
- Misses       4592     4832     +240     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review August 26, 2026 07:03
@ChrisRackauckas
ChrisRackauckas merged commit eb1c9e7 into JuliaSymbolics:master Aug 26, 2026
19 of 24 checks passed
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.

3 participants