3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-24 07:52:33 +00:00
z3/src/ast/simplifiers/euf_completion.cpp
Michael Tautschnig 09aaadf963
Sequence AST-creating arguments in rewriters for cross-compiler determinism (#10165)
### Problem

The order of evaluation of function arguments is unspecified in C++
(arguments are indeterminately sequenced since C++17). Compilers use
this freedom differently:

```c++
static int f(int i) { printf("%d ", i); return i; }
static void g(int, int, int) { printf("\n"); }
int main() { g(f(1), f(2), f(3)); }
```
| compiler/target | output |
|---|---|
| gcc 13, x86_64 | `3 2 1` |
| gcc 13, aarch64 | `1 2 3` |
| clang 18, x86_64 | `1 2 3` |

Z3 has many call sites where **two or more arguments each create AST
nodes**, e.g. (before this PR, `bv_rewriter.cpp:876`):

```c++
result = m.mk_ite(c, m_mk_extract(high, low, t), m_mk_extract(high, low, e));
```

The two extract nodes are hash-consed and receive their AST ids in
evaluation order, so the id assignment differs between
compilers/targets. AST ids feed heuristic tie-breaking throughout the
solver (`bool_rewriter`'s `m_order_eq` equality-operand ordering,
id-based sorts in `array_rewriter`, case-split ordering, ...), so
**byte-identical input takes different solver paths depending on the
compiler and architecture z3 was built with**.

### Evidence

Investigated while chasing cross-platform proof-time instability in
CBMC/mldsa-native CI (diffblue/cbmc#8991), on byte-identical ~12 MB SMT2
instances (bit-vectors + arrays + quantifiers), with the `string_hash`
fix from #10163 applied to isolate this effect. Z3 4.15.3, gcc 13 on
x86_64 Linux and aarch64 Linux (Graviton):

* one instance: **17 s on x86_64 vs 1633 s on aarch64** (both `unsat`; a
sibling instance shows the reverse direction). Run-to-run within one
host: ±1 %.
* Instrumenting `ast_manager::register_node_core` with an order
fingerprint (running hash over `(node hash, node id)`) shows both
architectures construct **identical AST sequences up to registration
#41,789**, where x86_64 creates `(extract[0:0] #xFFFFFFFF)` before
`(extract[0:0] #xFFFFFFFE)` and aarch64 the other way around — from
identical call stacks at the `mk_ite`-over-two-`mk_extract` site quoted
above. All divergence between the two hosts flows from such events
(pointer/ASLR effects experimentally excluded: fingerprints are
invariant under `setarch -R` and across repeated runs).
* Sequencing that one site by hand moved the first divergence to
#248,118 — the analogous `mk_ite(c, mk_select(...), mk_select(...))`
site in `array_rewriter.cpp`. Sequencing that one, too, moved it to
#248,411, inside `nnf:👿:process_iff_xor` — i.e. the next layer of
the same onion.
* With the whole `ast/rewriter` layer swept (this PR), the instrumented
builds produce **identical AST construction traces on both architectures
throughout the entire rewriter phase** of this 546k-line industrial
instance; the first divergence left is the NNF one.

### Fix

Following the precedent of 37904b9e8, e113d39aa, 360193098, 93ff8c76d,
9b88aaf13 ("parameter evaluation order", `bool_rewriter`/`seq_rewriter`)
and the existing comments in `seq_rewriter.cpp` ("introduce temporaries
to ensure deterministic evaluation order..."), this PR hoists
AST-creating arguments into named temporaries with a defined evaluation
order, across `src/ast/rewriter/` — 126 call sites in 17 files. The
transformation is purely sequencing: it selects one of the two valid C++
evaluation orders and makes it the same everywhere. (Temporaries are raw
pointers in rewriter-local scope, matching the precedent commits;
nothing can trigger GC between creation and consumption.)

The sites were found with a small AST-argument scanner (statement-level
call sites whose argument list contains ≥ 2 top-level arguments that
each contain an AST-creating call); I am happy to share/contribute the
script. Known remaining work, deliberately out of scope here to keep the
diff reviewable:

* 41 sites in `src/ast/rewriter/` that need manual treatment (inside
`if` conditions, ternaries, or multi-statement expressions) — list
available on request;
* `src/ast/normal_forms/nnf.cpp` (`process_iff_xor`, proven divergent by
the trace above), ~7 sites in `src/ast/simplifiers/`, ~3 in
`src/ast/converters/`, ~9 in `src/ast/`;
* other theory/solver layers (`src/smt/`, `src/sat/`, ...) — divergences
there only matter after search starts, where paths have usually already
split, but a full sweep would be needed for bit-reproducibility across
compilers.

Together with #10163, this is a step towards z3 builds whose behaviour
does not depend on the compiler or target architecture — which matters
for verification CI that runs identical proofs on heterogeneous
platforms and expects comparable runtimes.

---------

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
2026-07-21 19:03:02 -07:00

1384 lines
48 KiB
C++

/*++
Copyright (c) 2022 Microsoft Corporation
Module Name:
euf_completion.cpp
Abstract:
Ground completion for equalities
Author:
Nikolaj Bjorner (nbjorner) 2022-10-30
Notes:
Create a congruence closure of E.
Select _simplest_ term in each equivalence class. A term is _simplest_
if it is smallest in a well-order, such as a ground Knuth-Bendix order.
A basic approach is terms that are of smallest depth, are values can be chosen as simplest.
Ties between equal-depth terms can be resolved arbitrarily.
Algorithm for extracting canonical form from an E-graph:
* Compute function canon(t) that maps every term in E to a canonical, least with respect to well-order relative to the congruence closure.
That is, terms that are equal modulo the congruence closure have the same canonical representative.
* Each f(t) = g(s) in E:
* add f(canon(t)) = canon(f(t)), g(canon(s)) = canon(g(s)) where canon(f(t)) = canon(g(s)) by construction.
* Each other g(t) in E:
* add g(canon(t)) to E.
* Note that canon(g(t)) = true because g(t) = true is added to congruence closure of E.
* We claim the new formula is equivalent.
* The dependencies for each rewrite can be computed by following the equality justification data-structure.
Conditional saturation:
- forall X . Body => Head
- propagate when (all assertions in) Body is merged with True
- insert expressions from Body into a watch list.
When elements of the watch list are merged by true/false
trigger rep-propagation with respect to body.
Mam optimization?
match(p, t, S) = suppose all variables in p are bound in S, check equality using canonization of p[S], otherwise prune instances from S.
--*/
#include "ast/ast_pp.h"
#include "ast/ast_util.h"
#include "ast/expr_abstract.h"
#include "ast/euf/euf_egraph.h"
#include "ast/euf/euf_arith_plugin.h"
#include "ast/euf/euf_bv_plugin.h"
#include "ast/rewriter/var_subst.h"
#include "ast/simplifiers/euf_completion.h"
#include "ast/shared_occs.h"
#include "ast/scoped_proof.h"
#include "params/smt_params_helper.hpp"
namespace euf {
completion::completion(ast_manager& m, dependent_expr_state& fmls) :
dependent_expr_simplifier(m, fmls),
m_egraph(m),
m_mam(mam::mk(*this, *this)),
m_canonical(m),
m_eargs(m),
m_expr_trail(m),
m_consequences(m),
m_canonical_proofs(m),
// m_infer_patterns(m, m_smt_params),
m_deps(m),
m_rewriter(m),
m_matcher(m, m_trail) {
m_tt = m_egraph.mk(m.mk_true(), 0, 0, nullptr);
m_ff = m_egraph.mk(m.mk_false(), 0, 0, nullptr);
m_rewriter.set_order_eq(true);
m_rewriter.set_flat_and_or(false);
std::function<void(euf::enode*, euf::enode*)> _on_merge =
[&](euf::enode* root, euf::enode* other) {
m_mam->on_merge(root, other);
watch_rule(root, other);
};
std::function<void(euf::enode*)> _on_make =
[&](euf::enode* n) {
m_mam->add_node(n, false);
};
m_egraph.set_on_merge(_on_merge);
m_egraph.set_on_make(_on_make);
m_egraph.add_plugin(alloc(arith_plugin, m_egraph));
m_egraph.add_plugin(alloc(bv_plugin, m_egraph));
std::function<void(ho_subst&)> on_match =
[&](ho_subst& s) {
IF_VERBOSE(1, s.display(verbose_stream() << "on-match\n") << "\n");
auto& b = *m_ho_binding;
auto* hoq = b.m_q;
auto* q = m_matcher.hoq2q(hoq);
// shrink binding
expr_ref_vector binding(m);
for (unsigned i = 0; i < s.size(); ++i)
binding.push_back(s.get(i));
if (binding.size() > q->get_num_decls()) {
bool change = true;
while (change) {
change = false;
for (unsigned i = 1; i < binding.size();) {
var_subst sub(m);
auto r = sub(binding.get(i), binding);
change |= r != binding.get(i);
m_rewriter(r);
binding[i] = r;
}
}
binding.reverse();
binding.shrink(q->get_num_decls());
binding.reverse();
}
else {
for (unsigned i = 0; i < binding.size();) {
expr_ref r(binding.get(i), m);
m_rewriter(r);
binding[i] = r;
}
}
IF_VERBOSE(1, verbose_stream() << binding << "\n");
apply_binding(b, q, binding);
};
m_matcher.set_on_match(on_match);
}
bool completion::should_stop() {
return
!m.inc() ||
m_egraph.inconsistent() ||
m_fmls.inconsistent() ||
resource_limits_exceeded();
}
void completion::updt_params(params_ref const& p) {
smt_params_helper sp(p);
m_max_instantiations = sp.qi_max_instances();
// m_max_generation = sp.qi_max_generation();
}
struct completion::push_watch_rule : public trail {
vector<ptr_vector<conditional_rule>>& m_rules;
unsigned idx;
push_watch_rule(vector<ptr_vector<conditional_rule>>& r, unsigned i) : m_rules(r), idx(i) {}
void undo() override {
m_rules[idx].pop_back();
}
};
struct completion::scoped_generation {
completion& c;
unsigned m_generation = 0;
scoped_generation(completion& c, unsigned g): c(c) {
m_generation = c.m_generation;
c.m_generation = g;
}
~scoped_generation() {
c.m_generation = m_generation;
}
};
void completion::push() {
if (m_side_condition_solver)
m_side_condition_solver->push();
m_egraph.push();
dependent_expr_simplifier::push();
}
void completion::pop(unsigned n) {
clear_propagation_queue();
dependent_expr_simplifier::pop(n);
m_egraph.pop(n);
if (m_side_condition_solver)
m_side_condition_solver->pop(n);
}
void completion::clear_propagation_queue() {
for (auto r : m_propagation_queue)
r->m_in_queue = false;
m_propagation_queue.reset();
}
void completion::watch_rule(enode* root, enode* other) {
auto oid = other->get_id();
if (oid >= m_rule_watch.size())
return;
if (m_rule_watch[oid].empty())
return;
auto is_true_or_false = m.is_true(root->get_expr()) || m.is_false(root->get_expr());
if (is_true_or_false) {
for (auto r : m_rule_watch[oid])
if (!r->m_in_queue)
r->m_in_queue = true,
m_propagation_queue.push_back(r);
}
else {
// root is not true or false, use root to watch rules
auto rid = root->get_id();
m_rule_watch.reserve(rid + 1);
for (auto r : m_rule_watch[oid]) {
m_rule_watch[rid].push_back(r);
get_trail().push(push_watch_rule(m_rule_watch, rid));
}
}
}
void completion::reduce() {
m_has_new_eq = true;
for (unsigned rounds = 0; m_has_new_eq && rounds <= 3 && !should_stop(); ++rounds) {
++m_epoch;
m_has_new_eq = false;
add_egraph();
map_canonical();
read_egraph();
IF_VERBOSE(1, verbose_stream() << "(euf.completion :rounds " << rounds << " :instances " << m_stats.m_num_instances << " :stop " << should_stop() << ")\n");
}
map_congruences();
for (auto c : m_consequences)
add_consequence(c);
TRACE(euf_completion, m_egraph.display(tout));
}
void completion::map_congruences() {
unsigned sz = qtail();
for (unsigned i = qhead(); i < sz; ++i) {
auto [f, p, d] = m_fmls[i]();
if (is_congruences(f))
map_congruence(to_app(f)->get_arg(0));
}
}
void completion::map_congruence(expr* t) {
auto n = m_egraph.find(t);
if (!n)
return;
expr_ref_vector args(m);
expr_mark visited;
proof_ref pr(m);
expr_dependency_ref dep(m);
auto canon = get_canonical(n->get_expr(), pr, dep);
args.push_back(canon);
visited.mark(canon);
for (auto s : enode_class(n)) {
expr_ref r(s->get_expr(), m);
m_rewriter(r);
if (visited.is_marked(r))
continue;
visited.mark(r);
args.push_back(r);
}
expr_ref cong(m);
cong = m.mk_app(symbol("congruence"), args.size(), args.data(), m.mk_bool_sort());
m_fmls.add(dependent_expr(m, cong, nullptr, nullptr));
}
void completion::add_consequence(expr* f) {
expr_ref r(f, m);
m_rewriter(r);
f = r.get();
auto cons = m.mk_app(symbol("consequence"), 1, &f, m.mk_bool_sort());
m_fmls.add(dependent_expr(m, cons, nullptr, nullptr));
}
void completion::add_egraph() {
m_nodes_to_canonize.reset();
unsigned sz = qtail();
for (unsigned i = qhead(); i < sz; ++i) {
auto [f, p, d] = m_fmls[i]();
add_constraint(f, p, d);
}
m_should_propagate = true;
while (m_should_propagate && !should_stop()) {
m_should_propagate = false;
m_egraph.propagate();
m_mam->propagate();
flush_binding_queue();
propagate_rules();
propagate_closures();
IF_VERBOSE(11, verbose_stream() << "propagate " << m_stats.m_num_instances << "\n");
if (!should_stop())
propagate_arithmetic();
if (!m_should_propagate && !should_stop())
propagate_all_rules();
}
TRACE(euf, m_egraph.display(tout));
}
unsigned completion::push_pr_dep(proof* pr, expr_dependency* d) {
unsigned sz = m_pr_dep.size();
SASSERT(!m.proofs_enabled() || pr);
m_pr_dep.push_back({ proof_ref(pr, m), d });
get_trail().push(push_back_vector(m_pr_dep));
return sz;
}
void completion::add_constraint(expr* f, proof* pr, expr_dependency* d) {
if (m_egraph.inconsistent())
return;
TRACE(euf_completion, tout << mk_pp(f, m) << "\n");
auto add_children = [&](enode* n) {
for (auto* ch : enode_args(n))
m_nodes_to_canonize.push_back(ch);
};
expr* x = nullptr, * y = nullptr, * nf = nullptr;
if (m.is_eq(f, x, y)) {
expr_ref y1(y, m);
m_rewriter(y1);
add_quantifiers(x);
add_quantifiers(y1);
enode* a = mk_enode(x);
enode* b = mk_enode(y1);
if (a->get_root() == b->get_root())
return;
expr_ref x1(x, m);
m_rewriter(x1);
// enode* a1 = mk_enode(x1);
// if (a->get_root() != a1->get_root())
// m_egraph.merge(a, a1, nullptr);
TRACE(euf, tout << "merge and propagate\n");
add_children(a);
add_children(b);
m_egraph.merge(a, b, to_ptr(push_pr_dep(pr, d)));
m_egraph.propagate();
m_should_propagate = true;
if (m_side_condition_solver && a->get_root() != b->get_root())
m_side_condition_solver->add_constraint(f, pr, d);
IF_VERBOSE(1, verbose_stream() << "eq: " << a->get_root_id() << " " << b->get_root_id() << " "
<< mk_pp(x, m) << " == " << y1 << "\n");
}
else if (m.is_not(f, nf)) {
expr_ref f1(nf, m);
m_rewriter(f1);
enode* n = mk_enode(f1);
if (m.is_false(n->get_root()->get_expr()))
return;
add_quantifiers(f1);
auto n_false = mk_enode(m.mk_false());
auto j = to_ptr(push_pr_dep(pr, d));
m_egraph.merge(n, n_false, j);
if (nf != f1)
m_egraph.merge(n, mk_enode(nf), nullptr);
m_egraph.propagate();
add_children(n);
m_should_propagate = true;
if (m_side_condition_solver)
m_side_condition_solver->add_constraint(f, pr, d);
IF_VERBOSE(1, verbose_stream() << "not: " << nf << "\n");
}
else if (is_congruences(f)) {
auto t = to_app(f)->get_arg(0);
expr_ref r(t, m);
m_rewriter(r);
auto a = mk_enode(t);
auto b = mk_enode(r);
m_egraph.merge(a, b, nullptr);
m_egraph.propagate();
}
else {
expr_ref f1(f, m);
if (!m.is_implies(f) && !is_quantifier(f)) {
m_rewriter(f1);
f = f1;
}
enode* n = mk_enode(f);
if (m.is_true(n->get_root()->get_expr()))
return;
IF_VERBOSE(1, verbose_stream() << "fml: " << mk_pp(f, m) << "\n");
m_egraph.merge(n, m_tt, to_ptr(push_pr_dep(pr, d)));
m_egraph.propagate();
add_children(n);
if (is_forall(f)) {
quantifier* q = to_quantifier(f);
#if 0
if (q->get_num_patterns() == 0) {
expr_ref tmp(m);
m_infer_patterns(q, tmp);
m_egraph.mk(tmp, 0, 0, nullptr); // ensure tmp is pinned within this scope.
q = to_quantifier(tmp);
}
#endif
for (unsigned i = 0; i < q->get_num_patterns(); ++i) {
auto p = to_app(q->get_pattern(i));
auto [q1, p1] = m_matcher.compile_ho_pattern(q, p);
ptr_vector<app> ground;
mam::ground_subterms(p, ground);
if (p1 != p)
mam::ground_subterms(p1, ground);
for (expr* g : ground)
mk_enode(g);
m_mam->add_pattern(q, p);
if (p != p1)
m_mam->add_pattern(q1, p1);
}
m_q2dep.insert(q, { pr, d });
get_trail().push(insert_obj_map(m_q2dep, q));
}
add_rule(f, pr, d);
if (!is_forall(f) && !m.is_implies(f) && !m.is_or(f)) {
add_quantifiers(f);
if (m_side_condition_solver)
m_side_condition_solver->add_constraint(f, pr, d);
}
}
}
void completion::add_quantifiers(expr* f) {
if (!has_quantifiers(f))
return;
ptr_vector<expr> bound;
add_quantifiers(bound, f);
}
void completion::add_quantifiers(ptr_vector<expr>& bound, expr* f) {
if (!has_quantifiers(f))
return;
ptr_vector<expr> todo;
todo.push_back(f);
expr_fast_mark1 visited;
for (unsigned j = 0; j < todo.size(); ++j) {
expr* t = todo[j];
if (visited.is_marked(t))
continue;
visited.mark(t);
if (!has_quantifiers(t))
continue;
if (is_app(t)) {
for (auto arg : *to_app(t))
todo.push_back(arg);
}
else if (is_quantifier(t)) {
auto q = to_quantifier(t);
auto nd = q->get_num_decls();
IF_VERBOSE(1, verbose_stream() << "bind " << mk_pp(q, m) << "\n");
for (unsigned i = 0; i < nd; ++i) {
auto name = std::string("bound!") + std::to_string(bound.size());
auto b = m.mk_const(name, q->get_decl_sort(i));
if (b->get_ref_count() == 0) {
m_expr_trail.push_back(b);
get_trail().push(push_back_vector(m_expr_trail));
}
bound.push_back(b);
}
expr_ref inst = var_subst(m)(q->get_expr(), bound);
if (!m_egraph.find(inst)) {
expr_ref clos(m);
m_closures.insert(q, { bound, inst });
get_trail().push(insert_map(m_closures, q));
// ensure that inst occurs in a foreign context to enable equality propagation
// on inst.
func_decl* f = m.mk_func_decl(symbol("clos!"), inst->get_sort(), m.mk_bool_sort());
clos = m.mk_app(f, inst);
mk_enode(clos);
// TODO: handle nested quantifiers after m_closures is updated to
// index on sort declaration prefix together with quantifier
// add_quantifiers(bound, inst);
}
bound.shrink(bound.size() - nd);
}
}
}
lbool completion::eval_cond(expr* f, proof_ref& pr, expr_dependency*& d) {
auto n = mk_enode(f);
if (m.is_true(n->get_root()->get_expr())) {
d = m.mk_join(d, explain_eq(n, n->get_root()));
if (m.proofs_enabled())
pr = prove_eq(n, n->get_root());
return l_true;
}
if (m.is_false(n->get_root()->get_expr()))
return l_false;
expr* g = nullptr;
if (m.is_not(f, g)) {
n = mk_enode(g);
if (m.is_false(n->get_root()->get_expr())) {
d = m.mk_join(d, explain_eq(n, n->get_root()));
if (m.proofs_enabled())
pr = prove_eq(n, n->get_root());
return l_true;
}
if (m.is_true(n->get_root()->get_expr()))
return l_false;
}
if (m_side_condition_solver && m_propagate_with_solver) {
expr_dependency* sd = nullptr;
if (m_side_condition_solver->is_true(f, pr, sd)) {
add_constraint(f, pr, sd);
d = m.mk_join(d, sd);
return l_true;
}
}
return l_undef;
}
void completion::add_rule(expr* f, proof* pr, expr_dependency* d) {
expr* x = nullptr, * y = nullptr;
expr_ref_vector body(m);
proof_ref pr_i(m), pr0(m);
expr_ref_vector prs(m);
expr_ref head(m);
if (m.is_implies(f, x, y)) {
head = y;
body.push_back(x);
}
else if (m.is_or(f)) {
for (auto arg : *to_app(f)) {
if (m.is_eq(arg)) {
if (head)
return;
head = arg;
}
else
body.push_back(arg);
}
if (!head)
return;
}
else
return;
flatten_and(body);
unsigned j = 0;
flet<bool> _propagate_with_solver(m_propagate_with_solver, true);
for (auto f : body) {
switch (eval_cond(f, pr_i, d)) {
case l_true:
if (m.proofs_enabled())
prs.push_back(pr_i);
break;
case l_false:
return;
case l_undef:
body[j++] = f;
break;
}
}
body.shrink(j);
if (m.proofs_enabled()) {
prs.push_back(pr);
if (body.empty()) {
prs.push_back(head);
pr0 = m.mk_app(symbol("rup"), prs.size(), prs.data(), m.mk_proof_sort());
}
}
if (body.empty())
add_constraint(head, pr0, d);
else {
euf::enode_vector _body;
for (auto* f : body)
_body.push_back(m_egraph.find(f)->get_root());
auto r = alloc(conditional_rule, _body, head, prs, d);
m_rules.push_back(r);
get_trail().push(new_obj_trail(r));
get_trail().push(push_back_vector(m_rules));
insert_watch(_body[0], r);
}
}
void completion::insert_watch(enode* n, conditional_rule* r) {
n = n->get_root();
if (m.is_not(n->get_expr()))
n = n->get_arg(0)->get_root();
m_rule_watch.reserve(n->get_id() + 1);
m_rule_watch[n->get_id()].push_back(r);
get_trail().push(push_watch_rule(m_rule_watch, n->get_id()));
}
void completion::propagate_all_rules() {
flet<bool> _propagate_with_solver(m_propagate_with_solver, true);
for (auto* r : m_rules)
if (!r->m_in_queue)
r->m_in_queue = true,
m_propagation_queue.push_back(r);
propagate_rules();
}
void completion::propagate_rules() {
for (unsigned i = 0; i < m_propagation_queue.size() && !should_stop(); ++i) {
auto r = m_propagation_queue[i];
r->m_in_queue = false;
propagate_rule(*r);
}
clear_propagation_queue();
}
void completion::propagate_rule(conditional_rule& r) {
if (!r.m_active)
return;
proof_ref pr(m);
for (unsigned i = r.m_watch_index; i < r.m_body.size(); ++i) {
auto* f = r.m_body.get(i);
switch (eval_cond(f->get_expr(), pr, r.m_dep)) {
case l_true:
get_trail().push(value_trail(r.m_watch_index));
get_trail().push(push_back_vector(r.m_proofs));
++r.m_watch_index;
r.m_proofs.push_back(pr);
break;
case l_false:
get_trail().push(value_trail(r.m_active));
r.m_active = false;
return;
default:
insert_watch(f, &r);
return;
}
}
if (r.m_body.empty()) {
if (m.proofs_enabled()) {
get_trail().push(push_back_vector(r.m_proofs));
r.m_proofs.push_back(r.m_head);
pr = m.mk_app(symbol("rup"), r.m_proofs.size(), r.m_proofs.data(), m.mk_proof_sort());
}
add_constraint(r.m_head, pr, r.m_dep);
get_trail().push(value_trail(r.m_active));
r.m_active = false;
}
}
//
// extract shared arithmetic terms T
// extract shared variables V
// add t = rewriter(t) to E-graph
// solve for V by solver producing theta
// add theta to E-graph
// add theta to canonize (?)
//
void completion::propagate_arithmetic() {
ptr_vector<expr> shared_terms, shared_vars;
expr_mark visited;
arith_util a(m);
bool merged = false;
for (auto n : m_egraph.nodes()) {
expr* e = n->get_expr();
if (!is_app(e))
continue;
app* t = to_app(e);
bool is_arith = a.is_arith_expr(t);
for (auto arg : *t) {
bool is_arith_arg = a.is_arith_expr(arg);
if (is_arith_arg == is_arith)
continue;
if (visited.is_marked(arg))
continue;
visited.mark(arg);
if (is_arith_arg)
shared_terms.push_back(arg);
else
shared_vars.push_back(arg);
}
}
for (auto t : shared_terms) {
auto tn = m_egraph.find(t);
if (!tn)
continue;
expr_ref r(t, m);
m_rewriter(r);
if (r == t)
continue;
auto n = m_egraph.find(t);
auto t_root = tn->get_root();
if (n && n->get_root() == t_root)
continue;
if (!n)
n = mk_enode(r);
TRACE(euf_completion, tout << "propagate-arith: " << mk_pp(t, m) << " -> " << r << "\n");
m_egraph.merge(tn, n, nullptr);
merged = true;
}
visited.reset();
for (auto v : shared_vars) {
if (visited.is_marked(v))
continue;
visited.mark(v);
vector<side_condition_solver::solution> sol;
expr_ref term(m), guard(m);
sol.push_back({ v, term, guard });
m_side_condition_solver->solve_for(sol);
for (auto [v, t, g] : sol) {
if (!t)
continue;
visited.mark(v);
auto a = mk_enode(v);
auto b = mk_enode(t);
if (a->get_root() == b->get_root())
continue;
TRACE(euf_completion, tout << "propagate-arith: " << m_egraph.bpp(a) << " -> " << m_egraph.bpp(b) << "\n");
IF_VERBOSE(1, verbose_stream() << "propagate-arith: " << m_egraph.bpp(a) << " -> " << m_egraph.bpp(b) << "\n");
m_egraph.merge(a, b, nullptr); // TODO guard justifies reason.
merged = true;
}
}
if (merged) {
m_egraph.propagate();
m_should_propagate = true;
}
}
void completion::propagate_closures() {
for (auto [q, clos] : m_closures) {
expr* body = clos.second;
auto n = m_egraph.find(body);
SASSERT(n);
#if 0
verbose_stream() << "class of " << mk_pp(body, m) << "\n";
for (auto s : euf::enode_class(n)) {
verbose_stream() << mk_pp(s->get_expr(), m) << "\n";
}
#endif
if (n->is_root())
continue;
auto qn = m_egraph.find(q);
#if 0
verbose_stream() << "class of " << mk_pp(q, m) << "\n";
for (auto s : euf::enode_class(qn)) {
verbose_stream() << mk_pp(s->get_expr(), m) << "\n";
}
#endif
expr_ref new_body = expr_ref(n->get_root()->get_expr(), m);
expr_ref new_q = expr_abstract(m, clos.first, new_body);
new_q = m.update_quantifier(q, new_q);
auto new_qn = m_egraph.find(new_q);
if (!new_qn)
new_qn = m_egraph.mk(new_q, qn->generation(), 0, nullptr);
if (new_qn->get_root() == qn->get_root())
continue;
m_egraph.merge(new_qn, qn, nullptr); // todo track dependencies
m_should_propagate = true;
}
}
binding* completion::tmp_binding(quantifier* q, app* pat, euf::enode* const* _binding) {
if (q->get_num_decls() > m_tmp_binding_capacity) {
void* mem = memory::allocate(sizeof(binding) + q->get_num_decls() * sizeof(euf::enode*));
m_tmp_binding = new (mem) binding(q, pat, 0, 0, 0);
m_tmp_binding_capacity = q->get_num_decls();
}
for (unsigned i = q->get_num_decls(); i-- > 0; )
m_tmp_binding->m_nodes[i] = _binding[i];
m_tmp_binding->m_pattern = pat;
m_tmp_binding->m_q = q;
return m_tmp_binding.get();
}
binding* completion::alloc_binding(quantifier* q, app* pat, euf::enode* const* _binding, unsigned max_generation, unsigned min_top, unsigned max_top) {
binding* b = tmp_binding(q, pat, _binding);
if (m_bindings.contains(b))
return nullptr;
for (unsigned i = q->get_num_decls(); i-- > 0; )
b->m_nodes[i] = b->m_nodes[i]->get_root();
if (m_bindings.contains(b))
return nullptr;
unsigned n = q->get_num_decls();
unsigned sz = sizeof(binding) + sizeof(euf::enode* const*) * n;
void* mem = get_region().allocate(sz);
b = new (mem) binding(q, pat, max_generation, min_top, max_top);
b->init(b);
for (unsigned i = 0; i < n; ++i)
b->m_nodes[i] = _binding[i]->get_root();
m_bindings.insert(b);
get_trail().push(insert_map<bindings, binding*>(m_bindings, b));
return b;
}
// callback when mam finds a binding
void completion::on_binding(quantifier* q, app* pat, enode* const* binding, unsigned max_global, unsigned min_top, unsigned max_top) {
if (should_stop())
return;
if (max_top >= m_max_generation)
return;
auto* b = alloc_binding(q, pat, binding, max_global, min_top, max_top);
if (!b)
return;
insert_binding(b);
}
void completion::insert_binding(binding* b) {
m_queue.reserve(b->m_max_top_generation + 1);
m_queue[b->m_max_top_generation].push_back(b);
}
void completion::flush_binding_queue() {
TRACE(euf_completion,
tout << "flush-queue\n";
for (unsigned i = 0; i < m_queue.size(); ++i)
tout << i << ": " << m_queue[i].size() << "\n";);
IF_VERBOSE(10,
verbose_stream() << "flush-queue\n";
for (unsigned i = 0; i < m_queue.size(); ++i)
verbose_stream() << i << ": " << m_queue[i].size() << "\n");
for (auto& g : m_queue) {
for (auto b : g)
apply_binding(*b);
g.reset();
}
}
void completion::apply_binding(binding& b) {
if (should_stop())
return;
expr_ref_vector _binding(m);
quantifier* q = b.m_q;
for (unsigned i = 0; i < q->get_num_decls(); ++i)
_binding.push_back(b.m_nodes[i]->get_expr());
if (m_matcher.is_ho_pattern(b.m_pattern)) {
flet<binding*> set_binding(m_ho_binding, &b);
m_matcher.refine_ho_match(b.m_pattern, _binding);
}
else
apply_binding(b, q, _binding);
}
void completion::apply_binding(binding& b, quantifier* q, expr_ref_vector const& s) {
var_subst subst(m);
expr_ref r = subst(q->get_expr(), s);
scoped_generation sg(*this, b.m_max_top_generation + 1);
auto [pr, d] = get_dependency(q);
if (pr)
pr = m.mk_quant_inst(m.mk_or(m.mk_not(q), r), s.size(), s.data());
m_consequences.push_back(r);
TRACE(euf_completion, tout << "new instantiation: " << r << " q: " << mk_pp(q, m) << "\n");
add_constraint(r, pr, d);
propagate_rules();
m_egraph.propagate();
}
void completion::read_egraph() {
if (m_egraph.inconsistent()) {
auto* d = explain_conflict();
proof_ref pr(m);
if (m.proofs_enabled())
pr = prove_conflict();
dependent_expr de(m, m.mk_false(), pr.get(), d);
m_fmls.update(0, de);
return;
}
unsigned sz = qtail();
for (unsigned i = qhead(); i < sz; ++i) {
auto [f, p, d] = m_fmls[i]();
expr_dependency_ref dep(d, m);
proof_ref pr(p, m);
expr_ref g = canonize_fml(f, pr, dep);
if (g != f) {
m_fmls.update(i, dependent_expr(m, g, pr, dep));
m_stats.m_num_rewrites++;
IF_VERBOSE(2, verbose_stream() << mk_bounded_pp(f, m, 3) << " -> " << mk_bounded_pp(g, m, 3) << "\n");
update_has_new_eq(g);
}
CTRACE(euf_completion, g != f, tout << mk_bounded_pp(f, m) << " -> " << mk_bounded_pp(g, m) << "\n");
}
}
bool completion::is_new_eq(expr* a, expr* b) {
enode* na = m_egraph.find(a);
enode* nb = m_egraph.find(b);
if (!na)
IF_VERBOSE(11, verbose_stream() << "not internalied " << mk_bounded_pp(a, m) << "\n");
if (!nb)
IF_VERBOSE(11, verbose_stream() << "not internalied " << mk_bounded_pp(b, m) << "\n");
if (na && nb && na->get_root() != nb->get_root())
IF_VERBOSE(11, verbose_stream() << m_egraph.bpp(na) << " " << m_egraph.bpp(nb) << "\n");
return !na || !nb || na->get_root() != nb->get_root();
}
void completion::update_has_new_eq(expr* g) {
expr* x, * y;
if (m_has_new_eq)
return;
else if (m.is_eq(g, x, y))
m_has_new_eq |= is_new_eq(x, y);
else if (m.is_and(g)) {
for (expr* arg : *to_app(g))
update_has_new_eq(arg);
}
else if (m.is_not(g, g))
m_has_new_eq |= is_new_eq(g, m.mk_false());
else
m_has_new_eq |= is_new_eq(g, m.mk_true());
}
enode* completion::mk_enode(expr* e) {
m_todo.push_back(e);
enode* n;
while (!m_todo.empty()) {
e = m_todo.back();
if (m_egraph.find(e)) {
m_todo.pop_back();
continue;
}
if (!is_app(e)) {
m_nodes_to_canonize.push_back(m_egraph.mk(e, m_generation, 0, nullptr));
m_todo.pop_back();
continue;
}
m_args.reset();
unsigned sz = m_todo.size();
for (expr* arg : *to_app(e)) {
n = m_egraph.find(arg);
if (n)
m_args.push_back(n);
else
m_todo.push_back(arg);
}
if (sz == m_todo.size()) {
n = m_egraph.mk(e, m_generation, m_args.size(), m_args.data());
if (m_egraph.get_plugin(e->get_sort()->get_family_id()))
m_egraph.add_th_var(n, m_th_var++, e->get_sort()->get_family_id());
if (!m.is_eq(e)) {
for (auto ch : m_args)
for (auto idv : euf::enode_th_vars(*ch))
m_egraph.register_shared(n, idv.get_id());
}
m_nodes_to_canonize.push_back(n);
m_todo.pop_back();
}
}
return m_egraph.find(e);
}
expr_ref completion::canonize_fml(expr* f, proof_ref& pr, expr_dependency_ref& d) {
auto is_nullary = [&](expr* e) {
return is_app(e) && to_app(e)->get_num_args() == 0;
};
expr* x, * y;
proof_ref pr1(m), pr2(m), pr3(m);
if (m.is_eq(f, x, y)) {
expr_ref x1 = canonize(x, pr1, d);
expr_ref y1 = canonize(y, pr2, d);
if (is_nullary(x)) {
SASSERT(x1 == x);
x1 = get_canonical(x, pr1, d);
}
if (is_nullary(y)) {
SASSERT(y1 == y);
y1 = get_canonical(y, pr2, d);
}
expr_ref r(m);
if (x == y)
r = expr_ref(m.mk_true(), m);
else if (x == x1 && y == y1)
r = m_rewriter.mk_eq(x, y);
else if (is_nullary(x) && is_nullary(y)) {
auto _seq0 = m_rewriter.mk_eq(x, x1);
auto _seq1 = m_rewriter.mk_eq(y, x1);
r = mk_and(_seq0, _seq1);
} else if (x == x1 && is_nullary(x))
r = m_rewriter.mk_eq(y1, x1);
else if (y == y1 && is_nullary(y))
r = m_rewriter.mk_eq(x1, y1);
else if (is_nullary(x)) {
auto _seq0 = m_rewriter.mk_eq(x, x1);
auto _seq1 = m_rewriter.mk_eq(y1, x1);
r = mk_and(_seq0, _seq1);
} else if (is_nullary(y)) {
auto _seq0 = m_rewriter.mk_eq(y, y1);
auto _seq1 = m_rewriter.mk_eq(x1, y1);
r = mk_and(_seq0, _seq1);
}
if (x1 == y1)
r = expr_ref(m.mk_true(), m);
else {
auto c = get_canonical(x, pr3, d);
if (c == x1)
r = m_rewriter.mk_eq(y1, c);
else if (c == y1)
r = m_rewriter.mk_eq(x1, c);
else {
auto _seq1009_0 = m_rewriter.mk_eq(x1, c);
auto _seq1009_1 = m_rewriter.mk_eq(y1, c);
r = mk_and(_seq1009_0, _seq1009_1);
}
}
if (m.proofs_enabled()) {
expr_ref_vector prs(m);
prs.push_back(pr);
if (pr1) prs.push_back(pr1);
if (pr2) prs.push_back(pr2);
if (pr3) prs.push_back(pr3);
prs.push_back(r);
pr = m.mk_app(symbol("euf"), prs.size(), prs.data(), m.mk_proof_sort());
}
return r;
}
if (m.is_not(f, x)) {
expr_ref x1 = canonize(x, pr1, d);
expr_ref r(mk_not(m, x1), m);
if (m.proofs_enabled()) {
expr* prs[3] = { pr, pr1, r };
pr = m.mk_app(symbol("euf"), 3, prs, m.mk_proof_sort());
}
return r;
}
return canonize(f, pr, d);
}
expr_ref completion::mk_and(expr* a, expr* b) {
if (m.is_true(a))
return expr_ref(b, m);
if (m.is_true(b))
return expr_ref(a, m);
return expr_ref(m.mk_and(a, b), m);
}
expr_ref completion::canonize(expr* f, proof_ref& pr, expr_dependency_ref& d) {
if (!is_app(f))
return expr_ref(f, m); // todo could normalize ground expressions under quantifiers
m_eargs.reset();
bool change = false;
expr_ref_vector prs(m);
for (expr* arg : *to_app(f)) {
proof_ref pr1(m);
m_eargs.push_back(get_canonical(arg, pr1, d));
change |= arg != m_eargs.back();
if (arg != m_eargs.back() && pr1)
prs.push_back(pr1);
}
expr_ref r(m);
if (m.is_eq(f))
r = m_rewriter.mk_eq(m_eargs.get(0), m_eargs.get(1));
else if (!change)
return expr_ref(f, m);
else
r = expr_ref(m_rewriter.mk_app(to_app(f)->get_decl(), m_eargs.size(), m_eargs.data()), m);
if (m.proofs_enabled()) {
prs.push_back(r);
pr = m.mk_app(symbol("euf"), prs.size(), prs.data(), m.mk_proof_sort());
}
return r;
}
expr_ref completion::get_canonical(quantifier* q, proof_ref& pr, expr_dependency_ref& d) {
std::pair<ptr_vector<expr>, expr*> clos;
// verbose_stream() << "canonize " << mk_pp(q, m) << "\n";
if (!m_closures.find(q, clos))
return expr_ref(q, m);
expr* body = clos.second;
SASSERT(m_egraph.find(body));
#if 0
verbose_stream() << "class of " << mk_pp(body, m) << "\n";
for (auto s : euf::enode_class(n)) {
verbose_stream() << mk_pp(s->get_expr(), m) << "\n";
}
#endif
// auto n = m_egraph.find(q);
#if 0
verbose_stream() << "class of " << mk_pp(q, m) << "\n";
for (auto s : euf::enode_class(n)) {
verbose_stream() << mk_pp(s->get_expr(), m) << "\n";
}
#endif
expr_ref new_body = get_canonical(body, pr, d);
expr_ref result = expr_abstract(m, clos.first, new_body);
result = m.update_quantifier(q, result);
if (m.proofs_enabled()) {
// add proof rule
//
// body = new_body
// ---------------------------
// Q x . body = Q x . new_body
NOT_IMPLEMENTED_YET();
}
return result;
}
expr_ref completion::get_canonical(expr* f, proof_ref& pr, expr_dependency_ref& d) {
expr_ref e(m);
if (has_quantifiers(f)) {
if (is_quantifier(f))
return get_canonical(to_quantifier(f), pr, d);
else if (is_app(f)) {
expr_ref_vector args(m);
for (auto arg : *to_app(f)) {
// TODO: pr reconstruction
args.push_back(get_canonical(arg, pr, d));
}
e = m.mk_app(to_app(f)->get_decl(), args);
if (!m_egraph.find(e))
return e;
f = e;
}
else
UNREACHABLE();
}
enode* n = m_egraph.find(f);
if (!n) n = mk_enode(f);
enode* r = n->get_root();
d = m.mk_join(d, explain_eq(n, r));
d = m.mk_join(d, m_deps.get(r->get_id(), nullptr));
if (m.proofs_enabled()) {
pr = prove_eq(n, r);
if (get_canonical_proof(r))
pr = m.mk_transitivity(pr, get_canonical_proof(r));
}
if (!m_canonical.get(r->get_id()))
m_canonical.setx(r->get_id(), r->get_expr());
return expr_ref(m_canonical.get(r->get_id()), m);
}
expr* completion::get_canonical(enode* n) {
if (m_epochs.get(n->get_id(), 0) == m_epoch)
return m_canonical.get(n->get_id());
else
return nullptr;
}
proof* completion::get_canonical_proof(enode* n) {
if (m_epochs.get(n->get_id(), 0) == m_epoch && n->get_id() < m_canonical_proofs.size())
return m_canonical_proofs.get(n->get_id());
else
return nullptr;
}
void completion::set_canonical(enode* n, expr* e, proof* pr) {
class vtrail : public trail {
expr_ref_vector& c;
unsigned idx;
expr_ref old_value;
public:
vtrail(expr_ref_vector& c, unsigned idx) :
c(c), idx(idx), old_value(c.get(idx), c.m()) {
}
void undo() override {
c[idx] = old_value;
old_value = nullptr;
}
};
SASSERT(e);
if (num_scopes() > 0 && m_canonical.size() > n->get_id())
m_trail.push(vtrail(m_canonical, n->get_id()));
m_canonical.setx(n->get_id(), e);
if (pr)
m_canonical_proofs.setx(n->get_id(), pr);
m_epochs.setx(n->get_id(), m_epoch, 0);
}
expr_dependency* completion::explain_eq(enode* a, enode* b) {
if (a == b)
return nullptr;
ptr_vector<size_t> just;
m_egraph.begin_explain();
m_egraph.explain_eq(just, nullptr, a, b);
m_egraph.end_explain();
expr_dependency* d = nullptr;
for (size_t* j : just)
d = m.mk_join(d, m_pr_dep[from_ptr(j)].second);
return d;
}
expr_dependency* completion::explain_conflict() {
ptr_vector<size_t> just;
m_egraph.begin_explain();
m_egraph.explain(just, nullptr);
m_egraph.end_explain();
expr_dependency* d = nullptr;
for (size_t* j : just)
d = m.mk_join(d, m_pr_dep[from_ptr(j)].second);
return d;
}
proof_ref completion::prove_eq(enode* a, enode* b) {
expr_ref_vector prs(m);
proof_ref pr(m);
ptr_vector<size_t> just;
m_egraph.begin_explain();
m_egraph.explain_eq(just, nullptr, a, b);
m_egraph.end_explain();
for (size_t* j : just)
prs.push_back(m_pr_dep[from_ptr(j)].first);
prs.push_back(m.mk_eq(a->get_expr(), b->get_expr()));
pr = m.mk_app(symbol("euf"), prs.size(), prs.data(), m.mk_proof_sort());
return pr;
}
proof_ref completion::prove_conflict() {
expr_ref_vector prs(m);
proof_ref pr(m);
ptr_vector<size_t> just;
m_egraph.begin_explain();
m_egraph.explain(just, nullptr);
m_egraph.end_explain();
for (size_t* j : just)
prs.push_back(m_pr_dep[from_ptr(j)].first);
prs.push_back(m.mk_false());
pr = m.mk_app(symbol("euf"), prs.size(), prs.data(), m.mk_proof_sort());
return pr;
}
void completion::collect_statistics(statistics& st) const {
st.update("euf-completion-rewrites", m_stats.m_num_rewrites);
st.update("euf-completion-instances", m_stats.m_num_instances);
m_egraph.collect_statistics(st);
}
bool completion::is_gt(expr* lhs, expr* rhs) const {
if (lhs == rhs)
return false;
// values are always less in ordering than non-values.
bool v1 = m.is_value(lhs);
bool v2 = m.is_value(rhs);
if (!v1 && v2)
return true;
if (v1 && !v2)
return false;
if (get_depth(lhs) > get_depth(rhs))
return true;
if (get_depth(lhs) < get_depth(rhs))
return false;
// slow path
auto n1 = get_num_exprs(lhs);
auto n2 = get_num_exprs(rhs);
if (n1 > n2)
return true;
if (n1 < n2)
return false;
if (is_app(lhs) && is_app(rhs)) {
app* l = to_app(lhs);
app* r = to_app(rhs);
if (l->get_decl()->get_id() != r->get_decl()->get_id())
return l->get_decl()->get_id() > r->get_decl()->get_id();
if (l->get_num_args() != r->get_num_args())
return l->get_num_args() > r->get_num_args();
for (unsigned i = 0; i < l->get_num_args(); ++i)
if (l->get_arg(i) != r->get_arg(i))
return is_gt(l->get_arg(i), r->get_arg(i));
UNREACHABLE();
}
if (is_quantifier(lhs) && is_quantifier(rhs)) {
expr* l = to_quantifier(lhs)->get_expr();
expr* r = to_quantifier(rhs)->get_expr();
return is_gt(l, r);
}
if (is_quantifier(lhs))
return true;
return false;
}
void completion::map_canonical() {
m_todo.reset();
enode_vector roots;
if (m_nodes_to_canonize.empty())
return;
for (unsigned i = 0; i < m_nodes_to_canonize.size(); ++i) {
enode* n = m_nodes_to_canonize[i]->get_root();
if (n->is_marked1())
continue;
n->mark1();
roots.push_back(n);
enode* rep = nullptr;
for (enode* k : enode_class(n))
if (!rep || m.is_value(k->get_expr()) || is_gt(rep->get_expr(), k->get_expr()))
rep = k;
// IF_VERBOSE(0, verbose_stream() << m_egraph.bpp(n) << " ->\n" << m_egraph.bpp(rep) << "\n";);
m_reps.setx(n->get_id(), rep, nullptr);
TRACE(euf_completion, tout << "rep " << m_egraph.bpp(n) << " -> " << m_egraph.bpp(rep) << "\n";
for (enode* k : enode_class(n)) tout << m_egraph.bpp(k) << "\n";);
m_todo.push_back(n->get_expr());
for (enode* arg : enode_args(n)) {
arg = arg->get_root();
if (!arg->is_marked1())
m_nodes_to_canonize.push_back(arg);
}
}
for (enode* r : roots)
r->unmark1();
// explain dependencies when no nodes are marked.
// explain_eq uses both mark1 and mark2 on e-nodes so
// we cannot call it inside the previous loop where mark1 is used
// to track which roots have been processed.
for (enode* r : roots) {
enode* rep = m_reps[r->get_id()];
auto* d = explain_eq(r, rep);
m_deps.setx(r->get_id(), d);
}
expr_ref new_expr(m);
expr_ref_vector prs(m);
while (!m_todo.empty()) {
expr* e = m_todo.back();
enode* n = m_egraph.find(e);
SASSERT(n->is_root());
enode* rep = m_reps[n->get_id()];
if (get_canonical(n))
m_todo.pop_back();
else if (get_depth(rep->get_expr()) == 0 || !is_app(rep->get_expr())) {
set_canonical(n, rep->get_expr(), nullptr);
m_todo.pop_back();
}
else {
m_eargs.reset();
unsigned sz = m_todo.size();
bool new_arg = false;
expr_dependency* d = m_deps.get(n->get_id(), nullptr);
proof_ref pr(m);
prs.reset();
for (enode* arg : enode_args(rep)) {
auto rarg = arg->get_root();
auto c = get_canonical(rarg);
if (c) {
m_eargs.push_back(c);
new_arg |= c != arg->get_expr();
d = m.mk_join(d, m_deps.get(rarg->get_id(), nullptr));
if (m.proofs_enabled() && c != arg->get_expr() && get_canonical_proof(rarg))
prs.push_back(get_canonical_proof(rarg));
}
else
m_todo.push_back(rarg->get_expr());
}
if (sz == m_todo.size()) {
m_todo.pop_back();
if (new_arg)
new_expr = m_rewriter.mk_app(to_app(rep->get_expr())->get_decl(), m_eargs.size(), m_eargs.data());
else
new_expr = rep->get_expr();
if (m.proofs_enabled() && new_arg) {
prs.push_back(m.mk_eq(n->get_expr(), new_expr));
pr = m.mk_app(symbol("euf"), prs.size(), prs.data(), m.mk_proof_sort());
}
set_canonical(n, new_expr, pr);
m_deps.setx(n->get_id(), d);
}
}
}
}
}