// -*- coding: utf-8 -*- // Copyright (C) 2014-2021 Laboratoire de Recherche et Développement de // l'Epita (LRDE). // // This file is part of Spot, a model checking library. // // Spot is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 3 of the License, or // (at your option) any later version. // // Spot is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public // License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace spot { namespace { class state_tgbasl final: public state { public: state_tgbasl(const state* s, bdd cond) : s_(s), cond_(cond) { } virtual ~state_tgbasl() { s_->destroy(); } virtual int compare(const state* other) const override { const state_tgbasl* o = down_cast(other); int res = s_->compare(o->real_state()); if (res != 0) return res; return cond_.id() - o->cond_.id(); } virtual size_t hash() const override { return wang32_hash(s_->hash()) ^ wang32_hash(cond_.id()); } virtual state_tgbasl* clone() const override { return new state_tgbasl(s_, cond_); } const state* real_state() const { return s_; } bdd cond() const { return cond_; } private: const state* s_; bdd cond_; }; class twasl_succ_iterator final : public twa_succ_iterator { public: twasl_succ_iterator(twa_succ_iterator* it, const state_tgbasl* state, bdd_dict_ptr d, bdd atomic_propositions) : it_(it), state_(state), aps_(atomic_propositions), d_(d) { } virtual ~twasl_succ_iterator() { delete it_; } // iteration virtual bool first() override { loop_ = false; done_ = false; need_loop_ = true; if (it_->first()) { cond_ = it_->cond(); next_edge(); } return true; } virtual bool next() override { if (cond_ != bddfalse) { next_edge(); return true; } if (!it_->next()) { if (loop_ || !need_loop_) done_ = true; loop_ = true; return !done_; } else { cond_ = it_->cond(); next_edge(); return true; } } virtual bool done() const override { return it_->done() && done_; } // inspection virtual state_tgbasl* dst() const override { if (loop_) return new state_tgbasl(state_->real_state(), state_->cond()); return new state_tgbasl(it_->dst(), one_); } virtual bdd cond() const override { if (loop_) return state_->cond(); return one_; } virtual acc_cond::mark_t acc() const override { if (loop_) return {}; return it_->acc(); } private: void next_edge() { one_ = bdd_satoneset(cond_, aps_, bddfalse); cond_ -= one_; if (need_loop_ && (state_->cond() == one_) && (state_ == it_->dst())) need_loop_ = false; } twa_succ_iterator* it_; const state_tgbasl* state_; bdd cond_; bdd one_; bdd aps_; bdd_dict_ptr d_; bool loop_; bool need_loop_; bool done_; }; class tgbasl final : public twa { public: tgbasl(const_twa_ptr a) : twa(a->get_dict()), a_(a) { copy_ap_of(a); copy_acceptance_of(a_); } virtual const state* get_init_state() const override { return new state_tgbasl(a_->get_init_state(), bddfalse); } virtual twa_succ_iterator* succ_iter(const state* state) const override { const state_tgbasl* s = down_cast(state); return new twasl_succ_iterator(a_->succ_iter(s->real_state()), s, a_->get_dict(), ap_vars()); } virtual std::string format_state(const state* state) const override { const state_tgbasl* s = down_cast(state); return (a_->format_state(s->real_state()) + ", " + bdd_format_formula(a_->get_dict(), s->cond())); } private: const_twa_ptr a_; }; typedef std::shared_ptr tgbasl_ptr; inline tgbasl_ptr make_tgbasl(const const_twa_ptr& aut) { return std::make_shared(aut); } typedef std::pair stutter_state; struct stutter_state_hash { size_t operator()(const stutter_state& s) const noexcept { return wang32_hash(s.first) ^ wang32_hash(s.second.id()); } }; // Associate the stutter state to its number. typedef std::unordered_map ss2num_map; // Queue of state to be processed. typedef std::deque queue_t; } twa_graph_ptr sl(const_twa_graph_ptr a) { // The result automaton uses numbered states. twa_graph_ptr res = make_twa_graph(a->get_dict()); bdd atomic_propositions = a->ap_vars(); // We use the same BDD variables as the input. res->copy_ap_of(a); res->copy_acceptance_of(a); // We are going to create self-loop labeled by {}, // and those should not be accepting. If they are, // we need to upgrade the acceptance condition. acc_cond::mark_t toadd = {}; if (res->acc().accepting({})) { unsigned ns = res->num_sets(); auto na = res->get_acceptance() & acc_cond::acc_code::inf({ns}); res->set_acceptance(ns + 1, na); toadd = {ns}; } // These maps make it possible to convert stutter_state to number // and vice-versa. ss2num_map ss2num; queue_t todo; unsigned s0 = a->get_init_state_number(); stutter_state s(s0, bddfalse); ss2num[s] = 0; res->new_state(); todo.emplace_back(s); while (!todo.empty()) { s = todo.front(); todo.pop_front(); unsigned src = ss2num[s]; bool self_loop_needed = true; for (auto& t : a->out(s.first)) { bdd all = t.cond; for (bdd one: minterms_of(t.cond, atomic_propositions)) { stutter_state d(t.dst, one); auto r = ss2num.emplace(d, ss2num.size()); unsigned dest = r.first->second; if (r.second) { todo.emplace_back(d); unsigned u = res->new_state(); assert(u == dest); (void)u; } // Create the edge. res->new_edge(src, dest, one, t.acc | toadd); if (src == dest) self_loop_needed = false; } } if (self_loop_needed && s.second != bddfalse) res->new_edge(src, src, s.second, {}); } res->merge_edges(); return res; } twa_graph_ptr sl2_inplace(twa_graph_ptr a) { // We are going to create self-loop labeled by {}, // and those should not be accepting. If they are, // we need to upgrade the acceptance condition. if (a->acc().accepting({})) { unsigned ns = a->num_sets(); auto na = a->get_acceptance() & acc_cond::acc_code::inf({ns}); a->set_acceptance(ns + 1, na); acc_cond::mark_t toadd = {ns}; for (auto& e: a->edges()) e.acc |= toadd; } // The self-loops we add should not be accepting, so try to find // an unsat mark, and upgrade the acceptance condition if // necessary. // // UM is a pair (bool, mark). If the Boolean is false, the // acceptance is always satisfiable. Otherwise, MARK is an // example of unsatisfiable mark. auto um = a->acc().unsat_mark(); if (!um.first) { auto m = a->set_buchi(); for (auto& e: a->edges()) e.acc = m; um.second = {}; } acc_cond::mark_t unsat = um.second; bdd atomic_propositions = a->ap_vars(); unsigned num_states = a->num_states(); unsigned num_edges = a->num_edges(); std::vector selfloops(num_states, bddfalse); std::map, unsigned> newstates; // Record all the conditions for which we can selfloop on each // state. for (auto& t: a->edges()) if (t.src == t.dst) selfloops[t.src] |= t.cond; for (unsigned t = 1; t <= num_edges; ++t) { auto& td = a->edge_storage(t); if (a->is_dead_edge(td)) continue; unsigned src = td.src; unsigned dst = td.dst; if (src != dst) { bdd all = td.cond; // If there is a self-loop with the whole condition on // either end of the edge, do not bother with it. if (bdd_implies(all, selfloops[src]) || bdd_implies(all, selfloops[dst])) continue; // Do not use td in the loop because the new_edge() // might invalidate it. auto acc = td.acc; for (bdd one: minterms_of(all, atomic_propositions)) { // Skip if there is a loop for this particular letter. if (bdd_implies(one, selfloops[src]) || bdd_implies(one, selfloops[dst])) continue; auto p = newstates.emplace(std::make_pair(dst, one.id()), 0); if (p.second) p.first->second = a->new_state(); unsigned tmp = p.first->second; // intermediate state unsigned i = a->new_edge(src, tmp, one, acc); assert(i > num_edges); i = a->new_edge(tmp, tmp, one, unsat); assert(i > num_edges); // unsat acceptance here to preserve the state-based property. i = a->new_edge(tmp, dst, one, unsat); assert(i > num_edges); (void)i; } } } if (num_states != a->num_states()) a->prop_keep({true, // state_based false, // inherently_weak false, false, // deterministic true, // complete false, // stutter inv. }); a->merge_edges(); return a; } twa_graph_ptr sl2(const_twa_graph_ptr a) { return sl2_inplace(make_twa_graph(a, twa::prop_set::all())); } twa_graph_ptr closure_inplace(twa_graph_ptr a) { // In the fin-less version of the closure, we can merge edges that // have the same src, letter, and destination by taking the union // of their marks. If some Fin() is used, we cannot do that. bool fin_less = !a->acc().uses_fin_acceptance(); a->prop_keep({false, // state_based false, // inherently_weak false, false, // deterministic true, // complete false, // stutter inv. }); unsigned n = a->num_states(); std::vector todo; std::vector > dst2trans(n); for (unsigned state = 0; state < n; ++state) { auto trans = a->out(state); for (auto it = trans.begin(); it != trans.end(); ++it) { todo.emplace_back(it.trans()); dst2trans[it->dst].emplace_back(it.trans()); } while (!todo.empty()) { auto t1 = a->edge_storage(todo.back()); todo.pop_back(); for (auto& t2 : a->out(t1.dst)) { bdd cond = t1.cond & t2.cond; if (cond != bddfalse) { bool need_new_trans = true; acc_cond::mark_t acc = t1.acc | t2.acc; for (auto& t: dst2trans[t2.dst]) { auto& ts = a->edge_storage(t); if (acc == ts.acc) { if (!bdd_implies(cond, ts.cond)) { ts.cond |= cond; if (std::find(todo.begin(), todo.end(), t) == todo.end()) todo.emplace_back(t); } need_new_trans = false; break; } else if (fin_less && cond == ts.cond) { acc |= ts.acc; if (ts.acc != acc) { ts.acc = acc; if (std::find(todo.begin(), todo.end(), t) == todo.end()) todo.emplace_back(t); } need_new_trans = false; break; } } if (need_new_trans) { // Load t2.dst first, because t2 can be // invalidated by new_edge(). auto dst = t2.dst; auto i = a->new_edge(state, dst, cond, acc); dst2trans[dst].emplace_back(i); todo.emplace_back(i); } } } } for (auto& it: dst2trans) it.clear(); } return a; } twa_graph_ptr closure(const_twa_graph_ptr a) { return closure_inplace(make_twa_graph(a, twa::prop_set::all())); } namespace { // The stutter check algorithm to use can be overridden via an // environment variable. static int default_stutter_check_algorithm() { static const char* stutter_check = getenv("SPOT_STUTTER_CHECK"); if (stutter_check) { char* endptr; long res = strtol(stutter_check, &endptr, 10); if (*endptr || res < 0 || res > 9) throw std::runtime_error("invalid value for SPOT_STUTTER_CHECK."); return res; } else { return 8; // The best variant, according to our benchmarks. } } } namespace { // The own_f and own_nf tell us whether we can modify the aut_f // and aut_nf automata inplace. static bool do_si_check(const_twa_graph_ptr aut_f, bool own_f, const_twa_graph_ptr aut_nf, bool own_nf, int algo) { auto cl = [](const_twa_graph_ptr a, bool own) { if (own) return closure_inplace(std::const_pointer_cast (std::move(a))); return closure(std::move(a)); }; auto sl_2 = [](const_twa_graph_ptr a, bool own) { if (own) return sl2_inplace(std::const_pointer_cast(std::move(a))); return sl2(std::move(a)); }; switch (algo) { case 1: // sl(aut_f) x sl(aut_nf) return product(sl(std::move(aut_f)), sl(std::move(aut_nf)))->is_empty(); case 2: // sl(cl(aut_f)) x aut_nf return product(sl(cl(std::move(aut_f), own_f)), std::move(aut_nf))->is_empty(); case 3: // (cl(sl(aut_f)) x aut_nf return product(closure_inplace(sl(std::move(aut_f))), std::move(aut_nf))->is_empty(); case 4: // sl2(aut_f) x sl2(aut_nf) return product(sl_2(std::move(aut_f), own_f), sl_2(std::move(aut_nf), own_nf)) ->is_empty(); case 5: // sl2(cl(aut_f)) x aut_nf return product(sl2_inplace(cl(std::move(aut_f), own_f)), std::move(aut_nf))->is_empty(); case 6: // (cl(sl2(aut_f)) x aut_nf return product(closure_inplace(sl_2(std::move(aut_f), own_f)), std::move(aut_nf))->is_empty(); case 7: // on-the-fly sl(aut_f) x sl(aut_nf) return otf_product(make_tgbasl(std::move(aut_f)), make_tgbasl(std::move(aut_nf)))->is_empty(); case 8: // cl(aut_f) x cl(aut_nf) return product(cl(std::move(aut_f), own_f), cl(std::move(aut_nf), own_nf))->is_empty(); default: throw std::runtime_error("is_stutter_invariant(): " "invalid algorithm number"); SPOT_UNREACHABLE(); } } bool is_stutter_invariant_aux(twa_graph_ptr aut_f, bool own_f, const_twa_graph_ptr aut_nf = nullptr, int algo = 0) { trival si = aut_f->prop_stutter_invariant(); if (si.is_known()) return si.is_true(); if (aut_nf) { trival si_n = aut_nf->prop_stutter_invariant(); if (si_n.is_known()) { bool res = si_n.is_true(); aut_f->prop_stutter_invariant(res); return res; } } if (algo == 0) algo = default_stutter_check_algorithm(); bool own_nf = false; if (!aut_nf) { aut_nf = complement(aut_f); own_nf = true; } bool res = do_si_check(aut_f, own_f, std::move(aut_nf), own_nf, algo); aut_f->prop_stutter_invariant(res); return res; } } bool is_stutter_invariant(twa_graph_ptr aut_f, const_twa_graph_ptr aut_nf, int algo) { return is_stutter_invariant_aux(aut_f, false, aut_nf, algo); } bool is_stutter_invariant(formula f, twa_graph_ptr aut_f) { if (f.is_ltl_formula() && f.is_syntactic_stutter_invariant()) return true; int algo = default_stutter_check_algorithm(); if (algo == 0 || algo == 9) // Etessami's check via syntactic transformation. { if (!f.is_ltl_formula()) throw std::runtime_error("Cannot use the syntactic " "stutter-invariance check " "for non-LTL formulas"); formula g = remove_x(f); bool res; if (algo == 0) // Equivalence check { tl_simplifier ls; res = ls.are_equivalent(f, g); } else { formula h = formula::Xor(f, g); res = ltl_to_tgba_fm(h, make_bdd_dict())->is_empty(); } return res; } // Prepare for an automata-based check. translator trans(aut_f ? aut_f->get_dict() : make_bdd_dict()); bool own_f = false; if (!aut_f) { aut_f = trans.run(f); own_f = true; } return is_stutter_invariant_aux(aut_f, own_f, trans.run(formula::Not(f))); } trival check_stutter_invariance(twa_graph_ptr aut, formula f, bool do_not_determinize, bool find_counterexamples) { trival is_stut = aut->prop_stutter_invariant(); if (!find_counterexamples && is_stut.is_known()) return is_stut.is_true(); twa_graph_ptr neg = nullptr; if (f) neg = translator(aut->get_dict()).run(formula::Not(f)); else if (!is_deterministic(aut) && do_not_determinize) return trival::maybe(); if (!find_counterexamples) return is_stutter_invariant(aut, std::move(neg)); // Procedure that may find a counterexample. if (!neg) neg = complement(aut); auto aword = product(closure(aut), closure(neg))->accepting_word(); if (!aword) { aut->prop_stutter_invariant(true); return true; } aword->simplify(); aword->use_all_aps(aut->ap_vars()); auto aaut = aword->as_automaton(); twa_word_ptr rword; if (aaut->intersects(aut)) { rword = sl2(aaut)->intersecting_word(neg); rword->simplify(); } else { rword = aword; aword = sl2(aaut)->intersecting_word(aut); aword->simplify(); } std::ostringstream os; os << *aword; aut->set_named_prop("accepted-word", new std::string(os.str())); os.str(""); os << *rword; aut->set_named_prop("rejected-word", new std::string(os.str())); aut->prop_stutter_invariant(false); return false; } std::vector stutter_invariant_states(const_twa_graph_ptr pos, formula f) { if (f.is_syntactic_stutter_invariant() || pos->prop_stutter_invariant().is_true()) return std::vector(pos->num_states(), true); auto neg = translator(pos->get_dict()).run(formula::Not(f)); return stutter_invariant_states(pos, neg); } // Based on an idea by Joachim Klein. std::vector stutter_invariant_states(const_twa_graph_ptr pos, const_twa_graph_ptr neg) { std::vector res(pos->num_states(), true); if (pos->prop_stutter_invariant().is_true()) return res; if (neg == nullptr) neg = complement(pos); auto product_states = [](const const_twa_graph_ptr& a) { return (a->get_named_prop>> ("product-states")); }; // Get the set of states (x,y) that appear in the product P1=pos*neg. std::set> pairs = [&]() { twa_graph_ptr prod = spot::product(pos, neg); auto goodstates = product_states(prod); std::set> pairs(goodstates->begin(), goodstates->end()); return pairs; }(); // Compute P2=cl(pos)*cl(neg). A state x of pos is stutter-sensitive // if there exists a state (x,y) in both P1 and P2 that as a successor // in the useful part of P2 and that is not in P1. twa_graph_ptr prod = spot::product(closure(pos), closure(neg)); auto prod_pairs = product_states(prod); scc_info si(prod, scc_info_options::TRACK_SUCCS | scc_info_options::TRACK_STATES_IF_FIN_USED); si.determine_unknown_acceptance(); unsigned n = prod->num_states(); bool sinv = true; for (unsigned s = 0; s < n; ++s) { if (!si.is_useful_scc(si.scc_of(s))) continue; if (pairs.find((*prod_pairs)[s]) == pairs.end()) continue; for (auto& e: prod->out(s)) if (si.is_useful_scc(si.scc_of(e.dst))) res[(*prod_pairs)[s].first] = sinv = false; } std::const_pointer_cast(pos)->prop_stutter_invariant(sinv); std::const_pointer_cast(neg)->prop_stutter_invariant(sinv); return res; } std::vector stutter_invariant_letters(const_twa_graph_ptr pos, formula f) { if (f.is_syntactic_stutter_invariant() || pos->prop_stutter_invariant().is_true()) { std::const_pointer_cast(pos)->prop_stutter_invariant(true); return stutter_invariant_letters(pos); } auto neg = translator(pos->get_dict()).run(formula::Not(f)); return stutter_invariant_letters(pos, neg); } std::vector stutter_invariant_letters(const_twa_graph_ptr pos, const_twa_graph_ptr neg) { unsigned ns = pos->num_states(); std::vector res(ns, bddtrue); if (pos->prop_stutter_invariant().is_true()) return res; if (neg == nullptr) neg = complement(pos); auto product_states = [](const const_twa_graph_ptr& a) { return (a->get_named_prop>> ("product-states")); }; // Get the set of states (x,y) that appear in the product P1=pos*neg. std::set> pairs = [&]() { twa_graph_ptr prod = spot::product(pos, neg); auto goodstates = product_states(prod); std::set> pairs(goodstates->begin(), goodstates->end()); return pairs; }(); // Compute P2=cl(pos)*cl(neg). A state x of pos is stutter-sensitive // if there exists a state (x,y) in both P1 and P2 that as a successor // in the useful part of P2 and that is not in P1. twa_graph_ptr prod = spot::product(closure(pos), closure(neg)); auto prod_pairs = product_states(prod); scc_info si(prod, scc_info_options::TRACK_SUCCS | scc_info_options::TRACK_STATES_IF_FIN_USED); si.determine_unknown_acceptance(); unsigned n = prod->num_states(); bool sinv = true; for (unsigned s = 0; s < n; ++s) { if (!si.is_useful_scc(si.scc_of(s))) continue; if (pairs.find((*prod_pairs)[s]) == pairs.end()) continue; for (auto& e: prod->out(s)) if (si.is_useful_scc(si.scc_of(e.dst))) { sinv = false; res[(*prod_pairs)[s].first] -= e.cond; } } std::const_pointer_cast(pos)->prop_stutter_invariant(sinv); std::const_pointer_cast(neg)->prop_stutter_invariant(sinv); return res; } namespace { static void highlight_vector(twa_graph_ptr aut, const std::vector& v, unsigned color) { // Create the highlight-states property only if it does not // exist already. auto hs = aut->get_named_prop>("highlight-states"); if (!hs) { hs = new std::map; aut->set_named_prop("highlight-states", hs); } unsigned n = v.size(); for (unsigned i = 0; i < n; ++i) if (v[i]) (*hs)[i] = color; } } void highlight_stutter_invariant_states(twa_graph_ptr pos, formula f, unsigned color) { highlight_vector(pos, stutter_invariant_states(pos, f), color); } void highlight_stutter_invariant_states(twa_graph_ptr pos, const_twa_graph_ptr neg, unsigned color) { highlight_vector(pos, stutter_invariant_states(pos, neg), color); } namespace { [[noreturn]] static void sistates_has_wrong_size(const char* fun) { throw std::runtime_error(std::string(fun) + "(): vector size should match " "the number of states"); } } int is_stutter_invariant_forward_closed(twa_graph_ptr aut, const std::vector& sistates) { unsigned ns = aut->num_states(); if (SPOT_UNLIKELY(sistates.size() != ns)) sistates_has_wrong_size("is_stutter_invariant_forward_closed"); for (unsigned s = 0; s < ns; ++s) { if (!sistates[s]) continue; for (auto& e : aut->out(s)) { if (!sistates[e.dst]) return e.dst; } } return -1; } std::vector make_stutter_invariant_forward_closed_inplace (twa_graph_ptr aut, const std::vector& sistates) { unsigned ns = aut->num_states(); if (SPOT_UNLIKELY(sistates.size() != ns)) sistates_has_wrong_size("make_stutter_invariant_forward_closed_inplace"); // Find the set of SI states that can jump into non-SI states. std::vector seed_states; bool pb = false; for (unsigned s = 0; s < ns; ++s) { if (!sistates[s]) continue; for (auto& e : aut->out(s)) if (!sistates[e.dst]) { seed_states.push_back(s); pb = true; break; } } if (!pb) // Nothing to change return sistates; // Find the set of non-SI states that are reachable from a seed // state, and give each of them a new number. std::vector new_number(ns, 0); std::vector prob_states; prob_states.reserve(ns); unsigned base = ns; std::vector dfs; dfs.reserve(ns); for (unsigned pb: seed_states) { dfs.push_back(pb); while (!dfs.empty()) { unsigned src = dfs.back(); dfs.pop_back(); for (auto& e: aut->out(src)) { unsigned dst = e.dst; if (sistates[dst] || new_number[dst]) continue; new_number[dst] = base++; dfs.push_back(dst); prob_states.push_back(dst); } } } // Actually duplicate the problematic states assert(base > ns); aut->new_states(base - ns); assert(aut->num_states() == base); for (unsigned ds: prob_states) { unsigned new_src = new_number[ds]; assert(new_src > 0); for (auto& e: aut->out(ds)) { unsigned dst = new_number[e.dst]; if (!dst) dst = e.dst; aut->new_edge(new_src, dst, e.cond, e.acc); } } // Redirect the transition coming out of the seed states // to the duplicate state. for (unsigned pb: seed_states) for (auto& e: aut->out(pb)) { unsigned ndst = new_number[e.dst]; if (ndst) e.dst = ndst; } // Create a new SI-state vector. std::vector new_sistates; new_sistates.reserve(base); new_sistates.insert(new_sistates.end(), sistates.begin(), sistates.end()); new_sistates.insert(new_sistates.end(), base - ns, true); return new_sistates; } }