[snapshot-regression-fix] Fix psmt infinite loop on theory-incomplete cubes (#3044)#9983
Merged
NikolajBjorner merged 1 commit intoJun 28, 2026
Merged
Conversation
The rewritten parallel tactic (psmt) hung indefinitely on benchmarks whose root cube is genuinely undetermined by an incomplete theory and cannot be refined further (e.g. `(distinct a b)` over `Int -> Bool` arrays, issue #3044). The worker `run()` loop treated every `l_undef` cube result as "conflict-limited": it escalated the per-thread conflict budget and re-checked / re-split the same cube. When the undef came from theory incompleteness rather than the conflict limit, the verdict never changed, so the worker re-checked the same cube forever. The batch manager had no terminal `unknown` state, so the only escape was another worker proving sat/unsat, which is impossible for a root-level theory-incomplete formula. The result was a wall-clock timeout instead of the expected `unknown`. Fix: distinguish genuine incompleteness from conflict-limit exhaustion using the per-cube `reason_unknown()`. Only `"max-conflicts-reached"` benefits from escalating/splitting; for any other reason the worker now records a sound `unknown` (new `is_unknown` batch-manager state) and stops working the branch. A definitive sat/unsat verdict from another branch may still override this soft `unknown`. The unknown reason is propagated to the goal and echoed (as the sequential path does), so the command output is restored to the expected `(incomplete (theory array))` / `unknown`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Summary
Fixes a
psmt(parallel SMT tactic) regression where the solver hangs to a wall-clock timeout instead of returningunknownon formulas whose root cube is genuinely undetermined by an incomplete theory.iss-3044/bug-1.smt2(from Z3 issue #3044)Divergence
The recorded oracle (expected) vs. current z3 (combined stdout+stderr,
-T:20):Root cause
The rewritten parallel tactic (
src/solver/parallel_tactical.cpp, introduced in #9824/#9825) hangs on this input.In the worker
run()loop, everyl_undefcube result was treated as if the per-cube conflict limit had been reached: the worker escalated the per-thread conflict budget (update_max_thread_conflicts) and re-checked / re-split the same cube. When thel_undefactually comes from theory incompleteness (here, the array theory cannot decide(distinct a b)overInt -> Bool) rather than the conflict limit, the verdict never changes, so the worker re-checks the same cube forever.Compounding this, the
batch_managerstate machine had no terminalunknownstate — the only way to finish was for some worker to provesat/unsat, which is impossible for a root-level theory-incomplete formula. The combination produced an infinite loop and a wall-clock timeout.The pre-rewrite parallel tactic avoided this: its
giveup()detected reasons starting with(incomplete/(sat.giveup, reported a soft undef, and echoed the reason toverbose_stream().Fix
All changes are confined to
src/solver/parallel_tactical.cpp(47 insertions, 4 deletions):l_undefcase, onlyreason_unknown() == "max-conflicts-reached"benefits from escalating the budget / splitting. For any other reason (incomplete theory, quantifiers, lambdas, resource limits, ...) re-checking is futile, so the worker records a soundunknownand stops working the branch.is_unknownbatch-manager state (set_unknown,get_result() -> l_undef, reason storage). It is a soft result: it does not cancel the other workers, and a definitivesat/unsatverdict from another branch may still override it (theset_sat/set_unsatguards now permit overridingis_unknown). Allset_unsatcall sites are global formula-unsat (core ⊆ assumptions, or independent of the tested backbone literal), so the override is sound; tree-closure unsat remains guarded byis_runningand cannot fire because the undef leaf stays open.reason_unknownis propagated to the result goal and echoed toverbose_stream(), reproducing the(incomplete (theory array))line that the sequential path / old parallel tactic emitted.Validation
Rebuilt the
./z3checkout (./configure && make -C build -j16) and re-ran the benchmark with the freshly built binary using the same options the snapshot capture uses (-T:20, combined stdout+stderr):This matches the recorded
bug-1.expected.outoracle byte-for-byte, and the benchmark now completes in ~0.5s (was: timeout). Verified stable across 8 consecutive runs. Basicpsmtsat/unsatchecks continue to produce correct results.Opened as a draft for human review.
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com