3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-06-20 15:40:37 +00:00
z3/src/sat/smt/array_solver.cpp
Copilot 5dfe836d38 Fix constant array UNSAT missed for small-domain store chains (#9907)
Z3 incorrectly returns SAT for formulas like `store(store(const(x), i0,
e0), i1, e1) = store(store(const(y), i0, e0), i1, e1) ∧ x ≠ y` when the
index sort has a small finite domain (e.g. `(_ BitVec 2)` = 4 elements).
The quantifier-based encoding of the same constraint correctly returns
UNSAT.

## Changes

### `src/sat/smt/array_solver.cpp` — `add_parent_lambda()`

When a store is added to an array's `m_parent_lambdas` after that array
already has `m_has_default = true` (i.e., a `default(array)` term
already exists in the e-graph), the default axiom for the new store was
silently dropped. This breaks the propagation chain:

```
default(const(x)) = x
  ↓ [via store default axiom]
default(store(const(x), i, v)) = x
  ↓ [via store default axiom]
default(store(store(const(x), ...), ...)) = x
  ↓ [EUF congruence from store equality]
x = y  →  contradiction
```

Fix: push `default_axiom(lambda)` in `add_parent_lambda` when
`m_has_default` is already set, so late-arriving stores still get their
default axioms instantiated.

```cpp
void solver::add_parent_lambda(theory_var v_child, euf::enode* lambda) {
    auto& d = get_var_data(find(v_child));
    ctx.push_vec(d.m_parent_lambdas, lambda);
    if (should_prop_upward(d))
        propagate_select_axioms(d, lambda);
    if (d.m_has_default)           // new: fire default axiom retroactively
        push_axiom(default_axiom(lambda));
}
```

### Known remaining gap

`mk_eq_core` in `array_rewriter.cpp` also has a related issue: the
unconditional store-chain expansion fires only when `domain_size >
num_lhs + num_rhs` (line 893), but the correct threshold is `domain_size
> max(num_lhs, num_rhs)`. With 4-element domain and 2 stores per side,
`2+2 = 4` equals the domain size so the expansion is skipped. The
variant gated by `m_expand_store_eq` already uses `max()` correctly
(line 911); the unconditional path needs the same fix.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-06-19 17:36:25 -07:00

288 lines
9.4 KiB
C++

/*++
Copyright (c) 2020 Microsoft Corporation
Module Name:
array_solver.cpp
Abstract:
Theory plugin for arrays
Author:
Nikolaj Bjorner (nbjorner) 2020-09-08
Notes:
A node n has attribtes:
parent_selects: { A[i] | A ~ n }
parent_lambdas: { store(A,i,v) | A ~ n } u { map(f, .., A, ..) | A ~ n }
lambdas: { const(v) | const(v) ~ n }
u { map(f,..) | map(f,..) ~ n }
u { store(A,i,v) | store(A,i,v) ~ n }
u { as-array(f) | as-array(f) ~ n }
The attributes are used for propagation.
When n1 is merged with n2, and n1 is the new root, the attributes from n2 are added to n1.
The merge also looks for new redexes.
Let A[j] in parent_selects(n2) :
lambda in parent_lambdas(n1)
-------------------------------
lambda[j] = beta-reduce(lambda[j])
lambda in lambdas(n1)
-------------------------------
lambda[j] = beta-reduce(lambda[j])
Beta reduction rules are:
beta-reduce(store(A,j,v)[i]) = if(i = j, v, A[j])
beta-reduce(map(f,A,B)[i]) = f(A[i],B[i])
beta-reduce(as-array(f)[i]) = f(i)
beta-reduce(const(v)[i]) = v
beta-reduce((lambda x M[x])[i]) = M[i]
For enforcing
store(A,j,v)[i] = beta-reduce(store(A,j,v)[i])
only the following axiom is instantiated:
- i = j or store(A,j,v)[i] = A[i]
The other required axiom, store(A,j,v)[j] = v
is added eagerly whenever store(A,j,v) is created.
Current setup: to enforce extensionality on lambdas,
also currently, as a base-line it is eager:
A ~ B, A = lambda x. M[x]
-------------------------------
A = B => forall i . M[i] = B[i]
A hypothetical refinement could use some limited HO pattern unification steps.
For example
lambda x y z . Y z y x = lambda x y z . X x z y
-> Y = lambda x y z . X ....
--*/
#include "ast/ast_ll_pp.h"
#include "sat/smt/array_solver.h"
#include "sat/smt/euf_solver.h"
namespace array {
solver::solver(euf::solver& ctx, theory_id id) :
th_euf_solver(ctx, symbol("array"), id),
a(m),
m_sort2epsilon(m),
m_sort2diag(m),
m_find(*this),
m_hash(*this),
m_eq(*this),
m_axioms(DEFAULT_HASHTABLE_INITIAL_CAPACITY, m_hash, m_eq)
{
m_constraint = alloc(sat::constraint_base);
m_constraint->initialize(m_constraint.get(), this);
}
sat::check_result solver::check() {
force_push();
// flet<bool> _is_redundant(m_is_redundant, true);
bool turn[2] = { false, false };
turn[s().rand()(2)] = true;
for (unsigned idx = 0; idx < 2; ++idx) {
if (turn[idx] && add_delayed_axioms())
return sat::check_result::CR_CONTINUE;
else if (!turn[idx] && add_interface_equalities())
return sat::check_result::CR_CONTINUE;
}
if (m_delay_qhead < m_axiom_trail.size())
return sat::check_result::CR_CONTINUE;
if (!check_lambdas())
return sat::check_result::CR_GIVEUP;
// validate_check();
return sat::check_result::CR_DONE;
}
void solver::pop_core(unsigned n) {
th_euf_solver::pop_core(n);
m_var_data.resize(get_num_vars());
}
euf::th_solver* solver::clone(euf::solver& dst_ctx) {
auto* result = alloc(solver, dst_ctx, get_id());
for (unsigned i = 0; i < get_num_vars(); ++i)
result->mk_var(ctx.copy(dst_ctx, var2enode(i)));
return result;
}
void solver::new_eq_eh(euf::th_eq const& eq) {
force_push();
m_find.merge(eq.v1(), eq.v2());
}
void solver::new_diseq_eh(euf::th_eq const& eq) {
force_push();
euf::enode* n1 = var2enode(eq.v1());
euf::enode* n2 = var2enode(eq.v2());
if (is_array(n1))
push_axiom(extensionality_axiom(n1, n2));
}
bool solver::unit_propagate() {
if (m_qhead == m_axiom_trail.size())
return false;
force_push();
bool prop = false;
ctx.push(value_trail<unsigned>(m_qhead));
for (; m_qhead < m_axiom_trail.size() && !s().inconsistent(); ++m_qhead)
if (propagate_axiom(m_qhead))
prop = true;
return prop;
}
void solver::merge_eh(theory_var v1, theory_var v2, theory_var, theory_var) {
euf::enode* n1 = var2enode(v1);
euf::enode* n2 = var2enode(v2);
TRACE(array, tout << "merge: " << ctx.bpp(n1) << " == " << ctx.bpp(n2) << "\n";);
SASSERT(n1->get_root() == n2->get_root());
SASSERT(v1 == find(v1));
expr* e1 = n1->get_expr();
expr* e2 = n2->get_expr();
auto& d1 = get_var_data(v1);
auto& d2 = get_var_data(v2);
if (d2.m_prop_upward && !d1.m_prop_upward)
set_prop_upward(v1);
if (d1.m_has_default && !d2.m_has_default)
add_parent_default(v2);
if (!d1.m_has_default && d2.m_has_default)
add_parent_default(v1);
for (euf::enode* lambda : d2.m_lambdas)
add_lambda(v1, lambda);
for (euf::enode* lambda : d2.m_parent_lambdas)
add_parent_lambda(v1, lambda);
for (euf::enode* select : d2.m_parent_selects)
add_parent_select(v1, select);
if (is_lambda(e1) || is_lambda(e2))
push_axiom(congruence_axiom(n1, n2));
}
void solver::add_parent_select(theory_var v_child, euf::enode* select) {
SASSERT(a.is_select(select->get_expr()));
SASSERT(select->get_arg(0)->get_sort() == var2expr(v_child)->get_sort());
v_child = find(v_child);
ctx.push_vec(get_var_data(v_child).m_parent_selects, select);
euf::enode* child = var2enode(v_child);
TRACE(array, tout << "v" << v_child << " - " << ctx.bpp(select) << " " << ctx.bpp(child) << " prop: " << should_prop_upward(get_var_data(v_child)) << "\n";);
TRACE(array, tout << "can beta reduce " << can_beta_reduce(child) << "\n";);
if (can_beta_reduce(child))
push_axiom(select_axiom(select, child));
propagate_parent_select_axioms(v_child);
}
void solver::add_lambda(theory_var v, euf::enode* lambda) {
SASSERT(can_beta_reduce(lambda));
auto& d = get_var_data(find(v));
if (should_set_prop_upward(d))
set_prop_upward(d);
ctx.push_vec(d.m_lambdas, lambda);
propagate_select_axioms(d, lambda);
if (should_set_prop_upward(d))
set_prop_upward_store(lambda);
}
void solver::add_parent_lambda(theory_var v_child, euf::enode* lambda) {
SASSERT(can_beta_reduce(lambda));
auto& d = get_var_data(find(v_child));
ctx.push_vec(d.m_parent_lambdas, lambda);
if (should_prop_upward(d))
propagate_select_axioms(d, lambda);
if (d.m_has_default)
push_axiom(default_axiom(lambda));
}
void solver::add_parent_default(theory_var v) {
auto& d = get_var_data(find(v));
ctx.push(value_trail(d.m_has_default));
d.m_has_default = true;
for (euf::enode* lambda : d.m_lambdas)
push_axiom(default_axiom(lambda));
propagate_parent_default(v);
}
void solver::propagate_select_axioms(var_data const& d, euf::enode* lambda) {
for (euf::enode* select : d.m_parent_selects)
push_axiom(select_axiom(select, lambda));
}
void solver::propagate_parent_default(theory_var v) {
auto& d = get_var_data(find(v));
for (euf::enode* lambda : d.m_parent_lambdas)
push_axiom(default_axiom(lambda));
}
void solver::propagate_parent_select_axioms(theory_var v) {
v = find(v);
expr* e = var2expr(v);
if (!a.is_array(e))
return;
auto& d = get_var_data(v);
for (euf::enode* lambda : d.m_lambdas)
propagate_select_axioms(d, lambda);
if (!should_prop_upward(d))
return;
for (euf::enode* lambda : d.m_parent_lambdas)
propagate_select_axioms(d, lambda);
}
void solver::set_prop_upward(theory_var v) {
auto& d = get_var_data(find(v));
if (d.m_prop_upward)
return;
ctx.push(reset_flag_trail(d.m_prop_upward));
d.m_prop_upward = true;
if (should_prop_upward(d))
propagate_parent_select_axioms(v);
set_prop_upward(d);
}
void solver::set_prop_upward_store(euf::enode* n) {
if (a.is_store(n->get_expr()))
set_prop_upward(n->get_arg(0)->get_th_var(get_id()));
}
void solver::set_prop_upward(var_data& d) {
for (auto* p : d.m_lambdas)
set_prop_upward_store(p);
}
/**
\brief Return the size of the equivalence class for array terms
that can be expressed as \lambda i : Index . [.. (select a i) ..]
*/
unsigned solver::get_lambda_equiv_size(var_data const& d) const {
return d.m_parent_selects.size() + 2 * d.m_lambdas.size();
}
bool solver::should_set_prop_upward(var_data const& d) const {
return get_config().m_array_always_prop_upward || get_lambda_equiv_size(d) >= 1;
}
bool solver::should_prop_upward(var_data const& d) const {
return !get_config().m_array_delay_exp_axiom && d.m_prop_upward;
}
bool solver::can_beta_reduce(expr* c) const {
return a.is_const(c) || a.is_as_array(c) || a.is_store(c) || is_lambda(c) || is_map_combinator(c);
}
}