3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-12 01:56:22 +00:00

Minor corrections

This commit is contained in:
CEisenhofer 2026-06-29 18:25:09 +02:00
parent 6f1eafaa5c
commit 93801ccc8e
4 changed files with 54 additions and 46 deletions

View file

@ -390,13 +390,15 @@ namespace euf {
zstring s;
if (m_seq.str.is_string(e, s) && !s.empty()) {
snode const* result = mk_char(s[s.length() - 1]);
for (unsigned i = s.length() - 1; i-- > 0; )
for (unsigned i = s.length() - 1; i-- > 0; ) {
result = mk_concat(mk_char(s[i]), result);
}
// register the original string expression as an alias
unsigned eid = e->get_id();
m_expr2snode.reserve(eid + 1, nullptr);
m_expr2snode[eid] = result;
m_alias_trail.push_back(eid);
m_pin.push_back(e);
mk_enode(e);
return result;
}

View file

@ -97,8 +97,7 @@ namespace euf {
// m_expr. snodes are allocated in m_region — which is never freed —
// but their m_expr field is owned by the egraph trail. Without this
// pin the egraph would release expressions on pop while clients still
// hold the matching snode* (e.g. inside nielsen_node str_mems, edge
// substitutions, or the partial-DFA cache), turning every later
// hold the matching snode*, turning every later
// get_expr() into a use-after-free. The pin grows monotonically; it
// is dropped only when sgraph itself is destroyed.
expr_ref_vector m_pin;

View file

@ -342,6 +342,25 @@ namespace seq {
nielsen_node::nielsen_node(nielsen_graph& graph, const unsigned id):
m_id(id), m_graph(graph), m_is_progress(true) { }
void nielsen_node::set_conflict(const backtrack_reason r, const dep_tracker confl) {
if (m_conflict_internal != nullptr && m_conflict_external_literal == sat::null_literal)
return;
// We prefer internal conflicts (we need it as a justification for general conflicts)
TRACE(seq, tout << "internal conflict " << (unsigned)r << "\n");
m_reason = r;
m_conflict_internal = confl;
m_conflict_external_literal = sat::null_literal;
}
void nielsen_node::set_external_conflict(const sat::literal lit, dep_tracker confl) {
if (m_reason != backtrack_reason::unevaluated)
return;
TRACE(seq, tout << "external conflict " << lit << "\n");
m_reason = backtrack_reason::external;
m_conflict_external_literal = ~lit;
m_conflict_internal = confl;
}
void nielsen_node::clone_from(nielsen_node const& parent) {
m_str_eq.reset();
m_str_deq.reset();
@ -550,9 +569,15 @@ namespace seq {
if (str_mems()[i] != n->str_mems()[i])
return false;
}
for (unsigned i = 0; i < char_ranges().size(); i++) {
// TODO: Check once more
if (char_ranges()[i] != n->char_ranges()[i])
// char_ranges is a u_map keyed by the symbolic-char snode id (NOT a vector),
// so it must be compared by key lookup, not by position. Sizes are equal
// (checked above), so a one-directional key+value match is a full match.
// Compare only the char_set (the constraint); deps differ between siblings,
// exactly as canonize_and_compute_node_hash hashes ranges+uid and ignores dep.
for (auto const& [uid, cr] : char_ranges()) {
if (!n->char_ranges().contains(uid))
return false;
if (cr.first != n->char_ranges().find(uid).first)
return false;
}
return true;
@ -2165,11 +2190,6 @@ namespace seq {
case backtrack_reason::regex_widening:
case backtrack_reason::symbol_clash:
case backtrack_reason::character_range:
// a sibling (subsumption / loop-cut) conflict depends only on the node's
// string signature: a cut equates two nodes by their string constraints,
// and a sibling closure is reached only when every leaf below is itself a
// string-only conflict or a cut. (Whether the closure is *cacheable* is a
// separate, stronger question handled via the lowlink in search_dfs.)
case backtrack_reason::sibling:
return true;
default:
@ -2263,7 +2283,7 @@ namespace seq {
// the conflict from this node's own constraint deps (a sound over-approx).
{
if (m_unsat_node_cache.contains(node)) {
node->set_conflict(backtrack_reason::sibling, node_all_deps(node));
node->set_conflict(backtrack_reason::sibling, nullptr /*we use the one of the sibling*/);
node->set_general_conflict();
node->m_unsat_cacheable = true;
++m_stats.m_num_simplify_conflict;
@ -2487,10 +2507,11 @@ namespace seq {
return search_result::unknown;
if (all_string_only) {
// Subsumption rule: this node is a string-only conflict. Internal
// (has children) closures carry no own deps — collect_conflict_deps
// recurses through them to the genuine conflict / cut leaves below.
node->set_conflict(backtrack_reason::sibling, nullptr);
// Subsumption rule: this node is a string-only conflict. It is a
// STOP point for collect_conflict_deps, so it must carry its own
// justification: node_all_deps over-approximates the input
// constraints behind its unsatisfiable string signature.
node->set_conflict(backtrack_reason::sibling, node_all_deps(node));
++m_stats.m_num_sibling_closure;
if (self_contained) {
// No cut escapes this subtree: the unsat is a property of the
@ -4834,17 +4855,14 @@ namespace seq {
while (!to_visit.empty()) {
nielsen_node const* n = to_visit.back();
to_visit.pop_back();
// Recurse through internal closures: children_failed nodes, and sibling
// (subsumption) closures that have children. The latter carry no own
// deps (m_conflict_internal == null) — their justification is the union
// of the genuine conflict / cut leaves below them, gathered by recursing
// (sound: collecting from all leaves never under-approximates). A sibling
// LEAF (a loop cut, or a transposition-cache hit; no children) instead
// contributes its own node_all_deps recorded in m_conflict_internal.
const bool recurse =
n->reason() == backtrack_reason::children_failed ||
(n->reason() == backtrack_reason::sibling && !n->outgoing().empty());
if (recurse) {
// Recurse only through children_failed nodes. A sibling (subsumption)
// node — whether a loop-cut leaf, a transposition-cache hit, or an
// internal string-only closure — is a STOP point: it carries its own
// node_all_deps justification in m_conflict_internal (a sound
// over-approximation of the input constraints that produced its
// unsatisfiable string signature), so we do NOT descend into its
// subtree (which may contain cut leaves whose targets lie outside it).
if (n->reason() == backtrack_reason::children_failed) {
for (unsigned i = n->outgoing().size(); i > 0; i--) {
nielsen_edge const* e = n->outgoing()[i - 1];
to_visit.push_back(e->tgt());
@ -4854,12 +4872,16 @@ namespace seq {
// not true anymore since we might have done a hot-restart where we previously created the child:
//SASSERT(n->outgoing().empty());
SASSERT(n->is_currently_conflict());
if (n->m_conflict_external_literal != sat::null_literal)
if (n->m_conflict_external_literal != sat::null_literal) {
std::cout << "Node " << n->id() << std::endl;
// We know from the outer solver that this literal is assigned true and contradicts node constraint
deps = m_dep_mgr.mk_join(deps, m_dep_mgr.mk_leaf(n->m_conflict_external_literal));
}
if (n->m_conflict_internal)
if (n->m_conflict_internal) {
std::cout << "Node " << n->id() << std::endl;
deps = m_dep_mgr.mk_join(deps, n->m_conflict_internal);
}
}
return deps;
}

View file

@ -687,24 +687,9 @@ namespace seq {
m_reason = backtrack_reason::children_failed;
}
void set_conflict(const backtrack_reason r, const dep_tracker confl) {
if (m_conflict_internal != nullptr && m_conflict_external_literal == sat::null_literal)
return;
// We prefer internal conflicts (we need it as a justification for general conflicts)
TRACE(seq, tout << "internal conflict " << (unsigned)r << "\n");
m_reason = r;
m_conflict_internal = confl;
m_conflict_external_literal = sat::null_literal;
}
void set_conflict(backtrack_reason r, dep_tracker confl);
void set_external_conflict(const sat::literal lit, dep_tracker confl) {
if (m_reason != backtrack_reason::unevaluated)
return;
TRACE(seq, tout << "external conflict " << lit << "\n");
m_reason = backtrack_reason::external;
m_conflict_external_literal = ~lit;
m_conflict_internal = confl;
}
void set_external_conflict(sat::literal lit, dep_tracker confl);
bool is_progress() const { return m_is_progress; }