3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-15 11:35:42 +00:00

Some cleanup/refactoring for stabilization code

This commit is contained in:
Clemens Eisenhofer 2026-07-10 09:40:25 +02:00
parent 596cc14e83
commit fa93f20d2e
2 changed files with 152 additions and 403 deletions

View file

@ -673,31 +673,14 @@ namespace seq {
bool nielsen_graph::projection_state_in_Q(expr* state, unsigned nu) {
if (!state || nu == 0)
return false;
const unsigned sid = state->get_id();
// Exact semantics: ν names the state set recorded when the view was
// created (paper: a view is identified by ν AND its recorded state
// set Q; see mark_reachable_projection_edges).
// set Q; see mark_reachable_projection_edges). Every minted ν has a
// snapshot; an unknown ν denotes the empty region.
const auto sit = m_projection_snapshots.find(nu);
if (sit != m_projection_snapshots.end())
return sit->second.m_ids.contains(sid);
// Fallback for a ν minted without snapshot (none are anymore; kept for
// robustness): r ∈ Q_ν iff r is incident to a partial-DFA edge whose
// extraction index lies in [1, ν] — the historical watermark, which
// over-approximates the intended Q by the union of all extractions.
auto incident = [&](std::unordered_map<unsigned, unsigned_vector> const &adj) {
auto it = adj.find(sid);
if (it == adj.end())
return false;
for (const unsigned edge_idx : it->second) {
if (edge_idx >= m_partial_dfa_edges.size())
continue;
const unsigned pidx = m_partial_dfa_edges[edge_idx].m_projection_idx;
if (pidx != 0 && pidx <= nu)
return true;
}
return false;
};
return incident(m_partial_dfa_out) || incident(m_partial_dfa_in);
SASSERT(sit != m_projection_snapshots.end());
return sit != m_projection_snapshots.end()
&& sit->second.m_ids.contains(state->get_id());
}
nielsen_node* nielsen_graph::mk_node() {
@ -785,7 +768,6 @@ namespace seq {
// m_mod_cnt.reset();
m_partial_dfa_edges.reset();
m_partial_dfa_out.clear();
m_partial_dfa_in.clear();
m_partial_dfa_edge_index.clear();
m_partial_dfa_pin.reset();
m_projection_extract_idx = 0;
@ -1130,7 +1112,7 @@ namespace seq {
// Deduplicate transitions by (src, dst) only — NOT by label. The
// Brzozowski automaton is deterministic, so the only a-transition out of
// a state is to δ_a(state); edge labels are never consulted by
// projection_state_in_Q / collect_scc_for_projection. Keying by label
// projection_state_in_Q / head_on_cycle. Keying by label
// would record the SAME transition twice when discovered once as a
// concrete char and once as a minterm range, spuriously inflating the
// SCC edge count and re-triggering cycle decomposition.
@ -1152,31 +1134,24 @@ namespace seq {
m_partial_dfa_edges.push_back(e);
m_partial_dfa_out[src_e->get_id()].push_back(edge_idx);
m_partial_dfa_in[dst_e->get_id()].push_back(edge_idx);
}
bool nielsen_graph::collect_scc_for_projection(euf::snode const* root_re, uint_set& scc) const {
scc.reset();
if (!root_re || !root_re->get_expr())
bool nielsen_graph::head_on_cycle(euf::snode const* head_re) const {
// Trigger gate for the cycle machinery: does some non-empty recorded
// path lead from head_re back to head_re? (Formerly a full SCC
// computation whose result was only ever consumed as this boolean.)
// Ids are expression ids (matching the keys of m_partial_dfa_out),
// stable across sgraph pops because the exprs are pinned in
// m_partial_dfa_pin.
if (!head_re || !head_re->get_expr())
return false;
// scc, fwd, bwd contain expression ids (matching the keys in
// m_partial_dfa_out / m_partial_dfa_in). Expression ids are stable
// across sgraph pops because we pin them in m_partial_dfa_pin.
const unsigned root_id = root_re->get_expr()->get_id();
uint_set fwd, bwd;
const unsigned root_id = head_re->get_expr()->get_id();
uint_set seen;
unsigned_vector stack;
stack.push_back(root_id);
while (!stack.empty()) {
unsigned s = stack.back();
stack.pop_back();
if (fwd.contains(s))
continue;
fwd.insert(s);
auto push_succs = [&](unsigned s) {
auto it = m_partial_dfa_out.find(s);
if (it == m_partial_dfa_out.end())
continue;
return;
for (const unsigned edge_idx : it->second) {
if (edge_idx >= m_partial_dfa_edges.size())
continue;
@ -1184,101 +1159,21 @@ namespace seq {
if (e.m_dst)
stack.push_back(e.m_dst->get_id());
}
}
stack.push_back(root_id);
};
push_succs(root_id); // start at the successors: a cycle needs >= 1 edge
while (!stack.empty()) {
unsigned s = stack.back();
const unsigned s = stack.back();
stack.pop_back();
if (bwd.contains(s))
continue;
bwd.insert(s);
auto it = m_partial_dfa_in.find(s);
if (it == m_partial_dfa_in.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_src)
stack.push_back(e.m_src->get_id());
}
}
for (const unsigned s : fwd) {
if (bwd.contains(s))
scc.insert(s);
}
if (!scc.contains(root_id))
return false;
if (scc.num_elems() > 1)
return true;
const auto it = m_partial_dfa_out.find(root_id);
if (it == m_partial_dfa_out.end())
return false;
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 && e.m_dst->get_id() == root_id)
if (s == root_id)
return true;
if (seen.contains(s))
continue;
seen.insert(s);
push_succs(s);
}
return false;
}
unsigned nielsen_graph::mark_scc_projection_edges(uint_set const& scc) {
// scc contains expr ids (see collect_scc_for_projection).
//
// Returns the number of edges *newly* added to the projection coverage
// by this call (edges that were previously unmarked). Crucially, the
// monotone extraction index is bumped ONLY when there is something new
// to mark. This makes "the explored SCC has grown" a *global* property
// of the SCC's edge set rather than a per-entry-state one: re-visiting
// an already-fully-marked SCC from a different state (e.g. a derivative
// state br of r, which shares the SCC {r, br}) marks nothing new and is
// therefore not treated as a fresh cycle to decompose. Without this,
// each state of the cycle would trigger its own redundant decomposition
// as the derivation walks around the SCC.
unsigned newly_marked = 0;
for (unsigned src_id : scc) {
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 || !scc.contains(e.m_dst->get_id()))
continue;
if (e.m_projection_idx == 0)
++newly_marked;
}
}
if (newly_marked == 0)
return 0;
++m_projection_extract_idx;
const unsigned extract_idx = m_projection_extract_idx;
for (unsigned src_id : scc) {
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 || !scc.contains(e.m_dst->get_id()))
continue;
if (e.m_projection_idx == 0)
e.m_projection_idx = extract_idx;
}
}
return newly_marked;
}
// -----------------------------------------------------------------------
// Landing decomposition support: Q = states forward-reachable from the head.
// -----------------------------------------------------------------------
@ -1313,11 +1208,7 @@ namespace seq {
// identified by ν AND its recorded state set Q). The returned ν names
// the EXACT forward-reachable set Q of head_re at this moment, stored
// in m_projection_snapshots; views gate on membership in that snapshot
// (projection_state_in_Q). The per-edge watermark is still written —
// as the fallback for a ν without snapshot — but no longer defines
// view semantics: on its own, "edges marked ≤ ν" is the union of ALL
// extractions up to ν, blurring this view's Q with unrelated heads'
// regions once exploration is partial (lazy mode).
// (projection_state_in_Q).
if (!head_re || !head_re->get_expr())
return 0;
const unsigned head_id = head_re->get_expr()->get_id();
@ -1350,23 +1241,6 @@ namespace seq {
const unsigned nu = ++m_projection_extract_idx;
// Watermark the in-Q edges (only previously unmarked ones) — fallback
// data only, see above.
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 = nu;
}
}
// Record the snapshot: the id set plus the state exprs (head first,
// then in-Q edge endpoints). Pin the head so every stored expr
// outlives sgraph pops (edge endpoints are already pinned by
@ -1396,39 +1270,20 @@ namespace seq {
void nielsen_graph::collect_projection_states(unsigned nu, svector<euf::snode const*>& out) {
// Enumeration counterpart of projection_state_in_Q — keep in sync with
// it (it is the membership test the view gates use in consume_view and
// comp_step). Exact semantics: the states of the ν-snapshot.
// comp_step). Exact semantics: the states of the ν-snapshot; every
// minted ν has one, an unknown ν denotes the empty region.
if (nu == 0)
return;
const auto sit = m_projection_snapshots.find(nu);
if (sit != m_projection_snapshots.end()) {
for (expr* ep : sit->second.m_states) {
// mk, not find: the exprs are pinned but their snodes may have
// been released by an sgraph pop.
euf::snode const* sn = m_sg.mk(ep);
if (sn)
out.push_back(sn);
}
SASSERT(sit != m_projection_snapshots.end());
if (sit == m_projection_snapshots.end())
return;
}
// Fallback for a ν without snapshot: the states incident to an edge
// with extraction index in [1, ν] (historical watermark).
uint_set added;
for (partial_dfa_edge const& e : m_partial_dfa_edges) {
if (e.m_projection_idx == 0 || e.m_projection_idx > nu)
continue;
for (expr* ep : { e.m_src, e.m_dst }) {
if (!ep)
continue;
const unsigned id = ep->get_id();
if (added.contains(id))
continue;
// mk, not find: the expr is pinned (m_partial_dfa_pin) but its
// snode may have been released by an sgraph pop since the edge
// was recorded (see the analogous collection of Qstates in
// apply_landing_decomposition).
euf::snode const* sn = m_sg.mk(ep);
if (sn) { added.insert(id); out.push_back(sn); }
}
for (expr* ep : sit->second.m_states) {
// mk, not find: the exprs are pinned but their snodes may have
// been released by an sgraph pop.
euf::snode const* sn = m_sg.mk(ep);
if (sn)
out.push_back(sn);
}
}
@ -2063,62 +1918,13 @@ namespace seq {
}
}
// consume symbolic characters via uniform derivatives
for (str_mem& mem : m_str_mem) {
SASSERT(mem.well_formed());
if (mem.is_primitive() || !mem.is_plain())
continue;
while (mem.m_str && !mem.m_str->is_empty()) {
// TODO: generalize this to work for reverse derivative as well.
euf::snode const* tok = mem.m_str->first();
if (!tok || !tok->is_char_or_unit())
break;
euf::snode const* src_re = mem.m_regex;
euf::snode const* next = nullptr;
{
seq_rewriter rw(m);
expr_ref d(rw.mk_derivative(mem.m_regex->get_expr()), m);
// Extract the inner char expression from seq.unit(?inner)
expr *inner_char = tok->arg0()->get_expr();
// substitute the inner char for the derivative variable in d
var_subst vs(m);
d = vs(d, inner_char);
th_rewriter thrw(m);
thrw(d);
// Record concrete minterm edges for src_re so cycle_decomp can
// detect SCCs lazily. Skip when the component is already fully
// explored (ensure_automaton_explored) — its edges are recorded.
if (src_re->is_ground()
&& !m_graph.m_explored_automaton.contains(src_re->get_expr()->get_id())) {
euf::snode_vector mts;
sg.compute_minterms(src_re, mts);
for (euf::snode const* mt : mts) {
euf::snode const* mt_deriv = sg.brzozowski_deriv(src_re, mt);
if (mt_deriv && !mt_deriv->is_fail())
m_graph.record_partial_derivative_edge(src_re, mt_deriv);
}
}
next = sg.mk(d);
}
if (next->is_fail()) {
TRACE(seq, tout << "empty regex" << spp(mem.m_regex, m) << "\n");
set_general_conflict();
set_conflict(backtrack_reason::regex, mem.m_dep);
return simplify_result::conflict;
}
mem.m_str = sg.drop_left(mem.m_str, 1);
mem.m_regex = next;
}
}
// NOTE: a second "consume symbolic characters via uniform derivatives"
// loop used to follow here. It was unreachable: the loop above already
// consumes every leading char/unit (concrete AND symbolic) through
// sg.brzozowski_deriv, which canonicalizes with th_rewriter — and being
// a second, different derivative-construction path it was exactly the
// canonicalization-divergence hazard the brzozowski_deriv comment warns
// about, so it was removed rather than kept in sync.
// consume leading characters of land-state view memberships (paper §5.3).
// m_regex is the current (plain) derivative state; we gate on whether it
@ -4230,8 +4036,7 @@ namespace seq {
// 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))
if (!head_on_cycle(R))
continue;
const unsigned nu = mark_reachable_projection_edges(R);
if (nu == 0)
@ -4334,11 +4139,8 @@ namespace seq {
// 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;
}
if (!head_on_cycle(R))
continue;
// Q = states forward-reachable from R (ids), and their snode handles.
uint_set Q;
@ -5294,7 +5096,7 @@ namespace seq {
// Branch 2..k: x → c · x' per JOINT minterm of every constraint on x.
// Option (b) — synchronize at var-split time. Instead of unwinding to
// a single symbolic char ?c and letting each of x's constraints (the
// primary membership, the stabilizer view, the cycle guard) derive ?c
// primary membership, any pinned land-state views) derive ?c
// into its own ite — which apply_regex_if_split then resolves
// independently, materializing a cross-product of their states — we
// branch directly on the joint minterm partition of all of x's
@ -6154,86 +5956,69 @@ namespace seq {
return r;
}
void nielsen_graph::prod_comp_key(prod_comp const& c, std::vector<unsigned>& key) {
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);
}
lbool nielsen_graph::tuple_accepting(vector<prod_comp> const& cs) const {
bool any_undef = false;
for (auto const& c : cs) {
const lbool a = comp_accepting(c);
if (a == l_false)
return l_false;
if (a == l_undef)
any_undef = true;
}
return any_undef ? l_undef : l_true;
}
bool nielsen_graph::step_tuple(vector<prod_comp> const& cur, euf::snode const* mt,
vector<prod_comp>& nxt) {
nxt.reset();
for (auto const& c : cur) {
prod_comp d = comp_step(c, mt);
if (d.m_dead)
return false;
nxt.push_back(d);
}
return true;
}
void nielsen_graph::joint_minterms(vector<prod_comp> const& comps, prod_comp const* extra,
euf::snode_vector& mts) {
// joint first-character partition = minterms of the intersection of
// all still-discriminating (non-sink, non-dead) component states.
expr* combined = nullptr;
auto add_state = [&](prod_comp const& c) {
if (c.m_sink || c.m_dead)
return;
combined = combined ? m_seq.re.mk_inter(combined, c.m_state->get_expr())
: c.m_state->get_expr();
};
if (extra)
add_state(*extra);
for (auto const& c : comps)
add_state(c);
if (!combined)
return; // no discriminating state: no character step possible
m_sg.compute_minterms(m_sg.mk(combined), mts);
}
lbool nielsen_graph::check_product_emptiness(vector<prod_comp> const& comps0, unsigned max_states) {
if (comps0.empty())
return l_false; // empty intersection = Σ* (non-empty)
auto encode = [](vector<prod_comp> const& cs) {
std::vector<unsigned> key;
key.reserve(cs.size() * 5);
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;
vector<vector<prod_comp>> work;
work.push_back(comps0);
visited.insert(encode(comps0));
unsigned explored = 0;
bool undef_acceptance = false; // some tuple's acceptance was undecidable
while (!work.empty()) {
if (!m.inc())
return l_undef;
if (explored >= max_states)
return l_undef;
vector<prod_comp> cur = work.back();
work.pop_back();
++explored;
bool any_dead = false;
for (auto const& c : cur) if (c.m_dead) { any_dead = true; break; }
if (any_dead)
continue;
// simultaneously accepting?
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)
return l_false; // found a common word
if (all_acc && any_undef)
// possibly accepting, but undecidable: exhaustion may no longer
// claim emptiness (pruning/subsuming on it would be unsound)
undef_acceptance = true;
// joint first-character partition = minterms of the intersection of
// all still-discriminating component states.
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; // no discriminating state and not accepting: dead end
euf::snode_vector mts;
m_sg.compute_minterms(m_sg.mk(combined), mts);
for (euf::snode const* mt : mts) {
vector<prod_comp> nxt;
bool dead = false;
for (auto const& c : cur) {
prod_comp d = comp_step(c, mt);
if (d.m_dead) { dead = true; break; }
nxt.push_back(d);
}
if (dead)
continue;
if (visited.insert(encode(nxt)).second)
work.push_back(nxt);
}
}
// exhausted with no accepting tuple → empty, unless some tuple's
// acceptance could not be decided
return undef_acceptance ? l_undef : l_true;
// Thin wrapper over the concatenation-aware engine: a single factor
// holding the whole tuple, with a trivially accepting Σ* right-hand
// side. A common word is then found exactly when the factor tuple is
// simultaneously accepting.
sort* re_sort = comps0[0].m_state->get_expr()->get_sort();
const expr_ref full(m_seq.re.mk_full_seq(re_sort), m);
const prod_comp rhs = prod_comp::mk_plain(m_sg.mk(full));
vector<vector<prod_comp>> factors;
factors.push_back(comps0);
return check_concat_product_emptiness(factors, rhs, max_states);
}
// -----------------------------------------------------------------------
@ -6270,14 +6055,9 @@ namespace seq {
std::vector<unsigned> key;
key.reserve(4 + cs.size() * 3);
key.push_back(idx);
auto push_comp = [&key](prod_comp const& c) {
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);
};
push_comp(r);
prod_comp_key(r, key);
for (auto const& c : cs)
push_comp(c);
prod_comp_key(c, key);
return key;
};
@ -6291,6 +6071,11 @@ namespace seq {
push_state(0, k == 0 ? vector<prod_comp>() : factors[0], rhs);
unsigned explored = 0;
// an ε-advance or final acceptance was undecidable somewhere: on
// exhaustion we may no longer claim emptiness (pruning on it would be
// unsound), but a definite common word found on another path still
// decides l_false.
bool undef_result = false;
while (!work.empty()) {
if (!m.inc())
@ -6310,15 +6095,12 @@ namespace seq {
// ε-advance / final acceptance: do all components of the current
// factor accept? (Trivially true for the Σ* empty factor and for
// the terminal index k.)
lbool allacc = l_true;
for (auto const& c : cur.m_comps) {
const lbool a = comp_accepting(c);
if (a == l_false) { allacc = l_false; break; }
if (a == l_undef) allacc = l_undef;
}
// the terminal index k.) An undecided acceptance forfeits only
// this state's ε-advance/word-end — character continuations are
// still explored.
const lbool allacc = tuple_accepting(cur.m_comps);
if (allacc == l_undef)
return l_undef; // cannot decide the factor split — do not prune
undef_result = true;
if (allacc == l_true) {
if (cur.m_idx >= k) {
@ -6327,7 +6109,7 @@ namespace seq {
if (racc == l_true)
return l_false; // found a common word
if (racc == l_undef)
return l_undef;
undef_result = true;
}
else
// ε-advance to the next factor (kept alongside the
@ -6342,38 +6124,22 @@ namespace seq {
// character step: joint first-character partition of the live
// component states (factor + rhs)
expr* combined = nullptr;
auto add_state = [&](prod_comp const& c) {
if (c.m_sink || c.m_dead)
return;
combined = combined ? m_seq.re.mk_inter(combined, c.m_state->get_expr())
: c.m_state->get_expr();
};
add_state(cur.m_rhs);
for (auto const& c : cur.m_comps)
add_state(c);
if (!combined)
continue;
euf::snode_vector mts;
m_sg.compute_minterms(m_sg.mk(combined), mts);
joint_minterms(cur.m_comps, &cur.m_rhs, mts);
for (euf::snode const* mt : mts) {
prod_comp r2 = comp_step(cur.m_rhs, mt);
if (r2.m_dead)
continue;
vector<prod_comp> nxt;
bool dead = false;
for (auto const& c : cur.m_comps) {
prod_comp d = comp_step(c, mt);
if (d.m_dead) { dead = true; break; }
nxt.push_back(d);
}
if (dead)
if (!step_tuple(cur.m_comps, mt, nxt))
continue;
push_state(cur.m_idx, nxt, r2);
}
}
return l_true; // exhausted with no accepting configuration → empty
// exhausted with no accepting configuration → empty, unless some
// acceptance/advance decision could not be made along the way
return undef_result ? l_undef : l_true;
}
bool nielsen_graph::collect_var_components(euf::snode const* var, nielsen_node const& node,
@ -6432,11 +6198,8 @@ namespace seq {
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);
}
for (auto const& c : cs)
prod_comp_key(c, key);
return key;
};
@ -6460,27 +6223,13 @@ namespace seq {
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) {
if (tuple_accepting(cur) == l_true) {
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);
joint_minterms(cur, nullptr, mts);
for (euf::snode const* mt : mts) {
char_set cs = m_seq_regex->minterm_to_char_set(mt->get_expr());
@ -6488,13 +6237,7 @@ namespace seq {
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)
if (!step_tuple(cur, mt, nxt))
continue;
if (visited.insert(encode(nxt)).second)
work.push_back({ nxt, w + zstring(ch) });

View file

@ -844,7 +844,6 @@ namespace seq {
expr* m_src = nullptr;
//expr* m_label = nullptr; // one-character regex label (char/minterm)
expr* m_dst = nullptr;
unsigned m_projection_idx = 0; // first extraction index that included this edge
};
struct partial_dfa_edge_key {
@ -943,7 +942,6 @@ namespace seq {
// the expression is pinned, unlike snode->id() which is reused on pop.
vector<partial_dfa_edge> m_partial_dfa_edges;
std::unordered_map<unsigned, unsigned_vector> m_partial_dfa_out;
std::unordered_map<unsigned, unsigned_vector> m_partial_dfa_in;
std::unordered_map<partial_dfa_edge_key, unsigned, partial_dfa_edge_key_hash> m_partial_dfa_edge_index;
// Pins every expression referenced by m_partial_dfa_edges so the
// egraph cannot release them on pop. We never shrink this — the
@ -952,11 +950,7 @@ namespace seq {
// Monotone snapshot index ν, bumped whenever a new view state set is
// recorded (mark_reachable_projection_edges). A view's Q is the EXACT
// snapshot stored under its ν in m_projection_snapshots (the paper's
// "recorded state set Q" of a view, Implementation Aspects); the
// per-edge watermark m_projection_idx ≤ ν is kept only as a fallback —
// on its own it would denote the union of ALL extractions up to ν,
// blurring a view's Q with unrelated heads' regions once exploration
// is partial (lazy mode).
// "recorded state set Q" of a view, Implementation Aspects).
unsigned m_projection_extract_idx = 0;
// ν → the snapshot of Q taken when the view index was minted. The
// state exprs are pinned via m_partial_dfa_pin, so the stored expr*
@ -1290,6 +1284,8 @@ namespace seq {
// l_true = empty, l_false = non-empty (a simultaneously accepting tuple
// was reached), l_undef = budget exhausted / inconclusive.
// Implemented as a thin wrapper over check_concat_product_emptiness
// (single factor holding the tuple, trivially accepting Σ* rhs).
lbool check_product_emptiness(vector<prod_comp> const& comps, unsigned max_states);
// Concatenation-aware variant (paper, "Pruning incrementally during
@ -1308,6 +1304,20 @@ namespace seq {
lbool comp_accepting(prod_comp const& c) const;
prod_comp comp_step(prod_comp const& c, euf::snode const* mt);
// Shared pieces of the synchronous product engines (tuple-emptiness
// wrapper, concatenation-aware search, witness search):
// visited-key encoding of one component;
static void prod_comp_key(prod_comp const& c, std::vector<unsigned>& key);
// all-components-accepting test of a tuple (l_true: all accept,
// l_false: some component rejects, l_undef: undecided);
lbool tuple_accepting(vector<prod_comp> const& cs) const;
// step every component of a tuple by one joint minterm; false iff a
// component died (the successor tuple is then to be discarded);
bool step_tuple(vector<prod_comp> const& cur, euf::snode const* mt, vector<prod_comp>& nxt);
// joint first-character partition of the live (non-sink, non-dead)
// component states, optionally including one extra component.
void joint_minterms(vector<prod_comp> const& comps, prod_comp const* extra, euf::snode_vector& mts);
// Build the product components for variable `var` from the node's
// primitive memberships (plain / land-state view). Joins their deps.
bool collect_var_components(euf::snode const* var, nielsen_node const& node,
@ -1321,13 +1331,9 @@ namespace seq {
// (edges are deduplicated by (src,dst); transition labels are unused).
void record_partial_derivative_edge(euf::snode const* src_re, euf::snode const* dst_re);
// Collect the SCC containing root_re in the current partial DFA.
// Returns false if no cyclic SCC containing root_re exists.
bool collect_scc_for_projection(euf::snode const* root_re, uint_set& scc) const;
// Mark SCC edges with a monotone extraction index and return the
// currently covered edge count for this extraction.
unsigned mark_scc_projection_edges(uint_set const& scc);
// Trigger gate for the cycle machinery: does some non-empty recorded
// path lead from head_re back to head_re in the partial DFA?
bool head_on_cycle(euf::snode const* head_re) const;
// 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
@ -1512,7 +1518,7 @@ namespace seq {
// BFS of Brzozowski derivatives from root_re up to `depth` steps,
// eagerly recording concrete minterm edges in the partial DFA so that
// collect_scc_for_projection can find cycles without first waiting for
// head_on_cycle can find cycles without first waiting for
// concrete children to record them one level at a time.
// Lazily record the complete reachable automaton of root_re into the
// partial DFA, once per regex component (cached in m_explored_automaton).