From 5dfe836d388cf59b8b4f7f7a3fb41e6b6e2dce4a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:31:44 -0600 Subject: [PATCH] Fix constant array UNSAT missed for small-domain store chains (#9907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/sat/smt/array_solver.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sat/smt/array_solver.cpp b/src/sat/smt/array_solver.cpp index dca1b3f51..d955a29d0 100644 --- a/src/sat/smt/array_solver.cpp +++ b/src/sat/smt/array_solver.cpp @@ -203,6 +203,8 @@ namespace array { 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) {