Fix fast-click auto-mode crash: overlapping runs + proof breaking proof listeners#3880
Merged
Conversation
Starting auto mode or a proof macro while a previous run on the same proof is still winding down runs two provers on one Proof concurrently and corrupts shared proof state (most easily triggered by rapidly clicking the auto-mode toolbar button). The symptom is a stray exception on a SwingWorker thread, e.g. a NullPointerException in NamespaceSet.copy from Goal.adaptNamespacesNewGoals, an "Expected an empty leaf node" assertion, or a null SemisequentTacletAppIndex.succIndex. MediatorProofControl guarded re-entry only via the mediator's inAutoMode flag, which ProofMacroWorker.done() clears -- but SwingWorker.done() fires on cancellation while the cooperative doInBackground is still applying rules, so the flag drops mid-winddown and the next click starts an overlapping run. Add an explicit AtomicBoolean guard in MediatorProofControl that startAutoMode/runMacro claim on entry and that is released only when the worker's background task has truly finished (doInBackground finally, with a backstop for the cancelled-before-start case). Also harden the analogous guard in DefaultProofControl (volatile autoModeThread, synchronized runMacro); it already released at true completion and the CLI runs blocking, so that is defence in depth.
unp1
marked this pull request as ready for review
July 1, 2026 15:36
unp1
enabled auto-merge
July 1, 2026 15:37
unp1
disabled auto-merge
July 1, 2026 16:31
unp1
enabled auto-merge
July 1, 2026 16:31
unp1
marked this pull request as draft
July 1, 2026 17:53
auto-merge was automatically disabled
July 1, 2026 17:53
Pull request was converted to draft
unp1
force-pushed
the
bubel/automode-reentrancy-fix
branch
from
July 1, 2026 19:10
4a5a0ff to
4647278
Compare
unp1
marked this pull request as ready for review
July 1, 2026 19:16
DependencyTracker (and every other RuleAppListener / ProofTreeListener) is notified from within a running rule application -- e.g. Proof.fireRuleApplied is called from Goal.apply, after the proof tree and namespaces have been updated for the step. A listener that threw there aborted the proof search mid-application and left the proof inconsistent (surfacing downstream as a NullPointerException in Goal.adaptNamespacesNewGoals). Guarding each listener individually does not scale to current and future listeners. Instead isolate listeners at the firing site: Proof.notifyListeners wraps every callback, logs a listener that throws and unregisters it, then keeps notifying the rest. fireRuleApplied and the fireProof* dispatches all go through it, so no observer can corrupt the proof. DependencyTracker keeps a little self-hygiene: on a tracking failure it records that its graph became incomplete (analyze() then returns no results) and rethrows, letting Proof log and drop it. Add ProofListenerIsolationTest (a throwing RuleAppListener neither stops the proof from closing nor keeps firing); verified it fails without the guard. This is the second, independent layer of the fast-click crash; layer one is the re-entrancy guard in this PR. Created with AI tooling support
unp1
force-pushed
the
bubel/automode-reentrancy-fix
branch
from
July 1, 2026 19:28
4647278 to
41b732f
Compare
unp1
enabled auto-merge
July 1, 2026 19:35
wadoon
approved these changes
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intended Change
Rapidly clicking the auto-mode / apply-strategy toolbar button can crash the proof with a stray
exception on a
SwingWorkerthread. The crash turned out to be layered — two independentcauses, both fixed here.
Layer 1 — overlapping auto-mode/macro runs
Starting the automatic proof search (auto mode or a proof macro) while a previous run on the same
proof is still winding down runs two provers on one
Proofconcurrently and corrupts shared proofstate. The symptom is a stray exception on a
SwingWorkerthread — e.g.NullPointerExceptioninNamespaceSet.copy(fromGoal.adaptNamespacesNewGoals),AssertionFailure: Expected an empty leaf node, or a nullSemisequentTacletAppIndex.succIndex.Root cause:
MediatorProofControlguards re-entry only through the mediator'sinAutoModeflag,which
ProofMacroWorker.done()clears. ButSwingWorker.done()fires on cancellation, while thecooperative
doInBackgroundis still applying rules — so the flag drops while the run is stillwinding down and the next click starts a second, overlapping run.
This PR adds an explicit re-entrancy guard (
AtomicBoolean autoModeActive) inMediatorProofControl:startAutoMode/runMacroclaim it on entry and it is released only whenthe worker's background task has truly finished (
doInBackground'sfinally, with a backstop forthe cancelled-before-start case). A new run is refused while one is active or winding down, so two
runs can never prove the same proof at once. It also hardens the analogous guard in
DefaultProofControl(volatileautoModeThread, synchronizedrunMacro); that control alreadyreleased at true completion and the CLI runs blocking, so this is defence-in-depth only.
Layer 2 — a listener must not be able to break the proof
Proof
notifies allRuleAppListeners andProofTreeListeners with bare dispatch loops, so *any* observer (now or in the future) could break the proof. Rather than have each listener guard itself, this PR isolates them at the firing site: a newProof.notifyListenershelper wraps every callback — a listener that throws is logged and **unregistered**, and the remaining listeners are still notified.fireRuleAppliedand thefireProof*` dispatches all route through it, so no listener can corrupt the proof.The problem appeared in this case (but it is not specific to the
DependencyTracker) with the slicing extension's always track setting on,DependencyTrackeris registered as aRuleAppListener, so itsruleApplied(..)runs insideGoal.apply(viaProof.fireRuleApplied), after the proof tree and namespaces have already been updated for the step.When tracking failed there — e.g.
IllegalStateException: found formula that was not produced by any rule!on a loop-scope invariant while auto mode re-applied the loop-scope rule — the exceptionpropagated out of
Goal.apply, aborting the run mid-application and leaving the proof inconsistent.That broken state is what then produced the
NullPointerExceptioninGoal.adaptNamespacesNewGoalson the next step.
DependencyTrackeradditionally records, on a tracking failure, that its dependency graph becameincomplete, so a later
analyze()returns no slice rather than one built from a partial graph.Type of pull request
Ensuring quality
corrupt it reliably; interrupting a single run and waiting does not), confirmed that the GUI
genuinely runs two auto-mode workers on one proof when fast-clicking, and verified that the guard
prevents the overlap. Final check: fast-clicking the auto button in the GUI no longer crashes.
ProofListenerIsolationTest— aRuleAppListenerthat always throws neitherstops the proof from closing nor keeps firing (it is unregistered after its first failure).
Verified it fails without the guard.
atomic CAS per run start; listener isolation only wraps the existing dispatch in a try/catch — no
effect on the proof-search hot path.
The contributions within this pull request are licensed under GPLv2 (only) for inclusion in KeY.
Additional Information and contact(s)
Created with AI tooling support
The contributions within this pull request are licensed under GPLv2 (only) for inclusion in KeY.