3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2025-04-14 04:48:45 +00:00
z3/src/muz/spacer/spacer_quant_generalizer.cpp
Arie Gurfinkel 9cdb63ae4a Handle conversion of quantified lemma to quantifier free
When a cube is updated, a lemma might loose all of its quantified
variables. In this case, it is effectively quantifier free
and might be a version of an already existing lemma.

For that reason, we convert it to quantifier free lemma when
this happens.
2018-06-14 16:08:48 -07:00

503 lines
16 KiB
C++

/*++
Copyright (c) 2017 Microsoft Corporation and Arie Gurfinkel
Module Name:
spacer_quant_generalizer.cpp
Abstract:
Quantified lemma generalizer.
Author:
Yakir Vizel
Arie Gurfinkel
Revision History:
--*/
#include "muz/spacer/spacer_context.h"
#include "muz/spacer/spacer_generalizers.h"
#include "muz/spacer/spacer_manager.h"
#include "ast/expr_abstract.h"
#include "ast/rewriter/var_subst.h"
#include "ast/for_each_expr.h"
#include "ast/factor_equivs.h"
#include "muz/spacer/spacer_term_graph.h"
#include "ast/rewriter/expr_safe_replace.h"
#include "ast/substitution/matcher.h"
#include "ast/expr_functors.h"
#include "muz/spacer/spacer_sem_matcher.h"
using namespace spacer;
namespace {
struct index_lt_proc : public std::binary_function<app*, app *, bool> {
arith_util m_arith;
index_lt_proc(ast_manager &m) : m_arith(m) {}
bool operator() (app *a, app *b) {
// XXX This order is a bit strange.
// XXX It does the job in our current application, but only because
// XXX we assume that we only compare expressions of the form (B + k),
// XXX where B is fixed and k is a number.
// XXX Might be better to specialize just for that specific use case.
rational val1, val2;
bool is_num1 = m_arith.is_numeral(a, val1);
bool is_num2 = m_arith.is_numeral(b, val2);
if (is_num1 && is_num2) {
return val1 < val2;
}
else if (is_num1 != is_num2) {
return is_num1;
}
is_num1 = false;
is_num2 = false;
// compare the first numeric argument of a to first numeric argument of b
// if available
for (unsigned i = 0, sz = a->get_num_args(); !is_num1 && i < sz; ++i) {
is_num1 = m_arith.is_numeral (a->get_arg(i), val1);
}
for (unsigned i = 0, sz = b->get_num_args(); !is_num2 && i < sz; ++i) {
is_num2 = m_arith.is_numeral(b->get_arg(i), val2);
}
if (is_num1 && is_num2) {
return val1 < val2;
}
else if (is_num1 != is_num2) {
return is_num1;
}
else {
return a->get_id() < b->get_id();
}
}
};
}
namespace spacer {
lemma_quantifier_generalizer::lemma_quantifier_generalizer(context &ctx) :
lemma_generalizer(ctx), m(ctx.get_ast_manager()), m_arith(m) {}
void lemma_quantifier_generalizer::collect_statistics(statistics &st) const
{
st.update("time.spacer.solve.reach.gen.quant", m_st.watch.get_seconds());
st.update("quantifier gen", m_st.count);
st.update("quantifier gen failures", m_st.num_failures);
}
// XXX What does this do?
void lemma_quantifier_generalizer::find_candidates(
expr *e, app_ref_vector &candidates)
{
if (!contains_selects(e, m)) return;
app_ref_vector indices(m);
get_select_indices(e, indices, m);
app_ref_vector extra(m);
expr_sparse_mark marked_args;
// Make sure not to try and quantify already-quantified indices
for (unsigned idx=0, sz = indices.size(); idx < sz; idx++) {
// skip expressions that already contain a quantified variable
if (has_zk_const(indices.get(idx))) {
continue;
}
app *index = indices.get(idx);
TRACE ("spacer_qgen",
tout << "Candidate: "<< mk_pp(index, m)
<< " in " << mk_pp(e, m) << "\n";);
extra.push_back(index);
// XXX expand one level of arguments. Might want to go deeper.
for (unsigned j = 0, asz = index->get_num_args(); j < asz; j++) {
expr *arg = index->get_arg(j);
if (!is_app(arg) || marked_args.is_marked(arg)) {continue;}
marked_args.mark(arg);
candidates.push_back (to_app(arg));
}
}
std::sort(candidates.c_ptr(), candidates.c_ptr() + candidates.size(),
index_lt_proc(m));
// keep actual select indecies in the order found at the back of
// candidate list
// XXX this corresponds to current implementation. Probably should
// XXX be sorted together with the rest of candidates
candidates.append(extra);
}
/* subs are the variables that might appear in the patterns */
void lemma_quantifier_generalizer::generate_patterns(
expr_ref_vector const &cube, app_ref_vector const &candidates,
var_ref_vector &subs, expr_ref_vector &patterns, unsigned offset)
{
if (candidates.empty()) return;
expr_safe_replace ses(m);
// Setup substitution
for (unsigned i=0; i < candidates.size(); i++) {
expr *cand = candidates.get(i);
var *var = m.mk_var(i+offset, get_sort(cand));
ses.insert(cand, var);
rational val;
if (m_arith.is_numeral(cand, val)) {
bool is_int = val.is_int();
ses.insert(
m_arith.mk_numeral(rational(val+1), is_int),
m_arith.mk_add(var, m_arith.mk_numeral(rational(1), is_int)));
ses.insert(
m_arith.mk_numeral(rational(-1*(val+1)), is_int),
m_arith.mk_add(m_arith.mk_sub(m_arith.mk_numeral(rational(0), is_int),var), m_arith.mk_numeral(rational(-1), is_int)));
}
subs.push_back(var);
}
// Generate patterns
// for every literal
for (unsigned j=0; j < cube.size(); j++) {
// abstract terms by variables
expr_ref pat(m);
ses(cube.get(j), pat);
if (pat.get() != cube.get(j)) {
// if abstraction is not identity
TRACE("spacer_qgen",
tout << "Pattern is: " << mk_pp(pat, m) << "\n";);
// store the pattern
patterns.push_back(pat);
}
}
}
void lemma_quantifier_generalizer::find_matching_expressions(
expr_ref_vector const &cube,
var_ref_vector const &subs, expr_ref_vector &patterns,
vector<expr_ref_vector> &idx_instances,
vector<bool> &dirty)
{
idx_instances.resize(subs.size(), expr_ref_vector(m));
// -- for every pattern
for (unsigned p = 0; p < patterns.size(); p++) {
expr *pattern = patterns.get(p);
// -- for every literal
for (unsigned j = 0; j < cube.size(); j++) {
if (dirty[j] || !is_app(cube.get(j))) continue;
app *lit = to_app(cube.get(j));
sem_matcher match(m);
bool pos;
substitution v_sub(m);
// allocate space for the largest variable in the pattern
unsigned max_idx = 0;
for (var* v : subs) {max_idx = std::max(max_idx, v->get_idx());}
v_sub.reserve(2, max_idx + 1);
if (!match(pattern, lit, v_sub, pos)) {
continue;
}
// expect positive matches only
if (!pos) {continue;}
dirty[j] = true;
for (unsigned v = 0; v < subs.size(); v++) {
expr_offset idx;
if (v_sub.find(subs.get(v), 0, idx)) {
TRACE ("spacer_qgen", tout << "INSTANCE IS: " << mk_pp(idx.get_expr(), m) << "\n";);
idx_instances[v].push_back(idx.get_expr());
}
}
}
}
}
void lemma_quantifier_generalizer::find_guards(
expr_ref_vector const &indices,
expr_ref &lower, expr_ref &upper)
{
if (indices.empty()) return;
auto minmax = std::minmax_element((app * *) indices.c_ptr(),
(app * *) indices.c_ptr() + indices.size(),
index_lt_proc(m));
lower = *minmax.first;
upper = *minmax.second;
}
void lemma_quantifier_generalizer::add_lower_bounds(
var_ref_vector const &subs,
app_ref_vector const &zks,
vector<expr_ref_vector> const &idx_instances,
expr_ref_vector &cube)
{
for (unsigned v = 0; v < subs.size(); v++) {
var *var = subs.get(v);
if (idx_instances[v].empty()) continue;
TRACE("spacer_qgen",
tout << "Finding lower bounds for " << mk_pp(var, m) << "\n";);
expr_ref low(m);
expr_ref up(m);
find_guards(idx_instances[v], low, up);
cube.push_back(m_arith.mk_ge(zks.get(var->get_idx()), low));
}
}
/// returns true if expression e contains a sub-expression of the form (select A idx) where
/// idx contains exactly one skolem from zks. Returns idx and the skolem
bool lemma_quantifier_generalizer::match_sk_idx(expr *e, app_ref_vector const &zks, expr *&idx, app *&sk) {
if (zks.size() != 1) return false;
contains_app has_zk(m, zks.get(0));
if (!contains_selects(e, m)) return false;
app_ref_vector indicies(m);
get_select_indices(e, indicies, m);
if (indicies.size() > 2) return false;
unsigned i=0;
if (indicies.size() == 1) {
if (!has_zk(indicies.get(0))) return false;
}
else {
if (has_zk(indicies.get(0)) && !has_zk(indicies.get(1)))
i = 0;
else if (!has_zk(indicies.get(0)) && has_zk(indicies.get(1)))
i = 1;
else if (!has_zk(indicies.get(0)) && !has_zk(indicies.get(1)))
return false;
}
idx = indicies.get(i);
sk = zks.get(0);
return true;
}
expr* times_minus_one(expr *e, arith_util &arith) {
expr *r;
if (arith.is_times_minus_one (e, r)) { return r; }
return arith.mk_mul(arith.mk_numeral(rational(-1), arith.is_int(get_sort(e))), e);
}
/** Attempts to rewrite a cube so that quantified variable appears as
a top level argument of select-term
Find sub-expression of the form (select A (+ sk!0 t)) and replaces
(+ sk!0 t) --> sk!0 and sk!0 --> (+ sk!0 (* (- 1) t))
Current implementation is an ugly hack for one special case
*/
void lemma_quantifier_generalizer::cleanup(expr_ref_vector &cube, app_ref_vector const &zks, expr_ref &bind) {
if (zks.size() != 1) return;
arith_util arith(m);
expr *idx = nullptr;
app *sk = nullptr;
expr_ref rep(m);
for (expr *e : cube) {
if (match_sk_idx(e, zks, idx, sk)) {
CTRACE("spacer_qgen", idx != sk,
tout << "Possible cleanup of " << mk_pp(idx, m) << " in "
<< mk_pp(e, m) << " on " << mk_pp(sk, m) << "\n";);
if (!arith.is_add(idx)) continue;
app *a = to_app(idx);
bool found = false;
expr_ref_vector kids(m);
expr_ref_vector kids_bind(m);
for (unsigned i = 0, sz = a->get_num_args(); i < sz; ++i) {
if (a->get_arg(i) == sk) {
found = true;
kids.push_back(a->get_arg(i));
kids_bind.push_back(bind);
}
else {
kids.push_back (times_minus_one(a->get_arg(i), arith));
kids_bind.push_back (times_minus_one(a->get_arg(i), arith));
}
}
if (!found) continue;
rep = arith.mk_add(kids.size(), kids.c_ptr());
bind = arith.mk_add(kids_bind.size(), kids_bind.c_ptr());
TRACE("spacer_qgen",
tout << "replace " << mk_pp(idx, m) << " with " << mk_pp(rep, m) << "\n";);
break;
}
}
if (rep) {
expr_safe_replace rw(m);
rw.insert(sk, rep);
rw.insert(idx, sk);
rw(cube);
TRACE("spacer_qgen",
tout << "Cleaned cube to: " << mk_and(cube) << "\n";);
}
}
void lemma_quantifier_generalizer::generalize_pattern_lits(expr_ref_vector &pats) {
// generalize pattern literals using 'x=t' --> 'x>=t'
for (unsigned i = 0, sz = pats.size(); i < sz; ++i) {
expr *e1, *e2;
expr *p = pats.get(i);
if (m.is_eq(p, e1, e2) && (is_var(e1) || is_var(e2))) {
if (m_arith.is_numeral(e1)) {
p = m_arith.mk_ge(e2, e1);
}
else if (m_arith.is_numeral(e2)) {
p = m_arith.mk_ge(e1, e2);
}
}
if (p != pats.get(i)) {
pats.set(i, p);
}
}
}
void lemma_quantifier_generalizer::operator()(lemma_ref &lemma) {
if (lemma->get_cube().empty()) return;
if (!lemma->has_pob()) return;
m_st.count++;
scoped_watch _w_(m_st.watch);
expr_ref_vector cube(m);
cube.append(lemma->get_cube());
TRACE("spacer_qgen",
tout << "initial cube: " << mk_and(lemma->get_cube()) << "\n";);
if (true) {
// -- re-normalize the cube
expr_ref c(m);
c = mk_and(cube);
normalize(c, c, true, true);
cube.reset();
flatten_and(c, cube);
TRACE("spacer_qgen",
tout << "normalized cube:\n" << mk_and(cube) << "\n";);
}
app_ref_vector skolems(m);
lemma->get_pob()->get_skolems(skolems);
int offset = skolems.size();
// for every literal
for (unsigned i=0; i < cube.size(); i++) {
expr *r = cube.get(i);
// generate candidates
app_ref_vector candidates(m);
find_candidates(r, candidates);
if (candidates.empty()) continue;
// for every candidate
for (unsigned arg=0, sz = candidates.size(); arg < sz; arg++) {
app_ref_vector cand(m);
cand.push_back(to_app(candidates.get(arg)));
var_ref_vector subs(m);
expr_ref_vector patterns(m);
// generate patterns for a candidate
generate_patterns(cube, cand, subs, patterns, offset);
// Currently, only support one variable per pattern
SASSERT(subs.size() == cand.size());
SASSERT(cand.size() == 1);
// Find matching expressions
vector<bool> dirty;
dirty.resize(cube.size(), false);
vector<expr_ref_vector> idx_instances;
find_matching_expressions(cube, subs, patterns, idx_instances, dirty);
expr_ref_vector new_cube(m);
// move all unmatched expressions to the new cube
for (unsigned c=0; c < cube.size(); c++) {
if (dirty[c] == false) {
new_cube.push_back(cube.get(c));
}
}
// everything moved, nothing left
if (new_cube.size() == cube.size()) continue;
// -- generalize equality in patterns
generalize_pattern_lits(patterns);
// ground quantified patterns in skolems
expr_ref gnd(m);
app_ref_vector zks(m);
ground_expr(mk_and(patterns), gnd, zks);
flatten_and(gnd, new_cube);
// compute lower bounds for quantified variables
add_lower_bounds(subs, zks, idx_instances, new_cube);
TRACE("spacer_qgen",
tout << "New CUBE is: " << mk_pp(mk_and(new_cube),m) << "\n";);
// check if the result is a lemma
unsigned uses_level = 0;
unsigned weakness = lemma->weakness();
pred_transformer &pt = lemma->get_pob()->pt();
if (pt.check_inductive(lemma->level(), new_cube, uses_level, weakness)) {
TRACE("spacer_qgen",
tout << "Quantifier Generalization Succeeded!\n"
<< "New CUBE is: " << mk_pp(mk_and(new_cube),m) << "\n";);
SASSERT(zks.size() >= offset);
SASSERT(cand.size() == 1);
// lift quantified variables to top of select
expr_ref bind(m);
bind = cand.get(0);
cleanup(new_cube, zks, bind);
// XXX better do that check before changing bind in cleanup()
// XXX Or not because substitution might introduce _n variable into bind
if (m_ctx.get_manager().is_n_formula(bind))
// XXX this creates an instance, but not necessarily the needed one
// XXX This is sound because any instance of
// XXX universal quantifier is sound
// XXX needs better long term solution. leave
// comment here for the future
m_ctx.get_manager().formula_n2o(bind, bind, 0);
lemma->update_cube(lemma->get_pob(), new_cube);
lemma->set_level(uses_level);
SASSERT(offset + 1 == zks.size());
// XXX Assumes that offset + 1 == zks.size();
for (unsigned sk=offset; sk < zks.size(); sk++)
lemma->add_skolem(zks.get(sk), to_app(bind));
return;
}
else {
++m_st.num_failures;
}
}
}
}
}