/*++ Copyright (c) 2020 Microsoft Corporation Module Name: state_graph.cpp Abstract: Data structure for incrementally tracking "live" and "dead" states in an abstract transition system. Author: Caleb Stanford (calebstanford-msr / cdstanford) 2020-7 Margus Veanes 2020-8 --*/ #include "state_graph.h" #include void state_graph::add_state_core(state s) { STRACE("state_graph", tout << "add(" << s << ") ";); SASSERT(!m_seen.contains(s)); // Ensure corresponding var in union find structure while (s >= m_state_ufind.get_num_vars()) { m_state_ufind.mk_var(); } // Initialize as unvisited m_seen.insert(s); m_unexplored.insert(s); m_targets.insert(s, state_set()); m_sources.insert(s, state_set()); m_sources_maybecycle.insert(s, state_set()); } void state_graph::remove_state_core(state s) { // This is a partial deletion -- the state is still seen and can't be // added again later. // The state should be unknown, and all edges to or from the state // should already have been renamed. STRACE("state_graph", tout << "del(" << s << ") ";); SASSERT(m_seen.contains(s)); SASSERT(!m_state_ufind.is_root(s)); SASSERT(m_unknown.contains(s)); m_targets.remove(s); m_sources.remove(s); m_sources_maybecycle.remove(s); m_unknown.remove(s); } void state_graph::mark_unknown_core(state s) { STRACE("state_graph", tout << "unk(" << s << ") ";); SASSERT(m_state_ufind.is_root(s)); SASSERT(m_unexplored.contains(s)); m_unexplored.remove(s); m_unknown.insert(s); } void state_graph::mark_live_core(state s) { STRACE("state_graph", tout << "live(" << s << ") ";); SASSERT(m_state_ufind.is_root(s)); SASSERT(m_unknown.contains(s)); m_unknown.remove(s); m_live.insert(s); } void state_graph::mark_dead_core(state s) { STRACE("state_graph", tout << "dead(" << s << ") ";); SASSERT(m_state_ufind.is_root(s)); SASSERT(m_unknown.contains(s)); m_unknown.remove(s); m_dead.insert(s); } /* Add edge to the graph. - If the annotation 'maybecycle' is false, then the user is sure that this edge will never be part of a cycle. - May already exist, in which case maybecycle = false overrides maybecycle = true. */ void state_graph::add_edge_core(state s1, state s2, bool maybecycle) { STRACE("state_graph", tout << "add(" << s1 << "," << s2 << "," << (maybecycle ? "y" : "n") << ") ";); SASSERT(m_state_ufind.is_root(s1)); SASSERT(m_state_ufind.is_root(s2)); if (s1 == s2) return; if (!m_targets[s1].contains(s2)) { // add new edge m_targets[s1].insert(s2); m_sources[s2].insert(s1); if (maybecycle) m_sources_maybecycle[s2].insert(s1); } else if (!maybecycle && m_sources_maybecycle[s2].contains(s1)) { // update existing edge m_sources_maybecycle[s2].remove(s1); } } void state_graph::remove_edge_core(state s1, state s2) { STRACE("state_graph", tout << "del(" << s1 << "," << s2 << ") ";); SASSERT(m_targets[s1].contains(s2)); SASSERT(m_sources[s2].contains(s1)); m_targets[s1].remove(s2); m_sources[s2].remove(s1); m_sources_maybecycle[s2].remove(s1); } void state_graph::rename_edge_core(state old1, state old2, state new1, state new2) { SASSERT(m_targets[old1].contains(old2)); SASSERT(m_sources[old2].contains(old1)); bool maybecycle = m_sources_maybecycle[old2].contains(old1); remove_edge_core(old1, old2); add_edge_core(new1, new2, maybecycle); } /* Merge two states or more generally a set of states into one, returning the new state. Also merges associated edges. Preconditions: - The set should be nonempty - Every state in the set should be unknown - Each state should currently exist - If passing a set of states by reference, it should not be a set from the edge relations, as merging states modifies edge relations. */ auto state_graph::merge_states(state s1, state s2) -> state { SASSERT(m_state_ufind.is_root(s1)); SASSERT(m_state_ufind.is_root(s2)); SASSERT(m_unknown.contains(s1)); SASSERT(m_unknown.contains(s2)); STRACE("state_graph", tout << "merge(" << s1 << "," << s2 << ") ";); m_state_ufind.merge(s1, s2); if (m_state_ufind.is_root(s2)) std::swap(s1, s2); // rename s2 to s1 in edges for (auto s_to: m_targets[s2]) { rename_edge_core(s2, s_to, s1, s_to); } for (auto s_from: m_sources[s2]) { rename_edge_core(s_from, s2, s_from, s1); } remove_state_core(s2); return s1; } auto state_graph::merge_states(state_set& s_set) -> state { SASSERT(s_set.num_elems() > 0); state prev_s = 0; // initialization here optional bool first_iter = true; for (auto s: s_set) { if (first_iter) { prev_s = s; first_iter = false; continue; } prev_s = merge_states(prev_s, s); } return prev_s; } /* If s is not live, mark it, and recurse on all states into s Precondition: s is live or unknown */ void state_graph::mark_live_recursive(state s) { SASSERT(m_live.contains(s) || m_unknown.contains(s)); vector to_search; to_search.push_back(s); while (to_search.size() > 0) { state x = to_search.back(); to_search.pop_back(); SASSERT(m_live.contains(x) || m_unknown.contains(x)); if (m_live.contains(x)) continue; mark_live_core(x); for (auto x_from: m_sources[x]) { to_search.push_back(x_from); } } } /* Check if all targets of a state are dead. Precondition: s is unknown */ bool state_graph::all_targets_dead(state s) { SASSERT(m_unknown.contains(s)); for (auto s_to: m_targets[s]) { // unknown pointing to live should have been marked as live! SASSERT(!m_live.contains(s_to)); if (m_unknown.contains(s_to) || m_unexplored.contains(s_to)) return false; } return true; } /* Check if s is now known to be dead. If so, mark and recurse on all states into s. Precondition: s is live, dead, or unknown */ void state_graph::mark_dead_recursive(state s) { SASSERT(m_live.contains(s) || m_dead.contains(s) || m_unknown.contains(s)); vector to_search; to_search.push_back(s); while (to_search.size() > 0) { state x = to_search.back(); to_search.pop_back(); if (!m_unknown.contains(x)) continue; if (!all_targets_dead(x)) continue; // x is unknown and all targets from x are dead mark_dead_core(x); for (auto x_from: m_sources[x]) { to_search.push_back(x_from); } } } /* Merge all cycles of unknown states containing s into one state. Return the new state Precondition: s is unknown. */ auto state_graph::merge_all_cycles(state s) -> state { SASSERT(m_unknown.contains(s)); // Visit states in a DFS backwards from s state_set visited; // all backwards edges pushed state_set resolved; // known in SCC or not state_set scc; // known in SCC resolved.insert(s); scc.insert(s); vector to_search; to_search.push_back(s); while (to_search.size() > 0) { state x = to_search.back(); if (!visited.contains(x)) { visited.insert(x); // recurse backwards only on maybecycle edges // and only on unknown states for (auto y: m_sources_maybecycle[x]) { if (m_unknown.contains(y)) to_search.push_back(y); } } else if (!resolved.contains(x)) { resolved.insert(x); to_search.pop_back(); // determine in SCC or not for (auto y: m_sources_maybecycle[x]) { if (scc.contains(y)) { scc.insert(x); break; } } } else { to_search.pop_back(); } } // scc is the union of all cycles containing s return merge_states(scc); } /* Exposed methods */ void state_graph::add_state(state s) { if (m_seen.contains(s)) return; STRACE("state_graph", tout << "[state_graph] adding state " << s << ": ";); add_state_core(s); CASSERT("state_graph", write_dgml()); CASSERT("state_graph", write_dot()); CASSERT("state_graph", check_invariant()); STRACE("state_graph", tout << std::endl;); } void state_graph::mark_live(state s) { STRACE("state_graph", tout << "[state_graph] marking live " << s << ": ";); SASSERT(m_unexplored.contains(s) || m_live.contains(s)); SASSERT(m_state_ufind.is_root(s)); if (m_unexplored.contains(s)) mark_unknown_core(s); mark_live_recursive(s); CASSERT("state_graph", write_dgml()); CASSERT("state_graph", write_dot()); CASSERT("state_graph", check_invariant()); STRACE("state_graph", tout << std::endl;); } void state_graph::add_edge(state s1, state s2, bool maybecycle) { STRACE("state_graph", tout << "[state_graph] adding edge " << s1 << "->" << s2 << ": ";); SASSERT(m_unexplored.contains(s1) || m_live.contains(s1)); SASSERT(m_state_ufind.is_root(s1)); SASSERT(m_seen.contains(s2)); s2 = m_state_ufind.find(s2); add_edge_core(s1, s2, maybecycle); if (m_live.contains(s2)) mark_live(s1); CASSERT("state_graph", write_dgml()); CASSERT("state_graph", write_dot()); CASSERT("state_graph", check_invariant()); STRACE("state_graph", tout << std::endl;); } void state_graph::mark_done(state s) { SASSERT(m_unexplored.contains(s) || m_live.contains(s)); SASSERT(m_state_ufind.is_root(s)); if (m_live.contains(s)) return; STRACE("state_graph", tout << "[state_graph] marking done " << s << ": ";); if (m_unexplored.contains(s)) mark_unknown_core(s); s = merge_all_cycles(s); mark_dead_recursive(s); // check if dead CASSERT("state_graph", write_dgml()); CASSERT("state_graph", write_dot()); CASSERT("state_graph", check_invariant()); STRACE("state_graph", tout << std::endl;); } unsigned state_graph::get_size() const { return m_state_ufind.get_num_vars(); } bool state_graph::is_seen(state s) const { return m_seen.contains(s); } bool state_graph::is_live(state s) const { return m_live.contains(m_state_ufind.find(s)); } bool state_graph::is_dead(state s) const { return m_dead.contains(m_state_ufind.find(s)); } bool state_graph::is_done(state s) const { return m_seen.contains(s) && !m_unexplored.contains(m_state_ufind.find(s)); } /* Pretty printing */ std::ostream& state_graph::display(std::ostream& o) const { o << "---------- State Graph ----------" << std::endl << "Seen:"; for (auto s : m_seen) { o << " " << s; state s_root = m_state_ufind.find(s); if (s_root != s) o << "(=" << s_root << ")"; } o << std::endl << "Live:" << m_live << std::endl << "Dead:" << m_dead << std::endl << "Unknown:" << m_unknown << std::endl << "Unexplored:" << m_unexplored << std::endl << "Edges:" << std::endl; for (auto s1 : m_seen) { if (m_state_ufind.is_root(s1)) { o << " " << s1 << " -> " << m_targets[s1] << std::endl; } } o << "---------------------------------" << std::endl; return o; } #ifdef Z3DEBUG /* Class invariants check (and associated auxiliary functions) check_invariant performs a sequence of SASSERT assertions, then always returns true. */ bool state_graph::is_subset(state_set set1, state_set set2) const { for (auto s1: set1) { if (!set2.contains(s1)) return false; } return true; } bool state_graph::is_disjoint(state_set set1, state_set set2) const { for (auto s1: set1) { if (set2.contains(s1)) return false; } return true; } #define ASSERT_FOR_ALL_STATES(STATESET, COND) { \ for (auto s: STATESET) { SASSERT(COND); }} ((void) 0) #define ASSERT_FOR_ALL_EDGES(EDGEREL, COND) { \ for (auto e: (EDGEREL)) { \ state s1 = e.m_key; for (auto s2: e.m_value) { SASSERT(COND); } \ }} ((void) 0) bool state_graph::check_invariant() const { // Check state invariants SASSERT(is_subset(m_live, m_seen)); SASSERT(is_subset(m_dead, m_seen)); SASSERT(is_subset(m_unknown, m_seen)); SASSERT(is_subset(m_unexplored, m_seen)); SASSERT(is_disjoint(m_live, m_dead)); SASSERT(is_disjoint(m_live, m_unknown)); SASSERT(is_disjoint(m_live, m_unexplored)); SASSERT(is_disjoint(m_dead, m_unknown)); SASSERT(is_disjoint(m_dead, m_unexplored)); SASSERT(is_disjoint(m_unknown, m_unexplored)); ASSERT_FOR_ALL_STATES(m_seen, s < m_state_ufind.get_num_vars()); ASSERT_FOR_ALL_STATES(m_seen, (m_state_ufind.is_root(s) == (m_live.contains(s) || m_dead.contains(s) || m_unknown.contains(s) || m_unexplored.contains(s)))); // Check edge invariants ASSERT_FOR_ALL_EDGES(m_sources_maybecycle, m_sources[s1].contains(s2)); ASSERT_FOR_ALL_EDGES(m_sources, m_targets[s2].contains(s1)); ASSERT_FOR_ALL_EDGES(m_targets, m_sources[s2].contains(s1)); ASSERT_FOR_ALL_EDGES(m_targets, m_state_ufind.is_root(s1) && m_state_ufind.is_root(s2)); ASSERT_FOR_ALL_EDGES(m_targets, s1 != s2); // Check relationship between states and edges ASSERT_FOR_ALL_EDGES(m_targets, !m_live.contains(s2) || m_live.contains(s1)); ASSERT_FOR_ALL_STATES(m_dead, is_subset(m_targets[s], m_dead)); ASSERT_FOR_ALL_STATES(m_unknown, !is_subset(m_targets[s], m_dead)); // For the "no cycles" of unknown states on maybecycle edges, // we only do a partial check for cycles of size 2 ASSERT_FOR_ALL_EDGES(m_sources_maybecycle, !(m_unknown.contains(s1) && m_unknown.contains(s2) && m_sources_maybecycle[s2].contains(s1))); STRACE("state_graph", tout << "(invariant passed) ";); return true; } /* Output the whole state graph in dgml format into the file '.z3-state-graph.dgml' */ bool state_graph::write_dgml() { std::ofstream dgml(".z3-state-graph.dgml"); dgml << "" << std::endl << "" << std::endl << "" << std::endl; for (auto s : m_seen) { if (m_state_ufind.is_root(s)) { dgml << "" << std::endl; if (m_dead.contains(s)) dgml << "" << std::endl; if (m_live.contains(s)) dgml << "" << std::endl; if (m_unexplored.contains(s)) dgml << "" << std::endl; dgml << "" << std::endl; dgml << "" << std::endl; dgml << "" << std::endl; } } dgml << "" << std::endl; dgml << "" << std::endl; for (auto s : m_seen) { if (m_state_ufind.is_root(s)) { for (auto t : m_targets[s]) { dgml << "" << std::endl; if (!m_sources_maybecycle[t].contains(s)) dgml << "" << std::endl; dgml << "" << std::endl; } dgml << "" << std::endl; dgml << "" << std::endl; } } dgml << "" << std::endl; dgml << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl << "" << std::endl; dgml.close(); return true; } /* Output the whole state graph in dot format into the file '.z3-state-graph.dot' */ bool state_graph::write_dot() { std::ofstream dot(".z3-state-graph.dot"); dot << "digraph \"state_graph\" {" << std::endl << "rankdir=TB" << std::endl << "node [peripheries=1,style=filled,fillcolor=white,fontsize=24]" << std::endl; for (auto s : m_seen) { if (m_state_ufind.is_root(s)) { dot << s << " [label=\""; auto r = s; dot << r; do { if (r != s) dot << "," << r; r = m_state_ufind.next(r); } while (r != s); dot << "\""; if (m_unexplored.contains(s)) dot << ",style=\"dashed,filled\""; if (m_dead.contains(s)) dot << ",color=tomato,fillcolor=tomato"; if (m_live.contains(s)) dot << ",fillcolor=green"; dot << "]" << std::endl; } } for (auto s : m_seen) { if (m_state_ufind.is_root(s)) for (auto t : m_targets[s]) { dot << s << " -> " << t; if (!m_sources_maybecycle[t].contains(s)) dot << "[style=bold]"; dot << std::endl; } } dot << "}" << std::endl; dot.close(); return true; } #endif