mirror of
https://github.com/Z3Prover/z3
synced 2026-07-12 01:56:22 +00:00
Fixed some partial automaton soundness problems
This commit is contained in:
parent
195a0486aa
commit
e8884faa23
6 changed files with 539 additions and 251 deletions
|
|
@ -37,9 +37,9 @@ COMMON_ARGS = ["model_validate=true"]
|
|||
# All three configurations are always run.
|
||||
SOLVERS = {
|
||||
"nseq_md": ["smt.string_solver=nseq", "smt.nseq.parikh=false", "smt.nseq.eager=false",
|
||||
"smt.nseq.regex_factorization_threshold=10000000", "smt.nseq.regex_factorization_eager=true"],
|
||||
"smt.nseq.regex_factorization_threshold=10000000", "smt.nseq.regex_factorization_eager=true", "smt.nseq.regex_dynamic_decomposition=false"],
|
||||
"nseq_pa": ["smt.string_solver=nseq", "smt.nseq.parikh=false", "smt.nseq.eager=false",
|
||||
"smt.nseq.regex_factorization_threshold=0", "smt.nseq.regex_factorization_eager=false"],
|
||||
"smt.nseq.regex_factorization_threshold=0", "smt.nseq.regex_factorization_eager=false", "smt.nseq.regex_dynamic_decomposition=true"],
|
||||
"seq": ["smt.string_solver=seq"],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ bool seq_split::complement(sort* seq_sort, split_set const& sp, split_set& resul
|
|||
split_set acc;
|
||||
push(acc, oracle, r.mk_complement(sp[0].m_d), full);
|
||||
push(acc, oracle, full, r.mk_complement(sp[0].m_n));
|
||||
for (unsigned i = 1; i < sp.size(); ++i) {
|
||||
for (unsigned i = 1; i < sp.size(); i++) {
|
||||
split_set next;
|
||||
push(next, oracle, r.mk_complement(sp[i].m_d), full);
|
||||
push(next, oracle, full, r.mk_complement(sp[i].m_n));
|
||||
|
|
@ -300,7 +300,7 @@ expr_ref seq_split::expand_fromre(expr* r, bool& ok) {
|
|||
}
|
||||
}
|
||||
expr_ref acc = mk_empty();
|
||||
for (unsigned i = 0; i <= str.length(); ++i) {
|
||||
for (unsigned i = 0; i <= str.length(); i++) {
|
||||
const expr_ref p(rex.mk_to_re(sq.str.mk_string(str.extract(0, i))), m);
|
||||
const expr_ref q(rex.mk_to_re(sq.str.mk_string(str.extract(i, str.length() - i))), m);
|
||||
acc = mk_union(acc, mk_single(p, q));
|
||||
|
|
@ -338,7 +338,7 @@ expr_ref seq_split::expand_fromre(expr* r, bool& ok) {
|
|||
app* ap = to_app(r);
|
||||
const unsigned n = ap->get_num_args();
|
||||
expr_ref acc = mk_empty();
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
for (unsigned i = 0; i < n; i++) {
|
||||
expr_ref left(m), right(m);
|
||||
if (i == 0)
|
||||
left = rex.mk_epsilon(seq_sort);
|
||||
|
|
@ -381,8 +381,9 @@ expr_ref seq_split::expand_fromre(expr* r, bool& ok) {
|
|||
app* ap = to_app(r);
|
||||
const unsigned n = ap->get_num_args();
|
||||
expr_ref acc = mk_fromre(ap->get_arg(0));
|
||||
for (unsigned i = 1; i < n; ++i)
|
||||
for (unsigned i = 1; i < n; i++) {
|
||||
acc = mk_inter(acc, mk_fromre(ap->get_arg(i)));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
|
|
@ -390,10 +391,24 @@ expr_ref seq_split::expand_fromre(expr* r, bool& ok) {
|
|||
if (rex.is_complement(r, a))
|
||||
return mk_compl(mk_fromre(a));
|
||||
|
||||
// abbreviation
|
||||
// difference: a \ b = a & ~b ; sigma(a \ b) = sigma(a) cap ~sigma(b).
|
||||
if (rex.is_diff(r, a, b))
|
||||
return mk_inter(mk_fromre(a), mk_compl(mk_fromre(b)));
|
||||
|
||||
// abbreviation
|
||||
// optional: a? = eps | a ; sigma(a?) = sigma(eps | a) = eps cup sigma(a)
|
||||
if (rex.is_opt(r, a)) {
|
||||
const expr_ref eps(rex.mk_epsilon(seq_sort), m);
|
||||
return mk_union(mk_single(eps, eps), mk_fromre(a));
|
||||
}
|
||||
|
||||
// loop
|
||||
unsigned l, h;
|
||||
if (rex.is_loop(r, a, l, h)) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// bounded loop / ite / other: not handled (paper "v1: bail").
|
||||
TRACE(seq, tout << "seq_split: unsupported regex " << mk_pp(r, m) << "\n";);
|
||||
ok = false;
|
||||
|
|
@ -624,7 +639,7 @@ void seq_split::simplify(split_set& pairs) const {
|
|||
|
||||
// 1. drop pairs with a bottom (empty-language) component.
|
||||
unsigned w = 0;
|
||||
for (unsigned i = 0; i < pairs.size(); ++i) {
|
||||
for (unsigned i = 0; i < pairs.size(); i++) {
|
||||
if (r.is_empty(pairs[i].m_d) || r.is_empty(pairs[i].m_n))
|
||||
continue;
|
||||
if (w != i)
|
||||
|
|
@ -650,7 +665,7 @@ void seq_split::simplify(split_set& pairs) const {
|
|||
|
||||
struct row { expr* d; expr* n; unsigned idx; };
|
||||
vector<row> rows;
|
||||
for (unsigned i = 0; i < pairs.size(); ++i)
|
||||
for (unsigned i = 0; i < pairs.size(); i++)
|
||||
rows.push_back({ pairs[i].m_d.get(), pairs[i].m_n.get(), i });
|
||||
|
||||
auto subsumes = [&](row const& a, row const& b) {
|
||||
|
|
@ -769,7 +784,7 @@ std::pair<expr_ref, expr_ref> seq_split::split_membership(expr* str, expr* regex
|
|||
// Build the constant lookahead c and (if non-empty) an oracle that
|
||||
// prunes splits whose postfix cannot match c.
|
||||
zstring c;
|
||||
for (i = 0; i < run_len; ++i) {
|
||||
for (i = 0; i < run_len; i++) {
|
||||
unsigned cv;
|
||||
VERIFY(seq().str.is_unit(tokens.get(run_start + i), ch));
|
||||
VERIFY(seq().is_const_char(ch, cv));
|
||||
|
|
@ -791,7 +806,7 @@ std::pair<expr_ref, expr_ref> seq_split::split_membership(expr* str, expr* regex
|
|||
// of each postfix
|
||||
if (!c.empty()) {
|
||||
unsigned w = 0;
|
||||
for (i = 0; i < result.size(); ++i) {
|
||||
for (i = 0; i < result.size(); i++) {
|
||||
expr* d = result[i].m_n;
|
||||
for (unsigned k = 0; d && !seq().re.is_empty(d) && k < c.length(); ++k) {
|
||||
d = m_rw.mk_derivative(seq().mk_char(c[k]), d);
|
||||
|
|
|
|||
|
|
@ -288,15 +288,12 @@ namespace seq {
|
|||
|
||||
bool str_mem::is_trivial(nielsen_node const* n) const {
|
||||
SASSERT(m_str && m_regex);
|
||||
if (m_kind == mem_kind::no_loop)
|
||||
// guard: discharged ⇒ Σ* (accepts all); ε has no non-empty lap-prefix.
|
||||
return m_discharged || m_str->is_empty();
|
||||
if (m_regex->is_full_seq())
|
||||
return true;
|
||||
if (!m_str->is_empty())
|
||||
return false;
|
||||
if (m_kind == mem_kind::stab_view)
|
||||
// ε ∈ stab(root,Q) iff current state ≡ root (i.e. root ∈ F={root}).
|
||||
// ε ∈ L_{Q,{s}}(state) iff current state ≡ acceptance state s (=m_root).
|
||||
return m_regex == m_root;
|
||||
return n->graph().sg().re_nullable(m_regex) == l_true;
|
||||
}
|
||||
|
|
@ -304,10 +301,8 @@ namespace seq {
|
|||
bool str_mem::is_contradiction(nielsen_node const* n) const {
|
||||
if (!(m_str && m_regex && m_str->is_empty()))
|
||||
return false;
|
||||
if (m_kind == mem_kind::no_loop)
|
||||
return false; // guard acceptance is always true on the empty word
|
||||
if (m_kind == mem_kind::stab_view)
|
||||
return m_regex != m_root; // ε ∉ stab(root,Q) when state ≢ root
|
||||
return m_regex != m_root; // ε ∉ view when current state ≢ acceptance s
|
||||
return n->graph().sg().re_nullable(m_regex) == l_false;
|
||||
}
|
||||
|
||||
|
|
@ -427,11 +422,14 @@ namespace seq {
|
|||
SASSERT(mem.m_regex != nullptr);
|
||||
if (mem.is_trivial(this))
|
||||
return;
|
||||
// check if root node contains this membership constraint already
|
||||
if (std::ranges::any_of(str_mems(),
|
||||
[&](const str_mem &e) { return e.m_regex == mem.m_regex && e.m_str == mem.m_str; }))
|
||||
// already present, no need to add again
|
||||
return;
|
||||
// Skip only a FULLY identical membership. The dedup must compare the
|
||||
// whole membership (kind/root/ν), not just (m_str,m_regex): a land-state
|
||||
// view (paper §5.3) shares (m_str,m_regex) with a plain membership on the
|
||||
// same variable+state, and two land-views on the same state differ only
|
||||
// in their acceptance root / ν. Deduping on (m_str,m_regex) alone would
|
||||
// silently drop such a view and lose the constraint (→ unsound leaf).
|
||||
if (std::ranges::any_of(str_mems(), [&](const str_mem &e) { return e == mem; }))
|
||||
return; // already present
|
||||
m_str_mem.push_back(mem);
|
||||
}
|
||||
|
||||
|
|
@ -1262,6 +1260,107 @@ namespace seq {
|
|||
return newly_marked;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Landing decomposition support: Q = states forward-reachable from the head.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
void nielsen_graph::collect_reachable_from_head(euf::snode const* head_re, uint_set& Q) const {
|
||||
Q.reset();
|
||||
if (!head_re || !head_re->get_expr())
|
||||
return;
|
||||
unsigned_vector stack;
|
||||
stack.push_back(head_re->get_expr()->get_id());
|
||||
while (!stack.empty()) {
|
||||
const unsigned s = stack.back();
|
||||
stack.pop_back();
|
||||
if (Q.contains(s))
|
||||
continue;
|
||||
Q.insert(s);
|
||||
auto it = m_partial_dfa_out.find(s);
|
||||
if (it == m_partial_dfa_out.end())
|
||||
continue;
|
||||
for (const unsigned edge_idx : it->second) {
|
||||
if (edge_idx >= m_partial_dfa_edges.size())
|
||||
continue;
|
||||
partial_dfa_edge const& e = m_partial_dfa_edges[edge_idx];
|
||||
if (e.m_dst)
|
||||
stack.push_back(e.m_dst->get_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned nielsen_graph::mark_reachable_projection_edges(euf::snode const* head_re) {
|
||||
// Generalizes mark_scc_projection_edges to the forward-reachable set:
|
||||
// mark every edge whose source is reachable from head_re with the current
|
||||
// extraction index ν, bumping ν iff something new was marked. Views/co-
|
||||
// views gate on projection_state_in_Q (edges marked ≤ ν), so the ν
|
||||
// returned here identifies exactly this Q.
|
||||
uint_set Q;
|
||||
collect_reachable_from_head(head_re, Q);
|
||||
unsigned newly_marked = 0;
|
||||
for (const unsigned src_id : Q) {
|
||||
auto it = m_partial_dfa_out.find(src_id);
|
||||
if (it == m_partial_dfa_out.end())
|
||||
continue;
|
||||
for (const unsigned edge_idx : it->second) {
|
||||
if (edge_idx >= m_partial_dfa_edges.size())
|
||||
continue;
|
||||
partial_dfa_edge const& e = m_partial_dfa_edges[edge_idx];
|
||||
if (!e.m_dst || !Q.contains(e.m_dst->get_id()))
|
||||
continue;
|
||||
if (e.m_projection_idx == 0)
|
||||
++newly_marked;
|
||||
}
|
||||
}
|
||||
if (newly_marked == 0)
|
||||
return m_projection_extract_idx; // Q already covered by the current ν
|
||||
|
||||
++m_projection_extract_idx;
|
||||
const unsigned extract_idx = m_projection_extract_idx;
|
||||
for (const unsigned src_id : Q) {
|
||||
auto it = m_partial_dfa_out.find(src_id);
|
||||
if (it == m_partial_dfa_out.end())
|
||||
continue;
|
||||
for (const unsigned edge_idx : it->second) {
|
||||
if (edge_idx >= m_partial_dfa_edges.size())
|
||||
continue;
|
||||
partial_dfa_edge& e = m_partial_dfa_edges[edge_idx];
|
||||
if (!e.m_dst || !Q.contains(e.m_dst->get_id()))
|
||||
continue;
|
||||
if (e.m_projection_idx == 0)
|
||||
e.m_projection_idx = extract_idx;
|
||||
}
|
||||
}
|
||||
return extract_idx;
|
||||
}
|
||||
|
||||
void nielsen_graph::compute_frontier(uint_set const& Q, svector<euf::snode const*> const& Qstates,
|
||||
vector<frontier_edge>& out_frontier) {
|
||||
// One lazy step from each Q state. Derive over the minterms of every
|
||||
// p ∈ Qstates: δ_mt(p) ∈ Q is an internal edge (recorded, closing cycles
|
||||
// for a future land-at-R); δ_mt(p) ∉ Q (and ≠ ⊥) is a frontier/escape
|
||||
// edge. Snapshot Qstates was collected by the caller BEFORE this call,
|
||||
// so recording internal edges (which appends to m_partial_dfa_edges) does
|
||||
// not disturb the iteration.
|
||||
for (euf::snode const* p : Qstates) {
|
||||
if (!m.inc())
|
||||
return;
|
||||
if (!p || !p->is_ground())
|
||||
continue;
|
||||
euf::snode_vector mts;
|
||||
m_sg.compute_minterms(p, mts);
|
||||
for (euf::snode const* mt : mts) {
|
||||
euf::snode const* q = m_sg.brzozowski_deriv(p, mt);
|
||||
if (!q || q->is_fail() || !q->is_ground())
|
||||
continue;
|
||||
if (Q.contains(q->get_expr()->get_id()))
|
||||
record_partial_derivative_edge(p, q); // internal edge
|
||||
else
|
||||
out_frontier.push_back(frontier_edge{ p, mt, q });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
euf::snode const* nielsen_graph::get_slice(euf::snode const* v, expr* left, expr* right) {
|
||||
SASSERT(v && v->get_expr() && left && right);
|
||||
SASSERT(v->is_var());
|
||||
|
|
@ -1804,15 +1903,15 @@ namespace seq {
|
|||
}
|
||||
}
|
||||
|
||||
// consume leading characters of view / guard memberships (Section 3.3).
|
||||
// consume leading characters of land-state view memberships (paper §5.3).
|
||||
// m_regex is the current (plain) derivative state; we gate on whether it
|
||||
// lies in Q_ν (projection_state_in_Q) and step with the ordinary
|
||||
// derivative, keeping the view/guard annotation.
|
||||
// derivative, keeping the view annotation.
|
||||
for (str_mem& mem : m_str_mem) {
|
||||
SASSERT(mem.well_formed());
|
||||
if (mem.is_plain())
|
||||
continue;
|
||||
if (consume_view_guard(mem))
|
||||
if (consume_view(mem))
|
||||
return simplify_result::conflict;
|
||||
}
|
||||
|
||||
|
|
@ -1864,8 +1963,8 @@ namespace seq {
|
|||
return simplify_result::proceed;
|
||||
}
|
||||
|
||||
bool nielsen_node::consume_view_guard(str_mem& mem) {
|
||||
SASSERT(!mem.is_plain());
|
||||
bool nielsen_node::consume_view(str_mem& mem) {
|
||||
SASSERT(mem.is_view());
|
||||
euf::sgraph& sg = m_graph.sg();
|
||||
|
||||
auto set_regex_conflict = [&]() {
|
||||
|
|
@ -1883,24 +1982,17 @@ namespace seq {
|
|||
// leave it for apply_regex_if_split.
|
||||
if (!c->is_ground() || c->kind() == euf::snode_kind::s_ite)
|
||||
break;
|
||||
const bool in_Q = m_graph.projection_state_in_Q(c->get_expr(), mem.m_nu);
|
||||
if (!in_Q) {
|
||||
if (mem.is_guard()) {
|
||||
// The run left Q: no lap from the start can complete within Q
|
||||
// anymore, so the guard is discharged (accepts every suffix).
|
||||
mem.m_discharged = true;
|
||||
return false;
|
||||
}
|
||||
// view: a^{-1} L_{Q,F}(c) = ∅ when c ∉ Q.
|
||||
if (!m_graph.projection_state_in_Q(c->get_expr(), mem.m_nu)) {
|
||||
// a^{-1} L_{Q,F}(c) = ∅ when c ∉ Q.
|
||||
set_regex_conflict();
|
||||
return true;
|
||||
}
|
||||
// Step with brzozowski_deriv for BOTH concrete and symbolic tokens.
|
||||
// This is essential: the partial-DFA states (and m_root) are produced
|
||||
// by brzozowski_deriv, so its canonicalization must be used here too —
|
||||
// otherwise the guard's resolved state never equals m_root by snode
|
||||
// identity and laps never close. For a symbolic unit it yields a
|
||||
// canonical ite residual that apply_regex_if_split later resolves.
|
||||
// otherwise the resolved state never equals m_root by snode identity.
|
||||
// For a symbolic unit it yields a canonical ite residual that
|
||||
// apply_regex_if_split later resolves.
|
||||
euf::snode const* next = sg.brzozowski_deriv(c, tok);
|
||||
if (!next)
|
||||
break;
|
||||
|
|
@ -1908,25 +2000,11 @@ namespace seq {
|
|||
mem.m_regex = next;
|
||||
if (next->is_fail()) {
|
||||
// view: derivative collapsed to ∅ — unsatisfiable.
|
||||
// guard: the lap can never close through ∅; treat as discharged.
|
||||
if (mem.is_guard()) { mem.m_discharged = true; return false; }
|
||||
set_regex_conflict();
|
||||
return true;
|
||||
}
|
||||
if (next->is_ground() && next->kind() != euf::snode_kind::s_ite) {
|
||||
// concrete next state resolved immediately
|
||||
if (mem.is_guard() && next == mem.m_root) {
|
||||
// a non-empty prefix completed a lap r→…→r within Q.
|
||||
set_regex_conflict();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// symbolic ite residual: defer to apply_regex_if_split, which
|
||||
// resolves the character and (for guards) detects a lap landing
|
||||
// back on the root.
|
||||
break;
|
||||
}
|
||||
if (!(next->is_ground() && next->kind() != euf::snode_kind::s_ite))
|
||||
break; // symbolic ite residual: defer to apply_regex_if_split
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2106,7 +2184,7 @@ namespace seq {
|
|||
++m_stats.m_num_unknown;
|
||||
return search_result::unknown;
|
||||
}
|
||||
catch(const std::exception&) {
|
||||
catch(const std::exception& e) {
|
||||
#ifdef Z3DEBUG
|
||||
std::string dot = to_dot();
|
||||
#endif
|
||||
|
|
@ -3381,9 +3459,11 @@ namespace seq {
|
|||
if (!harvest_mode() && apply_cycle_subsumption(node))
|
||||
return ++m_stats.m_mod_cycle_subsumption, true;
|
||||
|
||||
// Priority 6: CycleDecomp - stabilizer introduction for regex cycles using partial DFA projection
|
||||
// Priority 6: LandingDecomp - the core branching rule (paper §5.3):
|
||||
// split the leading variable by its landing state in the explored region
|
||||
// (land-at-s + escape-via-frontier). Subsumes character unwinding.
|
||||
// (regex-related: skipped in benchmark-harvest mode)
|
||||
if (!harvest_mode() && apply_cycle_decomposition(node))
|
||||
if (!harvest_mode() && apply_landing_decomposition(node))
|
||||
return ++m_stats.m_mod_star_intr, true;
|
||||
|
||||
// Priority 7: GPowerIntr - ground power introduction
|
||||
|
|
@ -3883,12 +3963,17 @@ namespace seq {
|
|||
if (!first->is_var())
|
||||
continue;
|
||||
euf::snode const* R = mem.m_regex;
|
||||
if (!R->is_ground() || R->kind() == euf::snode_kind::s_ite)
|
||||
continue;
|
||||
|
||||
// R must lie on a detected cycle with a marked SCC snapshot.
|
||||
// R must lie on a detected cycle in the explored region. Use the
|
||||
// same reachable-Q snapshot as apply_landing_decomposition so the
|
||||
// stabilizer view stab(R,Q_ν) matches the land-at-R view.
|
||||
ensure_automaton_explored(R);
|
||||
uint_set scc;
|
||||
if (!collect_scc_for_projection(R, scc))
|
||||
continue;
|
||||
const unsigned nu = m_projection_extract_idx;
|
||||
const unsigned nu = mark_reachable_projection_edges(R);
|
||||
if (nu == 0)
|
||||
continue;
|
||||
|
||||
|
|
@ -3924,105 +4009,156 @@ namespace seq {
|
|||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Modifier: apply_cycle_decomposition (paper Section "Cycle Decomposition")
|
||||
// Modifier: apply_landing_decomposition (paper §5.3 "Landing Decomposition")
|
||||
//
|
||||
// For a membership x·u ∈ R whose leading variable x sits on a detected cycle
|
||||
// (R lies in an SCC of the partial DFA) and that does not already carry a
|
||||
// matching cycle guard for the current SCC snapshot, split
|
||||
// x → x'·x''
|
||||
// and attach the two *view*/*guard* primitive constraints (Section 3.3):
|
||||
// x' ∈ stab(R, Q_ν) -- stabilizer view (F = {R})
|
||||
// noloop(x'', R, Q_ν) -- cycle guard (two-mode monitor)
|
||||
// The leading x' is immediately subsumed (its only constraint is the view,
|
||||
// and stab ⊆ stab trivially), so it is dropped from the primary constraint.
|
||||
// The core branching rule. For a plain non-primitive membership x·u ∈ R
|
||||
// (u ≠ ε, x a variable, R a ground regex) we split x by WHERE its value
|
||||
// lands in the explored region Q — the states forward-reachable from R in
|
||||
// the partial DFA G — using the frontier partition (Lemma 4.7):
|
||||
//
|
||||
// Unlike the old projection-operator encoding, the view and guard are kept
|
||||
// as *constraint metadata* over the plain state R and the ν-indexed explored
|
||||
// subautomaton Q_ν — nothing is materialized as a regex, which keeps the
|
||||
// reachable state space finite (termination).
|
||||
// Land-at-s (for each s ∈ Q):
|
||||
// pin x ∈_{Q_ν,{s}} R (land-state view, acceptance F = {s},
|
||||
// current state = R) and advance to u ∈ s.
|
||||
// x is removed outright (no split, no guard). s = R is stabilizer
|
||||
// absorption: one view swallows every lap of the R-cycle.
|
||||
//
|
||||
// Escape-via-(p,a) (for each frontier edge p ∈ Q, δ_a(p) ∉ Q):
|
||||
// substitute x → x1·a·x2 (x1,x2 fresh), pin x1 ∈_{Q_ν,{p}} R,
|
||||
// drop x1 from the active constraint (it lands at p) leaving
|
||||
// a·x2·u ∈ p; the normal char-consumption then steps p →a δ_a(p),
|
||||
// recording the new edge and growing Q.
|
||||
//
|
||||
// By Lemma 4.7 the blocks partition Σ*, so the branches are exhaustive and
|
||||
// pairwise disjoint. Character unwinding is the degenerate Q = {R} case
|
||||
// (land-at-R = x→ε; escape-via-(R,a) = x→a·x2), so this rule subsumes
|
||||
// apply_regex_var_split for ground regexes. Views are constraint metadata
|
||||
// over the plain state R and the ν-indexed explored subautomaton Q_ν
|
||||
// (projection_state_in_Q) — nothing is materialized as a regex.
|
||||
// Termination: a landing removes a variable (active term shrinks); each
|
||||
// escape consumes a fresh state of the finite monotone G.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
bool nielsen_graph::apply_cycle_decomposition(nielsen_node* node) {
|
||||
bool nielsen_graph::apply_landing_decomposition(nielsen_node* node) {
|
||||
if (!m_regex_dynamic_decomposition)
|
||||
return false;
|
||||
// Look for a str_mem with a variable-headed string
|
||||
for (unsigned mi = 0; mi < node->str_mems().size(); ++mi) {
|
||||
str_mem const& mem = node->str_mems()[mi];
|
||||
SASSERT(mem.well_formed());
|
||||
if (!mem.is_plain() || mem.is_primitive())
|
||||
continue;
|
||||
euf::snode const* first = mem.m_str->first();
|
||||
SASSERT(first);
|
||||
if (!first->is_var())
|
||||
euf::snode const* x = mem.m_str->first();
|
||||
SASSERT(x);
|
||||
if (!x->is_var())
|
||||
continue;
|
||||
euf::snode const* R = mem.m_regex;
|
||||
// R must be a settled ground state; an unresolved symbolic ite is
|
||||
// left to apply_regex_if_split, a non-ground regex to the var-split
|
||||
// fallback.
|
||||
if (!R->is_ground() || R->kind() == euf::snode_kind::s_ite)
|
||||
continue;
|
||||
|
||||
euf::snode const* x = first;
|
||||
euf::snode const* R = mem.m_regex;
|
||||
|
||||
// Lazily explore R's full automaton (once, cached) so that
|
||||
// collect_scc_for_projection sees the complete SCC of R and the
|
||||
// stabilizer view / cycle guard gate on a complete Q.
|
||||
// Eagerly explore R's reachable automaton (once, cached) so the SCC
|
||||
// gate below and the landing enumeration see the full explored region
|
||||
// Q — this is the automaton-based landing. Without it the cycle would
|
||||
// not be recorded before factorization/var_split fire, so landing
|
||||
// would never trigger. If exploration hits the resource limit Q is
|
||||
// left partial (a sound under-approximation) and the escape branches
|
||||
// recover completeness.
|
||||
ensure_automaton_explored(R);
|
||||
|
||||
// R must sit on a cycle (an SCC of the partial DFA).
|
||||
uint_set scc;
|
||||
if (!collect_scc_for_projection(R, scc))
|
||||
continue;
|
||||
// Mark the SCC edges; this gives a ν identifying the current Q_SCC.
|
||||
// (We trigger on absence of a matching guard, NOT on novelty.)
|
||||
mark_scc_projection_edges(scc);
|
||||
const unsigned nu = m_projection_extract_idx;
|
||||
if (nu == 0)
|
||||
continue;
|
||||
// Trigger: R must sit on a detected cycle in the explored G. This is
|
||||
// the same gate the old split-and-guard apply_cycle_decomposition
|
||||
// used; the acyclic growth/unwinding of x is left to the pre-existing
|
||||
// flow (apply_regex_var_split / apply_regex_factorization). Landing
|
||||
// then replaces the split+guard on cyclic heads with land-at-s +
|
||||
// escape, and is exhaustive on its own (frontier partition) so
|
||||
// preempting var_split at this node loses no words.
|
||||
{
|
||||
uint_set scc;
|
||||
if (!collect_scc_for_projection(R, scc))
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trigger condition: x must not already carry a cycle guard for the
|
||||
// current SCC snapshot ν. All states of one SCC share a single ν, so
|
||||
// the guard is keyed on ν rather than on the (changing) head R: as the
|
||||
// derivation walks the SCC the head moves, but the lineage is already
|
||||
// guarded against re-traversing the cycle. An already-decomposed
|
||||
// variable whose guard refers to a strictly smaller, stale ν is
|
||||
// re-decomposed to adopt the enlarged SCC.
|
||||
bool already_guarded = false;
|
||||
for (str_mem const& g : node->str_mems()) {
|
||||
if (g.is_guard() && g.m_nu >= nu
|
||||
&& g.m_str && g.m_str->first() == x) {
|
||||
already_guarded = true;
|
||||
break;
|
||||
// Q = states forward-reachable from R (ids), and their snode handles.
|
||||
uint_set Q;
|
||||
collect_reachable_from_head(R, Q);
|
||||
svector<euf::snode const*> Qstates;
|
||||
uint_set added;
|
||||
Qstates.push_back(R);
|
||||
added.insert(R->get_expr()->get_id());
|
||||
for (partial_dfa_edge const& e : m_partial_dfa_edges) {
|
||||
for (expr* ep : { e.m_src, e.m_dst }) {
|
||||
if (!ep) continue;
|
||||
const unsigned id = ep->get_id();
|
||||
if (!Q.contains(id) || added.contains(id)) continue;
|
||||
euf::snode const* sn = m_sg.find(ep);
|
||||
if (sn) { added.insert(id); Qstates.push_back(sn); }
|
||||
}
|
||||
}
|
||||
if (already_guarded)
|
||||
continue;
|
||||
|
||||
// One lazy exploration step: record internal (cycle-closing) edges
|
||||
// and collect the frontier (escape candidates).
|
||||
vector<frontier_edge> frontier;
|
||||
compute_frontier(Q, Qstates, frontier);
|
||||
|
||||
// Mark the reachable subautomaton, fixing the ν that identifies Q_ν
|
||||
// for every view created below. ν may be 0 in the Q = {R} bootstrap
|
||||
// (no in-Q edges yet); a view with ν = 0 denotes exactly {ε} at R,
|
||||
// which is precisely what unwinding needs.
|
||||
const unsigned nu = mark_reachable_projection_edges(R);
|
||||
|
||||
sort* seq_sort = x->get_expr()->get_sort();
|
||||
|
||||
// Construct the replacement x = x' x''
|
||||
euf::snode const* xp = m_sg.mk(m_sk.mk("cycle", x->get_expr(), R->get_expr(), seq_sort));
|
||||
euf::snode const* xpp = get_tail(x, compute_length_expr(xp).get());
|
||||
euf::snode const* xp_xpp = m_sg.mk_concat(xp, xpp);
|
||||
// (a) LAND-AT-s branches (progress: x removed).
|
||||
for (euf::snode const* s : Qstates) {
|
||||
nielsen_node* child = mk_child(node);
|
||||
mk_edge(node, child, "land", /*progress*/ true);
|
||||
str_mem& cm = child->m_str_mem[mi];
|
||||
cm.m_str = m_sg.drop_first(cm.m_str); // u
|
||||
cm.m_regex = s; // active becomes u ∈ s
|
||||
// x ∈_{Q_ν,{s}} R : state = R (start), root = s (acceptance).
|
||||
child->add_str_mem(str_mem::mk_view(m, x, R, s, nu, mem.m_dep));
|
||||
TRACE(seq, tout << "landing: land x=" << mk_pp(x->get_expr(), m)
|
||||
<< " at " << mk_pp(s->get_expr(), m)
|
||||
<< " R=" << mk_pp(R->get_expr(), m) << " nu=" << nu << "\n");
|
||||
}
|
||||
|
||||
nielsen_node* child = mk_child(node);
|
||||
SASSERT(child->m_str_mem[mi] == mem);
|
||||
nielsen_edge* e = mk_edge(node, child, "cycle decomp", false);
|
||||
const nielsen_subst s(x, xp_xpp, mem.m_dep);
|
||||
e->add_subst(s);
|
||||
child->apply_subst(m_sg, s);
|
||||
// (b) ESCAPE branches (non-progress: consumes a fresh state, grows Q).
|
||||
for (frontier_edge const& fe : frontier) {
|
||||
euf::snode const* p = fe.m_src;
|
||||
char_set cs = m_seq_regex->minterm_to_char_set(fe.m_mt->get_expr());
|
||||
if (cs.is_empty())
|
||||
continue;
|
||||
euf::snode const* aunit =
|
||||
m_sg.mk(m_seq.str.mk_unit(m_seq.mk_char(cs.first_char())));
|
||||
euf::snode const* x1 = mk_fresh_var(seq_sort);
|
||||
// x2 = x[|x1|+1:] (slice tail after the landed prefix and the char)
|
||||
const expr_ref after =
|
||||
normalize_arith(m, a.mk_add(compute_length_expr(x1).get(), a.mk_int(1)));
|
||||
euf::snode const* x2 = get_tail(x, after.get());
|
||||
euf::snode const* repl = m_sg.mk_concat(x1, m_sg.mk_concat(aunit, x2));
|
||||
|
||||
// remove from mi^th element of the child the leading token, as it is immediately subsumed
|
||||
SASSERT(child->m_str_mem[mi].m_str->first() == xp);
|
||||
child->m_str_mem[mi].m_str = dir_drop(m_sg, child->m_str_mem[mi].m_str, 1, true);
|
||||
nielsen_node* child = mk_child(node);
|
||||
nielsen_edge* e = mk_edge(node, child, "escape", /*progress*/ false);
|
||||
const nielsen_subst sub(x, repl, mem.m_dep);
|
||||
e->add_subst(sub);
|
||||
child->apply_subst(m_sg, sub);
|
||||
|
||||
// x' ∈ stab(R, Q_ν) (stabilizer view, F = {R}, current state = R)
|
||||
child->add_str_mem(str_mem::mk_view(m, xp, R, R, nu, mem.m_dep));
|
||||
// x1 lands at p: drop it from the active constraint, whose state
|
||||
// becomes p; the leading a is then consumed by simplify_and_init
|
||||
// (stepping p →a δ_a(p) and recording the edge).
|
||||
str_mem& cm = child->m_str_mem[mi];
|
||||
SASSERT(cm.m_str->first() == x1);
|
||||
cm.m_str = dir_drop(m_sg, cm.m_str, 1, true); // a·x2·u
|
||||
cm.m_regex = p;
|
||||
// x1 ∈_{Q_ν,{p}} R
|
||||
child->add_str_mem(str_mem::mk_view(m, x1, R, p, nu, mem.m_dep));
|
||||
TRACE(seq, tout << "landing: escape x=" << mk_pp(x->get_expr(), m)
|
||||
<< " via (" << mk_pp(p->get_expr(), m) << ", "
|
||||
<< cs.first_char() << ") R=" << mk_pp(R->get_expr(), m)
|
||||
<< " nu=" << nu << "\n");
|
||||
}
|
||||
|
||||
// noloop(x'', R, Q_ν) (cycle guard, two-mode monitor, state = R)
|
||||
child->add_str_mem(str_mem::mk_guard(m, xpp, R, R, nu, mem.m_dep));
|
||||
|
||||
TRACE(seq, tout << "cycle_decomp: x=" << mk_pp(x->get_expr(), m)
|
||||
<< " R=" << mk_pp(R->get_expr(), m) << " nu=" << nu << "\n");
|
||||
|
||||
#ifdef Z3DEBUG
|
||||
std::string dot = partial_dfa_to_dot(R, false);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -4108,29 +4244,6 @@ namespace seq {
|
|||
// complement bodies against runaway space blow-up.
|
||||
static const unsigned RF_LAZY_CAP = 1u << 20;
|
||||
|
||||
// The cycle machinery (apply_cycle_decomposition) owns variables it has put
|
||||
// under a noloop guard: factorizing such a variable's membership would split
|
||||
// it in a way that violates the guard's premise (that the variable is handled
|
||||
// by the stabilizer/guard decomposition), yielding a spurious conflict. So
|
||||
// factorization defers whenever the leading token is a guarded variable.
|
||||
static bool leading_var_guarded(nielsen_node const* node, euf::snode const* lead) {
|
||||
for (str_mem const& g : node->str_mems())
|
||||
if (g.is_guard() && g.m_str && g.m_str->first() == lead)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool nielsen_graph::mem_has_cycle_token(str_mem const& mem) const {
|
||||
static const symbol cycle_sym("cycle");
|
||||
SASSERT(mem.m_str);
|
||||
for (euf::snode const* t : mem.m_str->collect_tokens()) {
|
||||
expr* const e = t->get_expr();
|
||||
if (e && m_sk.is_skolem(cycle_sym, e))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
rf_state* nielsen_graph::mk_rf_state(nielsen_node* /*node*/, str_mem const& mem) {
|
||||
euf::snode const* const first = mem.m_str->first();
|
||||
SASSERT(first);
|
||||
|
|
@ -4267,18 +4380,18 @@ namespace seq {
|
|||
if (m_regex_factorization_threshold == 0)
|
||||
return false;
|
||||
|
||||
for (str_mem const& mem : node->str_mems())
|
||||
if (mem.is_view() || mem.is_guard() || mem_has_cycle_token(mem))
|
||||
// A node under landing control carries land-state views; the cycle
|
||||
// machinery (apply_landing_decomposition / apply_cycle_subsumption) owns
|
||||
// it, so factorization defers.
|
||||
for (str_mem const& mem : node->str_mems()) {
|
||||
if (mem.is_view())
|
||||
return false;
|
||||
}
|
||||
|
||||
// Continuation: resume the iterator handed down to this node by its
|
||||
// parent's "remaining splits" branch.
|
||||
if (rf_state* st = node->rf_cont()) {
|
||||
node->set_rf_cont(nullptr); // the iterator migrates to child B (or is dropped)
|
||||
// If the cycle machinery has, in the meantime, put the leading variable
|
||||
// under a guard, stop factorizing and defer (the iterator is dropped).
|
||||
if (leading_var_guarded(node, st->m_mem.m_str->first()))
|
||||
return false;
|
||||
dep_tracker conflict_dep = nullptr;
|
||||
switch (rf_step(node, st, conflict_dep)) {
|
||||
case rf_step_result::branched:
|
||||
|
|
@ -4297,27 +4410,18 @@ namespace seq {
|
|||
// Fresh: find the first factorizable membership and start an iterator.
|
||||
for (str_mem const& mem : node->str_mems()) {
|
||||
SASSERT(mem.well_formed());
|
||||
SASSERT(!mem.m_str->is_empty()); // should have been eliminated already
|
||||
|
||||
// split() handles all regex forms (incl. complement / intersection),
|
||||
// so the classical restriction is no longer needed.
|
||||
if (mem.m_str->is_empty() || mem.is_primitive())
|
||||
if (mem.is_primitive())
|
||||
continue;
|
||||
|
||||
// View / guard memberships (Section 3.3) are handled by the
|
||||
// cycle machinery and the synchronous product, not by factorization.
|
||||
// Land-state view memberships (paper §5.3) are handled by the cycle
|
||||
// machinery and the synchronous product, not by factorization.
|
||||
if (!mem.is_plain())
|
||||
continue;
|
||||
|
||||
// Defer to the cycle machinery when the leading variable is guarded.
|
||||
if (leading_var_guarded(node, mem.m_str->first()))
|
||||
continue;
|
||||
|
||||
// Defer when the membership mentions a cycle stabilizer token: the
|
||||
// cycle machinery owns it, and factorizing it re-expands the cycle
|
||||
// structure indefinitely (see mem_has_cycle_token).
|
||||
if (mem_has_cycle_token(mem))
|
||||
continue;
|
||||
|
||||
rf_state* st = mk_rf_state(node, mem);
|
||||
if (!st)
|
||||
continue; // unsupported regex shape → try the next membership
|
||||
|
|
@ -4659,7 +4763,7 @@ namespace seq {
|
|||
// Canonicalize with th_rewriter so that the resolved leaf shares
|
||||
// its snode id with the corresponding partial-DFA state (which is
|
||||
// built by brzozowski_deriv); otherwise un-simplified residuals
|
||||
// like (a|∅)·R≠a·R break view/guard Q-membership and lap checks.
|
||||
// like (a|∅)·R≠a·R break view Q-membership checks.
|
||||
euf::snode const* new_regex_snode = mk_rewrite(r);
|
||||
nielsen_node *child = mk_child(node);
|
||||
nielsen_edge* e = mk_edge(node, child, "regex if", true);
|
||||
|
|
@ -4669,12 +4773,6 @@ namespace seq {
|
|||
for (str_mem &cm : child->str_mems()) {
|
||||
if (cm == mem) {
|
||||
cm.m_regex = new_regex_snode;
|
||||
// A guard whose symbolic step lands back on the cycle
|
||||
// head closed a lap within Q → this branch is dead.
|
||||
if (cm.is_guard() && new_regex_snode == cm.m_root) {
|
||||
child->set_general_conflict();
|
||||
child->set_conflict(backtrack_reason::regex, cm.m_dep);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -5599,8 +5697,6 @@ namespace seq {
|
|||
if (c.m_complemented)
|
||||
return (c.m_sink || c.m_state != c.m_root) ? l_true : l_false;
|
||||
return (c.m_state == c.m_root) ? l_true : l_false;
|
||||
case mem_kind::no_loop:
|
||||
return l_true; // guard accepts on every prefix it has not failed on
|
||||
}
|
||||
return l_undef;
|
||||
}
|
||||
|
|
@ -5630,15 +5726,6 @@ namespace seq {
|
|||
r.m_state = d;
|
||||
return r;
|
||||
}
|
||||
case mem_kind::no_loop: {
|
||||
if (c.m_sink) return r; // discharged: Σ*
|
||||
if (!projection_state_in_Q(c.m_state->get_expr(), c.m_nu)) { r.m_sink = true; return r; }
|
||||
euf::snode const* d = m_sg.brzozowski_deriv(c.m_state, mt);
|
||||
if (!d || d->is_fail()) { r.m_sink = true; return r; } // lap cannot close through ∅
|
||||
if (d == c.m_root) { r.m_dead = true; return r; } // lap completed → forbidden
|
||||
r.m_state = d;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
|
@ -5733,9 +5820,6 @@ namespace seq {
|
|||
case mem_kind::stab_view:
|
||||
out.push_back(prod_comp::mk_view(mem.m_regex, mem.m_root, mem.m_nu, false));
|
||||
break;
|
||||
case mem_kind::no_loop:
|
||||
out.push_back(prod_comp::mk_guard(mem.m_regex, mem.m_root, mem.m_nu, mem.m_discharged));
|
||||
break;
|
||||
}
|
||||
dep = m_dep_mgr.mk_join(dep, mem.m_dep);
|
||||
found = true;
|
||||
|
|
@ -5743,6 +5827,111 @@ namespace seq {
|
|||
return found;
|
||||
}
|
||||
|
||||
bool nielsen_graph::product_witness(euf::snode const* var, nielsen_node const& node,
|
||||
unsigned len, zstring& out) {
|
||||
// Shortest-accepting-word search over the product of all of var's
|
||||
// primitive components (plain regexes AND land-state views), optionally
|
||||
// intersected with Σ^len. Mirrors check_product_emptiness but records
|
||||
// the spelled word so a SAT-leaf variable pinned to a land-state view
|
||||
// (F={s}) gets a concrete witness (ε alone is inadmissible when s≠head).
|
||||
vector<prod_comp> comps0;
|
||||
dep_tracker d = nullptr;
|
||||
collect_var_components(var, node, comps0, d);
|
||||
|
||||
if (len != UINT_MAX) {
|
||||
// force exact length via a Σ^len plain component
|
||||
sort* re_sort = nullptr;
|
||||
if (!comps0.empty() && comps0[0].m_state)
|
||||
re_sort = comps0[0].m_state->get_expr()->get_sort();
|
||||
if (re_sort) {
|
||||
const expr_ref sigma_n(
|
||||
m_seq.re.mk_loop(m_seq.re.mk_full_char(re_sort), len, len), m);
|
||||
comps0.push_back(prod_comp::mk_plain(m_sg.mk(sigma_n)));
|
||||
}
|
||||
}
|
||||
|
||||
if (comps0.empty()) {
|
||||
// no constraints: any word of the requested length (default ε)
|
||||
out = zstring();
|
||||
for (unsigned i = 0; i < (len == UINT_MAX ? 0u : len); ++i)
|
||||
out = out + zstring((unsigned)'a');
|
||||
return true;
|
||||
}
|
||||
|
||||
auto encode = [](vector<prod_comp> const& cs) {
|
||||
std::vector<unsigned> key;
|
||||
key.reserve(cs.size() * 3);
|
||||
for (auto const& c : cs) {
|
||||
key.push_back(static_cast<unsigned>(c.m_kind));
|
||||
key.push_back((c.m_complemented ? 1u : 0u) | (c.m_sink ? 2u : 0u) | (c.m_dead ? 4u : 0u));
|
||||
key.push_back(c.m_state ? c.m_state->id() : UINT_MAX);
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
std::set<std::vector<unsigned>> visited;
|
||||
// BFS (vector + head index) for a SHORTEST accepting word.
|
||||
vector<std::pair<vector<prod_comp>, zstring>> work;
|
||||
work.push_back({ comps0, zstring() });
|
||||
visited.insert(encode(comps0));
|
||||
unsigned head = 0;
|
||||
const unsigned MAX_STATES = 200000;
|
||||
|
||||
while (head < work.size()) {
|
||||
if (!m.inc() || head >= MAX_STATES)
|
||||
return false;
|
||||
vector<prod_comp> cur = work[head].first;
|
||||
zstring w = work[head].second;
|
||||
++head;
|
||||
|
||||
bool any_dead = false;
|
||||
for (auto const& c : cur) if (c.m_dead) { any_dead = true; break; }
|
||||
if (any_dead)
|
||||
continue;
|
||||
|
||||
bool all_acc = true, any_undef = false;
|
||||
for (auto const& c : cur) {
|
||||
const lbool a = comp_accepting(c);
|
||||
if (a == l_false) { all_acc = false; break; }
|
||||
if (a == l_undef) any_undef = true;
|
||||
}
|
||||
if (all_acc && !any_undef) {
|
||||
out = w;
|
||||
return true;
|
||||
}
|
||||
|
||||
expr* combined = nullptr;
|
||||
for (auto const& c : cur) {
|
||||
if (c.m_sink || c.m_dead) continue;
|
||||
combined = combined ? m_seq.re.mk_inter(combined, c.m_state->get_expr())
|
||||
: c.m_state->get_expr();
|
||||
}
|
||||
if (!combined)
|
||||
continue;
|
||||
euf::snode_vector mts;
|
||||
m_sg.compute_minterms(m_sg.mk(combined), mts);
|
||||
|
||||
for (euf::snode const* mt : mts) {
|
||||
char_set cs = m_seq_regex->minterm_to_char_set(mt->get_expr());
|
||||
if (cs.is_empty())
|
||||
continue;
|
||||
const unsigned ch = cs.first_char();
|
||||
vector<prod_comp> nxt;
|
||||
bool dead = false;
|
||||
for (auto const& c : cur) {
|
||||
prod_comp e = comp_step(c, mt);
|
||||
if (e.m_dead) { dead = true; break; }
|
||||
nxt.push_back(e);
|
||||
}
|
||||
if (dead)
|
||||
continue;
|
||||
if (visited.insert(encode(nxt)).second)
|
||||
work.push_back({ nxt, w + zstring(ch) });
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool nielsen_graph::check_leaf_regex(nielsen_node const& node, dep_tracker& dep) {
|
||||
SASSERT(m_seq_regex);
|
||||
|
||||
|
|
|
|||
|
|
@ -302,15 +302,17 @@ namespace seq {
|
|||
<< snode_label_html(p.deq.m_rhs, p.deq.m, false);
|
||||
}
|
||||
|
||||
// kind of a regex membership constraint (paper Section 3.3, "views"):
|
||||
// kind of a regex membership constraint (paper §5.3, "land-state views"):
|
||||
// - plain: ordinary u ∈ r
|
||||
// - stab_view: stabilizer view u ∈_{Q,{root}} root (acceptance set F={root})
|
||||
// - no_loop: cycle guard noloop(u, root, Q) (two-mode monitor)
|
||||
// For view/guard, m_regex holds the *current derivative state* (a plain
|
||||
// regex; starts at m_root) and Q is identified by the ν-index m_nu over
|
||||
// the partial DFA (projection_state_in_Q). This replaces the old re.proj
|
||||
// projection operator: m_regex is always a plain regex now.
|
||||
enum class mem_kind : unsigned char { plain, stab_view, no_loop };
|
||||
// - stab_view: land-state view u ∈_{Q,{s}} r (acceptance set F={s})
|
||||
// (the stabilizer is the special case s = r).
|
||||
// For a view, m_regex holds the *current derivative state* (a plain regex;
|
||||
// starts at the head r) and m_root holds the landing/acceptance state s.
|
||||
// Q is identified by the ν-index m_nu over the partial DFA
|
||||
// (projection_state_in_Q). m_regex is always a plain regex.
|
||||
// The old two-mode `no_loop` cycle guard has been removed: landing
|
||||
// decomposition (paper §5.3) needs no guard.
|
||||
enum class mem_kind : unsigned char { plain, stab_view };
|
||||
|
||||
// regex membership constraint: str in regex
|
||||
// mirrors ZIPT's StrMem
|
||||
|
|
@ -320,11 +322,10 @@ namespace seq {
|
|||
euf::snode const* m_regex; // assumed to be non-null (plain regex = current run state)
|
||||
dep_tracker m_dep;
|
||||
|
||||
// view / guard annotation (Section 3.3)
|
||||
// land-state view annotation (paper §5.3)
|
||||
mem_kind m_kind = mem_kind::plain;
|
||||
euf::snode const* m_root = nullptr; // cycle head r (view F={r}; guard lap head)
|
||||
euf::snode const* m_root = nullptr; // landing/acceptance state s (view F={s}; stabilizer: s=head)
|
||||
unsigned m_nu = 0; // ν: snapshot index identifying Q
|
||||
bool m_discharged = false; // guard monitor: false=watch, true=discharged
|
||||
|
||||
str_mem(ast_manager& m, euf::snode const* str, euf::snode const* regex, dep_tracker const& dep):
|
||||
m(m), m_str(str), m_regex(regex), m_dep(dep) {}
|
||||
|
|
@ -336,33 +337,25 @@ namespace seq {
|
|||
m_kind = other.m_kind;
|
||||
m_root = other.m_root;
|
||||
m_nu = other.m_nu;
|
||||
m_discharged = other.m_discharged;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// factory for a stabilizer view str ∈_{Q_ν,{root}} root (m_regex=state)
|
||||
// factory for a land-state view str ∈_{Q_ν,{root}} state (m_regex=state,
|
||||
// root = landing/acceptance state s; the stabilizer is state=root=head).
|
||||
static str_mem mk_view(ast_manager& m, euf::snode const* str, euf::snode const* state,
|
||||
euf::snode const* root, unsigned nu, dep_tracker const& dep) {
|
||||
str_mem r(m, str, state, dep);
|
||||
r.m_kind = mem_kind::stab_view; r.m_root = root; r.m_nu = nu;
|
||||
return r;
|
||||
}
|
||||
// factory for a cycle guard noloop(str, root, Q_ν) (m_regex=state)
|
||||
static str_mem mk_guard(ast_manager& m, euf::snode const* str, euf::snode const* state,
|
||||
euf::snode const* root, unsigned nu, dep_tracker const& dep) {
|
||||
str_mem r(m, str, state, dep);
|
||||
r.m_kind = mem_kind::no_loop; r.m_root = root; r.m_nu = nu;
|
||||
return r;
|
||||
}
|
||||
|
||||
bool is_plain() const { return m_kind == mem_kind::plain; }
|
||||
bool is_view() const { return m_kind == mem_kind::stab_view; }
|
||||
bool is_guard() const { return m_kind == mem_kind::no_loop; }
|
||||
|
||||
bool operator==(const str_mem& other) const {
|
||||
return m_str->similar(other.m_str, m) && m_regex == other.m_regex
|
||||
&& m_kind == other.m_kind && m_root == other.m_root
|
||||
&& m_nu == other.m_nu && m_discharged == other.m_discharged;
|
||||
&& m_nu == other.m_nu;
|
||||
}
|
||||
|
||||
bool operator<(const str_mem& other) const {
|
||||
|
|
@ -747,11 +740,11 @@ namespace seq {
|
|||
// Returns proceed, conflict, satisfied, or restart.
|
||||
simplify_result simplify_and_init(ptr_vector<nielsen_edge> const& cur_path);
|
||||
|
||||
// Consume leading concrete/symbolic characters of a view/guard membership
|
||||
// (Section 3.3): gate on Q_ν, step with the ordinary derivative, keeping
|
||||
// the annotation. Returns true if the constraint died (view left Q, or
|
||||
// guard completed a lap) — the caller reports a regex conflict.
|
||||
bool consume_view_guard(str_mem& mem);
|
||||
// Consume leading concrete/symbolic characters of a land-state view
|
||||
// membership (paper §5.3): gate on Q_ν, step with the ordinary
|
||||
// derivative, keeping the annotation. Returns true if the view died
|
||||
// (left Q or derivative collapsed to ∅) — caller reports a regex conflict.
|
||||
bool consume_view(str_mem& mem);
|
||||
|
||||
// true if all str_eqs are trivial and there are no str_mems
|
||||
bool is_satisfied() const;
|
||||
|
|
@ -1059,6 +1052,16 @@ namespace seq {
|
|||
// path of edges from root to sat_node (set when sat_node is set)
|
||||
ptr_vector<nielsen_edge> const& sat_path() const { return m_sat_path; }
|
||||
|
||||
// Model construction (called by seq_model): build a concrete word for
|
||||
// `var` satisfying ALL its primitive constraints on `node` simultaneously
|
||||
// (plain regexes AND land-state views), by a shortest-accepting-word
|
||||
// product search. If len != UINT_MAX the word is forced to that exact
|
||||
// length (Σ^len factor). Returns true and sets `out` on success. Used
|
||||
// for view-constrained variables at a SAT leaf, where a land-state view
|
||||
// (F={s}, s≠head) does not denote a plain regex and ε may be inadmissible.
|
||||
bool product_witness(euf::snode const* var, nielsen_node const& node,
|
||||
unsigned len, zstring& out);
|
||||
|
||||
// add constraints to the root node from external solver
|
||||
void add_str_eq(euf::snode const* lhs, euf::snode const* rhs, smt::enode* l, smt::enode* r) const;
|
||||
void add_str_deq(euf::snode const* lhs, euf::snode const* rhs, sat::literal l) const;
|
||||
|
|
@ -1253,17 +1256,20 @@ namespace seq {
|
|||
mem_kind m_kind = mem_kind::plain;
|
||||
bool m_complemented = false; // ~stab co-view (kind==stab_view)
|
||||
euf::snode const* m_state = nullptr; // current plain regex state
|
||||
euf::snode const* m_root = nullptr; // view/guard cycle head
|
||||
euf::snode const* m_root = nullptr; // land-state view acceptance state s
|
||||
unsigned m_nu = 0; // ν (Q snapshot)
|
||||
bool m_sink = false; // co-view became Σ* / guard discharged
|
||||
bool m_sink = false; // co-view became Σ*
|
||||
bool m_dead = false; // language collapsed to ∅
|
||||
|
||||
static prod_comp mk_plain(euf::snode const* s) { prod_comp c; c.m_state = s; return c; }
|
||||
static prod_comp mk_view(euf::snode const* s, euf::snode const* root, unsigned nu, bool compl_) {
|
||||
prod_comp c; c.m_kind = mem_kind::stab_view; c.m_state = s; c.m_root = root; c.m_nu = nu; c.m_complemented = compl_; return c;
|
||||
}
|
||||
static prod_comp mk_guard(euf::snode const* s, euf::snode const* root, unsigned nu, bool discharged) {
|
||||
prod_comp c; c.m_kind = mem_kind::no_loop; c.m_state = s; c.m_root = root; c.m_nu = nu; c.m_sink = discharged; return c;
|
||||
prod_comp c;
|
||||
c.m_kind = mem_kind::stab_view;
|
||||
c.m_state = s;
|
||||
c.m_root = root;
|
||||
c.m_nu = nu;
|
||||
c.m_complemented = compl_;
|
||||
return c;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1276,7 +1282,7 @@ namespace seq {
|
|||
prod_comp comp_step(prod_comp const& c, euf::snode const* mt);
|
||||
|
||||
// Build the product components for variable `var` from the node's
|
||||
// primitive memberships (plain / view / guard). Joins their deps.
|
||||
// primitive memberships (plain / land-state view). Joins their deps.
|
||||
bool collect_var_components(euf::snode const* var, nielsen_node const& node,
|
||||
vector<prod_comp>& out, dep_tracker& dep);
|
||||
|
||||
|
|
@ -1296,6 +1302,36 @@ namespace seq {
|
|||
// currently covered edge count for this extraction.
|
||||
unsigned mark_scc_projection_edges(uint_set const& scc);
|
||||
|
||||
// Landing decomposition (paper §5.3): Q is the set of explored states
|
||||
// forward-reachable from the head r in the partial DFA G, not merely r's
|
||||
// SCC. These generalize the SCC helpers above.
|
||||
|
||||
// Collect (into Q, as expr ids) every state forward-reachable from
|
||||
// head_re over the recorded partial-DFA edges. Always includes head_re.
|
||||
void collect_reachable_from_head(euf::snode const* head_re, uint_set& Q) const;
|
||||
|
||||
// Mark every forward-reachable edge from head_re with a monotone
|
||||
// extraction index ν (only previously-unmarked edges), bumping ν iff
|
||||
// something new was marked. Returns the ν identifying this Q
|
||||
// (0 if there is nothing marked, i.e. no reachable edge).
|
||||
unsigned mark_reachable_projection_edges(euf::snode const* head_re);
|
||||
|
||||
// A frontier edge (p, mt, q): p ∈ Q, mt a minterm of p, q = δ_mt(p) ∉ Q
|
||||
// and q ≠ ⊥. The escape candidates of the landing decomposition.
|
||||
struct frontier_edge {
|
||||
euf::snode const* m_src; // p ∈ Q
|
||||
euf::snode const* m_mt; // minterm
|
||||
euf::snode const* m_dst; // q = δ_mt(p) ∉ Q
|
||||
};
|
||||
|
||||
// One lazy exploration step around Q: for each p ∈ Qstates, take every
|
||||
// minterm derivative δ_mt(p). If δ_mt(p) ∈ Q record the internal edge
|
||||
// (closing cycles); otherwise (and ≠ ⊥) append it to out_frontier.
|
||||
// Q holds expr ids (as produced by collect_reachable_from_head);
|
||||
// Qstates are the corresponding snode handles (including the head).
|
||||
void compute_frontier(uint_set const& Q, svector<euf::snode const*> const& Qstates,
|
||||
vector<frontier_edge>& out_frontier);
|
||||
|
||||
euf::snode const* get_slice(euf::snode const* v, expr* left, expr* right);
|
||||
|
||||
euf::snode const* get_tail(euf::snode const* v, expr* cnt, bool fwd = true);
|
||||
|
|
@ -1377,10 +1413,18 @@ namespace seq {
|
|||
// branch into s ∈ th under condition c, and s ∈ el under condition ¬c.
|
||||
bool apply_regex_if_split(nielsen_node* node);
|
||||
|
||||
// cycle decomposition: for a str_mem x·s ∈ R where a partial DFA
|
||||
// cycle is detected, project SCC onto stabilizer constraint b.
|
||||
// Rewrites x into x'·x'' with x' ∈ b*, x'' ∈ complement((b ∩ complement(eps)) · Sigma*).
|
||||
bool apply_cycle_decomposition(nielsen_node* node);
|
||||
// landing decomposition (paper §5.3): the core branching rule for a
|
||||
// plain non-primitive membership x·u ∈ R (u ≠ ε, x a variable, R ground).
|
||||
// Splits x by WHERE its value lands in the explored region Q (states
|
||||
// forward-reachable from R):
|
||||
// - land-at-s (for each s ∈ Q): pin x ∈_{Q,{s}} R and advance to u ∈ s
|
||||
// (no split; s = R is stabilizer/cycle absorption);
|
||||
// - escape-via-(p,a) (for each frontier edge): x → x1·a·x2 with
|
||||
// x1 ∈_{Q,{p}} R, advancing to x2·u ∈ δ_a(p) and growing Q.
|
||||
// By the frontier partition (Lemma 4.7) these branches are exhaustive and
|
||||
// disjoint; character unwinding is the degenerate Q = {R} case. Replaces
|
||||
// the old split-and-guard apply_cycle_decomposition.
|
||||
bool apply_landing_decomposition(nielsen_node* node);
|
||||
|
||||
// cycle subsumption: for a str_mem x·rest ∈ R where x is constrained
|
||||
// to L(Reg_x) ⊆ L(stabilizer of R), simplify to rest ∈ R.
|
||||
|
|
@ -1411,14 +1455,6 @@ namespace seq {
|
|||
// disjunction is refuted → the continuation node is a regex conflict.
|
||||
bool apply_regex_factorization(nielsen_node* node);
|
||||
|
||||
// True if the membership's string mentions a `cycle` stabilizer token
|
||||
// (the (cycle x R) Skolem minted by apply_cycle_decomposition). Such a
|
||||
// membership is owned by the cycle machinery; factorization must defer to
|
||||
// it — re-splitting it re-expands the very structure the cycle rule is
|
||||
// trying to close, and the fresh slice variables each cycle step mints
|
||||
// defeat the exact-structural loop-cut → non-termination.
|
||||
bool mem_has_cycle_token(str_mem const& mem) const;
|
||||
|
||||
// Build a suspended factorization (boundary head/tail + split iterator)
|
||||
// for `mem`. Returns null if the regex shape is unsupported (the engine
|
||||
// cannot even start a split). Allocated into m_rf_states.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ namespace smt {
|
|||
m_var_values.reset();
|
||||
m_var_replacement.reset();
|
||||
m_var_regex.reset();
|
||||
m_view_vars.reset();
|
||||
m_trail.reset();
|
||||
|
||||
m_factory = alloc(seq_factory, m, m_seq.get_family_id(), mg.get_model());
|
||||
|
|
@ -106,6 +107,8 @@ namespace smt {
|
|||
|
||||
seq::nielsen_node* sat_node = nielsen.sat_node();
|
||||
SASSERT(sat_node); // in case we report sat, this has to point to a satisfied Nielsen node!
|
||||
m_nielsen = &nielsen;
|
||||
m_sat_node = sat_node;
|
||||
collect_var_regex_constraints(sat_node);
|
||||
|
||||
|
||||
|
|
@ -451,11 +454,45 @@ namespace smt {
|
|||
SASSERT(var->get_expr());
|
||||
SASSERT(m_seq.is_seq(var->get_expr()));
|
||||
auto srt = var->get_expr()->get_sort();
|
||||
|
||||
// check if this variable has regex constraints
|
||||
euf::snode const* re = nullptr;
|
||||
unsigned key = var_key(var);
|
||||
if (m_var_regex.find(key, re) && re) {
|
||||
|
||||
// Land-state view (paper §5.3): the variable is pinned to L_{Q,{s}}(head),
|
||||
// which is not a plain regex. Build a witness by the product search over
|
||||
// ALL of the variable's primitive constraints (view AND any plain regex).
|
||||
// This is authoritative for a view variable — we must NOT fall through to
|
||||
// the plain m_var_regex path below, which sees only the plain constraints
|
||||
// and would emit a word that violates the view (unsound witness).
|
||||
if (m_nielsen && m_sat_node && m_view_vars.contains(key)) {
|
||||
expr_ref len_e(m_seq.str.mk_length(var->get_expr()), m);
|
||||
rational len_val;
|
||||
unsigned len = UINT_MAX;
|
||||
if (get_arith_value(len_e, len_val) && len_val.is_unsigned())
|
||||
len = len_val.get_unsigned();
|
||||
zstring w;
|
||||
// Try the arith-assigned length first (keeps the model length-consistent);
|
||||
// if the view∩plain intersection has no word of that length (the arith
|
||||
// length and the regex are only loosely coupled), retry unconstrained so
|
||||
// the emitted word still satisfies every regex/view constraint.
|
||||
bool ok = m_nielsen->product_witness(var, *m_sat_node, len, w);
|
||||
if (!ok && len != UINT_MAX)
|
||||
ok = m_nielsen->product_witness(var, *m_sat_node, UINT_MAX, w);
|
||||
if (ok) {
|
||||
expr* witness = m_seq.str.mk_string(w);
|
||||
m_trail.push_back(witness);
|
||||
m_factory->register_value(witness);
|
||||
return witness;
|
||||
}
|
||||
IF_VERBOSE(1, verbose_stream() << "nseq view witness failed for "
|
||||
<< mk_pp(var->get_expr(), m) << "\n");
|
||||
// product search exhausted: fall through only to the length fallback,
|
||||
// never to the view-ignoring plain path (guarded by !m_view_vars below).
|
||||
}
|
||||
|
||||
// check if this variable has regex constraints (non-view vars only; a view
|
||||
// var is fully handled above — its plain constraints are already folded
|
||||
// into the product search).
|
||||
euf::snode const* re = nullptr;
|
||||
if (!m_view_vars.contains(key) && m_var_regex.find(key, re) && re) {
|
||||
expr* re_expr = re->get_expr();
|
||||
SASSERT(re_expr);
|
||||
|
||||
|
|
@ -596,11 +633,13 @@ namespace smt {
|
|||
if (mem.is_trivial(sat_node))
|
||||
continue; // empty string in nullable regex: already satisfied, no variable to constrain
|
||||
VERIFY(mem.is_primitive()); // everything else should have been eliminated already
|
||||
// TODO(view/guard witness): a stabilizer view / cycle guard does not
|
||||
// denote a plain regex on the variable; for now skip it during model
|
||||
// construction (handled by the dedicated view/guard witness below).
|
||||
if (!mem.is_plain())
|
||||
// A land-state view (paper §5.3) does not denote a plain regex on the
|
||||
// variable; mark it so mk_fresh_value builds its witness via the
|
||||
// product search (which respects the view AND any plain constraints).
|
||||
if (!mem.is_plain()) {
|
||||
m_view_vars.insert(var_key(mem.m_str));
|
||||
continue;
|
||||
}
|
||||
unsigned id = var_key(mem.m_str);
|
||||
euf::snode const* existing = nullptr;
|
||||
if (m_var_regex.find(id, existing) && existing) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,15 @@ namespace smt {
|
|||
// collected during init() from the state's str_mem list.
|
||||
u_map<euf::snode const*> m_var_regex;
|
||||
|
||||
// variables carrying a land-state view (paper §5.3) at the SAT leaf; their
|
||||
// witness is produced by the product search (m_nielsen->product_witness),
|
||||
// not by some_string_in_re over a plain regex.
|
||||
uint_set m_view_vars;
|
||||
|
||||
// the SAT leaf and its owning graph, captured in init() for view witnesses.
|
||||
seq::nielsen_graph* m_nielsen = nullptr;
|
||||
seq::nielsen_node const* m_sat_node = nullptr;
|
||||
|
||||
public:
|
||||
seq_model(ast_manager& m, context& ctx, seq_util& seq,
|
||||
seq_rewriter& rw, euf::sgraph& sg);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue