3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2025-04-07 18:05:21 +00:00

Initial commit of QGen

Controlled by fixedpoint.spacer.use_quanti_generalizer

measure cumulative time, number of invocations, and number of failed
SMT calls

Relaxing equality in a pattern: if a variable equals a numeral, relax with GE

pob::get_skolems() returns all skolems that might appear in the pob.
New skolems must be added above the largest index in that map,
even if they are not used in the pob itself.

pattern generalization should be done before the pattern is skolemized and
added into the new cube.
This commit is contained in:
Yakir Vizel 2017-12-18 13:52:53 -05:00 committed by Arie Gurfinkel
parent a1efb88318
commit 23a8e59493
10 changed files with 583 additions and 16 deletions

View file

@ -187,7 +187,8 @@ def_module_params('fixedpoint',
('spacer.reuse_pobs', BOOL, True, 'reuse POBs'),
('spacer.print_farkas_stats', BOOL, False, 'prints for each proof how many Farkas lemmas it contains and how many of these participate in the cut'),
('spacer.iuc.debug_proof', BOOL, False, 'prints proof used by unsat-core-learner for debugging purposes'),
('spacer.simplify_pob', BOOL, False, 'simplify POBs by removing redundant constraints')
('spacer.simplify_pob', BOOL, False, 'simplify POBs by removing redundant constraints'),
('spacer.use_quant_generalizer', BOOL, False, 'use quantified lemma generalizer'),
))

View file

@ -1894,7 +1894,6 @@ void pob::set_post(expr* post, app_ref_vector const &b) {
expr_safe_replace sub(m);
for (unsigned i = 0, sz = m_binding.size(); i < sz; ++i) {
expr* e;
e = m_binding.get(i);
pinned.push_back (mk_zk_const (m, i, get_sort(e)));
sub.insert (e, pinned.back());
@ -1939,7 +1938,6 @@ void pob::close () {
void pob::get_skolems(app_ref_vector &v) {
for (unsigned i = 0, sz = m_binding.size(); i < sz; ++i) {
expr* e;
e = m_binding.get(i);
v.push_back (mk_zk_const (get_ast_manager(), i, get_sort(e)));
}
@ -2273,6 +2271,11 @@ void context::init_lemma_generalizers(datalog::rule_set& rules)
fparams.m_ng_lift_ite = LI_FULL;
}
if (m_params.spacer_use_quant_generalizer()) {
m_lemma_generalizers.push_back(alloc(lemma_bool_inductive_generalizer, *this, 0, false));
m_lemma_generalizers.push_back(alloc(lemma_quantifier_generalizer, *this));
}
if (get_params().spacer_use_eqclass()) {
m_lemma_generalizers.push_back (alloc(lemma_eq_generalizer, *this));
}

View file

@ -527,7 +527,9 @@ public:
unsigned get_free_vars_size() { return m_binding.size(); }
app_ref_vector const &get_binding() const {return m_binding;}
/*
* Return skolem variables that appear in post
* Returns a map from variable id to skolems that implicitly
* represent them in the pob. Note that only some (or none) of the
* skolems returned actually appear in the post of the pob.
*/
void get_skolems(app_ref_vector& v);

View file

@ -326,4 +326,6 @@ void lemma_eq_generalizer::operator() (lemma_ref &lemma)
lemma->update_cube(lemma->get_pob(), core);
}
}
};

View file

@ -97,6 +97,54 @@ public:
void operator()(lemma_ref &lemma) override;
};
class lemma_quantifier_generalizer : public lemma_generalizer {
struct stats {
unsigned count;
unsigned num_failures;
stopwatch watch;
stats() {reset();}
void reset() {count = 0; num_failures = 0; watch.reset();}
};
ast_manager &m;
arith_util m_arith;
stats m_st;
public:
lemma_quantifier_generalizer(context &ctx);
virtual ~lemma_quantifier_generalizer() {}
virtual void operator()(lemma_ref &lemma);
virtual void collect_statistics(statistics& st) const;
virtual void reset_statistics() {m_st.reset();}
private:
bool match_sk_idx(expr *e, app_ref_vector const &zks, expr *&idx, app *&sk);
void cleanup(expr_ref_vector& cube, app_ref_vector const &zks, expr_ref &bind);
void generalize_pattern_lits(expr_ref_vector &pats);
void find_candidates(
expr *e,
app_ref_vector &candidate);
void generate_patterns(
expr_ref_vector const &cube,
app_ref_vector const &candidates,
var_ref_vector &subs,
expr_ref_vector &patterns,
unsigned offset);
void 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);
void find_guards(
expr_ref_vector const &indices,
expr_ref &lower,
expr_ref &upper);
void add_lower_bounds(
var_ref_vector const &subs,
app_ref_vector const &zks,
vector<expr_ref_vector> const &idx_instances,
expr_ref_vector &cube);
};
}
#endif

View file

@ -340,22 +340,27 @@ app* mk_zk_const(ast_manager &m, unsigned idx, sort *s) {
namespace find_zk_const_ns {
struct proc {
int m_max;
app_ref_vector &m_out;
proc (app_ref_vector &out) : m_out(out) {}
proc (app_ref_vector &out) : m_max(-1), m_out(out) {}
void operator() (var const * n) const {}
void operator() (app *n) const {
if (is_uninterp_const(n) &&
n->get_decl()->get_name().str().compare (0, 3, "sk!") == 0) {
m_out.push_back (n);
void operator() (app *n) {
int idx;
if (is_zk_const(n, idx)) {
m_out.push_back(n);
if (idx > m_max) {
m_max = idx;
}
}
}
void operator() (quantifier const *n) const {}
};
}
void find_zk_const(expr *e, app_ref_vector &res) {
int find_zk_const(expr *e, app_ref_vector &res) {
find_zk_const_ns::proc p(res);
for_each_expr (p, e);
return p.m_max;
}
namespace has_zk_const_ns {
@ -363,8 +368,8 @@ struct found {};
struct proc {
void operator() (var const *n) const {}
void operator() (app const *n) const {
if (is_uninterp_const(n) &&
n->get_decl()->get_name().str().compare(0, 3, "sk!") == 0) {
int idx;
if (is_zk_const(n, idx)) {
throw found();
}
}

View file

@ -339,8 +339,12 @@ public:
};
app* mk_zk_const (ast_manager &m, unsigned idx, sort *s);
void find_zk_const(expr* e, app_ref_vector &out);
int find_zk_const(expr* e, app_ref_vector &out);
inline int find_zk_const(expr_ref_vector const &v, app_ref_vector &out) {
return find_zk_const (mk_and(v), out);
}
bool has_zk_const(expr* e);
bool is_zk_const (const app *a, int &n);
struct sk_lt_proc {
bool operator()(const app* a1, const app* a2);

View file

@ -0,0 +1,499 @@
/*++
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);
v_sub.reserve(2, subs.size()+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 (false) {
// -- re-normalize the cube
expr_ref c(m);
c = mk_and(cube);
normalize(c, c, true, true);
cube.reset();
flatten_and(c, cube);
}
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);
// XXX Can candidates be empty and arguments not empty?
// XXX Why separate candidates and arguments?
// XXX Why are arguments processed before 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",
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);
// 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;
}
}
}
}
}

View file

@ -1317,9 +1317,9 @@ bool contains_selects(expr* fml, ast_manager& m)
return false;
}
void get_select_indices(expr* fml, app_ref_vector& indices, ast_manager& m)
void get_select_indices(expr* fml, app_ref_vector &indices, ast_manager& m)
{
array_util a_util(m);
array_util a_util(m);
if (!is_app(fml)) { return; }
ast_mark done;
ptr_vector<app> todo;

View file

@ -144,7 +144,10 @@ void compute_implicant_literals (model_evaluator_util &mev,
void simplify_bounds (expr_ref_vector &lemmas);
void normalize(expr *e, expr_ref &out, bool use_simplify_bounds = true, bool factor_eqs = false);
/** ground expression by replacing all free variables by skolem constants */
/** Ground expression by replacing all free variables by skolem
** constants. On return, out is the resulting expression, and vars is
** a map from variable ids to corresponding skolem constants.
*/
void ground_expr (expr *e, expr_ref &out, app_ref_vector &vars);