3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-15 11:35:42 +00:00
Commit graph

22460 commits

Author SHA1 Message Date
copilot-swe-agent[bot]
638e7b7bce
fix: simplify type variable collection condition in decl_collector::visit_sort 2026-07-13 03:00:50 +00:00
copilot-swe-agent[bot]
154f71102d
fix: collect type variables (poly_family_id sorts) in decl_collector::visit_sort
Type variables created via mk_type_var (e.g. from declare-type-var in SMT-LIB2
or TPTP polymorphic type vars) have poly_family_id. decl_collector::visit_sort
did not add them to m_sorts, so ast_pp_util::display_decls / solver::display
never emitted (declare-sort TypeVar 0) before uses, producing invalid SMT-LIB2.

Fix: add an else-if branch for poly_family_id alongside the existing is_uninterp
branch so type variables are pushed into m_sorts, topologically ordered by
order_deps (they have no dependencies, so they sort first), and declared before
other sorts and func_decls that reference them.
2026-07-13 02:59:45 +00:00
Copilot
0b8335e776
Set pi.avoid_skolems=false for TPTP solver runs (#10100)
The TPTP frontend was not forcing `pi.avoid_skolems=false`, so TPTP
problems could be solved with the default pattern-inference behavior
instead of the intended frontend-specific setting. This change applies
the override directly to the solver used by TPTP runs.

- **What changed**
- After constructing the TPTP solver, the frontend now sets
`pi.avoid_skolems=false` via solver parameters before `check_sat`.
- The override is scoped to the TPTP solver instance instead of mutating
process-global parameter state.

- **Why this shape**
- Keeps the TPTP behavior explicit at the point where the solver is
created.
  - Avoids leaking the parameter change into unrelated solver contexts.

- **Code sketch**
  ```c++
  ctx.set_solver_factory(mk_smt_strategic_solver_factory());
  params_ref solver_params;
  solver_params.set_bool("pi.avoid_skolems", false);
  ctx.get_solver()->updt_params(solver_params);
  ```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-12 19:21:25 -07:00
Nikolaj Bjorner
0000e16851 expose avoid-skolems parameter to deal with TPTP problems 2026-07-12 18:54:31 -07:00
Copilot
7d67dbfcfc
Add QE regression for unsound check-sat-using qe result on quantified reals (#10097)
`check-sat-using qe` was reported to return `unsat` on a satisfiable
quantified-real formula, while a subsequent `check-sat` on the same
assertion returned `sat`. This PR adds focused regression coverage for
that shape to prevent reintroduction.

- **Regression coverage for the reported formula**
  - Added `test_qe_regression_4175()` in `src/test/quant_solve.cpp`.
  - Parses and quantifier-eliminates:
    ```smt2
    (forall ((b Real)) (= (= r1 b) (= b 0)))
    ```

- **Behavioral oracle encoded in the test**
  - Verifies the QE result is satisfiable under `r1 = 0`.
  - Verifies the QE result is unsatisfiable under `r1 != 0`.
- This captures the intended semantics of the original formula and
guards against the unsound `unsat` outcome from the QE path.

- **Integration**
- Wires the new regression into `tst_quant_solve()` so it runs with
existing quantifier-solver test coverage.

Example snippet from the new test logic:

```cpp
solver.assert_expr(result);
solver.assert_expr(m.mk_eq(r1, zero));
VERIFY(l_true == solver.check());

solver.assert_expr(result);
solver.assert_expr(m.mk_not(m.mk_eq(r1, zero)));
VERIFY(l_false == solver.check());
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-12 18:53:15 -07:00
Copilot
4862a10ffd
Spacer QE: avoid assertion on extended arithmetic model values and add #3845 regression (#10096)
Spacer crashed in quantifier-elimination projection on certain HORN
inputs when model evaluation produced arithmetic expressions that were
not plain numerals. The failure was an assertion in
`spacer_qe_project.cpp` during sign/offset computation for projected
literals.

- **Projection robustness in Spacer arithmetic QE**
- Updated numeral extraction in `src/muz/spacer/spacer_qe_project.cpp`
from `is_numeral` to `is_extended_numeral` at all model-evaluation sites
used by projection.
- This covers evaluated arithmetic forms (e.g., normalized arithmetic
expressions) that are semantically numeric but not syntactic numerals,
preventing assertion failures in disequality/equality handling and bound
selection.

- **Regression coverage for the crashing HORN shape**
- Added a focused regression in `src/test/api_datalog.cpp` that
evaluates the reported Spacer/HORN input pattern through
`Z3_eval_smtlib2_string`.
- The test exercises the exact QE/projection path that previously
triggered the assertion.

```cpp
// Before
VERIFY(a.is_numeral(val, r));

// After
VERIFY(a.is_extended_numeral(val, r));
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-12 18:52:44 -07:00
Copilot
d99b6e7b08
Regenerate gh-aw lock workflows to remove stale model-multiplier runtime hook (#10099)
Multiple agentic workflows were failing at runtime with
`MODULE_NOT_FOUND` for `merge_awf_model_multipliers.cjs` under
`${RUNNER_TEMP}/gh-aw/actions`. The lock workflows had stale generated
runtime steps that no longer matched the current `gh-aw` actions bundle.

- **Root cause**
  - Generated `.lock.yml` workflows referenced a removed script:
- `node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"`

- **Change**
- Recompiled all agentic workflow sources (`.github/workflows/*.md`)
with current `gh aw` tooling.
  - Checked in regenerated lock artifacts:
    - `.github/workflows/*.lock.yml`
    - `.github/aw/actions-lock.json`
- Result: stale `merge_awf_model_multipliers.cjs` invocations were
eliminated from generated workflows.

- **Representative diff shape**
  ```yaml
  # removed from generated lock workflows
  - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" \
    node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"
  ```

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-12 18:30:19 -07:00
Copilot
b1aa1e6ddc
Add z3regex Python module to translate Python regex syntax into Z3 regular expressions (#10095)
This PR adds a first-class Python API helper for expressing regex
constraints in familiar Python regex syntax and translating them into Z3
regex terms. The translator is implemented as a separate module
(`z3regex.py`) as requested, keeping regex conversion logic isolated
from core API files.

- **New Python regex translator module**
  - Adds `src/api/python/z3/z3regex.py`.
- Introduces `regex_to_re(pattern, flags=0, ctx=None)` to convert parsed
Python regex constructs into Z3 regex expressions.
- Supports core regular constructs (literals, classes/ranges,
alternation, grouping, quantifiers, categories, wildcard).
- Raises `NotImplementedError` for unsupported non-regular constructs
(e.g., features outside regular languages).

- **API/package integration**
  - Exposes the module via `src/api/python/z3/__init__.py`.
- Includes `z3/z3regex.py` in Python binding file copy/install flow in
`src/api/python/CMakeLists.txt`.

- **Doctest entrypoint support**
- Extends `src/api/python/z3test.py` with `z3regex` mode so translator
doctests can be run consistently with existing Python API doctest flows.

```python
from z3 import *
from z3.z3regex import regex_to_re

x = String("x")
r = regex_to_re(r"(ab|cd)+\d{2}")

s = Solver()
s.add(InRe(x, r))
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-12 18:26:59 -07:00
Copilot
d9ad23a188
Ensure Nightly release source archive always matches the workflow commit (#10094)
The Nightly release occasionally exposed a mismatch between the commit
shown on the release page and the downloaded “Source code” archive. Root
cause was non-deterministic `Nightly` tag/release state during publish.

- **Deterministic Nightly tag lifecycle**
- Serialize Nightly workflow runs with a dedicated concurrency group to
prevent overlapping tag/release mutations.
- Replace best-effort cleanup with explicit release-exists checks and
failure-on-cleanup-error behavior.
  - Remove orphan `Nightly` tags even when no release exists.

- **Pin release to the exact workflow commit**
- Force-update `Nightly` to `${{ github.sha }}` and push it before
release creation.
- Verify remote tag SHA (including annotated-tag dereference shape)
matches `${{ github.sha }}`.
- Create release with `--verify-tag` so source archives are generated
from the validated tag, not an implicit/stale target.

- **Workflow behavior change (deploy section)**
  ```yaml
  concurrency:
    group: nightly-release
    cancel-in-progress: false
  ```

  ```bash
  git tag -f Nightly "${{ github.sha }}"
  git push --force origin refs/tags/Nightly
  gh release create Nightly --verify-tag ...
  ```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-12 17:32:45 -07:00
Nikolaj Bjorner
606a06fa5f fix warnings
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-12 16:45:24 -07:00
Nikolaj Bjorner
f7afe8e025 tout -> out
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-12 16:41:05 -07:00
Nikolaj Bjorner
bcc176fc47 prepare ground for general projection 2026-07-12 15:59:56 -07:00
Nikolaj Bjorner
eaceded5f1
Issue 438 (#10085)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-12 13:35:57 -07:00
Copilot
ba7b12c18c
Fix .NET API memory leak: NativeContext finalizer, delegate lifetime, GC memory pressure (#10090)
Creating and disposing `Context` instances causes unbounded native
memory growth (~12 GB for 100k contexts) because `NativeContext` had no
finalizer — if `Dispose()` was never called, the native Z3 context
leaked permanently. Additionally, both `Context` and `NativeContext` had
delegate lifetime and thread-safety issues in their disposal paths.

## `NativeContext.cs`

- **Add missing finalizer** `~NativeContext() { Dispose(); }` — the root
cause of permanent leaks when callers don't explicitly dispose
- **Atomic disposal** via `Interlocked.Exchange(ref m_ctx, IntPtr.Zero)`
— prevents double-free when `Dispose()` is called concurrently (e.g.
user code + finalizer race)
- **Delegate lifetime** — capture `errHandler` locally +
`GC.KeepAlive(errHandler)` after `Z3_del_context`; the GC could
otherwise collect the error handler callback before the native
destructor finishes
- **Remove dead code** — `GC.SuppressFinalize` in `InitContext()` and
`GC.ReRegisterForFinalize` in `Dispose()` were both no-ops (no finalizer
existed); the latter would have caused infinite finalization with the
new finalizer
- **GC memory pressure** — `GC.AddMemoryPressure(8MB)` on init /
`GC.RemoveMemoryPressure(8MB)` on dispose, guarded by
`m_memPressureAdded` flag, so the GC schedules finalizers promptly when
contexts accumulate

## `Context.cs`

- **Thread-safe disposal** — capture `ctx` and `errHandler` inside the
existing `lock(this)` block; previously both were read outside the lock,
allowing two concurrent callers to both capture the same non-zero `ctx`
and double-free it
- **Delegate lifetime** — same `errHandler` + `GC.KeepAlive` pattern as
`NativeContext`
- **`GC.SuppressFinalize` placement** — moved inside the `if (m_ctx !=
IntPtr.Zero)` block, before cleanup, per .NET best practice
- **GC memory pressure** — same add/remove pattern, conditioned on
`!is_external` via `m_memPressureAdded` flag

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-11 21:15:36 -07:00
Copilot
c4b0fe33bc
test: replace SASSERT with ENSURE, remove Windows-only guards (#10086)
Unit tests relied on `SASSERT()` which is a no-op in release builds
(`DEBUG_CODE` wrapper), silently skipping all assertions outside debug
mode. Several test files were also gated behind `#ifdef _WINDOWS`,
making them dead code on Linux/macOS CI.

## Changes

- **`SASSERT` → `ENSURE` in 20 test files (200 occurrences)**: `ENSURE`
maps to `VERIFY` and always executes regardless of build type, ensuring
test assertions are active in both debug and release builds.

- **`src/test/diff_logic.cpp`**: Removed `#ifdef _WINDOWS` wrapping the
entire file. No Windows-specific APIs were used; the guard only
prevented compilation on non-Windows platforms.

- **`src/test/dl_product_relation.cpp`**: Removed `#ifdef _WINDOWS`
guard around `tst_dl_product_relation()`. The function body has no
platform dependencies.

- **`src/test/sat_local_search.cpp`**: Replaced `sscanf_s`
(MSVC-specific) with portable `sscanf`; added return-value check to
detect malformed input. Previously, `build_instance()` unconditionally
returned `false` on non-Windows, making the SAT local search test a
no-op on Linux/macOS.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-11 21:14:59 -07:00
Copilot
b0a77d2a58
[CMake] Guard Z3_API_LOG_SYNC against Z3_SINGLE_THREADED (#10088)
`Z3_API_LOG_SYNC` uses `std::mutex` to serialize API log writes across
threads. When combined with `Z3_SINGLE_THREADED`, all mutex operations
become no-ops (`SINGLE_THREAD` define strips them), silently defeating
the synchronization.

## Changes

- **`CMakeLists.txt`**: Emit `FATAL_ERROR` when both
`Z3_API_LOG_SYNC=ON` and `Z3_SINGLE_THREADED=ON` are set:
  ```
CMake Error: Z3_API_LOG_SYNC requires threading support and cannot be
combined with Z3_SINGLE_THREADED
  ```
- **`README-CMake.md`**: Expand `Z3_API_LOG_SYNC` description to clarify
its purpose and document the incompatibility with `Z3_SINGLE_THREADED`.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-11 19:20:15 -07:00
Nikolaj Bjorner
14e1dc9896
Fix segfault in horn on formulas with unused quantified variables (#6… (#10091)
…158)

rule_manager::mk_query eliminates gaps in de Bruijn indices caused by
unused quantified variables via a substitution that renumbers the
remaining variables contiguously. This was done in a single pass, but
var_subst applies the rewriter, which can simplify away further variable
occurrences (e.g. collapsing ite terms such as (ite true a b) or (ite c
x x)), introducing new gaps. The leftover null sort was then
dereferenced, asserting in debug and segfaulting in release.

Iterate the gap-elimination until the free variables are contiguous.
Each iteration either compacts the indices or strictly reduces the set
of used variables, so it terminates.

Fixes the crash reported for:
(assert (forall ((a Bool)(b Bool)(d (_ BitVec 1))(e (_ BitVec 1))(f (_
BitVec 1))(g Bool))
(= (= f (ite a (_ bv0 1) (ite true (_ bv0 1) (ite b e e)))) g)))
  (check-sat-using horn)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-11 19:17:18 -07:00
Copilot
dfd2f328f6
fix: update nightly.yml mac-build-x64 to macOS 13.3 for C++20 compatibility (#10082)
`std::format` (C++20 `<format>`) pulls in `std::to_chars` for
floating-point formatting, which is only available on macOS 13.3+. The
Mac x64 CI job was targeting macOS 13.0, causing build errors in
`src/ast/`.

## Changes

Updated `nightly.yml` to raise the macOS deployment target for the x64
build:

- **`MACOSX_DEPLOYMENT_TARGET`**: `"13.0"` → `"13.3"` in the
`mac-build-x64` job
- **`--os` flag**: `osx-13.0` → `osx-13.3` in the `mk_unix_dist.py`
invocation

This matches the existing `mac-build-arm64` job, which already targets
macOS 13.3, and allows `std::format` to be used freely in `src/ast/`
without workarounds.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-11 08:26:26 -07:00
Copilot
efe5e946f1
Fix unsigned overflow in parallel solver conflict budget escalation (#10076)
Z3 4.16.0 introduced a cube-and-conquer parallel solver that regressed
easy QF_LIRA problems from <1s to hanging indefinitely. Workers start
with a 1000-conflict budget and multiply by 1.5× on each timeout, but
after ~38 escalations the `unsigned` cast overflows, causing the budget
to oscillate chaotically (e.g. 3.27B → 618M → 927M → … never reaching a
stable large value). For sub-cubes that require more conflicts than any
value in the oscillation window, the worker loops forever.

## Changes

- **`src/smt/smt_parallel.h`** – `update_max_thread_conflicts()`:
replace raw `(unsigned)(mul * val)` cast with saturating arithmetic that
caps at `UINT_MAX`, eliminating the UB and the oscillation:
  ```cpp
// Before – UB when product > UINT_MAX, budget oscillates after ~38
escalations
  m_config.m_threads_max_conflicts =
(unsigned)(m_config.m_max_conflict_mul *
m_config.m_threads_max_conflicts);

  // After – saturates at UINT_MAX
double next = m_config.m_max_conflict_mul *
m_config.m_threads_max_conflicts;
m_config.m_threads_max_conflicts = (next >= (double)UINT_MAX) ? UINT_MAX
: (unsigned)next;
  ```

- **`src/solver/parallel_tactical.cpp`** – Identical
saturating-arithmetic fix in the `parallel_tactical2` worker's
`update_max_thread_conflicts()`, which is the code path actually
exercised for QF_LIRA problems.

- **`src/smt/smt_parallel.cpp`** – Worker's initial per-cube conflict
budget is now sourced from `m_smt_params.m_threads_max_conflicts` (the
user-visible `smt.threads.max_conflicts` parameter) instead of being
hardcoded to 1000, so users who leave the parameter at its default get
an unlimited initial budget matching Z3 4.12.x portfolio behaviour.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-10 15:25:57 -07:00
Nikolaj Bjorner
af0fa8ecf3 change bit-vector AC simplification n to true 2026-07-10 15:00:32 -07:00
Nikolaj Bjorner
c3170108e6 remove ad-hoc disabling skolem shadowing in pattern inference
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-10 14:25:20 -07:00
Nikolaj Bjorner
382abb786a disable term enumeration by default
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-10 14:23:40 -07:00
Nikolaj Bjorner
1d425e55cd bug fixes to ho_matching - offset alignment inv_var_shift, callback scopes should not be nested, allow bindings that are not ground
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-09 19:16:12 -07:00
Lev Nachmanson
ed6e2a241d
opt: validate strict optimization optima faithfully with delta-rational bounds (#10059)
## Problem

Maximizing/minimizing under a **strict** inequality has a delta-rational
optimum. For

```smt2
(declare-const r Real)
(assert (< r 1))
(maximize r)
(check-sat)
(get-objectives)
```

the optimum is the supremum `1 - epsilon`, but z3 reported `r = 0`.

The same defect makes shared-symbol objectives report a value matching
**neither the model nor the true optimum** (issue #10028 follow-up).
Minimal reproducer — a 6-mark Golomb ruler (a `>32`-arg `distinct`, so
the objective is coupled to EUF) with a strict real objective `obj >
x5`, whose true optimum is `17 + epsilon`:

| case | before | after |
|---|---|---|
| `maximize r`, `r < 1` | `0`  | `1 - epsilon`  |
| `minimize r`, `r > 1` | `0`  | `1 + epsilon`  |
| Golomb `minimize obj`, `obj > x5` | `35/2` / `7+eps`  | `17 +
epsilon`  |

## Root cause

`check_bound` validates the LP hint by asserting `objective >= optimum`.
For a supremum `1 - epsilon` this is a **lower** bound whose value
carries a **negative** infinitesimal `(1, -1)`.

No `lconstraint_kind` can express that. The kind->infinitesimal map only
yields the *matching-sign* cases — `GT` -> lower `(r, +1)`, `LT` ->
upper `(r, -1)` — or zero (`GE`/`LE`). The opposite-sign lower bound
`(r, -1)` (i.e. `r >= r0 - delta`) is a *relaxation* that no strict
inequality produces. `opt_solver::mk_ge` therefore projected the
`-epsilon` away, turning `r >= 1 - epsilon` into the over-strong,
unsatisfiable `r >= 1`; validation failed and the strictly smaller
current model value was reported instead.

## Fix — carry the infinitesimal faithfully through the bound pipeline

- **`lp_api::bound`** gains an `eps` component so `get_value` returns
the true delta value (no spurious rational fixed-variable equality is
propagated to EUF).
- **`lar_base_constraint`** stores its right-hand side as a
delta-rational `impq` pair; `rhs()` returns the rational component,
`bound_eps()` the infinitesimal one.
- **`lar_solver`** bound activation/update threads the whole `impq`
bound, so a lower bound `(r, -1)` can be asserted. `constraint_holds`
accounts for it using the **same** strict-bounds delta that flattens the
model, computed **once per model**.
- **`theory_lra::mk_ge`** builds a *fresh* predicate for the `(r, -1)`
lower bound (to avoid colliding with an already-internalized `v >= r`
literal) and attaches `eps = -1`. **`opt_solver::mk_ge`** passes the
unprojected value to `theory_lra` / `theory_mi_arith` /
`theory_inf_arith` (whose bounds are already `inf_rational`).

The pair machinery is what makes the supremum both representable
(optimum `1 - epsilon`) and validatable; the reported witness model
remains the flattened rational (`find_delta_for_strict_bounds`),
consistent with the existing epsilon semantics.

## Validation

- Strict optima correct: `1-eps`, `1+eps`, bounded `2<r<5 -> 5-eps`, and
lex/box variants.
- Integer optima and the #10028 shared-symbol cases unchanged (Golomb
n=6/7/8 -> 17/25/34, consistent with the model).
- Unit tests **92/92** (release); no new debug-suite failures.
- Opt regression corpus (73 files, `model_validate=true`)
**byte-identical** to baseline.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-09 10:39:23 -07:00
Copilot
5a55ed7cfb
Nightly: fix Mac ARM64 build by targeting macOS 13.3 for std::format (#10075)
The Nightly **Mac ARM64 Build** job fails on current Xcode/libc++
because `std::format` pulls floating-point formatting paths that require
`std::to_chars` availability from macOS 13.3+. The job was still
compiling with a 13.0 deployment target.

- **Root cause**
  - ARM64 nightly job used:
    - `MACOSX_DEPLOYMENT_TARGET=13.0`
    - `mk_unix_dist.py --os=osx-13.0`
- This mismatched the effective libc++ requirements in the runner
toolchain.

- **Workflow change**
  - Updated only `.github/workflows/nightly.yml` for `mac-build-arm64`:
    - `MACOSX_DEPLOYMENT_TARGET: "13.0" -> "13.3"`
    - `--os=osx-13.0 -> --os=osx-13.3`

- **Resulting behavior**
- ARM64 nightly build target now aligns with the minimum OS level
required by the formatting code paths used by current compilation units.

```yaml
# .github/workflows/nightly.yml (mac-build-arm64)
env:
  MACOSX_DEPLOYMENT_TARGET: "13.3"

- name: Build
  run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk --arch=arm64 --os=osx-13.3
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-09 08:45:51 -07:00
Lev Nachmanson
2c16f44c0a
[coz3-deepperf-fix] lp: hoist loop-invariant pivot reads in HNF pivot_column_non_fractional (#10073)
## Summary

Hoists loop-invariant matrix reads out of the hot inner loop of
`pivot_column_non_fractional` in the Hermite Normal Form (HNF)
computation used by z3's linear-arithmetic integer solver. The
arithmetic is unchanged; the patch only removes repeated
permutation-indexed `mpq` accesses from an O(n2) elimination loop.

## Hotspot

`lp::hnf_calc::pivot_column_non_fractional<M>` (`src/math/lp/hnf.h:130`)
performs the Bareiss-style fraction-free Gaussian elimination step over
a matrix `m`:

```cpp
for (unsigned j = r + 1; j < m.column_count(); ++j)
    for (unsigned i = r + 1; i < m.row_count(); ++i)
        m[i][j] = (r > 0) ? (m[r][r]*m[i][j] - m[i][r]*m[r][j]) / m[r-1][r-1]
                          : (m[r][r]*m[i][j] - m[i][r]*m[r][j]);
```

For `general_matrix`, every `m[a][b]` builds a temporary `ref_row` and
performs two permutation-array indirections (row and column permutation
lookups in `src/math/lp/general_matrix.h`) before the underlying vector
access. Inside this double loop the terms `m[r][r]`, `m[r-1][r-1]` and
`m[r][j]` are re-read on every iteration even though they are invariant,
so those redundant indexed reads dominate the loop cost.

## Change and complexity argument

Rows `<= r` are never written by this loop — it only assigns `m[i][j]`
for `i > r`, `j > r` — so the three pivot entries in rows `<= r` are
loop-invariant:

- `m[r][r]` and `m[r-1][r-1]` are invariant across **both** loops → bind
`m[r][r]` to a reference and take a pointer to `m[r-1][r-1]` once before
the outer loop. The pointer is `nullptr` when `r == 0`, which also
encodes the existing "no division" case with a single branch.
- `m[r][j]` is invariant across the inner `i` loop → hoist it to a
reference at the top of the outer `j` loop.
- `m[i][j]` is bound to a reference so it is indexed once per iteration
instead of three times (two reads + one write).

This turns **O(n2)** repeated permutation-indexed `mpq` reads into
**O(1)/O(n)** hoisted reads. The operands, operation order, and division
are identical to the original, so the computed matrix is bit-for-bit the
same.

## Measurements

Profiled with callgrind (z3 built with the same configuration,
`model_validate=true`) on a representative integer-arithmetic problem
that exercises the HNF cut generator:

- Target function instructions: **6,793,150,548 → 6,241,093,226**
(0.9187×; its share of total drops 15.2% → 14.5%).
- Total program instructions: **44,727,385,309 → 43,036,726,143**
(0.9622×).
- Wall-clock: **5.53s → 4.89s (~11.6% faster)**.
- Differential correctness preserved: identical solver output before and
after the change.

## Logic class

Integer linear arithmetic — the HNF-based cut generation path in the
`lp` int-solver.

<!-- gh-aw-workflow-id: coz3-deepperf-fix -->

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 07:02:47 -07:00
Nikolaj Bjorner
aa6dddbdf0 update pattern inference to allow patterns with variables outside of scope
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-08 19:48:10 -07:00
Nikolaj Bjorner
30a4a84c79 move verbose out to where it is used 2026-07-08 18:54:03 -07:00
Nikolaj Bjorner
3f6a57e8a8
Improve generation accounting (#10008) (#10009)
Details in original PR: https://github.com/Z3Prover/z3/pull/10007

---------

---------

Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
Co-authored-by: Can Cebeci <can.cebeci99@gmail.com>
Co-authored-by: Can Cebeci <t-cancebeci@microsoft.com>
2026-07-08 15:43:41 -07:00
Nikolaj Bjorner
c56b2cbaa4 fix pattern inference to deal with binders properly, pin sorts in tptp_frontend 2026-07-08 15:29:05 -07:00
Nikolaj Bjorner
965db51b5c use patterns 2026-07-07 15:00:32 -07:00
Nikolaj Bjorner
7040a74d1a defer ho-matching to lazy mam
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-07 15:00:02 -07:00
Copilot
165a4a42bc
Fix debug-only well-sorted traversal freeing temporary assertions (#10067)
The Ubuntu `python make - MT` job was failing in unit tests because the
debug-time well-sorted check could invalidate freshly constructed
assertion expressions during solver entry. This surfaced as crashes in
`theory_dl` and `seq_rewriter`, not as logic bugs in those tests.

- **Root cause**
- `is_well_sorted` traversed the input through a temporary
`expr_ref`/`subterms` wrapper.
- For freshly built assertions passed directly into `assert_expr`, that
temporary ownership could drop the last refcount during validation and
free the AST before the solver used it.

- **Change**
- Reworked the traversal in `src/ast/well_sorted.cpp` to walk raw
`expr*` nodes explicitly.
- The checker now validates subterms without taking transient ownership
of the asserted expression.

- **Effect**
  - Debug validation remains intact.
- Temporary formulas survive the well-sorted check, so assertion-time
validation no longer corrupts the caller’s AST.

- **Representative change**
  ```cpp
  ptr_vector<expr> todo;
  expr_mark visited;
  todo.push_back(e);
  while (!todo.empty()) {
      expr* term = todo.back();
      todo.pop_back();
      if (visited.is_marked(term))
          continue;
      visited.mark(term, true);
      if (is_app(term)) {
          for (expr* arg : *to_app(term))
              if (!visited.is_marked(arg))
                  todo.push_back(arg);
          check_app(to_app(term));
      }
      else if (is_var(term)) {
          check_var(to_var(term));
      }
      else if (is_quantifier(term)) {
          check_quantifier(to_quantifier(term));
      }
  }
  ```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-07 13:26:44 -07:00
Lev Nachmanson
22c779c77c
[snapshot-regression-fix] Fix elim_uncnstr disabled by manager-wide has_type_vars() flag (iss-6260/small-2) (#10063)
## Summary

Fixes a completeness regression where `elim_uncnstr` was silently
disabled for ordinary (non-polymorphic) goals, detected by the
`snapshot-regression` corpus.

- **Originating discussion:**
https://github.com/Z3Prover/bench/discussions/3054
- **Benchmark:** `iss-6260/small-2.smt2` (corpus `Z3Prover/bench`,
`inputs/issues/iss-6260/`)
- **Divergence:** recorded oracle `sat` → current z3 produces `unknown`

### Divergence diff

```diff
--- small-2.expected.out (expected)
+++ produced (current z3)
@@ -1,3 +1,3 @@
-sat
+unknown
 (error "line 17 column 0: unexpected character")
 (error "line 17 column 1: unexpected character")
```

(The `(error ...)` lines are expected: the benchmark contains a stray
```` ``` ```` fence on line 17. Only the `sat` → `unknown` change is the
regression.)

## Root cause

`git bisect` over the regression window pins the flip to commit
`208cc5686` ("fix build"), which added `|| m().has_type_vars()` to the
`elim_uncnstr_tactic` guard and an equivalent `if (m.has_type_vars())
return;` to the `elim_unconstrained` simplifier.

`ast_manager::has_type_vars()` is a **manager-wide, sticky** flag: it is
set to `true` as soon as *any* type variable is created, and is never
reset. In particular `finite_set_decl_plugin::init()` creates type
variables `A`/`B` to define its polymorphic signatures. Those type
variables never occur in the user's assertions, but once the finite_set
plugin is initialized — which happens while processing this benchmark —
the flag is globally `true` (confirmed by instrumenting `mk_type_var`:
the only type vars created for this benchmark are the finite_set
signature vars `A` and `B`).

As a result `elim_uncnstr` bails out for goals that contain **no**
polymorphic terms at all, i.e. it is effectively disabled. Unconstrained
subterms that used to be eliminated now reach the theory solvers.

For this benchmark the (single) assertion is `(not (xor (>= x 0) (>= x1
0) (>= x 0) x4 (str.contains ...)))`. The duplicated `(>= x 0)` cancels
(`a xor a = false`), and the free Boolean `x4` can fix the parity
regardless of the value of the `str.contains` term, so the goal is
trivially `sat`. That `str.contains`/`str.replace_re` subterm is
unconstrained and was previously removed by `elim_uncnstr`; without that
elimination it reaches `theory_seq`, which marks `str.replace_re` as
unhandled and gives up in `final_check` → `unknown` (`incomplete (theory
seq)`).

## Fix

Make the guard precise. Keep `has_type_vars()` as a cheap pre-filter
(matching existing usage in `ast_translation.cpp` and
`ast_manager::has_type_var`), but only bail out when the goal / asserted
formulas **actually** contain type-variable typed terms, using the
existing `polymorphism::util::has_type_vars(expr*)`.

This preserves the polymorphism crash-protection (goals with genuine
type-variable terms still skip `elim_uncnstr`) while restoring
`elim_uncnstr` for the vast majority of goals that merely triggered a
polymorphic-plugin initialization. Both twin guards (the `elim_uncnstr`
tactic and the `elim_unconstrained` simplifier) are fixed consistently.

## Validation

Built this checkout and re-ran the benchmark (step 5 of the fixer
workflow):

- `./configure && make -C build -j$(nproc)` — Z3 version 4.17.0.
- Unpatched master reproduced the divergence: `z3 -T:20
iss-6260/small-2.smt2` → `unknown`.
- After the fix, `z3 -T:20 inputs/issues/iss-6260/small-2.smt2` produces
**exactly** the recorded oracle:

```
sat
(error "line 17 column 0: unexpected character")
(error "line 17 column 1: unexpected character")
(error "line 17 column 2: unexpected character")
```

- Regression sanity checks: sibling `iss-6260/small.smt2` unchanged
(`sat`); plain arithmetic unconstrained goals unchanged; polymorphic
goals with genuine type-variable terms (including `declare-type-var` and
forcing `(check-sat-using (then elim-uncnstr smt))`) still solve without
crashing — the guard still fires for those.

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




> Generated by [Fix a Z3 snapshot-regression
divergence](https://github.com/Z3Prover/bench/actions/runs/28844840657)
· 861.2 AIC · ⌖ 45.8 AIC · ⊞ 8.9K ·
[◷](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.63, model: claude-opus-4.8, id:
28844840657, workflow_id: snapshot-regression-fixer, run:
https://github.com/Z3Prover/bench/actions/runs/28844840657 -->

<!-- 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-07-07 13:02:37 -07:00
Can Cebeci
5fc2b04dea
Mark quantifier instances that lead to conflicts as relevant (#10064)
This patch fixes a corner case where quantifier conflicts can create
fresh terms that aren't marked as relevant.
I couldn't easily produce a minimal query that this patch turns stable,
nor did the patch stabilize the query I have been working on, but I
think the example below still illustrates the problem:

```
(set-option :auto_config false)
(set-option :type_check true)
(set-option :smt.case_split 3)
(set-option :smt.mbqi false)

(declare-fun R (Int) Bool)
(declare-fun S (Int) Bool)
(declare-fun dummy (Int) Bool)

(assert (or (R 0) (dummy 0)))

(assert (forall ((x Int)) (! 
    (and (not (R x)) (not (S x)))
    :pattern ((R x))
    :qid not_r_not_s
)))

(assert (forall ((x Int)) (! 
    (S x)
    :pattern ((S x))
    :qid s_true
)))
(check-sat)
```

The query is unstable (due to the same interaction between relevancy and
triggers in https://github.com/Z3Prover/z3/issues/7444): if the solver
assigns `(dummy 0)` first, it returns `unknown`.

If the solver assigns `(R 0)` first, we would expect an `unsat`. The
current implementation returns `unknown` because `not_r_not_s` leads to
a quantifier conflict, which creates `S(x)` without marking it (or any
of its ancestors) as relevant.

---------

Co-authored-by: Can Cebeci <t-cancebeci@microsoft.com>
Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-07 13:01:44 -07:00
Copilot
5f70ba10a9
Remove temporary LP batching parameter and make batched explanation unconditional (#10066)
This removes the temporary `lp.batch_explain_fixed_in_row` knob added
with the recent LP changes. The batched fixed-column explanation path is
kept as the only implementation, matching the follow-up review comments.

- **Problem**
- The new LP setting exposed a temporary fallback path that is no longer
needed.
- Keeping both paths added parameter surface area and settings plumbing
without a lasting behavioral distinction.

- **Changes**
  - **Remove parameter definition**
- Delete `lp.batch_explain_fixed_in_row` from LP parameter generation.
  - **Remove settings plumbing**
- Drop the stored field, accessor methods, and parameter update wiring
from `lp_settings`.
  - **Keep batched explanation as default behavior**
    - Remove the runtime branch in `lar_solver::explain_fixed_in_row`.
- Always linearize fixed-column witnesses together in a single
dependency pass.

- **Resulting simplification**
- The solver no longer carries a dead configuration toggle for fixed-row
explanation.
- The batched dependency-linearization path remains intact and is now
the sole code path.

```c++
void lar_solver::explain_fixed_in_row(unsigned row, explanation& ex) {
    auto& witnesses = m_imp->m_tmp_witnesses;
    witnesses.reset();
    for (auto const& c : get_row(row)) {
        if (!column_is_fixed(c.var()))
            continue;
        const column& ul = m_imp->m_columns[c.var()];
        witnesses.push_back(ul.lower_bound_witness());
        witnesses.push_back(ul.upper_bound_witness());
    }
    m_imp->m_tmp_dependencies.reset();
    m_imp->m_dependencies.linearize(witnesses, m_imp->m_tmp_dependencies);
    for (auto ci : m_imp->m_tmp_dependencies)
        ex.push_back(ci);
}
```

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-07 13:01:10 -07:00
Nikolaj Bjorner
334f4fa32b reduce leaks for sorts 2026-07-07 12:04:14 -07:00
Lev Nachmanson
ff7e22c055 make the batch explanation of fixed in row the default 2026-07-07 10:15:07 -07:00
Nikolaj Bjorner
b2f0d0682a fix loop bug in ho_matching and add throttle configurations 2026-07-07 09:20:32 -07:00
Copilot
d9d3be959c
Pin macOS wheel target to 13.0 in release and nightly workflows (#10054)
Wheels started inheriting `macosx_15_0` tags because macOS jobs run on
newer runners and no deployment target was pinned. That blocks wheel
installation on macOS 13/14 hosts and forces source builds.

- **macOS deployment target pinning**
- Added `MACOSX_DEPLOYMENT_TARGET: "13.0"` to both macOS build jobs
(`x64`, `arm64`) in:
    - `.github/workflows/release.yml`
    - `.github/workflows/nightly.yml`

- **Stable macOS artifact OS metadata**
- Updated macOS `mk_unix_dist.py` invocations to pass `--os=osx-13.0` in
both workflows, so artifact metadata stays aligned with the intended
minimum macOS compatibility used for Python wheel tagging.

```yaml
mac-build-x64:
  runs-on: macos-15
  env:
    MACOSX_DEPLOYMENT_TARGET: "13.0"
  steps:
    - name: Build
      run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk --arch=x64 --os=osx-13.0
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-06 17:55:01 -07:00
Nikolaj Bjorner
d1aaae6856 allow lambdas in select positions for model, ignore beta redex incompletness when using arrays for MBQI 2026-07-06 17:43:15 -07:00
Nikolaj Bjorner
ca6d6e3977 pattern_inference: use auto for structured binding; drop debug well_sorted asserts in rewriter
Fixes a build error (C3694) from an illegal specifier in the structured
binding at pattern_inference.cpp:127, and removes the temporary
is_well_sorted SASSERTs (and well_sorted.h include) from rewriter_def.h
that were used during pattern-inference diagnosis.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-06 17:14:42 -07:00
Nikolaj Bjorner
5534dba680 update well_sorted to check patterns, fix variable shift in pattern inference 2026-07-06 17:14:41 -07:00
Nikolaj Bjorner
470e966791 bugfixes to front-end and matcher 2026-07-06 17:14:41 -07:00
Nikolaj Bjorner
6c8a5cd853 fix HO-matcher imitation curry-order and instance-assembly ordering bugs
The higher-order matcher produced ill-typed instantiations that aborted
the solve (sort-mismatch / unbound-variable exceptions), making
smt.ho_matching=true net-negative on the TPTP THF benchmarks.

Two root causes:

1. Imitation rule (ho_matcher.cpp): the select chain 'pats' is collected
   outermost-first, i.e. in reverse application order. The imitating
   lambda must curry arguments in application order (first-applied select
   binds the outermost lambda). Reversing 'pats' before building the
   domain/argument/body vectors and the lambda-wrapping loop makes the
   constructed lambda's sort agree with the flex head variable. Fixes
   unit-test ho_matcher test6c/test6d (previously asserted at
   add_binding: v->get_sort() == t->get_sort()).

2. Instance assembly (smt_quantifier.cpp on_ho_match): the fixpoint
   binding substitution used var_subst with the default std_order=true
   while the binding vector is directly indexed (binding[k] = value for
   var k). This resolved chained HO variable references against the wrong
   slots and built ill-sorted terms (assertion at rewriter_def.h:52).
   Use direct (std_order=false) substitution to match the binding layout.

Also adds defensive guards as belt-and-suspenders: subst_sorts_match
skips sort-inconsistent substitutions, an is_ground check skips bindings
with leftover de Bruijn variables, and on_ho_match catches z3_exception
to skip an unusable heuristic instance rather than aborting the solve
(re-raising only on cancellation/resource-limit).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-06 17:14:40 -07:00
NikolajBjorner
72d27e1cbb smt: instrument ho-matching and ho-var term-enumeration statistics
Add -st statistics counters:
- ho-matching refinements / instances (default_qm_plugin, gated on
  smt.ho_matching)
- ho-var term-enum: terms produced by mf::ho_var::populate_inst_sets
  in the model finder

Wired via a new quantifier_manager_plugin::collect_statistics virtual
forwarded from quantifier_manager::collect_statistics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-06 17:14:39 -07:00
Nikolaj Bjorner
e20945c743 include code for dumpign tptp
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-07-06 17:14:38 -07:00
Lev Nachmanson
835679b27d
Revert #10052: eager-commit of infinitesimal LP hint is unsound for shared-symbol objectives (#10057)
## Summary

Reverts #10052, whose eager-commit of infinitesimal LP hints is
**unsound** for shared-symbol objectives.

#10052 added, in `opt_solver::maximize_objective`:

```cpp
if (val.is_finite() && !val.get_infinitesimal().is_zero() && val > m_objective_values[i])
    m_objective_values[i] = val;
```

on the premise that *"a non-zero infinitesimal ⇒ an exact, unattainable
strict optimum, so the LP hint is authoritative."* That holds for a
**pure LP**, but is **false when the objective is a shared symbol** with
another theory (e.g. the auxiliary uninterpreted function used to encode
a `distinct` with > 32 arguments). There the LP relaxation only yields a
*hint* that can be a strict **over-estimate**, and #10052 commits it
without validation — exactly the class of bound that #10028's
`check_bound` exists to reject.

## Counterexample

A 6-mark Golomb ruler over integers `x0..x5`, with the `distinct` padded
to > 32 arguments so `x5` becomes a shared symbol; objective is a real
`obj` with `obj > x5` (full file attached to #5720):

- Ground truth: `minimize x5` (integer) ⇒ **17**; since `obj > x5`, the
true optimum is **`17 + ε`**.
- With #10052, z3 reports **`(obj (+ 5.0 epsilon))`** — wrong and
**infeasible** (`obj < 6` is `unsat`; the returned model itself has `x5
= 35, obj = 49.5`).

| benchmark | with #10052 (master) | after this revert |
| --- | --- | --- |
| Golomb `bug.smt2` (shared symbol) | `(obj (+ 5.0 epsilon))` 
infeasible | `(obj 18)`  consistent |
| #5720 (`max r < 1`) | `1 - epsilon`  | `0`  (regression returns) |

## Tradeoff / follow-up

This revert restores soundness on the shared-symbol case but
**reintroduces the #5720 regression** (`max r<1` ⇒ `0`), which is why
**#5720 has been reopened**. A correct fix should preserve the strict
single-objective supremum **without** trusting an unvalidated
shared-symbol hint — e.g. gate the eager commit on `!has_shared` (pure
LP only), or validate the rational part of the hint while keeping the
infinitesimal. The same blind spot affects the alternative `check_bound`
guard proposed in #10051.

Re: #5720

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-06 12:56:13 -07:00
Lev Nachmanson
f37b435923
[coz3-deepperf-fix] Batch fixed-column bound-witness linearization per row in lar_solver (#10029)
### Summary

`lp_bound_propagator::explain_fixed_in_row` explained every fixed column
of a row
independently, calling `lar_solver::explain_fixed_column` once per fixed
column
(`src/math/lp/lar_solver.cpp`). Each such call linearizes the lower- and
upper-bound witnesses of a single column — a BFS over the `u_dependency`
DAG using
the dependency manager's mark bits — and inserts every reached leaf
constraint into
the `explanation`.

Fixed columns of the same row routinely share large portions of their
bound-witness
sub-DAGs (common ancestor constraints). The per-column scheme therefore
re-traverses
those shared sub-DAGs and re-inserts their leaves once for *every*
column, with an
independent mark/unmark cycle per column.

### Change

Add `lar_solver::explain_fixed_in_row(row, ex)`, which collects the
lower/upper
witnesses of all fixed columns in the row and linearizes them together
in a single
`u_dependency_manager::linearize` pass.
`lp_bound_propagator::explain_fixed_in_row`
and `explain_fixed_in_row_and_get_base` now delegate to it; the
base-column lookup in
the latter is unchanged. `explain_fixed_column` is kept for its
single-column caller.

### Why it is correct

`explanation` is a set — `push_back` deduplicates. Dependency
reachability is
monotone, so the union of the per-column leaf sets equals the leaf set
of the union
of all roots: the batched pass yields exactly the same explanation. The
manager's
mark bits guarantee each shared sub-DAG node is visited once, and the
`linearize(ptr_vector, ...)` overload already skips null/duplicate
roots.

### Complexity

For a row with `N` fixed columns:

- before: `O(Σ_j |witness-DAG(j)|)` traversal + `O(Σ_j leaves(j))` set
insertions, with `N` mark/unmark cycles;
- after: `O(|⋃_j witness-DAG(j)|)` traversal + `O(#distinct leaves)` set
insertions, with a single mark/unmark cycle.

Shared sub-DAGs are walked and their leaves inserted once instead of
once per column.

### Measured effect

Profiled with callgrind on a representative conflict-heavy `QF_SLIA`
input
(`model_validate=true`, bounded run), baseline vs. patched:

- `lp::lar_solver::explain_fixed_column` on the hot path:
`24,337,671,616 → 0`
retired instructions (59.2% → 0% of the run), replaced by the single
batched
  traversal;
- total retired instructions: `41,113,093,210 → 35,983,363,256` (×0.875,
≈ 12.5%
  fewer) — the net work removed by de-duplicating shared sub-DAGs;
- wall-clock: `6.428 s → 6.079 s` (≈ 5.4% faster);
- differential correctness preserved (identical results across the
validation inputs).

<!-- gh-aw-workflow-id: coz3-deepperf-fix -->
<!-- gh-aw-workflow-call-id: Z3Prover/bench/coz3-deepperf-fix -->

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-06 10:39:22 -07:00
Lev Nachmanson
e1f99b569d
[snapshot-regression-fix] seq_rewriter: re.range with a provably-empty bound must be the empty language (#10047)
## Summary

Fixes a Z3 output regression detected by the `Z3Prover/bench`
snapshot-regression corpus.

- **Originating discussion:**
https://github.com/Z3Prover/bench/discussions/3050
- **Benchmark:** `iss-5134/small.smt2`
(`inputs/issues/iss-5134/small.smt2` in `Z3Prover/bench`)
- **Kind:** `diff` — recorded oracle vs. current nightly z3
(`z3-4.17.0-x64-glibc-2.39`)

## Divergence

The benchmark constrains a string `a` using a regex that contains
`(re.range "" <ite>)`:

```smt2
(declare-fun a () String)
(assert (str.in_re a (re.* (re.union (str.to_re "b") (str.to_re (ite
 (str.in_re a (re.* (re.range "" (ite (str.in_re a (str.to_re "")) ""
 a)))) "" "a"))))))
(assert (not (str.in_re a (re.* (str.to_re "")))))
(check-sat)
(get-model)
```

Recorded oracle (**expected**) vs. current z3 (**current**):

```diff
-sat
-(
-  (define-fun a () String
-    "a")
-)
+unknown
+(error "line 7 column 10: model is not available")
```

## Root cause

Per SMT-LIB, `re.range` over an argument that is **not a single
character** denotes the empty language, so `(re.range "" X)` is
`re.none` regardless of `X` (the lower bound `""` is the empty string).

Before the *"Derive with ranges"* refactor (#9963 / #9965),
`seq_rewriter::mk_re_range` recognised this through several emptiness
checks, including a concrete non-single-character test and a `max_length
== 0` test:

```cpp
if (str().is_string(lo, slo) && slo.length() != 1) is_empty = true;
if (max_length(lo) == std::make_pair(true, rational(0))) is_empty = true;
if (max_length(hi) == std::make_pair(true, rational(0))) is_empty = true;
```

The refactor rewrote `mk_re_range` and kept only the `min_length(..) >
1` emptiness test (a bound provably **≥ 2** characters). That misses a
bound of length **exactly 0**: an empty-string bound has `min_length ==
0`, so it is no longer detected as empty, and `mk_re_range` returns
`BR_FAILED`, leaving `(re.range "" X)` symbolic. The new range-aware
derivative engine (`seq_derive.cpp`) then produces a *stuck* derivative
for such a range (its `is_unit_string("")` test fails), so the sequence
theory can no longer decide membership and the solver answers `unknown`
/ "model is not available".

## Fix

Restore the sound emptiness check the refactor dropped — a bound whose
`max_length` is provably `0` can never be a single character, so the
range is empty:

```cpp
// A bound that is provably of length 0 (e.g. the empty string "") can
// likewise never be a single character, so the range is empty.  Unlike a
// symbolic bound, max_length == 0 is a provable emptiness fact, so this is
// sound (it is never true for a model-dependent bound such as a variable).
if (max_length(lo) == std::make_pair(true, rational(0)))
    is_empty = true;
if (max_length(hi) == std::make_pair(true, rational(0)))
    is_empty = true;
```

This does **not** reintroduce the unsoundness the refactor guarded
against: `max_length == (true, 0)` is a *provable* emptiness fact and is
never true for a model-dependent (symbolic) bound, so `(re.range x x)`
is still correctly left symbolic (it denotes `{x}` whenever `x` is a
single character).

## Validation

Built the patched `./z3` checkout (`./configure && make -C build`) and
re-ran the benchmark with the option the snapshot capture uses
(`-T:20`):

- **Before the fix:** `z3 -T:20 small.smt2` → `unknown` + `(error "...
model is not available")` — reproduces the divergence.
- **After the fix:** `z3 -T:20 small.smt2` → `sat` + `(define-fun a ()
String "a")` — **exactly matches** the recorded oracle.

Additional checks with the rebuilt binary:
- Sibling benchmarks `iss-5134/bug.smt2` and `iss-5134/small-2.smt2`
still match their oracles.
- Symbolic bound not over-collapsed: `(str.in_re "a" (re.range x x))` →
`sat` (x = "a").
- `(re.range "" "a")` is the empty language: `(str.in_re "a" (re.range
"" "a"))` and `(str.in_re "" (re.range "" "a"))` → `unsat`.
- Ordinary ranges unaffected: `"b" ∈ (re.range "a" "c")` sat, `"d" ∈
(re.range "a" "c")` unsat, `(re.range "a" "a")` singleton.




> Generated by [Fix a Z3 snapshot-regression
divergence](https://github.com/Z3Prover/bench/actions/runs/28731229299)
· 592.3 AIC · ⌖ 39.1 AIC · ⊞ 8.9K ·
[◷](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.63, model: claude-opus-4.8, id:
28731229299, workflow_id: snapshot-regression-fixer, run:
https://github.com/Z3Prover/bench/actions/runs/28731229299 -->

<!-- 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-07-05 13:05:38 -07:00