3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-15 11:35:42 +00:00
z3/src/solver
Lev Nachmanson e87aaa6924
[snapshot-regression-fix] Fix psmt infinite loop on theory-incomplete cubes (#3044) (#9983)
## Summary

Fixes a `psmt` (parallel SMT tactic) regression where the solver hangs
to a wall-clock timeout instead of returning `unknown` on formulas whose
root cube is genuinely undetermined by an incomplete theory.

- **Originating discussion:**
https://github.com/Z3Prover/bench/discussions/2735
- **Benchmark:** `iss-3044/bug-1.smt2` (from [Z3 issue
#3044](https://github.com/Z3Prover/z3/issues/3044))

```smt2
(declare-fun a (Int) Bool)
(declare-fun b (Int) Bool)
(assert (distinct a b))
(check-sat-using psmt)
```

## Divergence

The recorded oracle (expected) vs. current z3 (combined stdout+stderr,
`-T:20`):

```diff
-(incomplete (theory array))
-unknown
+timeout
```

## Root cause

The rewritten parallel tactic (`src/solver/parallel_tactical.cpp`,
introduced in #9824/#9825) hangs on this input.

In the worker `run()` loop, every `l_undef` cube 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 the `l_undef` actually
comes from **theory incompleteness** (here, the array theory cannot
decide `(distinct a b)` over `Int -> Bool`) rather than the conflict
limit, the verdict never changes, so the worker re-checks the same cube
forever.

Compounding this, the `batch_manager` state machine had **no terminal
`unknown` state** — the only way to finish was for some worker to prove
`sat`/`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 to `verbose_stream()`.

## Fix

All changes are confined to `src/solver/parallel_tactical.cpp` (47
insertions, 4 deletions):

1. **Distinguish genuine incompleteness from conflict-limit
exhaustion.** In the worker `l_undef` case, only `reason_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 sound `unknown` and stops working the branch.
2. **Add a terminal `is_unknown` batch-manager state** (`set_unknown`,
`get_result() -> l_undef`, reason storage). It is a *soft* result: it
does not cancel the other workers, and a definitive `sat`/`unsat`
verdict from another branch may still override it (the
`set_sat`/`set_unsat` guards now permit overriding `is_unknown`). All
`set_unsat` call sites are global formula-unsat (core ⊆ assumptions, or
independent of the tested backbone literal), so the override is sound;
tree-closure unsat remains guarded by `is_running` and cannot fire
because the undef leaf stays open.
3. **Restore the reason output.** The captured `reason_unknown` is
propagated to the result goal and echoed to `verbose_stream()`,
reproducing the `(incomplete (theory array))` line that the sequential
path / old parallel tactic emitted.

## Validation

Rebuilt the `./z3` checkout (`./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):

```
$ z3 inputs/issues/iss-3044/bug-1.smt2 -T:20
(incomplete (theory array))
unknown
```

This matches the recorded `bug-1.expected.out` oracle **byte-for-byte**,
and the benchmark now completes in ~0.5s (was: timeout). Verified stable
across 8 consecutive runs. Basic `psmt` `sat`/`unsat` checks continue to
produce correct results.

Opened as a **draft** for human review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>




> Generated by [Fix a Z3 snapshot-regression
divergence](https://github.com/Z3Prover/bench/actions/runs/28313246856)
· 5.7K AIC · ⌖ 85.8 AIC · ⊞ 41.2K ·
[◷](https://github.com/search?q=repo%3AZ3Prover%2Fz3+%22gh-aw-workflow-id%3A+snapshot-regression-fixer%22&type=pullrequests)

<!-- gh-aw-agentic-workflow: Fix a Z3 snapshot-regression divergence,
engine: copilot, version: 1.0.60, model: claude-opus-4.8, id:
28313246856, workflow_id: snapshot-regression-fixer, run:
https://github.com/Z3Prover/bench/actions/runs/28313246856 -->

<!-- gh-aw-workflow-id: snapshot-regression-fixer -->
<!-- gh-aw-workflow-call-id: Z3Prover/bench/snapshot-regression-fixer
-->

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-28 11:20:32 -06:00
..
assertions Use "override" keyword where needed. (#9892) 2026-06-18 13:36:14 -06:00
check_logic.cpp Standardize for-loop increments to prefix form (++i) (#8199) 2026-02-18 20:57:29 -08:00
check_logic.h debug arith/mbi 2020-11-02 12:13:19 -08:00
check_sat_result.cpp refactor solver to include settable stats 2026-06-07 14:17:38 -07:00
check_sat_result.h refactor solver to include settable stats 2026-06-07 14:17:38 -07:00
CMakeLists.txt Parallel tactic (#9824) (#9825) 2026-06-26 10:36:15 -06:00
combined_solver.cpp refactor solver to include settable stats 2026-06-07 14:17:38 -07:00
combined_solver.h
combined_solver_params.pyg
mus.cpp Remove copies (#8583) 2026-02-18 21:02:22 -08:00
mus.h
parallel_params.pyg Parallel tactic (#9824) (#9825) 2026-06-26 10:36:15 -06:00
parallel_tactical.cpp [snapshot-regression-fix] Fix psmt infinite loop on theory-incomplete cubes (#3044) (#9983) 2026-06-28 11:20:32 -06:00
parallel_tactical.h Parallel tactic (#9824) (#9825) 2026-06-26 10:36:15 -06:00
preferred_value_propagator.h Remove redundant non-virtual destructors with = default (#8462) 2026-02-18 20:58:01 -08:00
progress_callback.h Use = default for virtual constructors. 2022-08-05 18:11:46 +03:00
simplifier_solver.cpp refactor solver to include settable stats 2026-06-07 14:17:38 -07:00
simplifier_solver.h Add simplification customization for SMTLIB2 2023-01-30 22:38:51 -08:00
slice_solver.cpp refactor solver to include settable stats 2026-06-07 14:17:38 -07:00
slice_solver.h A slice solver option for interactive use case 2024-10-08 09:24:52 -07:00
smt_logics.cpp benchmark patching 2026-05-20 13:32:23 -07:00
smt_logics.h SMT2 front-end: accept HO_ALL and normalize curried expression-head applications (#9636) 2026-05-26 18:39:38 -07:00
solver.cpp #7468 - add option (get-info :parameters) to display solver parameters that were updated globally and distinct from defaults 2025-02-10 11:57:14 -08:00
solver.h Parallel tactic (#9824) (#9825) 2026-06-26 10:36:15 -06:00
solver2tactic.cpp Standardize for-loop increments to prefix form (++i) (#8199) 2026-02-18 20:57:29 -08:00
solver2tactic.h move model and proof converters to self-contained module 2022-11-03 05:23:01 -07:00
solver_na2as.cpp display assumptions used 2025-09-11 10:20:55 -07:00
solver_na2as.h remove default destructors & some default constructors 2024-09-04 22:30:23 +01:00
solver_pool.cpp refactor solver to include settable stats 2026-06-07 14:17:38 -07:00
solver_pool.h
solver_preprocess.cpp move smt params to params directory, update release.yml 2025-06-09 10:47:22 -07:00
solver_preprocess.h add unconstrained elimination for sequences 2023-03-20 17:07:04 +01:00
tactic2solver.cpp refactor solver to include settable stats 2026-06-07 14:17:38 -07:00
tactic2solver.h