/*++ Copyright (c) 2022 Microsoft Corporation Module Name: eliminate_predicates.cpp Author: Nikolaj Bjorner (nbjorner) 2022-11-17. Notes: The simplifier - detects macros of the form p(x) = q(x) - other more general macro detection is TBD. For example {~p, a} {~p, b} {p, ~a, ~b} {p, C} {~p, D} defines p as a conjunction and we can obbtain {a, C}, {b, C} {~a, ~b, D } similar to propositional case. Instead the case is handled by predicate elimination when p only occurs positively outside of {~p, a} {~p, b} {p, ~a, ~b} The SAT based definition detection scheme creates clauses {a}, {b}, {~a,~b}, C, D and checks for an unsat core. The core {a}, {b}, {~a, ~b} maps back to a definition for p Then {p, C}, {~p, D} clauses are replaced based on the definition. Claim: {C, D} is a consequence when we have created resolvents {C,X}, {D,Y}, where X => p => Y => X (a task for a "Kitten"?) - Handle various permutations of variables that are arguments to p? - other SMT-based macro detection could be made here as well. The (legacy) macro finder is not very flexible and could be replaced by a module building on this one. - eliminates predicates p(x) that occur at most once in each clause and the number of occurrences is small. Two sets of disabled functions are tracked: forbidden from macros vs forbidden from elimination - forbidden from macros: uninterpreted functions in recursive definitions predicates before m_qhead arguments to as-array - forbidden from elimination: - forbidden from macros, - occurs more than once in some clause, or in nested occurrence. --*/ #include "util/uint_set.h" #include "ast/ast_pp.h" #include "ast/ast_ll_pp.h" #include "ast/ast_util.h" #include "ast/for_each_ast.h" #include "ast/recfun_decl_plugin.h" #include "ast/bv_decl_plugin.h" #include "ast/arith_decl_plugin.h" #include "ast/occurs.h" #include "ast/array_decl_plugin.h" #include "ast/rewriter/var_subst.h" #include "ast/rewriter/rewriter_def.h" #include "ast/simplifiers/eliminate_predicates.h" #include "ast/rewriter/th_rewriter.h" #include "ast/rewriter/macro_replacer.h" std::ostream& eliminate_predicates::clause::display(std::ostream& out) const { ast_manager& m = m_dep.get_manager(); for (sort* s : m_bound) out << mk_pp(s, m) << " "; for (auto const& [atom, sign] : m_literals) out << (sign ? "~" : "") << mk_bounded_pp(atom, m) << " "; return out; } eliminate_predicates::eliminate_predicates(ast_manager& m, dependent_expr_state& fmls): dependent_expr_simplifier(m, fmls), m_der(m), m_rewriter(m) {} void eliminate_predicates::add_use_list(clause& cl) { ast_mark seen; for (auto const& [atom, sign] : cl.m_literals) { if (!is_uninterp(atom)) { m_to_exclude.push_back(atom); continue; } func_decl* p = to_app(atom)->get_decl(); m_use_list.get(p, sign).push_back(&cl); if (!m_predicate_decls.is_marked(p)) { m_predicates.push_back(p); m_predicate_decls.mark(p, true); } if (seen.is_marked(p)) m_to_exclude.push_back(atom); else { seen.mark(p, true); m_to_exclude.append(to_app(atom)->get_num_args(), to_app(atom)->get_args()); } } } /** * Check that all arguments are distinct variables that are bound. */ bool eliminate_predicates::can_be_macro_head(expr* _head, unsigned num_bound) { if (!is_app(_head)) return false; app* head = to_app(_head); func_decl* f = head->get_decl(); if (m_fmls.frozen(f)) return false; if (m_is_macro.is_marked(f)) return false; if (f->is_associative()) return false; if (!is_uninterp(f)) return false; uint_set indices; for (expr* arg : *head) { if (!is_var(arg)) return false; unsigned idx = to_var(arg)->get_idx(); if (indices.contains(idx)) return false; if (idx >= num_bound) return false; indices.insert(idx); } return true; } /** * a quasi macro head is of the form * f(x,x) where x is the only bound variable * f(x,y,x+y+3,1) where x, y are the only bound variables */ bool eliminate_predicates::can_be_quasi_macro_head(expr* _head, unsigned num_bound) { if (!is_app(_head)) return false; app* head = to_app(_head); func_decl* f = head->get_decl(); if (m_fmls.frozen(f)) return false; if (m_is_macro.is_marked(f)) return false; if (f->is_associative()) return false; if (!is_uninterp(f)) return false; uint_set indices; for (expr* arg : *head) { if (occurs(f, arg)) return false; if (!is_macro_safe(arg)) return false; if (!is_var(arg)) continue; unsigned idx = to_var(arg)->get_idx(); if (indices.contains(idx)) continue; if (idx >= num_bound) return false; indices.insert(idx); } return indices.num_elems() == num_bound; } // // (= (f x y (+ x y)) s), where x y are all bound variables. // then replace (f x y z) by (if (= z (+ x y)) s (f' x y z)) // void eliminate_predicates::insert_quasi_macro(app* head, expr* body, clause& cl) { expr_ref _body(body, m); uint_set indices; expr_ref_vector args(m), eqs(m); var_ref new_var(m); app_ref lhs(m), rhs(m); func_decl_ref f1(m); sort_ref_vector sorts(m); svector names; unsigned num_decls = cl.m_bound.size(); func_decl* f = head->get_decl(); for (expr* arg : *head) { sorts.push_back(arg->get_sort()); names.push_back(symbol(std::string("x") + std::to_string(args.size()))); if (is_var(arg)) { unsigned idx = to_var(arg)->get_idx(); if (!indices.contains(idx)) { indices.insert(idx); args.push_back(arg); continue; } } new_var = m.mk_var(eqs.size() + num_decls, arg->get_sort()); args.push_back(new_var); eqs.push_back(m.mk_eq(arg, new_var)); } // forall vars . f(args) = if eqs then body else f'(args) f1 = m.mk_fresh_func_decl(f->get_name(), symbol::null, sorts.size(), sorts.data(), f->get_range()); lhs = m.mk_app(f, args); rhs = m.mk_ite(mk_and(eqs), body, m.mk_app(f1, args)); insert_macro(lhs, rhs, cl); } expr_ref eliminate_predicates::bind_free_variables_in_def(clause& cl, app* head, expr* def) { unsigned num_bound = cl.m_bound.size(); if (head->get_num_args() == num_bound) return expr_ref(def, m); // head(x) <=> forall yx', x = x' => def(yx') svector names; expr_ref_vector ors(m); expr_ref result(m); ors.push_back(def); for (unsigned i = 0; i < num_bound; ++i) names.push_back(symbol(i)); for (expr* arg : *head) { unsigned idx = to_var(arg)->get_idx(); ors.push_back(m.mk_not(m.mk_eq(arg, m.mk_var(idx + num_bound, arg->get_sort())))); } result = mk_or(ors); result = m.mk_forall(num_bound, cl.m_bound.data(), names.data(), result); rewrite(result); return result; } /** * cheap/simplistic heuristic to find definitions that are based on binary clauses * (or (head x) (not (def x)) * (or (not (head x)) (def x)) */ bool eliminate_predicates::try_find_binary_definition(func_decl* p, app_ref& head, expr_ref& def, expr_dependency_ref& dep) { if (m_fmls.frozen(p)) return false; expr_mark binary_pos, binary_neg; obj_map deps; auto is_def_predicate = [&](clause& cl, expr* atom) { return is_app(atom) && to_app(atom)->get_decl() == p && can_be_macro_head(to_app(atom), cl.m_bound.size()); }; auto add_def = [&](clause& cl, expr* atom1, bool sign1, expr* atom2, bool sign2) { if (is_def_predicate(cl, atom1) && !sign1) { if (sign2) binary_neg.mark(atom2); else binary_pos.mark(atom2); if (cl.m_dep) deps.insert(atom1, cl.m_dep); } }; for (auto* cl : m_use_list.get(p, false)) { if (cl->m_alive && cl->size() == 2) { auto const& [atom1, sign1] = cl->m_literals[0]; auto const& [atom2, sign2] = cl->m_literals[1]; add_def(*cl, atom1, sign1, atom2, sign2); add_def(*cl, atom2, sign2, atom1, sign1); } } auto is_def = [&](unsigned i, unsigned j, clause& cl) { auto const& [atom1, sign1] = cl.m_literals[i]; auto const& [atom2, sign2] = cl.m_literals[j]; expr_dependency* d = nullptr; if (is_def_predicate(cl, atom1) && sign1) { if (sign2 && binary_pos.is_marked(atom2) && is_macro_safe(atom2) && !occurs(p, atom2)) { head = to_app(atom1); def = bind_free_variables_in_def(cl, head, m.mk_not(atom2)); dep = cl.m_dep; if (deps.find(atom1, d)) dep = m.mk_join(dep, d); return true; } if (!sign2 && binary_neg.is_marked(atom2) && is_macro_safe(atom2) && !occurs(p, atom2)) { head = to_app(atom1); def = bind_free_variables_in_def(cl, head, atom2); dep = cl.m_dep; if (deps.find(atom1, d)) dep = m.mk_join(dep, d); return true; } } return false; }; for (auto* cl : m_use_list.get(p, true)) { if (cl->m_alive && cl->size() == 2) { if (is_def(0, 1, *cl)) return true; if (is_def(1, 0, *cl)) return true; } } return false; } bool eliminate_predicates::is_macro_safe(expr* e) { for (expr* arg : subterms::all(expr_ref(e, m))) if (is_app(arg) && m_is_macro.is_marked(to_app(arg)->get_decl())) return false; return true; } void eliminate_predicates::insert_macro(app* head, expr* def, clause& cl) { insert_macro(head, def, cl.m_dep); TRACE("elim_predicates", tout << "remove " << cl << "\n"); cl.m_alive = false; } void eliminate_predicates::insert_macro(app* head, expr* def, expr_dependency* dep) { unsigned num = head->get_num_args(); ptr_buffer vars, subst_args; subst_args.resize(num, nullptr); vars.resize(num, nullptr); for (unsigned i = 0; i < num; i++) { var* v = to_var(head->get_arg(i)); var* w = m.mk_var(i, v->get_sort()); unsigned idx = v->get_idx(); VERIFY(idx < num); SASSERT(subst_args[idx] == 0); subst_args[idx] = w; vars[i] = w; } var_subst sub(m, false); app_ref _head(m); expr_ref _def(m); expr_dependency_ref _dep(dep, m); _def = sub(def, subst_args.size(), subst_args.data()); _head = m.mk_app(head->get_decl(), vars); auto* info = alloc(macro_def, _head, _def, _dep); m_macros.insert(head->get_decl(), info); m_fmls.model_trail().push(head->get_decl(), _def, _dep, {}); // augment with definition for head m_is_macro.mark(head->get_decl(), true); TRACE("elim_predicates", tout << "insert " << _head << " " << _def << "\n"); ++m_stats.m_num_macros; } void eliminate_predicates::try_resolve_definition(func_decl* p) { app_ref head(m); expr_ref def(m); expr_dependency_ref dep(m); if (try_find_binary_definition(p, head, def, dep)) insert_macro(head, def, dep); } /** * Port of macros handled by macro_finder/macro_util */ void eliminate_predicates::try_find_macro(clause& cl) { if (!cl.m_alive) return; expr* x, * y; auto can_be_def = [&](expr* _x, expr* y) { if (!is_app(_x)) return false; app* x = to_app(_x); return can_be_macro_head(x, cl.m_bound.size()) && is_macro_safe(y) && x->get_num_args() == cl.m_bound.size() && !occurs(x->get_decl(), y); }; // (= (f x) t) if (cl.is_unit() && !cl.sign(0) && m.is_eq(cl.atom(0), x, y)) { if (can_be_def(x, y)) { insert_macro(to_app(x), y, cl); return; } if (can_be_def(y, x)) { insert_macro(to_app(y), x, cl); return; } } // not (= (p x) t) -> (p x) = (not t) if (cl.is_unit() && cl.sign(0) && m.is_iff(cl.atom(0), x, y)) { if (can_be_def(x, y)) { insert_macro(to_app(x), m.mk_not(y), cl); return; } if (can_be_def(y, x)) { insert_macro(to_app(y), m.mk_not(x), cl); return; } } // pseudo-macros: // (iff (= (f x) t) cond) // rewrites to (f x) = (if cond t else (k x)) // add clause (not (= (k x) t)) // // we will call them _conditioned_ macros auto can_be_conditioned = [&](expr* f, expr* t, expr* cond) { return can_be_def(f, t) && !occurs(to_app(f)->get_decl(), cond) && is_macro_safe(cond); }; auto make_conditioned = [&](app* f, expr* t, expr* cond) { func_decl* df = f->get_decl(); app_ref def(m), k(m), fml(m); func_decl_ref fn(m); fn = m.mk_fresh_func_decl(df->get_arity(), df->get_domain(), df->get_range()); m_fmls.model_trail().hide(fn); // hide definition of fn k = m.mk_app(fn, f->get_num_args(), f->get_args()); def = m.mk_ite(cond, t, k); insert_macro(f, def, cl); fml = m.mk_not(m.mk_eq(k, t)); clause* new_cl = init_clause(fml, cl.m_dep, UINT_MAX); add_use_list(*new_cl); m_clauses.push_back(new_cl); }; if (cl.is_unit() && !cl.sign(0) && m.is_iff(cl.atom(0), x, y)) { expr* z, * u; if (m.is_eq(x, z, u) && can_be_conditioned(z, u, y)) { make_conditioned(to_app(z), u, y); return; } if (m.is_eq(x, u, z) && can_be_conditioned(z, u, y)) { make_conditioned(to_app(z), u, y); return; } if (m.is_eq(y, z, u) && can_be_conditioned(z, u, x)) { make_conditioned(to_app(z), u, x); return; } if (m.is_eq(y, u, z) && can_be_conditioned(z, u, x)) { make_conditioned(to_app(z), u, x); return; } } // // other macros handled by macro_finder: // // arithmetic/bit-vectors // (= (+ (f x) s) t) // becomes (= (f x) (- t s)) // // (= (+ (* -1 (f x)) x) t) // becomes (= (f x) (- (- t s))) bv_util bv(m); arith_util a(m); auto is_add = [&](expr * e) { rational n; return a.is_add(e) || bv.is_bv_add(e); }; auto sub = [&](expr* t, expr* s) { if (a.is_int_real(t)) return expr_ref(a.mk_sub(t, s), m); else return expr_ref(bv.mk_bv_sub(t, s), m); }; auto subtract = [&](expr* t, app* s, unsigned i) { expr_ref result(t, m); unsigned j = 0; for (expr* arg : *s) { ++j; if (i != j) result = sub(result, arg); } return result; }; auto uminus = [&](expr* t) { if (a.is_int_real(t)) return expr_ref(a.mk_uminus(t), m); else return expr_ref(bv.mk_bv_neg(t), m); }; auto is_inverse = [&](expr*& t) { expr* x, * y; rational n; if (a.is_mul(t, x, y) && a.is_numeral(x, n) && n == -1) { t = y; return true; } if (bv.is_bv_mul(t, x, y) && bv.is_numeral(x, n) && n + 1 == rational::power_of_two(bv.get_bv_size(t))) { t = y; return true; } return false; }; auto find_arith_macro = [&](expr* x, expr* y) { if (!is_add(x)) return false; if (!is_macro_safe(y)) return false; unsigned i = 0; for (expr* arg : *to_app(x)) { ++i; bool inv = is_inverse(arg); if (!can_be_macro_head(arg, cl.m_bound.size())) continue; app* head = to_app(arg); func_decl* f = head->get_decl(); if (head->get_num_args() != cl.m_bound.size()) continue; if (occurs(f, y)) continue; unsigned j = 0; for (expr* arg2 : *head) { ++j; if (i == j) continue; if (occurs(f, arg2)) goto next; if (!is_macro_safe(arg2)) goto next; } { // arg = y - x - arg; expr_ref y1 = subtract(y, to_app(x), i); if (inv) y1 = uminus(y1); insert_macro(to_app(arg), y1, cl); return true; } next: ; } return false; }; if (cl.is_unit() && !cl.sign(0) && m.is_eq(cl.atom(0), x, y)) { if (find_arith_macro(x, y)) return; if (find_arith_macro(y, x)) return; } // // macro_finder also has: // (>= (+ (f x) s) t) // becomes (= (f x) (- t s (k x)) // add (>= (k x) 0) // why is this a real improvement? // // // quasi-macros // (= (f x y (+ x y)) s), where x y are all bound variables. // then replace (f x y z) by (if (= z (+ x y)) s (f' x y z)) // auto can_be_qdef = [&](expr* _x, expr* y) { if (!is_app(_x)) return false; app* x = to_app(_x); return can_be_quasi_macro_head(x, cl.m_bound.size()) && is_macro_safe(y) && !occurs(x->get_decl(), y); }; if (cl.is_unit() && m.is_eq(cl.atom(0), x, y) && !cl.m_bound.empty()) { if (!cl.sign(0) && can_be_qdef(x, y)) { insert_quasi_macro(to_app(x), y, cl); return; } else if (!cl.sign(0) && can_be_qdef(y, x)) { insert_quasi_macro(to_app(y), x, cl); return; } else if (cl.sign(0) && m.is_bool(y) && can_be_qdef(x, y)) { insert_quasi_macro(to_app(x), m.mk_not(y), cl); return; } else if (cl.sign(0) && m.is_bool(y) && can_be_qdef(y, x)) { insert_quasi_macro(to_app(y), m.mk_not(x), cl); return; } } if (cl.is_unit() && !cl.m_bound.empty()) { expr* body = cl.sign(0) ? m.mk_false() : m.mk_true(); expr* x = cl.atom(0); if (can_be_qdef(x, body)) { insert_quasi_macro(to_app(x), body, cl); return; } } } void eliminate_predicates::find_definitions() { for (auto* p : m_predicates) try_resolve_definition(p); for (auto* cl : m_clauses) try_find_macro(*cl); } void eliminate_predicates::rewrite(expr_ref& t) { proof_ref pr(m); m_der(t, t, pr); m_rewriter(t); } void eliminate_predicates::reduce_definitions() { if (m_macros.empty()) return; macro_replacer macro_expander(m); for (auto const& [k, v] : m_macros) macro_expander.insert(v->m_head, v->m_def, v->m_dep); for (unsigned i : indices()) { auto [f, p, d] = m_fmls[i](); expr_ref fml(f, m), new_fml(m); expr_dependency_ref dep(d, m); while (true) { macro_expander(fml, dep, new_fml, dep); if (new_fml == fml) break; rewrite(new_fml); fml = new_fml; } m_fmls.update(i, dependent_expr(m, fml, nullptr, dep)); } reset(); init_clauses(); } void eliminate_predicates::try_resolve(func_decl* p) { if (m_disable_elimination.is_marked(p)) return; if (m_fmls.frozen(p)) return; unsigned num_pos = 0, num_neg = 0; for (auto* cl : m_use_list.get(p, false)) if (cl->m_alive) ++num_pos; for (auto* cl : m_use_list.get(p, true)) if (cl->m_alive) ++num_neg; TRACE("elim_predicates", tout << "try resolve " << p->get_name() << " " << num_pos << " " << num_neg << "\n"); if (num_pos >= 4 && num_neg >= 2) return; if (num_neg >= 4 && num_pos >= 2) return; if (num_neg >= 3 && num_pos >= 3) return; for (auto* pos : m_use_list.get(p, false)) { for (auto* neg : m_use_list.get(p, true)) { clause* cl = resolve(p, *pos, *neg); if (!cl) continue; m_clauses.push_back(cl); add_use_list(*cl); process_to_exclude(m_disable_elimination); IF_VERBOSE(11, verbose_stream() << "resolve " << p->get_name() << "\n" << *pos << "\n" << *neg << "\n------\n" << *cl << "\n\n"); } } update_model(p); for (auto* pos : m_use_list.get(p, false)) pos->m_alive = false; for (auto* neg : m_use_list.get(p, true)) neg->m_alive = false; ++m_stats.m_num_eliminated; } // // update model for p // // Example, ground case: // {p, a} {p, b} {-p, c}, {-p, d} // p <=> !(a & b) // p <=> c & d // // Example non-ground cases // {p(t)} {p(s)} {~p(u)} // p(x) <=> (x = t or x = s) // p(x) <=> x != u // // {p(t), a, b} // p(x) <=> (x = t & !(a or b)) // // {~p(t), a, b} // ~p(x) <=> (x = t & !(a or b)) // p(x) <=> x = t => a or b // void eliminate_predicates::update_model(func_decl* p) { expr_ref_vector fmls(m); expr_ref def(m); expr_dependency_ref dep(m); unsigned numpos = 0, numneg = 0; vector deleted; for (auto* pos : m_use_list.get(p, false)) if (pos->m_alive) ++numpos; for (auto* neg : m_use_list.get(p, true)) if (neg->m_alive) ++numneg; if (numpos < numneg) { for (auto* pos : m_use_list.get(p, false)) if (pos->m_alive) { fmls.push_back(create_residue_formula(p, *pos)); dep = m.mk_join(dep, pos->m_dep); } def = mk_or(fmls); } else { for (auto* neg : m_use_list.get(p, true)) if (neg->m_alive) { fmls.push_back(mk_not(m, create_residue_formula(p, *neg))); dep = m.mk_join(dep, neg->m_dep); } def = mk_and(fmls); } rewrite(def); TRACE("elim_predicates", tout << "insert " << p->get_name() << " " << def << "\n"); m_fmls.model_trail().push(p, def, dep, deleted); } /** * Convert a clause that contains p(t) into a definition for p * forall y . (or p(t) C) * Into * exists y . x = t[y] & !(or C) */ expr_ref eliminate_predicates::create_residue_formula(func_decl* p, clause& cl) { unsigned num_args = p->get_arity(); unsigned num_bound = cl.m_bound.size(); expr_ref_vector ors(m), ands(m); expr_ref fml(m); app_ref patom(m); for (auto const& [atom, sign] : cl.m_literals) { if (is_app(atom) && to_app(atom)->get_decl() == p) { SASSERT(!patom); patom = to_app(atom); continue; } fml = sign ? m.mk_not(atom) : atom.get(); ors.push_back(fml); } if (!ors.empty()) { fml = mk_not(m, mk_or(ors)); ands.push_back(fml); } for (unsigned i = 0; i < num_args; ++i) { SASSERT(patom->get_arg(i)->get_sort() == p->get_domain(i)); ands.push_back(m.mk_eq(patom->get_arg(i), m.mk_var(num_bound + i, p->get_domain(i)))); } fml = m.mk_and(ands); if (num_bound > 0) { svector names; for (unsigned i = 0; i < num_bound; ++i) names.push_back(symbol(i)); fml = m.mk_exists(num_bound, cl.m_bound.data(), names.data(), fml, 1); } return fml; } /** * Resolve p in clauses pos, neg where p occurs only once. */ eliminate_predicates::clause* eliminate_predicates::resolve(func_decl* p, clause& pos, clause& neg) { var_shifter sh(m); expr_dependency_ref dep(m); dep = m.mk_join(pos.m_dep, neg.m_dep); expr_ref new_lit(m); expr_ref_vector lits(m); expr* plit = nullptr, * nlit = nullptr; for (auto const& [lit, sign] : pos.m_literals) if (is_app(lit) && to_app(lit)->get_decl() == p) plit = lit; else lits.push_back(sign ? m.mk_not(lit) : lit.get()); for (auto const & [lit, sign] : neg.m_literals) { if (is_app(lit) && to_app(lit)->get_decl() == p) nlit = lit; else { sh(lit, pos.m_bound.size(), new_lit); lits.push_back(sign ? m.mk_not(new_lit) : new_lit.get()); } } sh(nlit, pos.m_bound.size(), new_lit); for (unsigned i = 0; i < p->get_arity(); ++i) { expr* a = to_app(plit)->get_arg(i); expr* b = to_app(new_lit)->get_arg(i); if (a != b) lits.push_back(m.mk_not(m.mk_eq(a, b))); } expr_ref cl = mk_or(lits); ptr_vector bound; bound.append(neg.m_bound); bound.append(pos.m_bound); if (!bound.empty()) { svector names; for (unsigned i = 0; i < bound.size(); ++i) names.push_back(symbol(i)); cl = m.mk_forall(bound.size(), bound.data(), names.data(), cl, 1); } rewrite(cl); if (m.is_true(cl)) return nullptr; return init_clause(cl, dep, UINT_MAX); } void eliminate_predicates::try_resolve() { for (auto* f : m_predicates) try_resolve(f); } /** * Process the terms m_to_exclude, walk all subterms. * Uninterpreted function declarations in these terms are added to 'exclude_set' */ void eliminate_predicates::process_to_exclude(ast_mark& exclude_set) { ast_mark visited; struct proc { ast_mark& to_exclude; proc(ast_mark& f) : to_exclude(f) {} void operator()(func_decl* f) { if (is_uninterp(f)) to_exclude.mark(f, true); } void operator()(ast* s) {} }; proc proc(exclude_set); for (expr* e : m_to_exclude) for_each_ast(proc, visited, e); m_to_exclude.reset(); } eliminate_predicates::clause* eliminate_predicates::init_clause(unsigned i) { auto [f, p, d] = m_fmls[i](); return init_clause(f, d, i); } /** * Create a clause from a formula. */ eliminate_predicates::clause* eliminate_predicates::init_clause(expr* f, expr_dependency* d, unsigned i) { clause* cl = alloc(clause, m, d); cl->m_fml = f; cl->m_fml_index = i; while (is_forall(f)) { cl->m_bound.append(to_quantifier(f)->get_num_decls(), to_quantifier(f)->get_decl_sorts()); f = to_quantifier(f)->get_expr(); } expr_ref_vector ors(m); flatten_or(f, ors); for (expr* lit : ors) { bool sign = m.is_not(lit, lit); cl->m_literals.push_back({ expr_ref(lit, m), sign }); } // extend macro detection to exploit bijective functions? // f(+ x 1) = g(x) -> f(x) = g(- x 1) // init_injective(*cl); // init_surjective(*cl); return cl; } /** * functions in the prefix of qhead are fully disabled * Create clauses from the suffix, and process subeterms of clauses to be disabled from * eliminations. */ void eliminate_predicates::init_clauses() { m_fmls.freeze_suffix(); for (unsigned i : indices()) { clause* cl = init_clause(i); add_use_list(*cl); m_clauses.push_back(cl); } process_to_exclude(m_disable_elimination); } /** * Ad hoc recognize surjectivity axioms * - exists y . f(y) = x */ void eliminate_predicates::init_surjective(clause const& cl) { if (!cl.is_unit()) return; if (cl.sign(0)) return; if (!is_exists(cl.atom(0))) return; } /** * Ad hoc recognize injectivity axioms * - f(x) = f(y) => x = y */ void eliminate_predicates::init_injective(clause const& cl) { if (cl.size() != 2) return; if (cl.m_bound.size() != 2) return; if (cl.sign(0) == cl.sign(1)) return; expr* a = cl.atom(0), *b = cl.atom(1); if (!cl.sign(0) && cl.sign(1)) std::swap(a, b); expr* x, *y, *fx, *fy; if (!m.is_eq(a, fx, fy)) return; if (!m.is_eq(b, x, y)) return; auto is_fx = [&](expr* _fx, func_decl*& f, unsigned& idx) { if (!is_app(_fx)) return false; app* fx = to_app(_fx); if (fx->get_num_args() != 1) return false; if (!is_var(fx->get_arg(0))) return false; f = fx->get_decl(); idx = to_var(fx->get_arg(0))->get_idx(); return true; }; func_decl* f1, *f2; unsigned idx1, idx2; if (!is_fx(fx, f1, idx1)) return; if (!is_fx(fy, f2, idx2)) return; if (idx1 == idx2 || f1 != f2) return; auto check_var = [&](expr* x, unsigned& idx) { if (!is_var(x)) return false; idx = to_var(x)->get_idx(); return true; }; if (!check_var(x, idx1) || !check_var(y, idx2)) return; if (idx1 == idx2) return; m_is_injective.mark(f1, true); } /** * Convert clauses to m_fmls */ void eliminate_predicates::decompile() { for (clause* cl : m_clauses) { if (m_fmls.inconsistent()) break; if (cl->m_fml_index != UINT_MAX) { if (cl->m_alive) continue; dependent_expr de(m, m.mk_true(), nullptr, nullptr); m_fmls.update(cl->m_fml_index, de); } else if (cl->m_alive) { expr_ref new_cl = cl->m_fml; dependent_expr de(m, new_cl, nullptr, cl->m_dep); m_fmls.add(de); } } } void eliminate_predicates::reset() { m_predicates.reset(); m_predicate_decls.reset(); m_to_exclude.reset(); m_disable_elimination.reset(); m_is_macro.reset(); m_is_injective.reset(); m_is_surjective.reset(); for (auto const& [k, v] : m_macros) dealloc(v); m_macros.reset(); m_clauses.reset(); m_use_list.reset(); } void eliminate_predicates::reduce() { reset(); init_clauses(); find_definitions(); reduce_definitions(); try_resolve(); decompile(); reset(); }