3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2025-11-05 22:06:03 +00:00

Merge branch 'finite-sets' into copilot/add-rewrite-rules-finite-set-rewriter

This commit is contained in:
Nikolaj Bjorner 2025-10-15 18:45:57 +02:00 committed by GitHub
commit c502c62997
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 347 additions and 18 deletions

View file

@ -204,6 +204,9 @@ bool finite_set_decl_plugin::are_distinct(app* e1, app* e2) const {
return true; return true;
if (r.is_singleton(e1) && r.is_empty(e2)) if (r.is_singleton(e1) && r.is_empty(e2))
return true; return true;
if(r.is_singleton(e1) && r.is_singleton(e2))
return m_manager->are_distinct(e1, e2);
// TODO: could be extended to cases where we can prove the sets are different by containing one element // TODO: could be extended to cases where we can prove the sets are different by containing one element
// that the other doesn't contain. Such as (union (singleton a) (singleton b)) and (singleton c) where c is different from a, b. // that the other doesn't contain. Such as (union (singleton a) (singleton b)) and (singleton c) where c is different from a, b.
return false; return false;

View file

@ -34,14 +34,7 @@ where the signature is defined in finite_set_decl_plugin.h.
*/ */
class finite_set_rewriter { class finite_set_rewriter {
finite_set_util m_util; finite_set_util m_util;
// Rewrite rules for set operations public:
br_status mk_union(unsigned num_args, expr * const * args, expr_ref & result);
br_status mk_intersect(unsigned num_args, expr * const * args, expr_ref & result);
br_status mk_difference(expr * arg1, expr * arg2, expr_ref & result);
br_status mk_subset(expr * arg1, expr * arg2, expr_ref & result);
br_status mk_singleton(expr * arg, expr_ref & result);
br_status mk_in(expr * elem, expr * set, expr_ref & result);
public:
finite_set_rewriter(ast_manager & m, params_ref const & p = params_ref()): finite_set_rewriter(ast_manager & m, params_ref const & p = params_ref()):
m_util(m) { m_util(m) {
} }
@ -52,5 +45,11 @@ public:
br_status mk_app_core(func_decl * f, unsigned num_args, expr * const * args, expr_ref & result); br_status mk_app_core(func_decl * f, unsigned num_args, expr * const * args, expr_ref & result);
// Rewrite rules for set operations
br_status mk_union(unsigned num_args, expr * const * args, expr_ref & result);
br_status mk_intersect(unsigned num_args, expr * const * args, expr_ref & result);
br_status mk_difference(expr * arg1, expr * arg2, expr_ref & result);
br_status mk_subset(expr * arg1, expr * arg2, expr_ref & result);
}; };

View file

@ -57,6 +57,7 @@ z3_add_component(smt
theory_char.cpp theory_char.cpp
theory_datatype.cpp theory_datatype.cpp
theory_dense_diff_logic.cpp theory_dense_diff_logic.cpp
theory_finite_set.cpp
theory_diff_logic.cpp theory_diff_logic.cpp
theory_dl.cpp theory_dl.cpp
theory_dummy.cpp theory_dummy.cpp

View file

@ -40,6 +40,7 @@ Revision History:
#include "smt/theory_pb.h" #include "smt/theory_pb.h"
#include "smt/theory_fpa.h" #include "smt/theory_fpa.h"
#include "smt/theory_polymorphism.h" #include "smt/theory_polymorphism.h"
#include "smt/theory_finite_set.h"
namespace smt { namespace smt {
@ -784,6 +785,10 @@ namespace smt {
m_context.register_plugin(alloc(smt::theory_char, m_context)); m_context.register_plugin(alloc(smt::theory_char, m_context));
} }
void setup::setup_finite_set() {
m_context.register_plugin(alloc(smt::theory_finite_set, m_context));
}
void setup::setup_special_relations() { void setup::setup_special_relations() {
m_context.register_plugin(alloc(smt::theory_special_relations, m_context, m_manager)); m_context.register_plugin(alloc(smt::theory_special_relations, m_context, m_manager));
} }
@ -807,6 +812,7 @@ namespace smt {
setup_dl(); setup_dl();
setup_seq_str(st); setup_seq_str(st);
setup_fpa(); setup_fpa();
setup_finite_set();
setup_special_relations(); setup_special_relations();
setup_polymorphism(); setup_polymorphism();
setup_relevancy(st); setup_relevancy(st);

View file

@ -102,6 +102,7 @@ namespace smt {
void setup_seq_str(static_features const & st); void setup_seq_str(static_features const & st);
void setup_seq(); void setup_seq();
void setup_char(); void setup_char();
void setup_finite_set();
void setup_card(); void setup_card();
void setup_sls(); void setup_sls();
void setup_i_arith(); void setup_i_arith();

View file

@ -0,0 +1,262 @@
/*++
Copyright (c) 2025 Microsoft Corporation
Module Name:
theory_finite_set.cpp
Abstract:
Theory solver for finite sets.
Implements axiom schemas for finite set operations.
Author:
GitHub Copilot Agent 2025
Revision History:
--*/
#include "smt/theory_finite_set.h"
#include "smt/smt_context.h"
#include "smt/smt_model_generator.h"
#include "ast/ast_pp.h"
namespace smt {
theory_finite_set::theory_finite_set(context& ctx):
theory(ctx, ctx.get_manager().mk_family_id("finite_set")),
u(m),
m_axioms(m)
{
// Setup the add_clause callback for axioms
std::function<void(expr_ref_vector const &)> add_clause_fn =
[this](expr_ref_vector const& clause) {
this->add_clause(clause);
};
m_axioms.set_add_clause(add_clause_fn);
}
bool theory_finite_set::internalize_atom(app * atom, bool gate_ctx) {
TRACE(finite_set, tout << "internalize_atom: " << mk_pp(atom, m) << "\n";);
internalize_term(atom);
// Track membership elements (set.in)
expr* elem = nullptr, *set = nullptr;
if (u.is_in(atom, elem, set)) {
auto n = ctx.get_enode(elem);
if (!m_elements.contains(n)) {
m_elements.insert(n);
ctx.push_trail(insert_obj_trail(m_elements, n));
}
}
return true;
}
bool theory_finite_set::internalize_term(app * term) {
TRACE(finite_set, tout << "internalize_term: " << mk_pp(term, m) << "\n";);
// Internalize all arguments first
for (expr* arg : *term)
ctx.internalize(arg, false);
// Create boolean variable for Boolean terms
if (m.is_bool(term) && !ctx.b_internalized(term)) {
bool_var bv = ctx.mk_bool_var(term);
ctx.set_var_theory(bv, get_id());
}
// Create enode for the term if needed
enode* e = nullptr;
if (ctx.e_internalized(term))
e = ctx.get_enode(term);
else
e = ctx.mk_enode(term, false, m.is_bool(term), true);
// Attach theory variable if this is a set
if (!is_attached_to_var(e))
ctx.attach_th_var(e, this, mk_var(e));
return true;
}
void theory_finite_set::new_eq_eh(theory_var v1, theory_var v2) {
TRACE(finite_set, tout << "new_eq_eh: v" << v1 << " = v" << v2 << "\n";);
// When two sets are equal, propagate membership constraints
// This is handled by congruence closure, so no additional work needed here
}
void theory_finite_set::new_diseq_eh(theory_var v1, theory_var v2) {
TRACE(finite_set, tout << "new_diseq_eh: v" << v1 << " != v" << v2 << "\n";);
// Disequalities could trigger extensionality axioms
// For now, we rely on the final_check to handle this
}
final_check_status theory_finite_set::final_check_eh() {
TRACE(finite_set, tout << "final_check_eh\n";);
// walk all parents of elem in congruence table.
// if a parent is of the form elem' in S u T, or similar.
// create clauses for elem in S u T.
expr* elem1 = nullptr, *set1 = nullptr;
for (auto elem : m_elements) {
if (!ctx.is_relevant(elem))
continue;
for (auto p : enode::parents(elem)) {
if (!u.is_in(p->get_expr(), elem1, set1))
continue;
if (elem->get_root() != p->get_arg(0)->get_root())
continue; // elem is then equal to set1 but not elem1. This is a different case.
if (!ctx.is_relevant(p))
continue;
for (auto sib : *p->get_arg(1))
instantiate_axioms(elem->get_expr(), sib->get_expr());
}
}
if (instantiate_false_lemma())
return FC_CONTINUE;
if (instantiate_unit_propagation())
return FC_CONTINUE;
if (instantiate_free_lemma())
return FC_CONTINUE;
return FC_DONE;
}
void theory_finite_set::instantiate_axioms(expr* elem, expr* set) {
TRACE(finite_set, tout << "instantiate_axioms: " << mk_pp(elem, m) << " in " << mk_pp(set, m) << "\n";);
struct insert_obj_pair_table : public trail {
obj_pair_hashtable<expr, expr> &table;
expr *a, *b;
insert_obj_pair_table(obj_pair_hashtable<expr, expr> &t, expr *a, expr *b) :
table(t), a(a), b(b) {}
void undo() override {
table.erase({a, b});
}
};
if (m_lemma_exprs.contains({elem, set}))
return;
m_lemma_exprs.insert({elem, set});
ctx.push_trail(insert_obj_pair_table(m_lemma_exprs, elem, set));
// Instantiate appropriate axiom based on set structure
if (u.is_empty(set)) {
m_axioms.in_empty_axiom(elem);
}
else if (u.is_singleton(set)) {
m_axioms.in_singleton_axiom(elem, set);
}
else if (u.is_union(set)) {
m_axioms.in_union_axiom(elem, set);
}
else if (u.is_intersect(set)) {
m_axioms.in_intersect_axiom(elem, set);
}
else if (u.is_difference(set)) {
m_axioms.in_difference_axiom(elem, set);
}
else if (u.is_range(set)) {
m_axioms.in_range_axiom(elem, set);
}
else if (u.is_map(set)) {
m_axioms.in_map_axiom(elem, set);
m_axioms.in_map_image_axiom(elem, set);
}
else if (u.is_select(set)) {
m_axioms.in_select_axiom(elem, set);
}
// Instantiate size axioms for singleton sets
// TODO, such axioms don't belong here
if (u.is_singleton(set)) {
m_axioms.size_singleton_axiom(set);
}
}
void theory_finite_set::add_clause(expr_ref_vector const& clause) {
TRACE(finite_set, tout << "add_clause: " << clause << "\n");
ctx.push_trail(push_back_vector(m_lemmas));
m_lemmas.push_back(clause);
}
theory * theory_finite_set::mk_fresh(context * new_ctx) {
return alloc(theory_finite_set, *new_ctx);
}
void theory_finite_set::display(std::ostream & out) const {
out << "theory_finite_set:\n";
}
void theory_finite_set::init_model(model_generator & mg) {
TRACE(finite_set, tout << "init_model\n";);
// Model generation will use default interpretation for sets
// The model will be constructed based on the membership literals that are true
}
model_value_proc * theory_finite_set::mk_value(enode * n, model_generator & mg) {
TRACE(finite_set, tout << "mk_value: " << mk_pp(n->get_expr(), m) << "\n";);
// For now, return nullptr to use default model construction
// A complete implementation would construct explicit set values
// based on true membership literals
return nullptr;
}
bool theory_finite_set::instantiate_false_lemma() {
for (auto const& clause : m_lemmas) {
bool all_false = all_of(clause, [&](expr *e) { return ctx.find_assignment(e) == l_false; });
if (!all_false)
continue;
assert_clause(clause);
return true;
}
return false;
}
bool theory_finite_set::instantiate_unit_propagation() {
for (auto const &clause : m_lemmas) {
expr *undef = nullptr;
bool is_unit_propagating = true;
for (auto e : clause) {
switch (ctx.find_assignment(e)) {
case l_false: continue;
case l_true: is_unit_propagating = false; break;
case l_undef:
if (undef != nullptr)
is_unit_propagating = false;
undef = e;
break;
}
if (!is_unit_propagating)
break;
}
if (!is_unit_propagating || undef == nullptr)
continue;
assert_clause(clause);
return true;
}
return false;
}
bool theory_finite_set::instantiate_free_lemma() {
for (auto const& clause : m_lemmas) {
if (any_of(clause, [&](expr *e) { return ctx.find_assignment(e) == l_true; }))
continue;
assert_clause(clause);
return true;
}
return false;
}
void theory_finite_set::assert_clause(expr_ref_vector const &clause) {
literal_vector lclause;
for (auto e : clause)
lclause.push_back(mk_literal(e));
ctx.mk_th_axiom(get_id(), lclause);
}
} // namespace smt

View file

@ -57,20 +57,75 @@ Abstract:
----------------------------------------------- -----------------------------------------------
x in v3 <=> p(x) and x in v4 x in v3 <=> p(x) and x in v4
Rules are encoded in src/ast/rewriter/finite_set_axioms.cpp as clauses.
The central claim is that the above rules are sufficient to The central claim is that the above rules are sufficient to
decide satisfiability of finite set constraints. decide satisfiability of finite set constraints for a subset
of operations, namely union, intersection, difference, singleton, membership.
Model construction proceeds by selecting every set.in(x_i, v) for a Model construction proceeds by selecting every set.in(x_i, v) for a
congruence root v. Then the set of elements { x_i | set.in(x_i, v) } congruence root v. Then the set of elements { x_i | set.in(x_i, v) }
is the interpretation. is the interpretation.
This approach, however, does not work with ranges, or is impractical. This approach for model-construction, however, does not work with ranges, or is impractical.
For ranges, we are going to extract an interpretation for set variable v For ranges we can adapt a different model construction approach.
directly from a set of range expressions.
Inductively, the interpretation of a range expression is given by the When introducing select and map, decidability can be lost.
range itself. It proceeds by taking unions, intersections and differences of range
interpretations.
For Boolean lattice constraints given by equality, subset, strict subset and union, intersections, For Boolean lattice constraints given by equality, subset, strict subset and union, intersections,
the theory solver uses a stand-alone satisfiability checker for Boolean algebras to close branches. the theory solver uses a stand-alone satisfiability checker for Boolean algebras to close branches.
Instructions for copilot:
1. Override relevant methods for smt::theory. Add them to the signature and add stubs or implementations in
theory_finite_set.cpp.
2. In final_check_eh add instantiation of theory axioms following the outline in the inference rules above.
An example of how to instantiate axioms is in theory_arrays_base.cpp and theroy_datatypes.cpp and other theory files.
--*/ --*/
#pragma once
#include "ast/ast.h"
#include "ast/ast_pp.h"
#include "ast/finite_set_decl_plugin.h"
#include "ast/rewriter/finite_set_axioms.h"
#include "util/obj_pair_hashtable.h"
#include "smt/smt_theory.h"
namespace smt {
class theory_finite_set : public theory {
friend class theory_finite_set_test;
finite_set_util u;
finite_set_axioms m_axioms;
obj_hashtable<enode> m_elements; // set of all 'x' where there is an 'x in S' atom
vector<expr_ref_vector> m_lemmas;
obj_pair_hashtable<expr, expr> m_lemma_exprs;
protected:
// Override relevant methods from smt::theory
bool internalize_atom(app * atom, bool gate_ctx) override;
bool internalize_term(app * term) override;
void new_eq_eh(theory_var v1, theory_var v2) override;
void new_diseq_eh(theory_var v1, theory_var v2) override;
final_check_status final_check_eh() override;
theory * mk_fresh(context * new_ctx) override;
char const * get_name() const override { return "finite_set"; }
void display(std::ostream & out) const override;
void init_model(model_generator & mg) override;
model_value_proc * mk_value(enode * n, model_generator & mg) override;
// Helper methods for axiom instantiation
void instantiate_axioms(expr* elem, expr* set);
void add_clause(expr_ref_vector const& clause);
void assert_clause(expr_ref_vector const &clause);
bool instantiate_false_lemma();
bool instantiate_unit_propagation();
bool instantiate_free_lemma();
lbool truth_value(expr *e);
public:
theory_finite_set(context& ctx);
~theory_finite_set() override {}
};
} // namespace smt

View file

@ -37,7 +37,7 @@ static void tst_finite_set_basic() {
ENSURE(fsets.is_finite_set(finite_set_int.get())); ENSURE(fsets.is_finite_set(finite_set_int.get()));
// Test creating empty set // Test creating empty set
app_ref empty_set(fsets.mk_empty(int_sort), m); app_ref empty_set(fsets.mk_empty(finite_set_int), m);
ENSURE(fsets.is_empty(empty_set.get())); ENSURE(fsets.is_empty(empty_set.get()));
ENSURE(empty_set->get_sort() == finite_set_int.get()); ENSURE(empty_set->get_sort() == finite_set_int.get());

View file

@ -70,6 +70,8 @@ X(eq_der, top_sort, "top sort")
X(expr_substitution_simplifier, expr_substitution_simplifier, "expr substitution simplifier") X(expr_substitution_simplifier, expr_substitution_simplifier, "expr substitution simplifier")
X(expr_substitution_simplifier, propagate_values, "propagate values") X(expr_substitution_simplifier, propagate_values, "propagate values")
X(finite_set, finite_set, "finite set")
X(fm_model_converter, fm_model_converter, "fm model converter") X(fm_model_converter, fm_model_converter, "fm model converter")
X(fm_model_converter, fm_mc, "fm mc") X(fm_model_converter, fm_mc, "fm mc")