mirror of
https://github.com/Z3Prover/z3
synced 2026-08-08 15:02:09 +00:00
# Conflicts: # src/ast/rewriter/seq_monadic.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 57b9b87e-950a-49ea-bbb3-ed585646a5a9
1111 lines
40 KiB
C++
1111 lines
40 KiB
C++
/*++
|
|
Copyright (c) 2026 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
seq_monadic.cpp
|
|
|
|
Abstract:
|
|
|
|
Whole-language monadic decomposition for regex membership. See seq_monadic.h.
|
|
Automaton-based (product-reachability); reach(q) is never materialized as a regex, and
|
|
the disjunction produced by the decomposition is never materialized as a DNF: it is
|
|
explored as a depth-first search tree with per-variable emptiness pruning.
|
|
|
|
Generic in the element sort. The decomposition, liveness and product-reachability
|
|
are element-agnostic; only the *guard algebra* over the derivative cofactor guards
|
|
depends on the element sort. For the character sort it is the exact, compact
|
|
seq::range_predicate; for any other element sort it is a candidate-basis over the
|
|
element values mentioned by the guards (sound and complete for the
|
|
{true,false,=,<=,and,or,not} grammar the derivatives emit). The same guard algebra
|
|
yields the concrete element used to build a witness sequence.
|
|
|
|
TODOs:
|
|
- create a validation harness: expose certificates for correctness that can be checked.
|
|
- take into account shape of terms to prune the search space (e.g., if the term is xax, then retain the effect of
|
|
intersecting with .*a.*).
|
|
- connect to semi-linear pruning, such as xx in (ab)*a is unsat due to parity
|
|
- support units of non-values (element variables).
|
|
Model construction would assign values to the elements.
|
|
- make unsat core tracking less naive by tracking dependencies at a finer grain.
|
|
- add selective tracing TRACE(seq, ..).
|
|
- revisit DFS to select next membership constraint to explore base on the current state.
|
|
In the current state include current set of variable intersection membership constraints.
|
|
The next membership constraint to explore is preferrably for a variable that was just
|
|
explored and we can check the variable intersection membership constraints if the new
|
|
expansion is feasible. Constant characters are consumed at the same time to also prune
|
|
the choice.
|
|
- separate out "live-state" and enumerator over reachable live states:
|
|
- make it share live states between callers.
|
|
- make it expose an iterator instead of using vectors of live states to allow on-demand expansion of live states.
|
|
- make use of DFS exploration of derivatives to extract live states without visiting all states up front.
|
|
- use it in seq_regex legacy mode that also has this notion.
|
|
|
|
|
|
|
|
Author:
|
|
|
|
Nikolaj Bjorner / Margus Veanes 2026
|
|
|
|
--*/
|
|
|
|
#include "ast/rewriter/seq_monadic.h"
|
|
#include "ast/rewriter/guard_set.h"
|
|
#include <set>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <tuple>
|
|
#include <functional>
|
|
#include <algorithm>
|
|
#include <unordered_set>
|
|
|
|
namespace {
|
|
char const* mode_name(seq::transition_mode mode) {
|
|
switch (mode) {
|
|
case seq::transition_mode::brzozowski_tm:
|
|
return "brzozowski";
|
|
case seq::transition_mode::light_antimirov_tm:
|
|
return "light-antimirov";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
char const* bail_name(unsigned i) {
|
|
static char const* const names[] = {
|
|
"unsupported",
|
|
"state-cap",
|
|
"dnf-cap",
|
|
"budget",
|
|
"resource",
|
|
"nullability",
|
|
"guard"
|
|
};
|
|
return i < std::size(names) ? names[i] : "unknown";
|
|
}
|
|
|
|
char const* result_name(lbool r) {
|
|
return r == l_true ? "sat" : r == l_false ? "unsat" : "unknown";
|
|
}
|
|
}
|
|
|
|
|
|
expr_ref seq_monadic::der_elem(expr* r, expr* elem) {
|
|
expr* cached = nullptr;
|
|
if (m_der_cache.find(r, elem, cached))
|
|
return expr_ref(cached, m);
|
|
expr_ref d = m_rw.mk_derivative(elem, r); // mk_derivative(element, regex)
|
|
// Normalize: for a general element sort the derivative by a non-matching constant can
|
|
// leave a ground guard (e.g. (= 1 2)) unfolded; simplifying collapses such dead
|
|
// branches to re.empty so nullability/emptiness stay decidable.
|
|
expr_ref d2(m);
|
|
m_thrw(d, d2);
|
|
m_pin.push_back(r); // keep the cache keys and value alive
|
|
m_pin.push_back(elem);
|
|
m_pin.push_back(d2);
|
|
m_der_cache.insert(r, elem, d2);
|
|
return d2;
|
|
}
|
|
|
|
lbool seq_monadic::nullable(expr* r) {
|
|
// Nullability is a structural property, and the seq plugin already computes it as
|
|
// part of the regex info -- cached by expr id and, unlike seq_rewriter's op_cache
|
|
// (capped at 10000 and flushed whole), never evicted. Use it whenever it is
|
|
// determined; only fall back to building the symbolic nullability formula for the
|
|
// regexes whose info leaves it undetermined.
|
|
lbool i = re().get_info(r).nullable;
|
|
if (i != l_undef)
|
|
return i;
|
|
char v = 0;
|
|
if (m_nullable_cache.find(r, v))
|
|
return v == 1 ? l_true : v == 0 ? l_false : l_undef;
|
|
expr_ref nb = m_rw.is_nullable(r);
|
|
lbool res = m.is_true(nb) ? l_true : m.is_false(nb) ? l_false : l_undef;
|
|
m_pin.push_back(r);
|
|
m_nullable_cache.insert(r, res == l_true ? 1 : res == l_false ? 0 : 2);
|
|
return res;
|
|
}
|
|
|
|
expr_ref_pair_vector const& seq_monadic::derivative_cofactors(expr* r) {
|
|
++m_stats.m_cofactor_calls;
|
|
return m_rw.get_derive().get_cached_cofactors(m_config.m_mode, r);
|
|
}
|
|
|
|
bool seq_monadic::live_states(expr* R, expr_ref_vector& out) {
|
|
obj_map<expr, unsigned> id;
|
|
expr_ref_vector states(m);
|
|
vector<svector<unsigned>> succ;
|
|
bool_vector maybe_null;
|
|
auto intern = [&](expr* s) -> unsigned {
|
|
unsigned k;
|
|
if (id.find(s, k)) return k;
|
|
k = states.size();
|
|
id.insert(s, k);
|
|
states.push_back(s);
|
|
succ.push_back(svector<unsigned>());
|
|
maybe_null.push_back(nullable(s) != l_false); // unknown nullability => keep (conservative)
|
|
return k;
|
|
};
|
|
intern(R);
|
|
const unsigned STATE_CAP = 1u << 12;
|
|
for (unsigned i = 0; i < states.size(); ++i) {
|
|
if (states.size() > STATE_CAP) {
|
|
m_stats.inc_bail(bail_reason::state_cap);
|
|
return false;
|
|
}
|
|
if (!m.inc()) {
|
|
m_stats.inc_bail(bail_reason::resource);
|
|
return false;
|
|
}
|
|
expr_ref_pair_vector const& cof = derivative_cofactors(states.get(i));
|
|
for (auto const& [g, t] : cof) {
|
|
if (re().is_empty(t)) continue;
|
|
unsigned k = intern(t); // MUST precede succ[i] indexing: intern may
|
|
succ[i].push_back(k); // grow (realloc) succ, invalidating succ[i]&
|
|
}
|
|
}
|
|
unsigned n = states.size();
|
|
bool_vector live;
|
|
live.resize(n, false);
|
|
for (unsigned i = 0; i < n; ++i)
|
|
live[i] = maybe_null[i];
|
|
for (bool ch = true; ch; ) {
|
|
ch = false;
|
|
for (unsigned i = 0; i < n; ++i)
|
|
if (!live[i])
|
|
for (unsigned j : succ[i])
|
|
if (live[j]) { live[i] = true; ch = true; break; }
|
|
}
|
|
for (unsigned i = 0; i < n; ++i)
|
|
if (live[i]) { out.push_back(states.get(i)); m_pin.push_back(states.get(i)); }
|
|
return true;
|
|
}
|
|
|
|
expr_ref_vector const* seq_monadic::live_states_cached(expr* R) {
|
|
expr_ref_vector* v = nullptr;
|
|
if (m_live_cache.find(R, v))
|
|
return v; // may be null: previously gave up on R
|
|
v = alloc(expr_ref_vector, m);
|
|
if (!live_states(R, *v)) {
|
|
dealloc(v);
|
|
v = nullptr;
|
|
}
|
|
m_pin.push_back(R); // keep the key alive for the cache's lifetime
|
|
m_live_cache.insert(R, v);
|
|
return v;
|
|
}
|
|
|
|
void seq_monadic::reset_live_cache() {
|
|
for (auto const& [k, v] : m_live_cache)
|
|
dealloc(v);
|
|
m_live_cache.reset();
|
|
}
|
|
|
|
lbool seq_monadic::product_nonempty(svector<component> const& comps, expr_ref* witness_word) {
|
|
unsigned n = comps.size();
|
|
if (n == 0) {
|
|
if (witness_word)
|
|
*witness_word = expr_ref(u().str.mk_empty(m_seq_sort), m);
|
|
return l_true;
|
|
}
|
|
expr_ref var0(m.mk_var(0, m_elem_sort), m); // the element variable the guards range over
|
|
|
|
typedef std::vector<unsigned> key;
|
|
struct key_hash {
|
|
size_t operator()(key const& k) const {
|
|
size_t h = 1469598103934665603ull;
|
|
for (unsigned x : k)
|
|
h = (h ^ x) * 1099511628211ull;
|
|
return h;
|
|
}
|
|
};
|
|
|
|
// Product states are held in a flat stack of stride n; `visited` owns one copy of the
|
|
// id-tuple of every discovered state. Both avoid a per-state heap allocation, which
|
|
// dominated the search when this ran per derivative step of the outer decomposition.
|
|
ptr_vector<expr> work;
|
|
std::unordered_set<key, key_hash> visited;
|
|
key kbuf;
|
|
kbuf.resize(n);
|
|
|
|
ptr_vector<expr> st;
|
|
st.resize(n);
|
|
ptr_vector<expr> cur;
|
|
cur.resize(n);
|
|
|
|
auto fill_key = [&](ptr_vector<expr> const& s) -> key const& {
|
|
for (unsigned i = 0; i < n; ++i)
|
|
kbuf[i] = s[i]->get_id();
|
|
return kbuf;
|
|
};
|
|
|
|
bool undecided = false;
|
|
auto is_accept = [&]() -> bool {
|
|
for (unsigned i = 0; i < n; ++i) {
|
|
if (comps[i].target) {
|
|
if (st[i] != comps[i].target) return false;
|
|
}
|
|
else {
|
|
lbool nb = nullable(st[i]);
|
|
if (nb == l_true) continue;
|
|
if (nb == l_false) return false;
|
|
undecided = true; return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// tree of first-discovery edges for witness reconstruction (only built when a
|
|
// witness is requested): child-key -> (parent-key, element read on the edge).
|
|
std::map<key, std::pair<key, expr*>> parent;
|
|
key start_key;
|
|
start_key.resize(n);
|
|
for (unsigned i = 0; i < n; ++i) {
|
|
work.push_back(comps[i].state);
|
|
start_key[i] = comps[i].state->get_id();
|
|
}
|
|
visited.insert(start_key);
|
|
|
|
auto reconstruct = [&](key end_key) -> expr_ref {
|
|
ptr_vector<expr> elems; // collected in accept..start order
|
|
key k = end_key;
|
|
while (k != start_key) {
|
|
auto it = parent.find(k);
|
|
if (it == parent.end()) break; // safety (should not happen)
|
|
elems.push_back(it->second.second);
|
|
k = it->second.first;
|
|
}
|
|
expr_ref_vector es(m); // start..accept order
|
|
for (unsigned idx = elems.size(); idx-- > 0; )
|
|
es.push_back(u().str.mk_unit(elems[idx]));
|
|
if (es.empty())
|
|
return expr_ref(u().str.mk_empty(m_seq_sort), m);
|
|
return expr_ref(u().str.mk_concat(es.size(), es.data(), m_seq_sort), m);
|
|
};
|
|
|
|
// Hoisted out of the search loop: the per-component cofactor vectors are owned by the
|
|
// cofactor cache and stay valid for the whole search, so they are referenced rather
|
|
// than copied (copying re-materialized every branch as expr_ref pairs on every pop).
|
|
svector<expr_ref_pair_vector const*> branches;
|
|
branches.resize(n);
|
|
key st_key;
|
|
bool bail = false;
|
|
std::function<void(unsigned, guard_set const&)> rec =
|
|
[&](unsigned i, guard_set const& acc) {
|
|
if (bail) return;
|
|
if (i == n) {
|
|
key const& ck = fill_key(cur);
|
|
if (visited.find(ck) == visited.end()) {
|
|
visited.insert(ck);
|
|
if (witness_word) {
|
|
expr_ref e(m);
|
|
if (acc.eval(&e) == l_true) {
|
|
m_pin.push_back(e);
|
|
parent[ck] = { st_key, e.get() };
|
|
}
|
|
}
|
|
for (unsigned j = 0; j < n; ++j)
|
|
work.push_back(cur[j]);
|
|
}
|
|
return;
|
|
}
|
|
for (auto const& [g, t] : *branches[i]) {
|
|
if (re().is_empty(t)) continue;
|
|
guard_set nacc = acc;
|
|
nacc.conjoin(g);
|
|
lbool ne = nacc.eval(nullptr);
|
|
if (ne == l_undef) {
|
|
m_stats.inc_bail(bail_reason::guard);
|
|
bail = true;
|
|
return;
|
|
}
|
|
if (ne == l_false) continue; // empty joint guard: prune
|
|
cur[i] = t;
|
|
rec(i + 1, nacc);
|
|
if (bail) return;
|
|
}
|
|
};
|
|
|
|
while (!work.empty()) {
|
|
if (m_budget == 0) {
|
|
m_stats.inc_bail(bail_reason::budget);
|
|
m_giveup = true;
|
|
return l_undef;
|
|
}
|
|
if (!m.inc()) {
|
|
m_stats.inc_bail(bail_reason::resource);
|
|
m_giveup = true;
|
|
return l_undef;
|
|
}
|
|
--m_budget;
|
|
for (unsigned i = n; i-- > 0; ) {
|
|
st[i] = work.back();
|
|
work.pop_back();
|
|
}
|
|
if (is_accept()) {
|
|
if (witness_word)
|
|
*witness_word = reconstruct(fill_key(st));
|
|
return l_true;
|
|
}
|
|
if (undecided) {
|
|
m_stats.inc_bail(bail_reason::nullability);
|
|
return l_undef;
|
|
}
|
|
|
|
for (unsigned i = 0; i < n; ++i)
|
|
branches[i] = &derivative_cofactors(st[i]);
|
|
|
|
// joint transitions = cartesian product of the branches with the guards
|
|
// conjoined; prune as soon as the accumulated guard is empty, bail on unknown.
|
|
if (witness_word)
|
|
st_key = fill_key(st);
|
|
guard_set top(m, u(), m_elem_sort, var0, &m_rp_cache);
|
|
rec(0, top);
|
|
if (bail)
|
|
return l_undef;
|
|
}
|
|
return l_false;
|
|
}
|
|
|
|
bool seq_monadic::parse_term(expr* t, vector<atom>& atoms) {
|
|
if (u().str.is_concat(t))
|
|
return all_of(*to_app(t), [&](expr* arg) { return parse_term(arg, atoms); });
|
|
if (u().str.is_empty(t))
|
|
return true; // epsilon: contributes nothing
|
|
zstring s;
|
|
if (u().str.is_string(t, s)) {
|
|
for (unsigned i = 0; i < s.length(); ++i) {
|
|
expr* elem = u().str.mk_char(s, i);
|
|
atoms.push_back(atom(m, false, nullptr, elem));
|
|
}
|
|
return true;
|
|
}
|
|
expr *elem = nullptr;
|
|
if (u().str.is_unit(t, elem) && m.is_value(elem)) { // seq.unit of a constant element
|
|
atoms.push_back(atom(m, false, nullptr, elem));
|
|
return true;
|
|
}
|
|
// uninterpreted constant of sequence sort => a sequence variable
|
|
if (is_var(t)) {
|
|
atoms.push_back(atom(m, true, t, nullptr));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
unsigned seq_monadic::var_index(expr* v) {
|
|
unsigned vi;
|
|
if (m_var_idx.find(v, vi))
|
|
return vi;
|
|
vi = m_vars.size();
|
|
m_var_idx.insert(v, vi);
|
|
m_vars.push_back(v);
|
|
m_groups.push_back(svector<component>());
|
|
return vi;
|
|
}
|
|
|
|
void seq_monadic::reset_search() {
|
|
m_seq_sort = nullptr;
|
|
m_elem_sort = nullptr;
|
|
m_atoms.reset();
|
|
m_regexes.reset();
|
|
m_vars.reset();
|
|
m_var_idx.reset();
|
|
m_groups.reset();
|
|
m_last_occ.reset();
|
|
m_group_cache.clear();
|
|
m_der_cache.reset();
|
|
m_nullable_cache.reset();
|
|
m_undef_vars = 0;
|
|
m_cursors.reset();
|
|
m_last_var = UINT_MAX;
|
|
reset_live_cache();
|
|
}
|
|
|
|
bool seq_monadic::prepare(membership_vec const& memberships) {
|
|
reset_search();
|
|
for (auto const& [term, regex, d] : memberships) {
|
|
if (!u().is_re(regex, m_seq_sort)) {
|
|
m_stats.inc_bail(bail_reason::unsupported);
|
|
return false;
|
|
}
|
|
if (!u().is_seq(m_seq_sort, m_elem_sort)) {
|
|
m_stats.inc_bail(bail_reason::unsupported);
|
|
return false;
|
|
}
|
|
vector<atom> atoms;
|
|
if (!parse_term(term, atoms)) {
|
|
m_stats.inc_bail(bail_reason::unsupported);
|
|
return false;
|
|
}
|
|
m_regexes.push_back(regex);
|
|
m_atoms.push_back(atoms);
|
|
m_pin.push_back(regex);
|
|
}
|
|
// A variable's component group is complete once the search passes the variable's
|
|
// last occurrence; positions are compared in search order, i.e. lexicographically
|
|
// on (membership index, atom index).
|
|
for (unsigned mi = 0; mi < m_atoms.size(); ++mi) {
|
|
vector<atom> const& atoms = m_atoms[mi];
|
|
for (unsigned i = 0; i < atoms.size(); ++i) {
|
|
if (!atoms[i].is_var)
|
|
continue;
|
|
expr* v = atoms[i].var.get();
|
|
var_index(v);
|
|
m_last_occ.insert(v, (static_cast<uint64_t>(mi) << 32) | i);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
lbool seq_monadic::group_nonempty(unsigned vi) {
|
|
svector<component> const& g = m_groups[vi];
|
|
group_sig& sig = m_sig_buf;
|
|
sig.clear();
|
|
for (auto const& c : g)
|
|
sig.push_back({ c.state->get_id(), c.target ? c.target->get_id() : UINT_MAX });
|
|
std::sort(sig.begin(), sig.end());
|
|
sig.erase(std::unique(sig.begin(), sig.end()), sig.end());
|
|
auto it = m_group_cache.find(sig);
|
|
if (it != m_group_cache.end())
|
|
return it->second;
|
|
// Collapse duplicated components: they constrain the variable identically, and the
|
|
// product search is exponential in the number of components.
|
|
svector<component> comps;
|
|
if (sig.size() == g.size())
|
|
comps = g;
|
|
else {
|
|
std::set<std::pair<unsigned, unsigned>> seen;
|
|
for (auto const& c : g)
|
|
if (seen.insert({ c.state->get_id(), c.target ? c.target->get_id() : UINT_MAX }).second)
|
|
comps.push_back(c);
|
|
}
|
|
lbool r = product_nonempty(comps, nullptr);
|
|
m_group_cache.emplace(sig, r); // sig is m_sig_buf; emplace copies it
|
|
return r;
|
|
}
|
|
|
|
lbool seq_monadic::leaf() {
|
|
if (m_undef_vars > 0)
|
|
return l_undef; // some variable's emptiness test gave up
|
|
if (!m_config.m_model)
|
|
return l_true;
|
|
m_model.reset();
|
|
for (unsigned vi = 0; vi < m_groups.size(); ++vi) {
|
|
if (m_groups[vi].empty())
|
|
continue;
|
|
expr_ref w(m);
|
|
lbool ne = product_nonempty(m_groups[vi], &w);
|
|
if (ne != l_true) { // groups were already shown non-empty;
|
|
m_model.reset(); // only reachable if the search was cut short
|
|
return ne;
|
|
}
|
|
m_pin.push_back(w);
|
|
m_model.insert(m_vars[vi], w.get());
|
|
}
|
|
return l_true;
|
|
}
|
|
|
|
lbool seq_monadic::dfs_membership(unsigned mi) {
|
|
if (mi == m_atoms.size())
|
|
return leaf();
|
|
return dfs_atoms(mi, 0, m_regexes.get(mi));
|
|
}
|
|
|
|
lbool seq_monadic::dfs_atoms(unsigned mi, unsigned i, expr* R) {
|
|
if (m_giveup)
|
|
return l_undef; // unwind the whole search, don't keep branching
|
|
if (m_budget == 0) {
|
|
m_stats.inc_bail(bail_reason::budget);
|
|
m_giveup = true;
|
|
return l_undef;
|
|
}
|
|
if (!m.inc()) {
|
|
m_stats.inc_bail(bail_reason::resource);
|
|
m_giveup = true;
|
|
return l_undef;
|
|
}
|
|
--m_budget;
|
|
vector<atom> const& atoms = m_atoms[mi];
|
|
if (i == atoms.size()) { // the rest of this membership is epsilon
|
|
lbool nb = nullable(R);
|
|
if (nb == l_true)
|
|
return dfs_membership(mi + 1);
|
|
if (nb == l_false)
|
|
return l_false;
|
|
m_stats.inc_bail(bail_reason::nullability);
|
|
return l_undef; // undecidable nullability
|
|
}
|
|
atom const& a = atoms[i];
|
|
if (!a.is_var) { // a constant element is consumed by a derivative
|
|
expr_ref d = der_elem(R, a.elem.get());
|
|
if (re().is_empty(d))
|
|
return l_false;
|
|
m_pin.push_back(d);
|
|
return dfs_atoms(mi, i + 1, d);
|
|
}
|
|
|
|
// A variable: the last atom is a plain membership in R, otherwise the variable drives
|
|
// the derivative automaton from R to some live state q, which splits the search.
|
|
bool last_atom = (i + 1 == atoms.size());
|
|
ptr_vector<expr> targets;
|
|
if (last_atom)
|
|
targets.push_back(nullptr);
|
|
else {
|
|
expr_ref_vector const* Q = live_states_cached(R);
|
|
if (!Q)
|
|
return l_undef;
|
|
for (expr* q : *Q)
|
|
targets.push_back(q);
|
|
}
|
|
|
|
unsigned vi = var_index(a.var.get());
|
|
uint64_t pos = (static_cast<uint64_t>(mi) << 32) | i;
|
|
uint64_t last = 0;
|
|
bool finalize = m_last_occ.find(a.var.get(), last) && last == pos;
|
|
bool any_undef = false;
|
|
for (expr* target : targets) {
|
|
m_groups[vi].push_back(component{ a.var.get(), R, target });
|
|
// The group's emptiness test has to be run at some point anyway; running it as
|
|
// soon as the group is complete (or as soon as it holds several components, where
|
|
// an inconsistency can first arise) prunes the entire subtree below.
|
|
lbool ne = l_true;
|
|
if (re().is_empty(R))
|
|
ne = l_false;
|
|
else if (finalize || m_groups[vi].size() > 1)
|
|
ne = group_nonempty(vi);
|
|
lbool r;
|
|
if (ne == l_false)
|
|
r = l_false;
|
|
else {
|
|
if (ne == l_undef)
|
|
++m_undef_vars;
|
|
r = last_atom ? dfs_membership(mi + 1) : dfs_atoms(mi, i + 1, target);
|
|
if (ne == l_undef)
|
|
--m_undef_vars;
|
|
}
|
|
m_groups[vi].pop_back();
|
|
if (r == l_true)
|
|
return l_true;
|
|
if (r == l_undef) {
|
|
if (m_giveup)
|
|
return l_undef;
|
|
any_undef = true;
|
|
}
|
|
}
|
|
return any_undef ? l_undef : l_false;
|
|
}
|
|
|
|
// ---- state-based search driver ------------------------------------------------------
|
|
//
|
|
// This is an alternative to the strictly positional dfs_membership/dfs_atoms above. The
|
|
// positional search finishes membership 0 entirely, then membership 1, and so on, so two
|
|
// memberships that share a variable only intersect that variable's components deep in the
|
|
// tree -- after the first membership's alignment was chosen blindly. The state-based
|
|
// search keeps a *cursor* per membership and, at each step, expands ONE variable across
|
|
// ALL memberships whose current head is that variable, intersecting the per-variable
|
|
// components (m_groups) immediately. An infeasible choice for a shared variable is thus
|
|
// pruned as soon as it is made, rather than after committing to a full membership.
|
|
//
|
|
// A search state is:
|
|
// - the set of active (non-complete) cursors == active membership constraints,
|
|
// - the per-variable component groups (m_groups) == variable intersection constraints,
|
|
// - the last expanded variable (m_last_var) == locality hint for the next choice.
|
|
// Every non-complete cursor has a variable head (leading constants are eagerly consumed by
|
|
// advance_cursor / initial_normalize). The state is complete when every cursor is
|
|
// complete, and accepting when additionally every variable group is non-empty.
|
|
|
|
lbool seq_monadic::advance_cursor(cursor& c, unsigned mi, expr* target) {
|
|
vector<atom> const& atoms = m_atoms[mi];
|
|
// Step past the head variable. target == null encodes "the variable is the last atom",
|
|
// i.e. a plain membership component: nothing follows, the cursor is complete.
|
|
if (!target) {
|
|
c.i = atoms.size();
|
|
c.complete = true;
|
|
return l_true;
|
|
}
|
|
c.i += 1;
|
|
c.R = target;
|
|
// Eagerly consume the constant atoms following the variable (mirrors dfs_atoms walking
|
|
// a run of constants via der_elem), so that the cursor again exposes a variable head.
|
|
while (c.i < atoms.size() && !atoms[c.i].is_var) {
|
|
expr_ref d = der_elem(c.R, atoms[c.i].elem.get());
|
|
if (re().is_empty(d))
|
|
return l_false; // dead: this continuation is empty
|
|
m_pin.push_back(d);
|
|
c.R = d;
|
|
c.i += 1;
|
|
}
|
|
if (c.i == atoms.size()) { // the remaining tail is epsilon
|
|
c.complete = true;
|
|
lbool nb = nullable(c.R);
|
|
if (nb == l_false)
|
|
return l_false;
|
|
if (nb == l_undef) {
|
|
m_stats.inc_bail(bail_reason::nullability);
|
|
return l_undef; // tail nullability undecidable
|
|
}
|
|
return l_true;
|
|
}
|
|
c.complete = false; // stopped on a variable head
|
|
return l_true;
|
|
}
|
|
|
|
lbool seq_monadic::initial_normalize() {
|
|
for (unsigned mi = 0; mi < m_cursors.size(); ++mi) {
|
|
cursor& c = m_cursors[mi];
|
|
vector<atom> const& atoms = m_atoms[mi];
|
|
while (c.i < atoms.size() && !atoms[c.i].is_var) {
|
|
expr_ref d = der_elem(c.R, atoms[c.i].elem.get());
|
|
if (re().is_empty(d))
|
|
return l_false; // this membership is already empty
|
|
m_pin.push_back(d);
|
|
c.R = d;
|
|
c.i += 1;
|
|
}
|
|
// prepare() guarantees every membership has a variable, so c.i now points at a
|
|
// variable head (c.complete stays false). A membership of only constants would
|
|
// have been rejected by prepare().
|
|
c.complete = (c.i == atoms.size());
|
|
if (c.complete) {
|
|
// Defensive: no variable head (shouldn't happen); require the tail nullable.
|
|
lbool nb = nullable(c.R);
|
|
if (nb == l_false)
|
|
return l_false;
|
|
if (nb == l_undef)
|
|
++m_undef_vars;
|
|
}
|
|
}
|
|
return l_true;
|
|
}
|
|
|
|
lbool seq_monadic::accept_state() {
|
|
if (m_undef_vars > 0)
|
|
return l_undef; // some group / tail nullability gave up
|
|
if (!m_config.m_model)
|
|
return l_true; // groups already shown non-empty
|
|
m_model.reset();
|
|
for (unsigned vi = 0; vi < m_groups.size(); ++vi) {
|
|
if (m_groups[vi].empty())
|
|
continue;
|
|
expr_ref w(m);
|
|
lbool ne = product_nonempty(m_groups[vi], &w);
|
|
if (ne != l_true) {
|
|
m_model.reset();
|
|
return ne;
|
|
}
|
|
m_pin.push_back(w);
|
|
m_model.insert(m_vars[vi], w.get());
|
|
}
|
|
return l_true;
|
|
}
|
|
|
|
lbool seq_monadic::search() {
|
|
if (m_giveup)
|
|
return l_undef;
|
|
if (m_budget == 0) {
|
|
m_stats.inc_bail(bail_reason::budget);
|
|
m_giveup = true;
|
|
return l_undef;
|
|
}
|
|
if (!m.inc()) {
|
|
m_stats.inc_bail(bail_reason::resource);
|
|
m_giveup = true;
|
|
return l_undef;
|
|
}
|
|
--m_budget;
|
|
|
|
// Gather the head variables of the active cursors and how often each occurs as a head.
|
|
unsigned best_vi = UINT_MAX, best_cnt = 0;
|
|
obj_map<expr, unsigned> head_cnt;
|
|
for (unsigned mi = 0; mi < m_cursors.size(); ++mi) {
|
|
cursor const& c = m_cursors[mi];
|
|
if (c.complete)
|
|
continue;
|
|
expr* v = m_atoms[mi][c.i].var.get();
|
|
unsigned cnt = 0;
|
|
head_cnt.find(v, cnt);
|
|
head_cnt.insert(v, ++cnt);
|
|
unsigned vi = m_var_idx[v];
|
|
// Prefer the most frequent head variable; break ties toward the smallest index so
|
|
// the choice is deterministic. m_last_var (locality) is applied afterwards.
|
|
if (cnt > best_cnt || (cnt == best_cnt && (best_vi == UINT_MAX || vi < best_vi))) {
|
|
best_cnt = cnt;
|
|
best_vi = vi;
|
|
}
|
|
}
|
|
if (best_vi == UINT_MAX)
|
|
return accept_state(); // every cursor complete
|
|
|
|
// Locality: if the last expanded variable is still an active head, expand it next --
|
|
// its freshly chosen continuation can be checked against the intersection immediately.
|
|
unsigned vi = best_vi;
|
|
if (m_last_var != UINT_MAX && m_last_var < m_vars.size()) {
|
|
unsigned lc = 0;
|
|
if (head_cnt.find(m_vars[m_last_var], lc) && lc > 0)
|
|
vi = m_last_var;
|
|
}
|
|
|
|
// All cursors whose current head is variable vi are expanded together at this step.
|
|
svector<unsigned> S;
|
|
expr* vv = m_vars[vi];
|
|
for (unsigned mi = 0; mi < m_cursors.size(); ++mi) {
|
|
cursor const& c = m_cursors[mi];
|
|
if (!c.complete && m_atoms[mi][c.i].var.get() == vv)
|
|
S.push_back(mi);
|
|
}
|
|
return choose_cont(vi, S, 0);
|
|
}
|
|
|
|
lbool seq_monadic::choose_cont(unsigned vi, svector<unsigned> const& S, unsigned k) {
|
|
if (m_giveup)
|
|
return l_undef;
|
|
if (k == S.size()) {
|
|
unsigned saved = m_last_var;
|
|
m_last_var = vi;
|
|
lbool r = search();
|
|
m_last_var = saved;
|
|
return r;
|
|
}
|
|
unsigned mi = S[k];
|
|
cursor& c = m_cursors[mi];
|
|
vector<atom> const& atoms = m_atoms[mi];
|
|
expr* R = c.R;
|
|
uint64_t pos = (static_cast<uint64_t>(mi) << 32) | c.i;
|
|
uint64_t last = 0;
|
|
bool finalize = m_last_occ.find(atoms[c.i].var.get(), last) && last == pos;
|
|
bool last_atom = (c.i + 1 == atoms.size());
|
|
|
|
// Enumerate this cursor's continuations for variable vi: a plain membership (null) when
|
|
// the variable is the last atom, otherwise every live reach target of R.
|
|
ptr_vector<expr> targets;
|
|
if (last_atom)
|
|
targets.push_back(nullptr);
|
|
else {
|
|
expr_ref_vector const* Q = live_states_cached(R);
|
|
if (!Q)
|
|
return l_undef; // gave up enumerating targets
|
|
for (expr* q : *Q)
|
|
targets.push_back(q);
|
|
}
|
|
|
|
bool any_undef = false;
|
|
for (expr* target : targets) {
|
|
m_groups[vi].push_back(component{ atoms[c.i].var.get(), R, target });
|
|
// Intersect immediately: prune as soon as vi's accumulated components are empty.
|
|
// The test is forced once the group is complete (past vi's last occurrence) so the
|
|
// accepting state does not need to re-verify; running it earlier (size > 1) prunes.
|
|
lbool ne;
|
|
if (re().is_empty(R))
|
|
ne = l_false;
|
|
else if (finalize || m_groups[vi].size() > 1)
|
|
ne = group_nonempty(vi);
|
|
else
|
|
ne = l_true;
|
|
if (ne == l_false) {
|
|
m_groups[vi].pop_back();
|
|
continue; // infeasible continuation for vi: prune
|
|
}
|
|
cursor saved = c; // save/restore cursor across the branch
|
|
lbool adv = advance_cursor(c, mi, target);
|
|
if (adv == l_false) {
|
|
c = saved;
|
|
m_groups[vi].pop_back();
|
|
continue;
|
|
}
|
|
unsigned undef_here = (ne == l_undef ? 1u : 0u) + (adv == l_undef ? 1u : 0u);
|
|
m_undef_vars += undef_here;
|
|
lbool r = choose_cont(vi, S, k + 1);
|
|
m_undef_vars -= undef_here;
|
|
c = saved;
|
|
m_groups[vi].pop_back();
|
|
if (r == l_true)
|
|
return l_true;
|
|
if (r == l_undef) {
|
|
if (m_giveup)
|
|
return l_undef;
|
|
any_undef = true;
|
|
}
|
|
}
|
|
return any_undef ? l_undef : l_false;
|
|
}
|
|
|
|
lbool seq_monadic::decide(membership_vec const& memberships) {
|
|
m_last_search_memberships = memberships;
|
|
m_model.reset();
|
|
reset_search(); // clear the caches before dropping the
|
|
m_pin.reset(); // pins that keep their keys alive
|
|
m_rp_cache.maybe_reset(1u << 16);
|
|
m_rw.get_derive().maybe_reset_cached_cofactors(1u << 16);
|
|
m_budget = 200000;
|
|
m_giveup = false;
|
|
lbool r = l_true; // empty conjunction is vacuously true
|
|
if (!memberships.empty() && !prepare(memberships))
|
|
r = l_undef;
|
|
else if (!memberships.empty()) {
|
|
if (m_config.m_state_search) {
|
|
// Build one cursor per membership at its regex start; initial_normalize consumes
|
|
// leading constants so every active cursor exposes a variable head.
|
|
m_cursors.reset();
|
|
for (unsigned mi = 0; mi < m_atoms.size(); ++mi)
|
|
m_cursors.push_back(cursor{ 0, m_regexes.get(mi), false });
|
|
m_last_var = UINT_MAX;
|
|
lbool norm = initial_normalize();
|
|
r = (norm == l_false) ? l_false : search();
|
|
}
|
|
else
|
|
r = dfs_membership(0);
|
|
}
|
|
if (r != l_true)
|
|
m_model.reset();
|
|
m_last_search_result = r;
|
|
return r;
|
|
}
|
|
|
|
lbool seq_monadic::solve(expr* term, expr* R) {
|
|
m_core.reset();
|
|
membership_vec mv;
|
|
mv.push_back({ expr_ref(term, m), expr_ref(R, m), nullptr });
|
|
m_last_result = decide(mv);
|
|
return m_last_result;
|
|
}
|
|
|
|
void seq_monadic::add(expr* term, expr* regex, void* d) {
|
|
m_memberships.push_back({ expr_ref(term, m), expr_ref(regex, m), d });
|
|
m_undo_trail.push(push_back_vector(m_memberships));
|
|
}
|
|
|
|
namespace {
|
|
// Restores a membership term on backtrack. The previous term is pinned by this trail
|
|
// object's own expr_ref and released in undo() (the trail region does not run
|
|
// destructors, mirroring obj_ref_trail).
|
|
class set_term_trail : public trail {
|
|
vector<std::tuple<expr_ref, expr_ref, void*>>& m_v;
|
|
unsigned m_idx;
|
|
expr_ref m_old;
|
|
public:
|
|
set_term_trail(vector<std::tuple<expr_ref, expr_ref, void*>>& v, unsigned idx, expr* old, ast_manager& m):
|
|
m_v(v), m_idx(idx), m_old(old, m) {}
|
|
void undo() override {
|
|
std::get<0>(m_v[m_idx]) = m_old;
|
|
m_old.reset();
|
|
}
|
|
};
|
|
}
|
|
|
|
void seq_monadic::set_term(void* d, expr* term) {
|
|
for (unsigned i = 0; i < m_memberships.size(); ++i) {
|
|
if (std::get<2>(m_memberships[i]) != d)
|
|
continue;
|
|
expr_ref& t = std::get<0>(m_memberships[i]);
|
|
if (t.get() == term)
|
|
return;
|
|
m_undo_trail.push(set_term_trail(m_memberships, i, t, m));
|
|
t = term;
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool seq_monadic::can_decide_term(expr* term) {
|
|
vector<atom> atoms;
|
|
return parse_term(term, atoms);
|
|
}
|
|
|
|
void seq_monadic::add_lo(expr* term, unsigned lo, void* d) {
|
|
if (lo == 0)
|
|
return;
|
|
sort* re_sort = re().mk_re(term->get_sort());
|
|
expr_ref all_char(re().mk_full_char(re_sort), m);
|
|
expr_ref prefix(re().mk_loop_proper(all_char, lo, lo), m);
|
|
expr_ref all(re().mk_full_seq(re_sort), m);
|
|
expr_ref regex(re().mk_concat(prefix, all), m);
|
|
add(term, regex, d);
|
|
}
|
|
|
|
void seq_monadic::add_hi(expr* term, unsigned hi, void* d) {
|
|
sort* re_sort = re().mk_re(term->get_sort());
|
|
expr_ref all_char(re().mk_full_char(re_sort), m);
|
|
expr_ref regex(re().mk_loop_proper(all_char, 0, hi), m);
|
|
add(term, regex, d);
|
|
}
|
|
|
|
void seq_monadic::add_len(expr* term, unsigned len, void* d) {
|
|
sort* re_sort = re().mk_re(term->get_sort());
|
|
expr_ref all_char(re().mk_full_char(re_sort), m);
|
|
expr_ref regex(re().mk_loop_proper(all_char, len, len), m);
|
|
add(term, regex, d);
|
|
}
|
|
|
|
|
|
void seq_monadic::minimize_core(membership_vec const& memberships) {
|
|
m_core.reset();
|
|
if (!m_config.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).
|
|
membership_vec keep(memberships);
|
|
for (unsigned i = 0; i < keep.size(); ) {
|
|
membership_vec trial(keep);
|
|
trial.erase(trial.begin() + i);
|
|
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();
|
|
lbool r = decide(m_memberships);
|
|
if (r == l_false) {
|
|
minimize_core(m_memberships);
|
|
m_model.reset();
|
|
}
|
|
m_last_result = r;
|
|
return m_last_result;
|
|
}
|
|
|
|
std::ostream& seq_monadic::display(std::ostream& out) const {
|
|
auto display_expr = [&](expr* e) {
|
|
if (e)
|
|
out << mk_pp(e, m);
|
|
else
|
|
out << "null";
|
|
};
|
|
|
|
out << "(seq-monadic\n"
|
|
<< " :mode " << mode_name(m_config.m_mode) << "\n"
|
|
<< " :generate-model " << (m_config.m_model ? "true" : "false") << "\n"
|
|
<< " :minimize-core " << (m_config.m_min_core ? "true" : "false") << "\n"
|
|
<< " :last-result " << result_name(m_last_result) << "\n"
|
|
<< " :budget " << m_budget << "\n"
|
|
<< " :giveup " << (m_giveup ? "true" : "false") << "\n"
|
|
<< " :sequence-sort ";
|
|
if (m_seq_sort)
|
|
out << mk_pp(m_seq_sort, m);
|
|
else
|
|
out << "null";
|
|
out << "\n :element-sort ";
|
|
if (m_elem_sort)
|
|
out << mk_pp(m_elem_sort, m);
|
|
else
|
|
out << "null";
|
|
|
|
out << "\n :memberships (";
|
|
for (unsigned i = 0; i < m_memberships.size(); ++i) {
|
|
auto const& [term, regex, dep] = m_memberships[i];
|
|
out << "\n [" << i << "] ";
|
|
display_expr(term);
|
|
out << " in ";
|
|
display_expr(regex);
|
|
out << " :dependency " << dep;
|
|
}
|
|
if (!m_memberships.empty())
|
|
out << "\n ";
|
|
out << ")\n :model (";
|
|
for (auto const& [var, value] : m_model) {
|
|
out << "\n ";
|
|
display_expr(var);
|
|
out << " -> ";
|
|
display_expr(value);
|
|
}
|
|
if (!m_model.empty())
|
|
out << "\n ";
|
|
out << ")\n :core (";
|
|
for (void* dep : m_core)
|
|
out << " " << dep;
|
|
out << " )";
|
|
|
|
out << "\n :last-internal-search\n"
|
|
<< " (:result " << result_name(m_last_search_result)
|
|
<< "\n :memberships (";
|
|
for (unsigned i = 0; i < m_last_search_memberships.size(); ++i) {
|
|
auto const& [term, regex, dep] = m_last_search_memberships[i];
|
|
out << "\n [" << i << "] ";
|
|
display_expr(term);
|
|
out << " in ";
|
|
display_expr(regex);
|
|
out << " :dependency " << dep;
|
|
}
|
|
if (!m_last_search_memberships.empty())
|
|
out << "\n ";
|
|
out << ")\n :variables (";
|
|
for (expr* var : m_vars) {
|
|
out << " ";
|
|
display_expr(var);
|
|
}
|
|
out << " )\n :parsed-memberships (";
|
|
for (unsigned mi = 0; mi < m_atoms.size(); ++mi) {
|
|
out << "\n [" << mi << "] :regex ";
|
|
display_expr(m_regexes.get(mi));
|
|
out << " :atoms (";
|
|
for (atom const& a : m_atoms[mi]) {
|
|
out << " " << (a.is_var ? "var:" : "elem:");
|
|
display_expr(a.is_var ? a.var.get() : a.elem.get());
|
|
}
|
|
out << " )";
|
|
}
|
|
if (!m_atoms.empty())
|
|
out << "\n ";
|
|
out << ")\n :groups (";
|
|
for (unsigned vi = 0; vi < m_groups.size(); ++vi) {
|
|
out << "\n ";
|
|
display_expr(m_vars[vi]);
|
|
out << " (";
|
|
for (component const& c : m_groups[vi]) {
|
|
out << "\n ";
|
|
display_expr(c.state);
|
|
if (c.target) {
|
|
out << " -> ";
|
|
display_expr(c.target);
|
|
}
|
|
else {
|
|
out << " nullable";
|
|
}
|
|
}
|
|
if (!m_groups[vi].empty())
|
|
out << "\n ";
|
|
out << ")";
|
|
}
|
|
if (!m_groups.empty())
|
|
out << "\n ";
|
|
out << ")\n"
|
|
<< " :undefined-variables " << m_undef_vars << "\n"
|
|
<< " :group-cache-size " << m_group_cache.size() << "\n"
|
|
<< " :derivative-cache-size " << m_der_cache.size() << "\n"
|
|
<< " :nullable-cache-size " << m_nullable_cache.size() << "\n"
|
|
<< " :live-cache-size " << m_live_cache.size() << "\n"
|
|
<< " :pinned-expressions " << m_pin.size() << ")\n";
|
|
|
|
out << " :statistics\n"
|
|
<< " (:cofactor-calls " << m_stats.m_cofactor_calls << "\n"
|
|
<< " :states " << m_stats.m_states;
|
|
for (unsigned i = 0; i < static_cast<unsigned>(bail_reason::num_reasons); ++i)
|
|
out << "\n :bail-" << bail_name(i) << " " << m_stats.m_bails[i];
|
|
return out << "))\n";
|
|
}
|
|
|
|
void seq_monadic::collect_statistics(::statistics& st) const {
|
|
static char const* const bail_names[] = {
|
|
"seq monadic bail unsupported",
|
|
"seq monadic bail state cap",
|
|
"seq monadic bail dnf cap",
|
|
"seq monadic bail budget",
|
|
"seq monadic bail resource",
|
|
"seq monadic bail nullability",
|
|
"seq monadic bail guard"
|
|
};
|
|
st.update("seq monadic cofactor calls", m_stats.m_cofactor_calls);
|
|
st.update("seq monadic states", m_stats.m_states);
|
|
for (unsigned i = 0; i < static_cast<unsigned>(bail_reason::num_reasons); ++i)
|
|
st.update(bail_names[i], m_stats.m_bails[i]);
|
|
}
|