optional<expr> mk_class_instance(environment const & env, io_state const & ios, local_context const & ctx,
                                 name const & prefix, expr const & type, bool relax_opaque, bool use_local_instances,
                                 unifier_config const & cfg) {
    auto C = std::make_shared<class_instance_context>(env, ios, prefix, relax_opaque, use_local_instances);
    if (!is_ext_class(C->tc(), type))
        return none_expr();
    expr meta       = ctx.mk_meta(C->m_ngen, some_expr(type), type.get_tag());
    unsigned depth  = 0;
    constraint c    = mk_class_instance_cnstr(C, ctx, meta, depth);
    unifier_config new_cfg(cfg);
    new_cfg.m_discard        = true;
    new_cfg.m_use_exceptions = true;
    new_cfg.m_pattern        = true;
    new_cfg.m_kind           = C->m_conservative ? unifier_kind::VeryConservative : unifier_kind::Liberal;
    try {
        auto seq = unify(env, 1, &c, C->m_ngen.mk_child(), substitution(), new_cfg);
        while (true) {
            auto p = seq.pull();
            lean_assert(p);
            substitution s = p->first.first;
            expr r = s.instantiate_all(meta);
            if (!has_expr_metavar_relaxed(r))
                return some_expr(r);
            seq = p->second;
        }
    } catch (exception &) {
        return none_expr();
    }
}
constraint mk_class_instance_root_cnstr(std::shared_ptr<class_instance_context> const & C, local_context const & _ctx,
                                        expr const & m, bool is_strict, unifier_config const & cfg, delay_factor const & factor) {
    environment const & env = C->env();
    justification j         = mk_failed_to_synthesize_jst(env, m);

    auto choice_fn = [=](expr const & meta, expr const & meta_type, substitution const & s,
                         name_generator const & ngen) {
        environment const & env  = C->env();
        auto cls_name_it = is_ext_class(C->tc(), meta_type);
        if (!cls_name_it) {
            // do nothing, since type is not a class.
            return lazy_list<constraints>(constraints());
        }
        local_context ctx        = _ctx.instantiate(substitution(s));
        pair<expr, justification> mj = update_meta(meta, s);
        expr new_meta            = mj.first;
        justification new_j      = mj.second;
        unsigned depth           = 0;
        constraint c             = mk_class_instance_cnstr(C, ctx, new_meta, depth);
        unifier_config new_cfg(cfg);
        new_cfg.m_discard        = false;
        new_cfg.m_use_exceptions = false;
        new_cfg.m_pattern        = true;
        new_cfg.m_kind           = C->m_conservative ? unifier_kind::VeryConservative : unifier_kind::Liberal;

        auto to_cnstrs_fn = [=](substitution const & subst, constraints const & cnstrs) -> constraints {
            substitution new_s = subst;
            // some constraints may have been postponed (example: universe level constraints)
            constraints  postponed = map(cnstrs,
                                         [&](constraint const & c) {
                                             // we erase internal justifications
                                             return update_justification(c, mk_composite1(j, new_j));
                                         });
            metavar_closure cls(new_meta);
            cls.add(meta_type);
            bool relax     = C->m_relax;
            constraints cs = cls.mk_constraints(new_s, new_j, relax);
            return append(cs, postponed);
        };

        auto no_solution_fn = [=]() {
            if (is_strict)
                return lazy_list<constraints>();
            else
                return lazy_list<constraints>(constraints());
        };

        unify_result_seq seq1    = unify(env, 1, &c, ngen, substitution(), new_cfg);
        unify_result_seq seq2    = filter(seq1, [=](pair<substitution, constraints> const & p) {
                substitution new_s = p.first;
                expr result = new_s.instantiate(new_meta);
                // We only keep complete solutions (modulo universe metavariables)
                return !has_expr_metavar_relaxed(result);
            });

        if (get_class_unique_class_instances(C->m_ios.get_options())) {
            optional<expr> solution;
            substitution subst;
            constraints  cnstrs;
            for_each(seq2, [&](pair<substitution, constraints> const & p) {
                    subst  = p.first;
                    cnstrs = p.second;
                    expr next_solution = subst.instantiate(new_meta);
                    if (solution) {
                        throw_class_exception(m, [=](formatter const & fmt) {
                                format r = format("ambiguous class-instance resolution, "
                                                  "there is more than one solution");
                                r += pp_indent_expr(fmt, *solution);
                                r += compose(line(), format("and"));
                                r += pp_indent_expr(fmt, next_solution);
                                return r;
                            });
                    } else {
                        solution = next_solution;
                    }
                });
            if (!solution) {
                return no_solution_fn();
            } else {
                // some constraints may have been postponed (example: universe level constraints)
                return lazy_list<constraints>(to_cnstrs_fn(subst, cnstrs));
            }
        } else {
            if (try_multiple_instances(env, *cls_name_it)) {
                lazy_list<constraints> seq3 = map2<constraints>(seq2, [=](pair<substitution, constraints> const & p) {
                        return to_cnstrs_fn(p.first, p.second);
                    });
                if (is_strict) {
                    return seq3;
                } else {
                    // make sure it does not fail by appending empty set of constraints
                    return append(seq3, lazy_list<constraints>(constraints()));
                }
            } else {
                auto p  = seq2.pull();
                if (!p)
                    return no_solution_fn();
                else
                    return lazy_list<constraints>(to_cnstrs_fn(p->first.first, p->first.second));
            }
        }
    };
    bool owner = false;
    bool relax = C->m_relax;
    return mk_choice_cnstr(m, choice_fn, factor, owner, j, relax);
}
/** \brief Create a "choice" constraint that postpones the resolution of a calc proof step.

    By delaying it, we can perform quick fixes such as:
      - adding symmetry
      - adding !
      - adding subst
*/
constraint mk_calc_proof_cnstr(environment const & env, options const & opts,
                               old_local_context const & _ctx, expr const & m, expr const & _e,
                               constraint_seq const & cs, unifier_config const & cfg,
                               info_manager * im, update_type_info_fn const & fn) {
    justification j         = mk_failed_to_synthesize_jst(env, m);
    auto choice_fn = [=](expr const & meta, expr const & _meta_type, substitution const & _s) {
        old_local_context ctx = _ctx;
        expr e            = _e;
        substitution s    = _s;
        expr meta_type    = _meta_type;
        type_checker_ptr tc = mk_type_checker(env);
        constraint_seq new_cs = cs;
        expr e_type = tc->infer(e, new_cs);
        e_type      = s.instantiate(e_type);
        tag g       = e.get_tag();
        bool calc_assistant = get_elaborator_calc_assistant(opts);

        if (calc_assistant) {
            // add '!' is needed
            while (is_norm_pi(*tc, e_type, new_cs)) {
                binder_info bi = binding_info(e_type);
                if (!bi.is_implicit() && !bi.is_inst_implicit()) {
                    if (!has_free_var(binding_body(e_type), 0)) {
                        // if the rest of the type does not reference argument,
                        // then we also stop consuming arguments
                        break;
                    }
                }
                expr imp_arg = ctx.mk_meta(some_expr(binding_domain(e_type)), g);
                e            = mk_app(e, imp_arg, g);
                e_type       = instantiate(binding_body(e_type), imp_arg);
            }
            if (im)
                fn(e);
        }
        e_type = head_beta_reduce(e_type);

        expr const & meta_type_fn = get_app_fn(meta_type);
        expr const & e_type_fn    = get_app_fn(e_type);
        if (is_constant(meta_type_fn) && (!is_constant(e_type_fn) || const_name(e_type_fn) != const_name(meta_type_fn))) {
            // try to make sure meta_type and e_type have the same head symbol
            if (!try_normalize_to_head(env, const_name(meta_type_fn), e_type, new_cs) &&
                is_constant(e_type_fn)) {
                try_normalize_to_head(env, const_name(e_type_fn), meta_type, new_cs);
            }
        }

        auto try_alternative = [&](expr const & e, expr const & e_type, constraint_seq fcs, bool conservative) {
            justification new_j = mk_type_mismatch_jst(e, e_type, meta_type);
            if (!tc->is_def_eq(e_type, meta_type, new_j, fcs))
                throw unifier_exception(new_j, s);
            buffer<constraint> cs_buffer;
            fcs.linearize(cs_buffer);
            metavar_closure cls(meta);
            cls.add(meta_type);
            cls.mk_constraints(s, j, cs_buffer);

            unifier_config new_cfg(cfg);
            new_cfg.m_discard      = false;
            new_cfg.m_kind         = conservative ? unifier_kind::Conservative : unifier_kind::Liberal;
            unify_result_seq seq   = unify(env, cs_buffer.size(), cs_buffer.data(), substitution(), new_cfg);
            auto p = seq.pull();
            lean_assert(p);
            substitution new_s     = p->first.first;
            constraints  postponed = map(p->first.second,
                                         [&](constraint const & c) {
                                             // we erase internal justifications
                                             return update_justification(c, j);
                                         });
            expr new_e = new_s.instantiate(e);
            if (conservative && has_expr_metavar_relaxed(new_s.instantiate_all(e)))
                throw_elaborator_exception("solution contains metavariables", e);
            if (im)
                im->instantiate(new_s);
            constraints r = cls.mk_constraints(new_s, j);
            buffer<expr> locals;
            expr mvar  = get_app_args(meta, locals);
            expr val   = Fun(locals, new_e);
            r = cons(mk_eq_cnstr(mvar, val, j), r);
            return append(r, postponed);
        };

        if (!get_elaborator_calc_assistant(opts)) {
            bool conservative = false;
            return try_alternative(e, e_type, new_cs, conservative);
        } else {
            // TODO(Leo): after we have the simplifier and rewriter tactic, we should revise
            // this code. It is "abusing" the higher-order unifier.

            {
                // Try the following possible intrepretations using a "conservative" unification procedure.
                // That is, we only unfold definitions marked as reducible.
                // Assume pr is the proof provided.

                // 1. pr
                bool conservative = true;
                try { return try_alternative(e, e_type, new_cs, conservative); } catch (exception & ex) {}

                // 2. eq.symm pr
                constraint_seq symm_cs = new_cs;
                auto symm  = apply_symmetry(env, ctx, tc, e, e_type, symm_cs, g);
                if (symm) {
                    try { return try_alternative(symm->first, symm->second, symm_cs, conservative); } catch (exception &) {}
                }

                // 3. subst pr (eq.refl lhs)
                constraint_seq subst_cs = new_cs;
                if (auto subst = apply_subst(env, ctx, tc, e, e_type, meta_type, subst_cs, g)) {
                    try { return try_alternative(subst->first, subst->second, subst_cs, conservative); } catch (exception&) {}
                }

                // 4. subst (eq.symm pr) (eq.refl lhs)
                if (symm) {
                    constraint_seq subst_cs = symm_cs;
                    if (auto subst = apply_subst(env, ctx, tc, symm->first, symm->second,
                                                 meta_type, subst_cs, g)) {
                        try { return try_alternative(subst->first, subst->second, subst_cs, conservative); }
                        catch (exception&) {}
                    }
                }
            }

            {
                // Try the following possible insterpretations using the default unification procedure.

                // 1. pr
                bool conservative = false;
                std::unique_ptr<throwable> saved_ex;
                try {
                    return try_alternative(e, e_type, new_cs, conservative);
                } catch (exception & ex) {
                    saved_ex.reset(ex.clone());
                }

                // 2. eq.symm pr
                constraint_seq symm_cs = new_cs;
                auto symm  = apply_symmetry(env, ctx, tc, e, e_type, symm_cs, g);
                if (symm) {
                    try { return try_alternative(symm->first, symm->second, symm_cs, conservative); }
                    catch (exception &) {}
                }

                // We use the exception for the first alternative as the error message
                saved_ex->rethrow();
                lean_unreachable();
            }
        }
    };
    bool owner = false;
    return mk_choice_cnstr(m, choice_fn, to_delay_factor(cnstr_group::Epilogue), owner, j);
}