Don't delete the constraints that the solver couldn't satisfy - #2
Don't delete the constraints that the solver couldn't satisfy#2BoykoNeov wants to merge 2 commits into
Conversation
PruneOrphans() and PruneRequestsAndConstraints() tag the requests and constraints that they mean to delete and then call RemoveTagged(), which removes every element whose tag is nonzero -- but neither of them clears the tags first. System::Solve() tags the constraints whose equations it couldn't satisfy, so that they can be listed for the user, and so the regeneration that immediately follows a failed solve deleted exactly those constraints: fail to solve a sketch once, and the constraints that failed are gone, along with any chance of correcting the value that caused it. Clear the tags before setting them. The request lists get the same treatment, not because anything tags a request today, but because they are pruned by the same tag-then-RemoveTagged() protocol, and would lose requests the same way if anything ever did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This fixes the bug but causes a memory leak |
Group::Clear() frees every other dynamically-allocated member of a group, but not solved.remove -- the list that System::Solve() fills with the constraints whose equations it couldn't satisfy, and that FindWhichToRemoveToFixJacobian() fills with the ones it suggests removing to fix a redundant system. SolveGroup() empties that list before every solve, so it never grows during a session, but whatever is in it when the sketch is closed, the group is deleted, or the process exits is leaked. Nothing aliases the list: the undo stack zeroes solved when it shallow copies a group, and SolveGroup() clears the list directly rather than through Group::Clear(), so the report shown in the text window is unaffected. The leak is as old as the list, but it needs a solve to fail before there is anything to leak, and until the preceding commit's test nothing in the suite made one fail -- so the sanitizer build had nothing to find. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Written by Claude Opus 5 — both this text and the code it describes; posted by @BoykoNeov. @ruevs Fixed — pushed Short version: the leak is not caused by clearing the tags. Full write-up, including the answer to @phkahler's "did 3.0 or 3.1 delete the constraints too?" (they did not — the regression came in after v3.1 and first shipped in v3.2), is on the upstream PR: solvespace#1744 (comment) |
Fixes a data-loss bug that is independent of any particular sketch: when a solve fails, SolveSpace deletes the constraints that failed.
I found this while working on solvespace#1247 — the constraint count dropped from 7 to 5 after a failed solve, and a later
FindByIdon one of the missing handles asserted. It is not specific to that model or to that issue; any group that fails to solve loses the constraints the solver couldn't satisfy, and with them the value the user needs to edit to recover.Root cause
PruneOrphans()andPruneRequestsAndConstraints()(src/generate.cpp) both follow the same protocol: walk the list, settag = 1on the elements to delete, then callRemoveTagged(), which removes every element whose tag is nonzero. Neither of them clears the tags first, so they delete their own selection plus whatever happened to be tagged already.System::Solve()tags exactly the constraints whose equations it couldn't satisfy — that is itsdidnt_converge:path, where the tag is used to avoid listing the same constraint twice while building the report. A failed solve is immediately followed by a regeneration, the regeneration prunes, and the prune sees those leftover tags as delete-me marks.So the constraints that fail are precisely the constraints that get destroyed.
Why the fix is safe
SK.request.ClearTags()/SK.constraint.ClearTags()before each tagging loop. Two things I checked rather than assumed:generate.cpp:227and:265, and neither sets tags beforehand.System::Solve()builds the list; what the UI actually shows comes from thebadlist, which this doesn't touch.The request lists get the same treatment. Nothing tags a request today, so that half is a no-op — but they are pruned by the identical tag-then-
RemoveTagged()protocol and would lose requests the same way if anything ever did.Regression test
test/core/prune/failed_solve_keeps_constraints— load a right triangle with an angle dimension, set that angle to 95°, which no triangle of this shape has (the direction cosine is100/sqrt(100² + h²), positive for everyh, andcos 95°is not), so the solver gives up and tags. The test then asserts the request and constraint counts are unchanged and that the angle constraint is still there with its value intact — a bare count would also pass if the wrong constraint vanished and another appeared.Without the fix it fails on the count; with it, it passes.
Verification
Notes for review
test/core/prune/angle.slvsis byte-identical to the fixture used by my SolveSpace fails to solve solvable constraints solvespace/solvespace#1247 PR. They are separate PRs on purpose — this fix is four lines ingenerate.cppand has nothing to do with the Newton step — so each carries its own copy rather than one depending on the other. If both are taken, the only conflict is one line intest/CMakeLists.txt; with both entries kept, the combination builds and passes (265 / 943, verified by a trial merge). Deduplicating the fixture afterwards would be a fine follow-up.🤖 Generated with Claude Code