mirror of
https://github.com/Z3Prover/z3
synced 2026-07-15 03:25:43 +00:00
seq_monadic: whole-language monadic decomposition for regex membership (log-only diagnostic)
Add a standalone, minterm-free decision procedure for the regex-membership
fragment where the term is a concatenation of string variables and constant
characters (e.g. x.a.x in R), based on a whole-language split + monadic
decomposition. It does NOT use Nielsen word-equation splitting and does NOT
touch seq_split.
x.u in r <=> OR_i ( x in reach(q_i) /\ u in q_i ) over sigma(r)
reach(q) is never materialized as a regex (regex/GNFA state-elimination blows
up super-polynomially, k! on lattice-shaped automata). Instead a variable's
constraint is a conjunction of components <state0,target> and emptiness is
decided by a lazy automaton PRODUCT-REACHABILITY BFS over tuples of component
states: transitions are the cartesian product of brz_derivative_cofactors
branches with pairwise-conjoined seq::range_predicate guards (the fast,
canonical range algebra) - minterm-free throughout. A global work budget bails
to l_undef on the rare many-occurrence blowup. Termination is by finiteness of
ACI-canonical derivative states.
Wire it as a log-only diagnostic in theory_nseq::final_check_eh behind
IF_VERBOSE(1): group compound memberships per term (intersecting regexes),
build per-variable base constraints, and log MONADIC-VERDICT ... time-ms. The
change is purely additive and inert at -v:0; production solving is unchanged.
Add tst_seq_monadic (23 cases): nested complement (L3-02 unsat, L3-03 sat),
multiple/repeated variables (x.a.y, x.y.x), per-variable base constraints, and
the bounded-loop regression x.y.x in [0-9]{n} (exercises live_states on a
counted automaton).
Over the 325 multivariable-membership benchmarks the diagnostic decides 249,
of which 230 agree with the authoritative status; the only 2 mismatches are a
length-only limitation (|x|=2k not yet extracted) in the safe direction
(sat-where-unsat-by-length). 0 unsound unsat, 0 crashes. Up to ~23,500x faster
than the Nielsen path on the nested-complement / counted-complement class.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
10cab8b70d
commit
85467986ad
8 changed files with 733 additions and 0 deletions
|
|
@ -42,6 +42,7 @@ z3_add_component(rewriter
|
|||
seq_derive.cpp
|
||||
seq_subset.cpp
|
||||
seq_split.cpp
|
||||
seq_monadic.cpp
|
||||
seq_derive.cpp
|
||||
seq_range_collapse.cpp
|
||||
seq_range_predicate.cpp
|
||||
|
|
|
|||
366
src/ast/rewriter/seq_monadic.cpp
Normal file
366
src/ast/rewriter/seq_monadic.cpp
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
/*++
|
||||
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); minterm-free; reach(q) is never materialized
|
||||
as a regex.
|
||||
|
||||
Author:
|
||||
|
||||
Nikolaj Bjorner / Margus Veanes 2026
|
||||
|
||||
--*/
|
||||
|
||||
#include "ast/rewriter/seq_monadic.h"
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
|
||||
// Cofactor guard `pred` (a Boolean over the character x = (:var 0)) -> the canonical
|
||||
// range_predicate of the characters satisfying it. Returns false on a construct outside
|
||||
// {true,false,and,or,not,=,char.<=} over x (then the product engine bails to l_undef).
|
||||
static bool guard_to_rp(ast_manager& m, seq_util& sq, expr* x, expr* pred,
|
||||
unsigned maxc, seq::range_predicate& out) {
|
||||
expr* a = nullptr, * b = nullptr; unsigned c = 0;
|
||||
if (m.is_true(pred)) { out = seq::range_predicate::top(maxc); return true; }
|
||||
if (m.is_false(pred)) { out = seq::range_predicate::empty(maxc); return true; }
|
||||
if (m.is_eq(pred, a, b)) {
|
||||
if (a == x && sq.is_const_char(b, c)) { out = seq::range_predicate::singleton(c, maxc); return true; }
|
||||
if (b == x && sq.is_const_char(a, c)) { out = seq::range_predicate::singleton(c, maxc); return true; }
|
||||
return false;
|
||||
}
|
||||
if (sq.is_char_le(pred, a, b)) {
|
||||
if (b == x && sq.is_const_char(a, c)) { out = seq::range_predicate::range(c, maxc, maxc); return true; }
|
||||
if (a == x && sq.is_const_char(b, c)) { out = seq::range_predicate::range(0, c, maxc); return true; }
|
||||
return false;
|
||||
}
|
||||
if (m.is_not(pred, a)) {
|
||||
seq::range_predicate s(maxc);
|
||||
if (!guard_to_rp(m, sq, x, a, maxc, s)) return false;
|
||||
out = ~s; return true;
|
||||
}
|
||||
if (m.is_and(pred)) {
|
||||
out = seq::range_predicate::top(maxc);
|
||||
for (expr* arg : *to_app(pred)) {
|
||||
seq::range_predicate s(maxc);
|
||||
if (!guard_to_rp(m, sq, x, arg, maxc, s)) return false;
|
||||
out = out & s;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (m.is_or(pred)) {
|
||||
out = seq::range_predicate::empty(maxc);
|
||||
for (expr* arg : *to_app(pred)) {
|
||||
seq::range_predicate s(maxc);
|
||||
if (!guard_to_rp(m, sq, x, arg, maxc, s)) return false;
|
||||
out = out | s;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
expr_ref seq_monadic::der_char(expr* r, unsigned ch) {
|
||||
expr_ref c(u().mk_char(ch), m);
|
||||
return m_rw.mk_derivative(c, r); // mk_derivative(element, regex)
|
||||
}
|
||||
|
||||
void seq_monadic::live_states(expr* R, ptr_vector<expr>& out, bool& ok) {
|
||||
ok = true;
|
||||
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>());
|
||||
expr_ref nb = m_rw.is_nullable(s);
|
||||
maybe_null.push_back(!m.is_false(nb)); // 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.inc()) { ok = false; return; }
|
||||
expr_ref_pair_vector cof(m);
|
||||
m_rw.brz_derivative_cofactors(states.get(i), cof);
|
||||
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)); }
|
||||
}
|
||||
|
||||
lbool seq_monadic::product_nonempty(svector<component> const& comps) {
|
||||
unsigned n = comps.size();
|
||||
if (n == 0)
|
||||
return l_true;
|
||||
unsigned maxc = u().max_char();
|
||||
sort* cs = u().mk_char_sort();
|
||||
expr_ref var0(m.mk_var(0, cs), m); // the character variable the guards range over
|
||||
|
||||
svector<expr*> start;
|
||||
for (auto const& c : comps)
|
||||
start.push_back(c.state);
|
||||
|
||||
auto id_key = [&](svector<expr*> const& st) {
|
||||
std::vector<unsigned> k;
|
||||
k.reserve(st.size());
|
||||
for (expr* e : st) k.push_back(e->get_id());
|
||||
return k;
|
||||
};
|
||||
bool undecided = false;
|
||||
auto is_accept = [&](svector<expr*> const& st) -> bool {
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
if (comps[i].target) {
|
||||
if (st[i] != comps[i].target) return false;
|
||||
}
|
||||
else {
|
||||
expr_ref nb = m_rw.is_nullable(st[i]);
|
||||
if (m.is_true(nb)) continue;
|
||||
if (m.is_false(nb)) return false;
|
||||
undecided = true; return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
std::set<std::vector<unsigned>> visited;
|
||||
std::vector<svector<expr*>> work;
|
||||
work.push_back(start);
|
||||
visited.insert(id_key(start));
|
||||
|
||||
while (!work.empty()) {
|
||||
if (m_budget == 0) { m_giveup = true; return l_undef; }
|
||||
--m_budget;
|
||||
if (!m.inc())
|
||||
return l_undef;
|
||||
svector<expr*> st = work.back();
|
||||
work.pop_back();
|
||||
if (is_accept(st))
|
||||
return l_true;
|
||||
if (undecided)
|
||||
return l_undef;
|
||||
// per-component cofactor branches, guards converted to range_predicates
|
||||
std::vector<std::vector<std::pair<expr*, seq::range_predicate>>> branches(n);
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
expr_ref_pair_vector cof(m);
|
||||
m_rw.brz_derivative_cofactors(st[i], cof);
|
||||
for (auto const& [g, t] : cof) {
|
||||
if (re().is_empty(t)) continue;
|
||||
seq::range_predicate rp(maxc);
|
||||
if (!guard_to_rp(m, u(), var0, g, maxc, rp))
|
||||
return l_undef; // non-range guard: bail (sound)
|
||||
m_pin.push_back(t); // keep the derivative target alive
|
||||
branches[i].push_back(std::make_pair((expr*) t, rp));
|
||||
}
|
||||
}
|
||||
// joint transitions = cartesian product of the branches with the guards
|
||||
// conjoined; prune as soon as the accumulated guard range is empty.
|
||||
svector<expr*> cur;
|
||||
cur.resize(n);
|
||||
std::function<void(unsigned, seq::range_predicate const&)> rec =
|
||||
[&](unsigned i, seq::range_predicate const& acc) {
|
||||
if (i == n) {
|
||||
auto k = id_key(cur);
|
||||
if (visited.find(k) == visited.end()) {
|
||||
visited.insert(k);
|
||||
work.push_back(cur);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (auto const& pr : branches[i]) {
|
||||
seq::range_predicate nacc = acc & pr.second;
|
||||
if (nacc.is_empty()) continue;
|
||||
cur[i] = pr.first;
|
||||
rec(i + 1, nacc);
|
||||
}
|
||||
};
|
||||
rec(0, seq::range_predicate::top(maxc));
|
||||
}
|
||||
return l_false;
|
||||
}
|
||||
|
||||
bool seq_monadic::parse_term(expr* t, svector<atom>& atoms, expr*& the_var) {
|
||||
if (u().str.is_concat(t)) {
|
||||
app* a = to_app(t);
|
||||
for (unsigned i = 0; i < a->get_num_args(); ++i)
|
||||
if (!parse_term(a->get_arg(i), atoms, the_var))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
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)
|
||||
atoms.push_back(atom{ false, nullptr, s[i] });
|
||||
return true;
|
||||
}
|
||||
if (u().str.is_unit(t)) {
|
||||
expr* ch = to_app(t)->get_arg(0);
|
||||
unsigned cv = 0;
|
||||
if (u().is_const_char(ch, cv)) {
|
||||
atoms.push_back(atom{ false, nullptr, cv });
|
||||
return true;
|
||||
}
|
||||
return false; // symbolic (non-constant) unit: unsupported
|
||||
}
|
||||
// uninterpreted 0-ary constant of sequence sort => a string variable
|
||||
if (is_app(t) && to_app(t)->get_num_args() == 0 &&
|
||||
to_app(t)->get_family_id() == null_family_id) {
|
||||
the_var = t; // mark that at least one variable occurs
|
||||
atoms.push_back(atom{ true, t, 0 });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void seq_monadic::decompose(svector<atom> const& atoms, unsigned i, expr* R,
|
||||
vector<disjunct>& out, bool& ok) {
|
||||
if (!ok)
|
||||
return;
|
||||
if (m_giveup) { ok = false; return; }
|
||||
m_pin.push_back(R);
|
||||
if (i == atoms.size()) {
|
||||
expr_ref nb = m_rw.is_nullable(R);
|
||||
if (m.is_true(nb))
|
||||
out.push_back(disjunct()); // empty conjunction = true
|
||||
else if (!m.is_false(nb))
|
||||
ok = false; // undecidable nullability => bail
|
||||
return;
|
||||
}
|
||||
atom const& a = atoms[i];
|
||||
if (!a.is_var) {
|
||||
expr_ref d = der_char(R, a.ch);
|
||||
decompose(atoms, i + 1, d, out, ok);
|
||||
return;
|
||||
}
|
||||
if (i + 1 == atoms.size()) { // last atom: membership component a.var in R
|
||||
disjunct D;
|
||||
D.push_back(component{ a.var, R, nullptr });
|
||||
out.push_back(D);
|
||||
return;
|
||||
}
|
||||
// a variable with a non-empty rest: split over the live states q of R (midpoints)
|
||||
ptr_vector<expr> Q;
|
||||
live_states(R, Q, ok);
|
||||
if (!ok)
|
||||
return;
|
||||
const unsigned DISJUNCT_CAP = 1u << 13;
|
||||
for (expr* q : Q) {
|
||||
vector<disjunct> sub;
|
||||
decompose(atoms, i + 1, q, sub, ok);
|
||||
if (!ok)
|
||||
return;
|
||||
for (disjunct const& sd : sub) {
|
||||
if (out.size() > DISJUNCT_CAP || m_budget == 0) { m_giveup = true; ok = false; return; }
|
||||
--m_budget;
|
||||
disjunct D(sd);
|
||||
D.push_back(component{ a.var, R, q }); // reach component: a.var drives R -> q
|
||||
out.push_back(D);
|
||||
}
|
||||
}
|
||||
simplify_dnf(out);
|
||||
}
|
||||
|
||||
void seq_monadic::simplify_dnf(vector<disjunct>& dnf) {
|
||||
std::set<std::vector<std::tuple<unsigned, unsigned, unsigned>>> seen;
|
||||
vector<disjunct> result;
|
||||
for (disjunct const& D : dnf) {
|
||||
bool dead = false;
|
||||
for (auto const& c : D)
|
||||
if (re().is_empty(c.state)) { dead = true; break; }
|
||||
if (dead)
|
||||
continue;
|
||||
std::vector<std::tuple<unsigned, unsigned, unsigned>> sig;
|
||||
sig.reserve(D.size());
|
||||
for (auto const& c : D)
|
||||
sig.push_back(std::make_tuple(c.var->get_id(), c.state->get_id(),
|
||||
c.target ? c.target->get_id() : UINT_MAX));
|
||||
std::sort(sig.begin(), sig.end());
|
||||
if (seen.insert(sig).second)
|
||||
result.push_back(D);
|
||||
}
|
||||
dnf.swap(result);
|
||||
}
|
||||
|
||||
lbool seq_monadic::solve(expr* term, expr* R) {
|
||||
obj_map<expr, expr*> none;
|
||||
return solve(term, R, none);
|
||||
}
|
||||
|
||||
lbool seq_monadic::solve(expr* term, expr* R, obj_map<expr, expr*> const& var_extra) {
|
||||
if (!u().is_re(R, m_seq_sort))
|
||||
return l_undef;
|
||||
svector<atom> atoms;
|
||||
expr* the_var = nullptr;
|
||||
if (!parse_term(term, atoms, the_var))
|
||||
return l_undef;
|
||||
if (!the_var)
|
||||
return l_undef; // no variable: ground membership, not our case
|
||||
m_pin.reset();
|
||||
m_pin.push_back(R);
|
||||
m_budget = 200000; // global work budget: bail fast on DNF explosion
|
||||
m_giveup = false;
|
||||
bool ok = true;
|
||||
vector<disjunct> dnf;
|
||||
decompose(atoms, 0, R, dnf, ok);
|
||||
if (!ok)
|
||||
return l_undef;
|
||||
|
||||
bool any_undef = false;
|
||||
for (disjunct const& D : dnf) {
|
||||
// group components by variable, add the extra per-variable constraints
|
||||
obj_map<expr, unsigned> idx;
|
||||
vector<svector<component>> groups;
|
||||
auto bucket = [&](expr* v) -> unsigned {
|
||||
unsigned gi;
|
||||
if (idx.find(v, gi)) return gi;
|
||||
gi = groups.size(); idx.insert(v, gi); groups.push_back(svector<component>());
|
||||
return gi;
|
||||
};
|
||||
for (auto const& c : D)
|
||||
groups[bucket(c.var)].push_back(c);
|
||||
for (auto const& kv : var_extra)
|
||||
groups[bucket(kv.m_key)].push_back(component{ kv.m_key, kv.m_value, nullptr });
|
||||
|
||||
bool has_empty = false, has_undef = false;
|
||||
for (auto const& g : groups) {
|
||||
lbool ne = product_nonempty(g);
|
||||
if (ne == l_false) { has_empty = true; break; } // this variable has no value
|
||||
if (ne == l_undef) has_undef = true;
|
||||
}
|
||||
if (has_empty) continue;
|
||||
if (has_undef) { any_undef = true; continue; }
|
||||
return l_true; // all variables satisfiable => sat
|
||||
}
|
||||
return any_undef ? l_undef : l_false;
|
||||
}
|
||||
111
src/ast/rewriter/seq_monadic.h
Normal file
111
src/ast/rewriter/seq_monadic.h
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
Module Name:
|
||||
|
||||
seq_monadic.h
|
||||
|
||||
Abstract:
|
||||
|
||||
Whole-language monadic decomposition for regex membership of a term that is a
|
||||
concatenation of string variables and constant characters, e.g. x.a.x in R.
|
||||
|
||||
Self-contained decision procedure: NO Nielsen splitting (seq_split), NO minterms,
|
||||
and NO materialization of reach(q) as a regex. It relies only on the symbolic
|
||||
Brzozowski derivative (brz_derivative_cofactors as a transition regex) and on
|
||||
automaton product-reachability for emptiness.
|
||||
|
||||
Method. For a term x.u in R and the whole-language split, x drives the derivative
|
||||
automaton of R from R to some live state q, and the rest u must be accepted from q:
|
||||
|
||||
x.u in R <=> OR_{q live} ( x reaches q in A_R /\ u in q ).
|
||||
|
||||
Decomposing u recursively (a leading constant is consumed by a derivative, a leading
|
||||
variable splits again, the last variable is a plain membership) yields a DNF whose
|
||||
disjuncts are conjunctions of per-variable *components*:
|
||||
|
||||
- reach component <var, state0, q> : the variable's value drives the
|
||||
derivative automaton from state0 to q
|
||||
- membership component<var, state0, null> : the variable's value is in L(state0)
|
||||
|
||||
reach(q) is therefore NEVER built as a regex (which state-elimination would blow up
|
||||
super-polynomially for lattice-shaped automata). Instead the constraints on a
|
||||
variable are decided directly by a lazy product-reachability search over tuples of
|
||||
component states: a product state accepts iff every reach component is at its target
|
||||
and every membership component is nullable; transitions are the product of the
|
||||
components' cofactor branches with pairwise-conjoined range guards (minterm-free).
|
||||
This stays in the product-of-state-counts regime, never the path-enumeration (k!)
|
||||
regime of regex state-elimination.
|
||||
|
||||
Supports single / multiple / repeated variables, and per-variable extra constraints
|
||||
(base membership + length-regex) via `var_extra`.
|
||||
|
||||
Author:
|
||||
|
||||
Nikolaj Bjorner / Margus Veanes 2026
|
||||
|
||||
--*/
|
||||
#pragma once
|
||||
|
||||
#include "ast/rewriter/seq_rewriter.h"
|
||||
#include "ast/rewriter/seq_range_predicate.h"
|
||||
#include "util/lbool.h"
|
||||
#include "util/obj_hashtable.h"
|
||||
|
||||
class seq_monadic {
|
||||
ast_manager& m;
|
||||
seq_rewriter& m_rw;
|
||||
sort* m_seq_sort = nullptr; // sequence sort of the regex under analysis
|
||||
expr_ref_vector m_pin; // pins derivative states referenced by components
|
||||
unsigned m_budget = 0; // global work budget (decompose disjuncts + product pops)
|
||||
bool m_giveup = false; // set when the budget is exhausted
|
||||
|
||||
seq_util& u() const { return m_rw.u(); }
|
||||
seq_util::rex& re() const { return m_rw.u().re; }
|
||||
|
||||
// A term atom: a string variable or a constant character.
|
||||
struct atom { bool is_var; expr* var; unsigned ch; };
|
||||
|
||||
// A component of one variable's constraint. As the variable's value w is read,
|
||||
// the current state is derived from `state`; the component accepts when
|
||||
// target ? (current == target) -- reach component (w drives A from state to target)
|
||||
// : nullable(current) -- membership component (w in L(state))
|
||||
struct component { expr* var; expr* state; expr* target; };
|
||||
|
||||
typedef svector<component> disjunct; // a conjunction of components (a DNF disjunct)
|
||||
|
||||
// Brzozowski derivative of regex `r` by the concrete character `ch`.
|
||||
expr_ref der_char(expr* r, unsigned ch);
|
||||
|
||||
// Live reachable derivative states of R (BFS over cofactor targets + liveness
|
||||
// least-fixpoint). These are the split states q. Sets `ok` false on a cap overrun.
|
||||
void live_states(expr* R, ptr_vector<expr>& out, bool& ok);
|
||||
|
||||
// Product-reachability emptiness of a conjunction of components (all on one
|
||||
// variable). l_false = empty (unsat), l_true = non-empty (sat), l_undef = gave up
|
||||
// (cap overrun, non-range guard, or undecidable nullability).
|
||||
lbool product_nonempty(svector<component> const& comps);
|
||||
|
||||
// Flatten a str.++ term into atoms; false on an unsupported shape (non-constant unit).
|
||||
bool parse_term(expr* term, svector<atom>& atoms, expr*& the_var);
|
||||
|
||||
// Monadic decomposition: append to `out` the DNF disjuncts for atoms[i..] in R,
|
||||
// threading the current derivative state R. `ok` false on give-up.
|
||||
void decompose(svector<atom> const& atoms, unsigned i, expr* R,
|
||||
vector<disjunct>& out, bool& ok);
|
||||
|
||||
// Drop disjuncts with a syntactically-empty component and dedup identical disjuncts.
|
||||
void simplify_dnf(vector<disjunct>& dnf);
|
||||
|
||||
public:
|
||||
seq_monadic(seq_rewriter& rw) : m(rw.m()), m_rw(rw), m_pin(rw.m()) {}
|
||||
|
||||
// Decide (str.in_re term R) for a term that is a concatenation of string variables
|
||||
// (possibly repeated / several distinct) and constant characters.
|
||||
// l_true = sat, l_false = unsat, l_undef = unsupported shape / gave up.
|
||||
lbool solve(expr* term, expr* R);
|
||||
|
||||
// As above, with extra per-variable constraints (e.g. a base membership intersected
|
||||
// with a length-regex): `var_extra` maps a variable to a regex it must also satisfy.
|
||||
lbool solve(expr* term, expr* R, obj_map<expr, expr*> const& var_extra);
|
||||
};
|
||||
|
|
@ -23,6 +23,7 @@ Author:
|
|||
#include "util/trail.h"
|
||||
|
||||
#include <stack>
|
||||
#include <chrono>
|
||||
|
||||
namespace smt {
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ namespace smt {
|
|||
m_axioms(m_th_rewriter),
|
||||
m_regex(m_sg),
|
||||
m_model(m, ctx, m_seq, m_rewriter, m_sg),
|
||||
m_monadic(m_rewriter),
|
||||
m_relevant_lengths(m)
|
||||
{
|
||||
std::function<void(expr_ref_vector const&)> add_clause =
|
||||
|
|
@ -888,6 +890,68 @@ namespace smt {
|
|||
<< num_eqs << " eqs, " << num_mems << " mems\n";);
|
||||
}
|
||||
|
||||
// Diagnostic (log-only): run the whole-language monadic decomposition on the
|
||||
// collected memberships and log its verdict. Invoked under verbosity>=1 only;
|
||||
// never changes the solver's answer.
|
||||
void theory_nseq::run_monadic_diagnostic() {
|
||||
obj_map<expr, expr*> var_extra;
|
||||
expr_ref_vector pin(m);
|
||||
auto is_var = [&](expr* t) {
|
||||
return is_app(t) && to_app(t)->get_num_args() == 0 &&
|
||||
to_app(t)->get_family_id() == null_family_id;
|
||||
};
|
||||
// pass 1: per-variable base memberships (x in R) -> extra constraint on x
|
||||
for (auto const& item : m_prop_queue) {
|
||||
if (!std::holds_alternative<mem_item>(item)) continue;
|
||||
auto const& mem = std::get<mem_item>(item);
|
||||
if (!mem.m_str || !mem.m_regex) continue;
|
||||
expr* t = mem.m_str->get_expr();
|
||||
expr* R = mem.m_regex->get_expr();
|
||||
if (!t || !R || !is_var(t)) continue;
|
||||
expr* prev = nullptr;
|
||||
if (var_extra.find(t, prev)) {
|
||||
expr_ref in = m_rewriter.mk_regex_inter_normalize(prev, R);
|
||||
pin.push_back(in);
|
||||
var_extra.insert(t, in);
|
||||
}
|
||||
else
|
||||
var_extra.insert(t, R);
|
||||
}
|
||||
// pass 2: group compound-term memberships by term and intersect their regexes
|
||||
// (a term may carry several memberships that must all hold), then solve once.
|
||||
obj_map<expr, expr*> term_re;
|
||||
ptr_vector<expr> terms;
|
||||
for (auto const& item : m_prop_queue) {
|
||||
if (!std::holds_alternative<mem_item>(item)) continue;
|
||||
auto const& mem = std::get<mem_item>(item);
|
||||
if (!mem.m_str || !mem.m_regex) continue;
|
||||
expr* t = mem.m_str->get_expr();
|
||||
expr* R = mem.m_regex->get_expr();
|
||||
if (!t || !R || !m_seq.str.is_concat(t)) continue;
|
||||
expr* prev = nullptr;
|
||||
if (term_re.find(t, prev)) {
|
||||
expr_ref in = m_rewriter.mk_regex_inter_normalize(prev, R);
|
||||
pin.push_back(in);
|
||||
term_re.insert(t, in);
|
||||
}
|
||||
else {
|
||||
term_re.insert(t, R);
|
||||
terms.push_back(t);
|
||||
}
|
||||
}
|
||||
for (expr* t : terms) {
|
||||
expr* R = nullptr;
|
||||
term_re.find(t, R);
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
lbool v = m_monadic.solve(t, R, var_extra);
|
||||
double ms = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::high_resolution_clock::now() - t0).count();
|
||||
verbose_stream() << "MONADIC-VERDICT "
|
||||
<< (v == l_true ? "sat" : v == l_false ? "unsat" : "undef")
|
||||
<< " time-ms " << ms << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
final_check_status theory_nseq::final_check_eh(unsigned /*final_check_round*/) {
|
||||
try {
|
||||
// Always assert non-negativity for all string theory vars,
|
||||
|
|
@ -902,6 +966,11 @@ namespace smt {
|
|||
return std::holds_alternative<eq_item>(item) || std::holds_alternative<deq_item>(item) || std::holds_alternative<mem_item>(item);
|
||||
});
|
||||
|
||||
// Diagnostic (verbosity>=1, log-only): whole-language monadic verdict on the
|
||||
// collected memberships. Placed before the early-exit / hot-restart / rebuild
|
||||
// branches so it is exercised on as many membership benchmarks as possible.
|
||||
IF_VERBOSE(1, run_monadic_diagnostic(););
|
||||
|
||||
// there is nothing to do for the string solver, as there are no string constraints
|
||||
if (!has_eq_or_diseq_or_mem && m_ho_terms.empty() && !has_unhandled_preds()) {
|
||||
if (!check_stoi_coherence()) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ Author:
|
|||
|
||||
#include "ast/seq_decl_plugin.h"
|
||||
#include "ast/rewriter/seq_rewriter.h"
|
||||
#include "ast/rewriter/seq_monadic.h"
|
||||
#include "ast/rewriter/seq_axioms.h"
|
||||
#include "ast/euf/euf_egraph.h"
|
||||
#include "ast/euf/euf_sgraph.h"
|
||||
|
|
@ -50,6 +51,12 @@ namespace smt {
|
|||
seq::axioms m_axioms;
|
||||
seq::seq_regex m_regex; // regex membership pre-processing
|
||||
seq_model m_model; // model construction helper
|
||||
seq_monadic m_monadic; // whole-language monadic-decomposition solver (diagnostic)
|
||||
|
||||
// Diagnostic (verbosity>=1 only, log-only): run the whole-language monadic
|
||||
// decomposition solver on the collected memberships and log its sat/unsat
|
||||
// verdict. Does NOT affect the solver's answer.
|
||||
void run_monadic_diagnostic();
|
||||
|
||||
// propagation queue items (variant over the distinct propagation cases)
|
||||
using eq_item = tracked_str_eq; // string equality
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ add_executable(test-z3
|
|||
sls_test.cpp
|
||||
sls_seq_plugin.cpp
|
||||
seq_split.cpp
|
||||
seq_monadic.cpp
|
||||
small_object_allocator.cpp
|
||||
smt2print_parse.cpp
|
||||
smt_context.cpp
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@
|
|||
X(finite_set) \
|
||||
X(finite_set_rewriter) \
|
||||
X(seq_split) \
|
||||
X(seq_monadic) \
|
||||
X(fpa) \
|
||||
X(seq_regex_bisim) \
|
||||
X(term_enumeration) \
|
||||
|
|
|
|||
177
src/test/seq_monadic.cpp
Normal file
177
src/test/seq_monadic.cpp
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
Module Name:
|
||||
|
||||
seq_monadic.cpp
|
||||
|
||||
Abstract:
|
||||
|
||||
Unit tests for the whole-language monadic-decomposition membership solver in
|
||||
ast/rewriter/seq_monadic.cpp. Mirrors the validated Python prototype
|
||||
(files/solve_proto.py): single-variable repeated-membership shapes x.a.x in R.
|
||||
|
||||
Author:
|
||||
|
||||
Nikolaj Bjorner / Margus Veanes 2026
|
||||
|
||||
--*/
|
||||
|
||||
#include "ast/ast.h"
|
||||
#include "ast/reg_decl_plugins.h"
|
||||
#include "ast/seq_decl_plugin.h"
|
||||
#include "ast/rewriter/seq_rewriter.h"
|
||||
#include "ast/rewriter/seq_monadic.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct plugin_registrar {
|
||||
plugin_registrar(ast_manager& m) { reg_decl_plugins(m); }
|
||||
};
|
||||
|
||||
class seq_monadic_test {
|
||||
ast_manager m;
|
||||
plugin_registrar m_reg;
|
||||
seq_rewriter m_rw;
|
||||
seq_monadic m_mon;
|
||||
seq_util u;
|
||||
sort_ref m_str; // String sort
|
||||
sort_ref m_re; // RegEx sort over m_str
|
||||
unsigned m_fail = 0;
|
||||
|
||||
seq_util::rex& re() { return u.re; }
|
||||
|
||||
// regex builders
|
||||
expr_ref word(char const* s) { return expr_ref(re().mk_to_re(u.str.mk_string(zstring(s))), m); }
|
||||
expr_ref cat(expr* a, expr* b) { return expr_ref(re().mk_concat(a, b), m); }
|
||||
expr_ref alt(expr* a, expr* b) { return expr_ref(re().mk_union(a, b), m); }
|
||||
expr_ref star(expr* a) { return expr_ref(re().mk_star(a), m); }
|
||||
expr_ref inter(expr* a, expr* b) { return expr_ref(re().mk_inter(a, b), m); }
|
||||
expr_ref comp(expr* a) { return expr_ref(re().mk_complement(a), m); }
|
||||
expr_ref dotstar() { return expr_ref(re().mk_full_seq(m_re), m); }
|
||||
expr_ref rng(char lo, char hi) {
|
||||
char sl[2] = { lo, 0 }, sh[2] = { hi, 0 };
|
||||
return expr_ref(re().mk_range(u.str.mk_string(zstring(sl)), u.str.mk_string(zstring(sh))), m);
|
||||
}
|
||||
expr_ref loop(expr* r, unsigned lo, unsigned hi) { return expr_ref(re().mk_loop(r, lo, hi), m); }
|
||||
|
||||
// string-term builders
|
||||
expr_ref var(char const* nm) { return expr_ref(m.mk_const(nm, m_str), m); }
|
||||
expr_ref sword(char const* s) { return expr_ref(u.str.mk_string(zstring(s)), m); }
|
||||
expr_ref sconcat(expr* a, expr* b) { return expr_ref(u.str.mk_concat(a, b), m); }
|
||||
// term x . w . x (w a constant word)
|
||||
expr_ref xwx(expr* x, char const* w) { return sconcat(x, sconcat(sword(w), x)); }
|
||||
// term x . a . y (two distinct variables)
|
||||
expr_ref xay(expr* x, expr* y) { return sconcat(x, sconcat(sword("a"), y)); }
|
||||
// term x . y . x
|
||||
expr_ref xyx(expr* x, expr* y) { return sconcat(x, sconcat(y, x)); }
|
||||
|
||||
static char const* s(lbool l) { return l == l_true ? "sat" : l == l_false ? "unsat" : "undef"; }
|
||||
|
||||
void check(char const* name, expr* term, expr* R, lbool expected) {
|
||||
lbool got = m_mon.solve(term, R);
|
||||
bool ok = (got == expected);
|
||||
if (!ok) ++m_fail;
|
||||
std::cout << (ok ? " OK " : " FAIL ") << name
|
||||
<< " got=" << s(got) << " expected=" << s(expected) << "\n";
|
||||
}
|
||||
|
||||
void check_extra(char const* name, expr* term, expr* R,
|
||||
obj_map<expr, expr*> const& ve, lbool expected) {
|
||||
lbool got = m_mon.solve(term, R, ve);
|
||||
bool ok = (got == expected);
|
||||
if (!ok) ++m_fail;
|
||||
std::cout << (ok ? " OK " : " FAIL ") << name
|
||||
<< " got=" << s(got) << " expected=" << s(expected) << "\n";
|
||||
}
|
||||
|
||||
public:
|
||||
seq_monadic_test() : m_reg(m), m_rw(m), m_mon(m_rw), u(m), m_str(m), m_re(m) {
|
||||
m_str = u.str.mk_string_sort();
|
||||
m_re = re().mk_re(m_str);
|
||||
}
|
||||
|
||||
void run() {
|
||||
expr_ref x = var("x");
|
||||
expr_ref a = word("a");
|
||||
expr_ref b = word("b");
|
||||
expr_ref ab = cat(a, b);
|
||||
expr_ref sig = dotstar(); // Sigma*
|
||||
expr_ref saas = cat(sig, cat(cat(a, a), sig)); // Sigma* a a Sigma*
|
||||
expr_ref sbbs = cat(sig, cat(cat(b, b), sig)); // Sigma* b b Sigma*
|
||||
|
||||
std::cout << "=== seq_monadic: single-variable membership (x.a.x in R) ===\n";
|
||||
|
||||
// sanity
|
||||
check("(a|b)* x.a.x", xwx(x, "a"), star(alt(a, b)), l_true);
|
||||
check("b* x.a.x", xwx(x, "a"), star(b), l_false);
|
||||
check("Sig*aaSig* x.a.x", xwx(x, "a"), saas, l_true);
|
||||
check("x in (a|b)* ", x, star(alt(a, b)), l_true);
|
||||
check("x in b* (x=aa) ", xwx(x, "a"), star(b), l_false);
|
||||
|
||||
// ALT = (a|b)* & ~(Sig*aaSig*) & ~(Sig*bbSig*) (strictly alternating)
|
||||
expr_ref altre = inter(star(alt(a, b)), inter(comp(saas), comp(sbbs)));
|
||||
check("ALT x.a.x", xwx(x, "a"), altre, l_true);
|
||||
|
||||
// R*.S complement family
|
||||
check("~(a*.b) x.a.x", xwx(x, "a"), comp(cat(star(a), b)), l_true);
|
||||
|
||||
// L3-02 ~((ab)*.~((ab)*)) -> unsat (odd length)
|
||||
check("L3-02 x.a.x", xwx(x, "a"),
|
||||
comp(cat(star(ab), comp(star(ab)))), l_false);
|
||||
|
||||
// L3-03 ~(a*.~(b*.~((ab)*))) -> sat
|
||||
check("L3-03 x.a.x", xwx(x, "a"),
|
||||
comp(cat(star(a), comp(cat(star(b), comp(star(ab)))))), l_true);
|
||||
|
||||
std::cout << "=== seq_monadic: multi-variable ===\n";
|
||||
expr_ref y = var("y");
|
||||
check("(a|b)* x.a.y", xay(x, y), star(alt(a, b)), l_true);
|
||||
check("b* x.a.y", xay(x, y), star(b), l_false);
|
||||
check("L3-02 x.a.y", xay(x, y), comp(cat(star(ab), comp(star(ab)))), l_true);
|
||||
check("L3-03 x.a.y", xay(x, y),
|
||||
comp(cat(star(a), comp(cat(star(b), comp(star(ab)))))), l_true);
|
||||
check("empty ~Sig* x.y.x", xyx(x, y), comp(dotstar()), l_false);
|
||||
check("Sig* x.y.x", xyx(x, y), dotstar(), l_true);
|
||||
check("(a|b)* x.y.x", xyx(x, y), star(alt(a, b)), l_true);
|
||||
|
||||
std::cout << "=== seq_monadic: per-variable constraints ===\n";
|
||||
expr_ref digitp = cat(rng('0', '9'), star(rng('0', '9'))); // [0-9]+
|
||||
obj_map<expr, expr*> ve;
|
||||
ve.insert(y, digitp);
|
||||
// y must be in the (a|b)* tail AND in [0-9]+ -> empty -> unsat
|
||||
check_extra("(a|b)* & y in[0-9]+ x.a.y", xay(x, y), star(alt(a, b)), ve, l_false);
|
||||
// y any digits, x/'a' anything -> sat
|
||||
check_extra("Sig* & y in[0-9]+ x.a.y", xay(x, y), dotstar(), ve, l_true);
|
||||
|
||||
// Bounded loop (re.loop) with repeated variable -- exercises live_states on a
|
||||
// counted automaton (t04-exact benchmark family). Regression for a
|
||||
// reference-invalidation bug in live_states (succ[i].push_back(intern(t))).
|
||||
std::cout << "=== seq_monadic: bounded loop (t04-exact family) ===\n";
|
||||
expr_ref clsr = rng('0', '9'); // [0-9]
|
||||
expr_ref digitS = star(clsr); // [0-9]*
|
||||
expr_ref loop22 = loop(clsr, 2, 2); // [0-9]{2}
|
||||
check("[0-9]{2} x ", x, loop22, l_true); // x = "00"
|
||||
check("[0-9]{2} x.a.x", xwx(x, "a"), loop22, l_false); // 'a' not a digit
|
||||
obj_map<expr, expr*> ve2; ve2.insert(x, digitp); ve2.insert(y, digitS);
|
||||
// x.y.x in [0-9]{2}, x in [0-9]+, y in [0-9]* -> sat (x="0", y="")
|
||||
check_extra("[0-9]{2} & x[0-9]+ y[0-9]* x.y.x", xyx(x, y), loop22, ve2, l_true);
|
||||
obj_map<expr, expr*> ve3; ve3.insert(x, digitp);
|
||||
check_extra("[0-9]{2} & x[0-9]+ x.y.x", xyx(x, y), loop22, ve3, l_true);
|
||||
obj_map<expr, expr*> ve4; ve4.insert(x, digitp);
|
||||
// x.y.x in [0-9]{3}, x in [0-9]+ -> sat (x=1 digit, y=1 digit)
|
||||
check_extra("[0-9]{3} & x[0-9]+ x.y.x", xyx(x, y), loop(clsr, 3, 3), ve4, l_true);
|
||||
|
||||
std::cout << "=== seq_monadic: " << (m_fail == 0 ? "ALL PASS" : "FAILURES") << " ("
|
||||
<< m_fail << " fail) ===\n";
|
||||
ENSURE(m_fail == 0);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
void tst_seq_monadic() {
|
||||
seq_monadic_test t;
|
||||
t.run();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue