3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-08-03 04:33:28 +00:00

seq_monadic: add unsat-core extraction with minimization toggle

check() now records the dependencies of an unsat subset in m_core, exposed via
ptr_vector<u_dependency> const& core(). On l_false it calls minimize_core:
with the new m_min_core flag (set_min_core, default true) it deletion-minimizes
to a minimal unsat subset containing only constraints that participate in the
contradiction; with the flag off it returns all membership dependencies.

Add unit tests asserting the core omits irrelevant constraints (e.g. x in a*,
x in ~a*, y in b* has core {x-constraints} only), exercising both flag states.
The test harness runs with minimization disabled by default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 57b9b87e-950a-49ea-bbb3-ed585646a5a9
This commit is contained in:
Nikolaj Bjorner 2026-07-31 20:53:30 -07:00
parent 1e09b4e6ae
commit 1ba30df028
3 changed files with 141 additions and 4 deletions

View file

@ -424,10 +424,8 @@ void seq_monadic::add(expr* term, expr* regex, u_dependency* d) {
m_memberships.push_back({ expr_ref(term, m), expr_ref(regex, m), d });
}
lbool seq_monadic::check() {
lbool seq_monadic::decide(vector<std::tuple<expr_ref, expr_ref, u_dependency*>> const& memberships) {
m_model.reset();
vector<std::tuple<expr_ref, expr_ref, u_dependency*>> memberships;
memberships.swap(m_memberships); // consume the asserted memberships
if (memberships.empty())
return l_true; // empty conjunction is vacuously true
m_pin.reset();
@ -463,3 +461,42 @@ lbool seq_monadic::check() {
}
return decide_dnf(combined);
}
void seq_monadic::minimize_core(vector<std::tuple<expr_ref, expr_ref, u_dependency*>> const& memberships) {
m_core.reset();
if (!m_min_core) {
// No minimization: the core is simply every asserted membership's dependency.
for (auto const& [term, regex, d] : memberships)
if (d)
m_core.push_back(d);
return;
}
// Deletion-based minimization: start from the full unsat set and try to drop each
// membership; a membership is kept only if removing it makes the set no longer
// provably unsat. The result is a minimal unsat subset (relevant constraints only).
vector<std::tuple<expr_ref, expr_ref, u_dependency*>> keep(memberships);
unsigned i = 0;
while (i < keep.size()) {
vector<std::tuple<expr_ref, expr_ref, u_dependency*>> trial;
for (unsigned j = 0; j < keep.size(); ++j)
if (j != i)
trial.push_back(keep[j]);
if (decide(trial) == l_false)
keep.swap(trial); // membership i is not needed for unsat
else
++i; // membership i is needed; keep it
}
for (auto const& [term, regex, d] : keep)
if (d)
m_core.push_back(d);
}
lbool seq_monadic::check() {
m_core.reset();
vector<std::tuple<expr_ref, expr_ref, u_dependency*>> memberships;
memberships.swap(m_memberships); // consume the asserted memberships
lbool r = decide(memberships);
if (r == l_false)
minimize_core(memberships);
return r;
}

View file

@ -80,9 +80,11 @@ private:
unsigned m_budget = 0; // global work budget (decompose disjuncts + product pops)
bool m_giveup = false; // set when the budget is exhausted
bool m_gen_model = true; // whether solve()/check() extract a feasible model
bool m_min_core = true; // whether check() minimizes the unsat core (else: all deps)
obj_map<expr, expr*> m_model; // last extracted model (var -> witness); see get_model()
obj_map<expr, expr_ref_pair_vector*> m_cofactor_cache; // memoizes derivative_cofactors per regex
vector<std::tuple<expr_ref, expr_ref, u_dependency*>> m_memberships; // asserted (term in regex, dep) for check()
ptr_vector<u_dependency> m_core; // dependencies of an unsat subset, filled by check() on l_false
seq_util& u() const { return m_rw.u(); }
seq_util::rex& re() const { return m_rw.u().re; }
@ -141,6 +143,15 @@ private:
// (var -> witness).
lbool decide_dnf(vector<disjunct> const& dnf);
// Decide a CONJUNCTION of memberships jointly (the core algorithm behind check()):
// multiplies the per-membership DNFs and decides emptiness. Does not touch
// m_memberships or m_core; fills m_model on l_true when model generation is enabled.
lbool decide(vector<std::tuple<expr_ref, expr_ref, u_dependency*>> const& memberships);
// Given an unsatisfiable membership set, extract a minimal unsatisfiable subset by
// deletion and collect the (non-null) dependencies of its members into m_core.
void minimize_core(vector<std::tuple<expr_ref, expr_ref, u_dependency*>> const& memberships);
public:
seq_monadic(seq_rewriter& rw, transition_mode mode = transition_mode::light_antimirov) :
m(rw.m()), m_rw(rw), m_thrw(rw.m()), m_mode(mode), m_pin(rw.m()) {}
@ -164,6 +175,10 @@ public:
// l_true = sat, l_false = unsat, l_undef = unsupported shape / gave up.
lbool solve(expr* term, expr* R);
// Enable/disable unsat-core minimization (default: enabled). When disabled, core()
// returns the dependencies of all asserted memberships (no deletion-based shrinking).
void set_min_core(bool b) { m_min_core = b; }
// Assert a membership (term in regex) to be decided jointly by the next check().
// `d` carries the dependency used for unsat-core tracking and may be nullptr.
// Memberships accumulate until check() consumes them.
@ -176,6 +191,11 @@ public:
// union of DNFs; a negated membership ~(t in R) is just t in complement(R)).
// Per-variable extra constraints are expressed as extra memberships (v in R').
// Consumes the asserted memberships. l_true = sat (empty conjunction is sat),
// l_false = unsat, l_undef = gave up.
// l_false = unsat, l_undef = gave up. On l_false, core() holds the dependencies
// of a minimal unsatisfiable subset.
lbool check();
// Dependencies of a minimal unsatisfiable subset from the last check() that returned
// l_false (nullptr dependencies are omitted). Empty otherwise.
ptr_vector<u_dependency> const& core() const { return m_core; }
};