/*++
Copyright (c) 2020 Microsoft Corporation

Module Name:

    dt_solver.h

Abstract:

    Theory plugin for algebraic datatypes

Author:

    Nikolaj Bjorner (nbjorner) 2020-09-08

--*/

#include "sat/smt/dt_solver.h"
#include "sat/smt/euf_solver.h"
#include "sat/smt/array_solver.h"

namespace euf {
    class solver;
}

namespace dt {

    solver::solver(euf::solver& ctx, theory_id id) :
        th_euf_solver(ctx, ctx.get_manager().get_family_name(id), id),
        dt(m),
        m_autil(m),
        m_sutil(m),
        m_find(*this),
        m_args(m)
    {}

    solver::~solver() {
        std::for_each(m_var_data.begin(), m_var_data.end(), delete_proc<var_data>());
        m_var_data.reset();
    }

    void solver::clone_var(solver& src, theory_var v) {
        enode* n = src.ctx.copy(ctx, src.var2enode(v));
        VERIFY(v == th_euf_solver::mk_var(n));
        m_var_data.push_back(alloc(var_data));
        var_data* d_dst = m_var_data[v];
        var_data* d_src = src.m_var_data[v];
        ctx.attach_th_var(n, this, v);
        if (d_src->m_constructor && !d_dst->m_constructor)
            d_dst->m_constructor = src.ctx.copy(ctx, d_src->m_constructor);
        for (auto* r : d_src->m_recognizers)
            d_dst->m_recognizers.push_back(src.ctx.copy(ctx, r));
    }

    euf::th_solver* solver::clone(euf::solver& dst_ctx) {
        auto* result = alloc(solver, dst_ctx, get_id());
        for (unsigned v = 0; v < get_num_vars(); ++v)
            result->clone_var(*this, v);
        return result;
    }

    solver::final_check_st::final_check_st(solver& s) : s(s) {
        SASSERT(s.m_to_unmark1.empty());
        SASSERT(s.m_to_unmark2.empty());
        s.m_used_eqs.reset();
        s.m_dfs.reset();
        s.m_parent.reset();
    }

    solver::final_check_st::~final_check_st() {
        s.clear_mark();
    }

    void solver::clear_mark() {
        for (enode* n : m_to_unmark1)
            n->unmark1();
        for (enode* n : m_to_unmark2)
            n->unmark2();
        m_to_unmark1.reset();
        m_to_unmark2.reset();
    }

    void solver::oc_mark_on_stack(enode* n) {
        n = n->get_root();
        n->mark1();
        m_to_unmark1.push_back(n);
    }

    void solver::oc_mark_cycle_free(enode* n) {
        n = n->get_root();
        n->mark2();
        m_to_unmark2.push_back(n);
    }

    void solver::oc_push_stack(enode* n) {
        m_dfs.push_back(std::make_pair(EXIT, n));
        m_dfs.push_back(std::make_pair(ENTER, n));
    }

    /**
       \brief Assert the axiom (antecedent => lhs = rhs)
       antecedent may be null_literal
    */
    void solver::assert_eq_axiom(enode* n1, expr* e2, literal antecedent) {
        expr* e1 = n1->get_expr();
        euf::th_proof_hint* ph = ctx.mk_smt_prop_hint(name(), antecedent, e1, e2);
        if (antecedent == sat::null_literal)
            add_unit(eq_internalize(e1, e2), ph);
        else if (s().value(antecedent) == l_true) {
            euf::enode* n2 = e_internalize(e2);
            ctx.propagate(n1, n2, euf::th_explain::propagate(*this, antecedent, n1, n2, ph));
        }
        else
            add_clause(~antecedent, eq_internalize(e1, e2), ph);
    }

    /**
       \brief Assert the equality (= n (c (acc_1 n) ... (acc_m n))) where
       where acc_i are the accessors of constructor c.
    */
    void solver::assert_is_constructor_axiom(enode* n, func_decl* c, literal antecedent) {
        TRACE(dt, tout << mk_pp(c, m) << " " << ctx.bpp(n) << "\n";);
        m_stats.m_assert_cnstr++;
        expr* e = n->get_expr();
        SASSERT(dt.is_constructor(c));
        SASSERT(is_datatype(e));
        SASSERT(c->get_range() == e->get_sort());
        m_args.reset();
        for (func_decl* d : *dt.get_constructor_accessors(c))
            m_args.push_back(m.mk_app(d, e));
        expr_ref con(m.mk_app(c, m_args), m);
        assert_eq_axiom(n, con, antecedent);
    }

    /**
       \brief Given a constructor n := (c a_1 ... a_m) assert the axioms
       (= (acc_1 n) a_1)
       ...
       (= (acc_m n) a_m)
    */
    void solver::assert_accessor_axioms(enode* n) {        
        SASSERT(is_constructor(n));
        expr* e = n->get_expr();
        func_decl* d = n->get_decl();
        unsigned i = 0;
        for (func_decl* acc : *dt.get_constructor_accessors(d)) {
            m_stats.m_assert_accessor++;
            app_ref acc_app(m.mk_app(acc, e), m);
            assert_eq_axiom(n->get_arg(i), acc_app);
            ++i;
        }
    }

    /**
       \brief Sign a conflict for r := is_mk(a), c := mk(...), not(r),  and c == a.
    */
    void solver::sign_recognizer_conflict(enode* c, enode* r) {
        SASSERT(is_constructor(c));
        SASSERT(is_recognizer(r));
        SASSERT(dt.get_recognizer_constructor(r->get_decl()) == c->get_decl());
        SASSERT(c->get_root() == r->get_arg(0)->get_root());
        TRACE(dt, tout << ctx.bpp(c) << "\n" << ctx.bpp(r) << "\n";);
        literal l = ctx.enode2literal(r);
        SASSERT(s().value(l) == l_false);
        clear_mark();
        auto* ph = ctx.mk_smt_hint(name(), ~l, c, r->get_arg(0));
        ctx.set_conflict(euf::th_explain::conflict(*this, ~l, c, r->get_arg(0), ph));
    }

    /**
       \brief Given a field update n := { r with field := v } for constructor C, assert the axioms:
       (=> (is-C r) (= (acc_j n) (acc_j r))) for acc_j != field
       (=> (is-C r) (= (field n) v))         for acc_j != field
       (=> (not (is-C r)) (= n r))
       (=> (is-C r) (is-C n))
    */
    void solver::assert_update_field_axioms(enode* n) {
        m_stats.m_assert_update_field++;
        SASSERT(is_update_field(n));
        expr* own = n->get_expr();
        expr* arg1 = n->get_arg(0)->get_expr();
        func_decl* upd = n->get_decl();
        func_decl* acc = to_func_decl(upd->get_parameter(0).get_ast());
        func_decl* con = dt.get_accessor_constructor(acc);
        func_decl* rec = dt.get_constructor_is(con);
        ptr_vector<func_decl> const& accessors = *dt.get_constructor_accessors(con);
        app_ref rec_app(m.mk_app(rec, arg1), m);
        app_ref acc_app(m);
        literal is_con = mk_literal(rec_app);
        for (func_decl* acc1 : accessors) {
            enode* arg;
            if (acc1 == acc) {
                arg = n->get_arg(1);
            }
            else {
                acc_app = m.mk_app(acc1, arg1);
                arg = e_internalize(acc_app);
            }
            app_ref acc_own(m.mk_app(acc1, own), m);
            assert_eq_axiom(arg, acc_own, is_con);
        }
        // update_field is identity if 'n' is not created by a matching constructor.        
        assert_eq_axiom(n, arg1, ~is_con);
        app_ref n_is_con(m.mk_app(rec, own), m);
        literal _n_is_con = mk_literal(n_is_con);
        auto* ph = ctx.mk_smt_hint(name(), is_con, ~_n_is_con);
        add_clause(~is_con, _n_is_con, ph);
    }

    euf::theory_var solver::mk_var(enode* n) {
        if (is_attached_to_var(n))
            return n->get_th_var(get_id());
        euf::theory_var r = th_euf_solver::mk_var(n);
        VERIFY(r == static_cast<theory_var>(m_find.mk_var()));
        SASSERT(r == static_cast<int>(m_var_data.size()));
        m_var_data.push_back(alloc(var_data));
        var_data* d = m_var_data[r];
        ctx.attach_th_var(n, this, r);
        if (is_constructor(n)) {
            d->m_constructor = n;
            assert_accessor_axioms(n);
        }
        else if (is_update_field(n)) 
            assert_update_field_axioms(n);        
        else if (!is_recognizer(n)) {
            sort* s = n->get_sort();
            if (dt.get_datatype_num_constructors(s) == 1)
                assert_is_constructor_axiom(n, dt.get_datatype_constructors(s)->get(0));
            else if (get_config().m_dt_lazy_splits == 0 || (get_config().m_dt_lazy_splits == 1 && !s->is_infinite()))
                mk_split(r, false);
        }
        return r;
    }


    /**
       \brief Create a new case split for v. That is, create the atom (is_mk v) and mark it as relevant.
       If first is true, it means that v does not have recognizer yet.
    */
    void solver::mk_split(theory_var v, bool is_final) {
        m_stats.m_splits++;
        v = m_find.find(v);
        enode* n = var2enode(v);
        sort* srt = n->get_sort();
        if (dt.is_enum_sort(srt)) {
            mk_enum_split(v);
            return;
        }
                    
        func_decl* non_rec_c = dt.get_non_rec_constructor(srt);
        unsigned non_rec_idx = dt.get_constructor_idx(non_rec_c);        
        var_data* d = m_var_data[v];
        enode* recognizer = d->m_recognizers.get(non_rec_idx, nullptr);
        SASSERT(!d->m_constructor);
        SASSERT(!recognizer || ctx.value(recognizer) == l_false || !is_final);
        
        TRACE(dt, tout << ctx.bpp(n) << " non_rec_c: " << non_rec_c->get_name() << " #rec: " << d->m_recognizers.size() << "\n";);

        if (!recognizer && non_rec_c->get_arity() == 0) {
            sat::literal eq = eq_internalize(n->get_expr(), m.mk_const(non_rec_c));
            s().set_phase(eq);
            if (s().value(eq) == l_false)
                mk_enum_split(v);
        }
        else if (!recognizer) 
            mk_recognizer_constructor_literal(non_rec_c, n);        
        else if (ctx.value(recognizer) == l_false) 
            mk_enum_split(v);
    }

    sat::literal solver::mk_recognizer_constructor_literal(func_decl* c, euf::enode* n) {
        func_decl* r = dt.get_constructor_is(c);
        app_ref r_app(m.mk_app(r, n->get_expr()), m);
        sat::literal lit = mk_literal(r_app);
        s().set_phase(lit);
        return lit;
    }

    void solver::mk_enum_split(theory_var v) {
        enode* n = var2enode(v);
        var_data* d = m_var_data[v];
        sort* srt = n->get_sort();
        auto const& constructors = *dt.get_datatype_constructors(srt);
        unsigned sz = constructors.size();
        int start = s().rand()();
        m_lits.reset();
        sat::literal lit;
        for (unsigned i = 0; i < sz; ++i) {
            unsigned j = (i + start) % sz;
            func_decl* c = constructors[j];
            if (c->get_arity() > 0) {
                enode* curr = d->m_recognizers.get(j, nullptr);
                if (curr && ctx.value(curr) != l_false)
                    return;
                lit = mk_recognizer_constructor_literal(c, n);
                if (!curr)                     
                    return;               
                if (s().value(lit) != l_false)
                    return;
                m_lits.push_back(~lit);
            }
            else {
                lit = eq_internalize(n->get_expr(), m.mk_const(c));
                switch (s().value(lit)) {
                case l_undef:
                    s().set_phase(lit);
                    return;
                case l_true:
                    return;
                case l_false:
                    m_lits.push_back(~lit);
                    break;
                }
            }
        }
        auto* ph = ctx.mk_smt_hint(name(), m_lits);
        ctx.set_conflict(euf::th_explain::conflict(*this, m_lits, ph));
    }

    /**
     * Remark: If s is an infinite sort, then it is not necessary to create
     * a theory variable. 
     * 
     * Actually, when the logical context has quantifiers, it is better to 
     * disable this optimization.
     * Example:
     *
     *   (forall (l list) (a Int) (= (len (cons a l)) (+ (len l) 1)))
     *   (assert (> (len a) 1)
     *   
     * If the theory variable is not created for 'a', then a wrong model will be generated.
     * 
     */
    void solver::apply_sort_cnstr(enode* n, sort* s) {
        TRACE(dt, tout << "apply_sort_cnstr: #" << ctx.bpp(n) << "\n";);
        force_push();
        if (!is_attached_to_var(n))
            mk_var(n);        
    }

    void solver::new_eq_eh(euf::th_eq const& eq) {
        force_push();
        m_find.merge(eq.v1(), eq.v2());
    }

    void solver::asserted(literal lit) {
        force_push();
        enode* n = bool_var2enode(lit.var());
        if (!is_recognizer(n))
            return;
        TRACE(dt, tout << "assigning recognizer: #" << n->get_expr_id() << " " << ctx.bpp(n) << "\n";);
        SASSERT(n->num_args() == 1);
        enode* arg = n->get_arg(0);
        theory_var tv = arg->get_th_var(get_id());
        tv = m_find.find(tv);
        var_data* d = m_var_data[tv];
        func_decl* r = n->get_decl();
        func_decl* c = dt.get_recognizer_constructor(r);
        if (!lit.sign()) {
            SASSERT(tv != euf::null_theory_var);
            if (d->m_constructor && d->m_constructor->get_decl() == c)
                return; // do nothing
            assert_is_constructor_axiom(arg, c, lit);
        }
        else if (d->m_constructor == nullptr)                  // make sure a constructor is attached 
            propagate_recognizer(tv, n);        
        else if (d->m_constructor->get_decl() == c)             // conflict
            sign_recognizer_conflict(d->m_constructor, n);
    }

    void solver::add_recognizer(theory_var v, enode* recognizer) {
        TRACE(dt, tout << "add recognizer " << v << " " << mk_pp(recognizer->get_expr(), m) << "\n";);
        v = m_find.find(v);
        var_data* d = m_var_data[v];
        sort* s = recognizer->get_decl()->get_domain(0);
        SASSERT(is_recognizer(recognizer));
        SASSERT(dt.is_datatype(s));
        if (d->m_recognizers.empty())
            d->m_recognizers.resize(dt.get_datatype_num_constructors(s), nullptr);

        SASSERT(d->m_recognizers.size() == dt.get_datatype_num_constructors(s));
        unsigned c_idx = dt.get_recognizer_constructor_idx(recognizer->get_decl());
        if (d->m_recognizers[c_idx])
            return;

        lbool val = ctx.value(recognizer);
        TRACE(dt, tout << "adding recognizer to v" << v << " rec: #" << recognizer->get_expr_id() << " val: " << val << "\n";);

        // do nothing... 
        // If recognizer assignment was already processed, then
        // d->m_constructor is already set.
        // Otherwise, it will be set when asserted is invoked.
        if (val == l_true)
            return;

        if (val == l_false && d->m_constructor) {
            // conflict
            if (d->m_constructor->get_decl() == dt.get_recognizer_constructor(recognizer->get_decl()))
                sign_recognizer_conflict(d->m_constructor, recognizer);
            return;
        }
        SASSERT(val == l_undef || (val == l_false && !d->m_constructor));
        ctx.push(set_vector_idx_trail(d->m_recognizers, c_idx));
        d->m_recognizers[c_idx] = recognizer;
        if (val == l_false)
            propagate_recognizer(v, recognizer);
    }

    /**
       \brief Propagate a recognizer assigned to false.
    */
    void solver::propagate_recognizer(theory_var v, enode* recognizer) {
        SASSERT(is_recognizer(recognizer));
        SASSERT(static_cast<int>(m_find.find(v)) == v);
        SASSERT(ctx.value(recognizer) == l_false);
        unsigned num_unassigned = 0;
        unsigned unassigned_idx = UINT_MAX;
        enode* n = var2enode(v);
        sort* srt = n->get_sort();
        var_data* d = m_var_data[v];
        if (d->m_recognizers.empty()) {
            add_recognizer(v, recognizer);
            return;
        }

        CTRACE(dt, d->m_recognizers.empty(), ctx.display(tout););
        SASSERT(!d->m_recognizers.empty());
        m_lits.reset();
        enode_pair_vector eqs;
        unsigned idx = 0;
        for (enode* r : d->m_recognizers) {
            if (r && ctx.value(r) == l_true)
                return; // nothing to be propagated
            if (r && ctx.value(r) == l_false) {
                SASSERT(r->num_args() == 1);
                m_lits.push_back(~ctx.enode2literal(r));
                if (n != r->get_arg(0)) {
                    // Argument of the current recognizer is not necessarily equal to n.
                    // This can happen when n and r->get_arg(0) are in the same equivalence class.
                    // We must add equality as an assumption to the conflict or propagation
                    SASSERT(n->get_root() == r->get_arg(0)->get_root());
                    eqs.push_back(euf::enode_pair(n, r->get_arg(0)));
                }
            }
            else {
                if (num_unassigned == 0)
                    unassigned_idx = idx;
                ++num_unassigned;
            }
            ++idx;
        }
        TRACE(dt, tout << "propagate " << num_unassigned << " eqs: " << eqs.size() << "\n";);
        if (num_unassigned == 0) {
            auto* ph = ctx.mk_smt_hint(name(), m_lits, eqs);
            ctx.set_conflict(euf::th_explain::conflict(*this, m_lits, eqs, ph));
        }
        else if (num_unassigned == 1) {
            // propagate remaining recognizer
            SASSERT(!m_lits.empty());
            enode* r = d->m_recognizers[unassigned_idx];
            literal consequent;
            if (r)
                consequent = ctx.enode2literal(r);
            else {
                func_decl* con = (*dt.get_datatype_constructors(srt))[unassigned_idx];
                func_decl* rec = dt.get_constructor_is(con);
                app_ref rec_app(m.mk_app(rec, n->get_expr()), m);
                consequent = mk_literal(rec_app);
            }
            euf::th_proof_hint* ph = nullptr;
            if (ctx.use_drat()) {
                m_lits.push_back(~consequent);
                ph = ctx.mk_smt_hint(name(), m_lits, eqs);
                m_lits.pop_back();
            }
            ctx.propagate(consequent, euf::th_explain::propagate(*this, m_lits, eqs, consequent, ph));
        }
        else if (get_config().m_dt_lazy_splits == 0 || (!srt->is_infinite() && get_config().m_dt_lazy_splits == 1))
            // there are more than 2 unassigned recognizers...
            // if eager splits are enabled... create new case split            
            mk_split(v, false);
    }

    void solver::merge_eh(theory_var v1, theory_var v2, theory_var, theory_var) {
        // v1 is the new root
        SASSERT(v1 == static_cast<int>(m_find.find(v1)));
        var_data* d1 = m_var_data[v1];
        var_data* d2 = m_var_data[v2];
        auto* con1 = d1->m_constructor;
        auto* con2 = d2->m_constructor;
        TRACE(dt, tout << "merging v" << v1 << " v" << v2 << "\n" << ctx.bpp(var2enode(v1)) << " == " << ctx.bpp(var2enode(v2)) << " " << ctx.bpp(con1) << " " << ctx.bpp(con2) << "\n";);
        if (con1 && con2 && con1->get_decl() != con2->get_decl())
            ctx.set_conflict(euf::th_explain::conflict(*this, con1, con2, ctx.mk_smt_hint(name(), con1, con2)));
        else if (con2 && !con1) {
            ctx.push(set_ptr_trail<enode>(d1->m_constructor));
            // check whether there is a recognizer in d1 that conflicts with con2;
            if (!d1->m_recognizers.empty()) {
                unsigned c_idx = dt.get_constructor_idx(con2->get_decl());
                enode* recognizer = d1->m_recognizers[c_idx];
                if (recognizer && ctx.value(recognizer) == l_false) {
                    sign_recognizer_conflict(con2, recognizer);
                    return;
                }
            }
            d1->m_constructor = con2;
        }
        for (enode* e : d2->m_recognizers)
            if (e)
                add_recognizer(v1, e);
    }

    ptr_vector<euf::enode> const& solver::get_array_args(enode* n) {
        m_nodes.reset();
        array::solver* th = dynamic_cast<array::solver*>(ctx.fid2solver(m_autil.get_family_id()));
        for (enode* p : th->parent_selects(n))
            m_nodes.push_back(p);
        app_ref def(m_autil.mk_default(n->get_expr()), m);
        m_nodes.push_back(ctx.get_enode(def));
        return m_nodes;
    }

    ptr_vector<euf::enode> const& solver::get_seq_args(enode* n, enode*& sibling) {
        m_nodes.reset();
        m_todo.reset();
        auto add_todo = [&](enode* n) {
            if (!n->is_marked1()) {
                n->mark1();
                m_todo.push_back(n);
            }
        };

        for (enode* sib : euf::enode_class(n)) {
            if (m_sutil.str.is_concat_of_units(sib->get_expr())) {
                add_todo(sib);
                sibling = sib;
                break;
            }
        }

            
        for (unsigned i = 0; i < m_todo.size(); ++i) {
            enode* n = m_todo[i];
            expr* e = n->get_expr();
            if (m_sutil.str.is_unit(e))
                m_nodes.push_back(n->get_arg(0));
            else if (m_sutil.str.is_concat(e)) 
                for (expr* arg : *to_app(e)) 
                    add_todo(ctx.get_enode(arg));
        }
        for (enode* n : m_todo)
            n->unmark1();

        return m_nodes;        
    }

    // Assuming `app` is equal to a constructor term, return the constructor enode
    inline euf::enode* solver::oc_get_cstor(enode* app) const {
        theory_var v = app->get_root()->get_th_var(get_id());
        if (v == euf::null_theory_var)
            return nullptr;
        v = m_find.find(v);
        var_data* d = m_var_data[v];
        return d->m_constructor;
    }

    void solver::explain_is_child(enode* parent, enode* child) {
        enode* parentc = oc_get_cstor(parent);
        if (parent != parentc)
            m_used_eqs.push_back(enode_pair(parent, parentc));

        // collect equalities on all children that may have been used.
        bool found = false;
        auto add = [&](enode* seq_arg) {
            if (seq_arg->get_root() == child->get_root()) {
                if (seq_arg != child)
                    m_used_eqs.push_back(enode_pair(seq_arg, child));
                found = true;
            }
        };
        for (enode* arg : euf::enode_args(parentc)) {
            add(arg);
            sort* s = arg->get_sort();
            if (m_autil.is_array(s) && dt.is_datatype(get_array_range(s)))
                for (enode* aarg : get_array_args(arg))
                    add(aarg);
            sort* se;
            if (m_sutil.is_seq(arg->get_sort(), se) && dt.is_datatype(se)) {
                enode* sibling = nullptr;
                for (enode* seq_arg : get_seq_args(arg, sibling))
                    add(seq_arg);
                if (sibling && sibling != arg)
                    m_used_eqs.push_back(enode_pair(arg, sibling));                
            }
        }
        
        VERIFY(found);
    }

    // explain the cycle root -> ... -> app -> root
    void solver::occurs_check_explain(enode* app, enode* root) {
        TRACE(dt, tout << "occurs_check_explain " << ctx.bpp(app) << " <-> " << ctx.bpp(root) << "\n";);

        // first: explain that root=v, given that app=cstor(...,v,...)

        explain_is_child(app, root);

        // now explain app=cstor(..,v,..) where v=root, and recurse with parent of app
        while (app->get_root() != root->get_root()) {
            enode* parent_app = m_parent[app->get_root()];
            explain_is_child(parent_app, app);
            SASSERT(is_constructor(parent_app));
            app = parent_app;
        }

        SASSERT(app->get_root() == root->get_root());
        if (app != root)
            m_used_eqs.push_back(enode_pair(app, root));

        TRACE(dt,
            tout << "occurs_check\n"; for (enode_pair const& p : m_used_eqs) tout << ctx.bpp(p.first) << " - " << ctx.bpp(p.second) << " ";);
    }

    // start exploring subgraph below `app`
    bool solver::occurs_check_enter(enode* app) {
        app = app->get_root();
        theory_var v = app->get_th_var(get_id());
        if (v == euf::null_theory_var)
            return false;
        v = m_find.find(v);
        var_data* d = m_var_data[v];
        if (!d->m_constructor)
            return false;
        enode* parent = d->m_constructor;
        oc_mark_on_stack(parent);
        
        auto process_arg = [&](enode* aarg) {
            if (oc_cycle_free(aarg)) 
                return false;                
            if (oc_on_stack(aarg)) {
                occurs_check_explain(parent, aarg);
                return true;
            }
            if (dt.is_datatype(aarg->get_sort())) {
                m_parent.insert(aarg->get_root(), parent);
                oc_push_stack(aarg);
            }
            return false;            
        };

        for (enode* arg : euf::enode_args(parent)) {
            if (oc_cycle_free(arg))
                continue;
            if (oc_on_stack(arg)) {
                // arg was explored before app, and is still on the stack: cycle
                occurs_check_explain(parent, arg);
                return true;
            }
            // explore `arg` (with parent)
            expr* earg = arg->get_expr();
            sort* s = earg->get_sort(), *se;
            enode* sibling;
            if (dt.is_datatype(s)) {
                m_parent.insert(arg->get_root(), parent);
                oc_push_stack(arg);
            }
            else if (m_sutil.is_seq(s, se) && dt.is_datatype(se)) {
                for (enode* sarg : get_seq_args(arg, sibling))
                    if (process_arg(sarg))
                        return true;
            }
            else if (m_autil.is_array(s) && dt.is_datatype(get_array_range(s))) {
                for (enode* sarg : get_array_args(arg))
                    if (process_arg(sarg))
                        return true;
            }
        }
        return false;
    }

    /**
       \brief Check if n can be reached starting from n and following equalities and constructors.
       For example, occur_check(a1) returns true in the following set of equalities:
       a1 = cons(v1, a2)
       a2 = cons(v2, a3)
       a3 = cons(v3, a1)
    */
    bool solver::occurs_check(enode* n) {
        TRACE(dt_verbose, tout << "occurs check: " << ctx.bpp(n) << "\n";);
        m_stats.m_occurs_check++;

        bool res = false;
        oc_push_stack(n);

        // DFS traversal from `n`. Look at top element and explore it.
        while (!res && !m_dfs.empty()) {
            stack_op op = m_dfs.back().first;
            enode* app = m_dfs.back().second;
            m_dfs.pop_back();

            if (oc_cycle_free(app))
                continue;

            TRACE(dt_verbose, tout << "occurs check loop: " << ctx.bpp(app) << (op == ENTER ? " enter" : " exit") << "\n";);

            switch (op) {
            case ENTER:
                res = occurs_check_enter(app);
                break;

            case EXIT:
                oc_mark_cycle_free(app);
                break;
            }
        }

        if (res) {
            clear_mark();
            ctx.set_conflict(euf::th_explain::conflict(*this, m_used_eqs, ctx.mk_smt_hint(name(), m_used_eqs)));
            TRACE(dt, tout << "occurs check conflict: " << ctx.bpp(n) << "\n";);
        }
        return res;
    }

    sat::check_result solver::check() {
        force_push();
        int num_vars = get_num_vars();
        sat::check_result r = sat::check_result::CR_DONE;
        final_check_st _guard(*this);
        int start = s().rand()();
        for (int i = 0; i < num_vars; i++) {
            theory_var v = (i + start) % num_vars;
            if (v != static_cast<int>(m_find.find(v)))
                continue;
            enode* node = var2enode(v);
            if (!is_datatype(node))
                continue;
            if (dt.is_recursive(node->get_sort()) && !oc_cycle_free(node) && occurs_check(node))
                return sat::check_result::CR_CONTINUE;
            if (get_config().m_dt_lazy_splits == 0)
                continue;
            if (m_var_data[v]->m_constructor)
                continue;
            clear_mark();
            mk_split(v, true);
            r = sat::check_result::CR_CONTINUE;
        }
        return r;
    }

    void solver::pop_core(unsigned num_scopes) {
        th_euf_solver::pop_core(num_scopes);
        std::for_each(m_var_data.begin() + get_num_vars(), m_var_data.end(), delete_proc<var_data>());
        m_var_data.shrink(get_num_vars());
        SASSERT(m_find.get_num_vars() == m_var_data.size());
        SASSERT(m_find.get_num_vars() == get_num_vars());
    }

    void solver::get_antecedents(literal l, sat::ext_justification_idx idx, literal_vector& r, bool probing) {
        auto& jst = euf::th_explain::from_index(idx);
        ctx.get_th_antecedents(l, jst, r, probing);
    }

    void solver::add_value(euf::enode* n, model& mdl, expr_ref_vector& values) {
        theory_var v = n->get_th_var(get_id());
        if (v == euf::null_theory_var) {
            values.set(n->get_root_id(), mdl.get_fresh_value(n->get_sort()));
            return;
        }
        v = m_find.find(v);
        SASSERT(v != euf::null_theory_var);
        enode* con = m_var_data[v]->m_constructor;
        func_decl* c_decl = con->get_decl();
        m_args.reset();
        for (enode* arg : euf::enode_args(con))
            m_args.push_back(values.get(arg->get_root_id()));
        values.set(n->get_root_id(), m.mk_app(c_decl, m_args));
    }

    bool solver::add_dep(euf::enode* n, top_sort<euf::enode>& dep) {
        if (!is_datatype(n->get_expr()))
            return false;
        theory_var v = n->get_th_var(get_id());
        if (v == euf::null_theory_var) 
            return false;
        euf::enode* con = m_var_data[m_find.find(v)]->m_constructor;
        TRACE(dt, display(tout) << ctx.bpp(n) << " con: " << ctx.bpp(con) << "\n";);
        if (con->num_args() == 0)
            dep.insert(n, nullptr);
        for (enode* arg : euf::enode_args(con))
            dep.add(n, arg->get_root());
        return true;
    }

    bool solver::include_func_interp(func_decl* f) const {
        if (!dt.is_accessor(f))
            return false;
        func_decl* con_decl = dt.get_accessor_constructor(f);
        for (enode* app : ctx.get_egraph().enodes_of(f)) {   
            enode* con = oc_get_cstor(app->get_arg(0));
            if (con && is_constructor(con) && con->get_decl() != con_decl) 
                return true;
        }
        return false; 
    }

    sat::literal solver::internalize(expr* e, bool sign, bool root) {
        if (!visit_rec(m, e, sign, root)) 
            return sat::null_literal;        
        auto lit = ctx.expr2literal(e);
        if (sign)
            lit.neg();
        return lit;
    }

    void solver::internalize(expr* e) {
        visit_rec(m, e, false, false);
    }

    bool solver::visit(expr* e) {
        if (visited(e))
            return true;
        if (!is_app(e) || to_app(e)->get_family_id() != get_id()) {
            ctx.internalize(e);
            if (is_datatype(e))
                mk_var(expr2enode(e));
            return true;
        }
        m_stack.push_back(sat::eframe(e));
        return false;
    }

    bool solver::visited(expr* e) {
        euf::enode* n = expr2enode(e);
        return n && n->is_attached_to(get_id());
    }

    bool solver::post_visit(expr* term, bool sign, bool root) {
        euf::enode* n = expr2enode(term);
        SASSERT(!n || !n->is_attached_to(get_id()));
        if (!n)
            n = mk_enode(term);
        SASSERT(!n->is_attached_to(get_id()));
        if (is_constructor(term) || is_update_field(term)) {
            for (enode* arg : euf::enode_args(n)) {
                sort* s = arg->get_sort();
                if (dt.is_datatype(s))
                    mk_var(arg);
                else if (m_autil.is_array(s) && dt.is_datatype(get_array_range(s))) {
                    app_ref def(m_autil.mk_default(arg->get_expr()), m);
                    mk_var(e_internalize(def));
                }
            }
            mk_var(n);
        }
        else if (is_recognizer(term)) {
            mk_var(n);
            enode* arg = n->get_arg(0);
            theory_var v = mk_var(arg);
            add_recognizer(v, n);
        }
        else {
            SASSERT(is_accessor(term));
            SASSERT(n->num_args() == 1);
            mk_var(n->get_arg(0));
            if (is_datatype(n))
                mk_var(n);
        }

        return true;
    }

    void solver::collect_statistics(::statistics& st) const {
        st.update("datatype occurs check", m_stats.m_occurs_check);
        st.update("datatype splits", m_stats.m_splits);
        st.update("datatype constructor ax", m_stats.m_assert_cnstr);
        st.update("datatype accessor ax", m_stats.m_assert_accessor);
        st.update("datatype update ax", m_stats.m_assert_update_field);
    }

    std::ostream& solver::display(std::ostream& out) const {
        unsigned num_vars = get_num_vars();
        if (num_vars > 0)
            out << "Theory datatype:\n";
        for (unsigned v = 0; v < num_vars; v++)
            display_var(out, v);
        return out;
    }

    void solver::display_var(std::ostream& out, theory_var v) const {
        var_data* d = m_var_data[v];
        out << "v" << v << " #" << var2expr(v)->get_id() << " -> v" << m_find.find(v) << " ";
        if (d->m_constructor)
            out << ctx.bpp(d->m_constructor);
        else
            out << "(null)";
        out << "\n";
    }
}
